diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index bf6ecbbee7c1b..45fd9bd7642c9 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -15,6 +15,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ModelRawContentChangedEvent, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelOptionsChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent, IModelContentChange } from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { ITextSnapshot } from 'vs/platform/files/common/files'; +import { SearchData } from 'vs/editor/common/model/textModelSearch'; /** * Vertical Lane in the overview ruler of the editor. @@ -1100,6 +1101,7 @@ export interface ITextBuffer { setEOL(newEOL: '\r\n' | '\n'): void; applyEdits(rawOperations: IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult; + findMatchesLineByLine?(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; } /** diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 370f06865d399..e99bd4ddfbfb1 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -9,6 +9,8 @@ import { CharCode } from 'vs/base/common/charCode'; import { Range } from 'vs/editor/common/core/range'; import { ITextSnapshot } from 'vs/platform/files/common/files'; import { leftest, righttest, updateTreeMetadata, rbDelete, fixInsert, NodeColor, SENTINEL, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; +import { SearchData, isValidMatch, Searcher, createFindMatch } from 'vs/editor/common/model/textModelSearch'; +import { FindMatch } from 'vs/editor/common/model'; // const lfRegex = new RegExp(/\r\n|\r|\n/g); @@ -535,6 +537,138 @@ export class PieceTreeBase { return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength; } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { + let buffer = this._buffers[node.piece.bufferIndex]; + let startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); + let start = this.offsetInBuffer(node.piece.bufferIndex, startCursor); + let end = this.offsetInBuffer(node.piece.bufferIndex, endCursor); + + let m: RegExpExecArray; + // Reset regex to search from the beginning + searcher.reset(start); + let ret: BufferCursor = { line: 0, column: 0 }; + + do { + m = searcher.next(buffer.buffer); + + if (m) { + if (m.index >= end) { + return resultLen; + } + this.positionInBuffer(node, m.index - startOffsetInBuffer, ret); + let lineFeedCnt = this.getLineFeedCnt(node.piece.bufferIndex, startCursor, ret); + result[resultLen++] = createFindMatch(new Range(startLineNumber + lineFeedCnt, ret.column + 1, startLineNumber + lineFeedCnt, ret.column + 1 + m[0].length), m, captureMatches); + + if (m.index + m[0].length >= end) { + return resultLen; + } + if (resultLen >= limitResultCount) { + return resultLen; + } + } + + } while (m); + + return resultLen; + } + + public findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] { + const result: FindMatch[] = []; + let resultLen = 0; + const searcher = new Searcher(searchData.wordSeparators, searchData.regex); + + let startPostion = this.nodeAt2(searchRange.startLineNumber, searchRange.startColumn); + let endPosition = this.nodeAt2(searchRange.endLineNumber, searchRange.endColumn); + let start = this.positionInBuffer(startPostion.node, startPostion.remainder); + let end = this.positionInBuffer(endPosition.node, endPosition.remainder); + + if (startPostion.node === endPosition.node) { + this.findMatchesInNode(startPostion.node, searcher, searchRange.startLineNumber, start, end, searchData, captureMatches, limitResultCount, resultLen, result); + return result; + } + + let startLineNumber = searchRange.startLineNumber; + + let currentNode = startPostion.node; + while (currentNode !== endPosition.node) { + let lineBreakCnt = this.getLineFeedCnt(currentNode.piece.bufferIndex, start, currentNode.piece.end); + + if (lineBreakCnt >= 1) { + // last line break position + let lineStarts = this._buffers[currentNode.piece.bufferIndex].lineStarts; + let nextLineStartOffset = lineStarts[start.line + lineBreakCnt]; + resultLen = this.findMatchesInNode(currentNode, searcher, startLineNumber, start, this.positionInBuffer(currentNode, nextLineStartOffset), searchData, captureMatches, limitResultCount, resultLen, result); + + if (resultLen >= limitResultCount) { + return result; + } + + startLineNumber += lineBreakCnt; + } + + // search for the remaining content + if (startLineNumber === searchRange.endLineNumber) { + const text = this.getLineContent(startLineNumber).substring(0, searchRange.endColumn - 1); + resultLen = this._findMatchesInLine(searchData, searcher, text, searchRange.endLineNumber, 0, resultLen, result, captureMatches, limitResultCount); + return result; + } + + resultLen = this._findMatchesInLine(searchData, searcher, this.getLineContent(startLineNumber), startLineNumber, 0, resultLen, result, captureMatches, limitResultCount); + + if (resultLen >= limitResultCount) { + return result; + } + + startLineNumber++; + startPostion = this.nodeAt2(startLineNumber, 1); + currentNode = startPostion.node; + start = this.positionInBuffer(startPostion.node, startPostion.remainder); + } + + if (startLineNumber === searchRange.endLineNumber) { + const text = this.getLineContent(startLineNumber).substring(0, searchRange.endColumn - 1); + resultLen = this._findMatchesInLine(searchData, searcher, text, searchRange.endLineNumber, 0, resultLen, result, captureMatches, limitResultCount); + return result; + } + + resultLen = this.findMatchesInNode(endPosition.node, searcher, startLineNumber, start, end, searchData, captureMatches, limitResultCount, resultLen, result); + return result; + } + + private _findMatchesInLine(searchData: SearchData, searcher: Searcher, text: string, lineNumber: number, deltaOffset: number, resultLen: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number): number { + const wordSeparators = searchData.wordSeparators; + if (!captureMatches && searchData.simpleSearch) { + const searchString = searchData.simpleSearch; + const searchStringLen = searchString.length; + const textLength = text.length; + + let lastMatchIndex = -searchStringLen; + while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) { + if (!wordSeparators || isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen)) { + result[resultLen++] = new FindMatch(new Range(lineNumber, lastMatchIndex + 1 + deltaOffset, lineNumber, lastMatchIndex + 1 + searchStringLen + deltaOffset), null); + if (resultLen >= limitResultCount) { + return resultLen; + } + } + } + return resultLen; + } + + let m: RegExpExecArray; + // Reset regex to search from the beginning + searcher.reset(0); + do { + m = searcher.next(text); + if (m) { + result[resultLen++] = createFindMatch(new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset), m, captureMatches); + if (resultLen >= limitResultCount) { + return resultLen; + } + } + } while (m); + return resultLen; + } + // #endregion // #region Piece Table @@ -744,7 +878,7 @@ export class PieceTreeBase { this.validateCRLFWithPrevNode(newNode); } - positionInBuffer(node: TreeNode, remainder: number): BufferCursor { + positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor { let piece = node.piece; let bufferIndex = node.piece.bufferIndex; let lineStarts = this._buffers[bufferIndex].lineStarts; @@ -780,6 +914,12 @@ export class PieceTreeBase { } } + if (ret) { + ret.line = mid; + ret.column = offset - midStart; + return null; + } + return { line: mid, column: offset - midStart diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 1f5fbd1dda51c..a6e6b0b4c909e 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -9,8 +9,9 @@ import { Position } from 'vs/editor/common/core/position'; import * as strings from 'vs/base/common/strings'; import { IValidatedEditOperation } from 'vs/editor/common/model/linesTextBuffer/linesTextBuffer'; import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; -import { IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, EndOfLinePreference, ITextBuffer, ApplyEditsResult, IInternalModelContentChange, FindMatch } from 'vs/editor/common/model'; import { ITextSnapshot } from 'vs/platform/files/common/files'; +import { SearchData } from 'vs/editor/common/model/textModelSearch'; export class PieceTreeTextBuffer implements ITextBuffer { private _pieceTree: PieceTreeBase; @@ -410,6 +411,10 @@ export class PieceTreeTextBuffer implements ITextBuffer { return contentChanges; } + findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] { + return this._pieceTree.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + } + // #endregion // #region helper diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 982916e15f8be..4ab3375d08c1f 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -30,7 +30,7 @@ import { getWordAtText } from 'vs/editor/common/model/wordHelper'; import { ModelLinesTokens, ModelTokensChangedEventBuilder } from 'vs/editor/common/model/textModelTokens'; import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; +import { TextModelSearch, SearchParams, SearchData } from 'vs/editor/common/model/textModelSearch'; import { TPromise } from 'vs/base/common/winjs.base'; import { IStringStream, ITextSnapshot } from 'vs/platform/files/common/files'; import { LinesTextBufferBuilder } from 'vs/editor/common/model/linesTextBuffer/linesTextBufferBuilder'; @@ -983,6 +983,10 @@ export class TextModel extends Disposable implements model.ITextModel { return new Range(1, 1, lineCount, this.getLineMaxColumn(lineCount)); } + private findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): model.FindMatch[] { + return this._buffer.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + } + public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] { this._assertNotDisposed(); @@ -993,6 +997,18 @@ export class TextModel extends Disposable implements model.ITextModel { searchRange = this.getFullModelRange(); } + if (!isRegex && searchString.indexOf('\n') < 0 && OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) { + // not regex, not multi line + const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); + const searchData = searchParams.parseSearchRequest(); + + if (!searchData) { + return []; + } + + return this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + } + return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount); } diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index 2f1526e618e5a..5deaa537ff251 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -116,7 +116,7 @@ export class SearchData { } } -function createFindMatch(range: Range, rawMatches: RegExpExecArray, captureMatches: boolean): FindMatch { +export function createFindMatch(range: Range, rawMatches: RegExpExecArray, captureMatches: boolean): FindMatch { if (!captureMatches) { return new FindMatch(range, null); } @@ -434,6 +434,11 @@ function leftIsWordBounday(wordSeparators: WordCharacterClassifier, text: string return true; } + if (charBefore === CharCode.CarriageReturn || charBefore === CharCode.LineFeed) { + // The character before the match is line break or carriage return. + return true; + } + if (matchLength > 0) { const firstCharInMatch = text.charCodeAt(matchStartIndex); if (wordSeparators.get(firstCharInMatch) !== WordCharacterClass.Regular) { @@ -457,6 +462,11 @@ function rightIsWordBounday(wordSeparators: WordCharacterClassifier, text: strin return true; } + if (charAfter === CharCode.CarriageReturn || charAfter === CharCode.LineFeed) { + // The character after the match is line break or carriage return. + return true; + } + if (matchLength > 0) { const lastCharInMatch = text.charCodeAt(matchStartIndex + matchLength - 1); if (wordSeparators.get(lastCharInMatch) !== WordCharacterClass.Regular) { @@ -468,14 +478,14 @@ function rightIsWordBounday(wordSeparators: WordCharacterClassifier, text: strin return false; } -function isValidMatch(wordSeparators: WordCharacterClassifier, text: string, textLength: number, matchStartIndex: number, matchLength: number): boolean { +export function isValidMatch(wordSeparators: WordCharacterClassifier, text: string, textLength: number, matchStartIndex: number, matchLength: number): boolean { return ( leftIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength) && rightIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength) ); } -class Searcher { +export class Searcher { private _wordSeparators: WordCharacterClassifier; private _searchRegex: RegExp; private _prevMatchStartIndex: number;