From 48a5f35d042fce5a4c5597dd7979999a3d23e16f Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 19 Mar 2018 16:58:41 -0700 Subject: [PATCH 1/4] findMatchesInNode should take startColumn into account. --- .../pieceTreeTextBuffer/pieceTreeBase.ts | 26 ++++++++++++------- .../pieceTreeTextBuffer.test.ts | 18 +++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index ccef44113f5..13c81a70acb 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -551,7 +551,7 @@ 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[]) { + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: 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); @@ -571,7 +571,9 @@ export class PieceTreeBase { } 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); + let retStartColumn = ret.line === startCursor.line ? ret.column - startCursor.column + startColumn : ret.column + 1; + let retEndColumn = retStartColumn + m[0].length; + result[resultLen++] = createFindMatch(new Range(startLineNumber + lineFeedCnt, retStartColumn, startLineNumber + lineFeedCnt, retEndColumn), m, captureMatches); if (m.index + m[0].length >= end) { return resultLen; @@ -603,7 +605,7 @@ export class PieceTreeBase { 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); + this.findMatchesInNode(startPostion.node, searcher, searchRange.startLineNumber, searchRange.startColumn, start, end, searchData, captureMatches, limitResultCount, resultLen, result); return result; } @@ -618,7 +620,8 @@ export class PieceTreeBase { let lineStarts = this._buffers[currentNode.piece.bufferIndex].lineStarts; let startOffsetInBuffer = this.offsetInBuffer(currentNode.piece.bufferIndex, currentNode.piece.start); let nextLineStartOffset = lineStarts[start.line + lineBreakCnt]; - resultLen = this.findMatchesInNode(currentNode, searcher, startLineNumber, start, this.positionInBuffer(currentNode, nextLineStartOffset - startOffsetInBuffer), searchData, captureMatches, limitResultCount, resultLen, result); + let startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1; + resultLen = this.findMatchesInNode(currentNode, searcher, startLineNumber, startColumn, start, this.positionInBuffer(currentNode, nextLineStartOffset - startOffsetInBuffer), searchData, captureMatches, limitResultCount, resultLen, result); if (resultLen >= limitResultCount) { return result; @@ -627,14 +630,15 @@ export class PieceTreeBase { startLineNumber += lineBreakCnt; } + let startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0; // 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); + const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1); + resultLen = this._findMatchesInLine(searchData, searcher, text, searchRange.endLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount); return result; } - resultLen = this._findMatchesInLine(searchData, searcher, this.getLineContent(startLineNumber), startLineNumber, 0, resultLen, result, captureMatches, limitResultCount); + resultLen = this._findMatchesInLine(searchData, searcher, this.getLineContent(startLineNumber).substr(startColumn), startLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount); if (resultLen >= limitResultCount) { return result; @@ -647,12 +651,14 @@ export class PieceTreeBase { } 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); + let startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0; + const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1); + resultLen = this._findMatchesInLine(searchData, searcher, text, searchRange.endLineNumber, startColumn, resultLen, result, captureMatches, limitResultCount); return result; } - resultLen = this.findMatchesInNode(endPosition.node, searcher, startLineNumber, start, end, searchData, captureMatches, limitResultCount, resultLen, result); + let startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1; + resultLen = this.findMatchesInNode(endPosition.node, searcher, startLineNumber, startColumn, start, end, searchData, captureMatches, limitResultCount, resultLen, result); return result; } diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index c65d165be38..e5b6970d888 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -1810,4 +1810,22 @@ suite('chunk based search', () => { assert.deepEqual(ret[1].range, new Range(3, 3, 3, 4)); assert.deepEqual(ret[2].range, new Range(4, 3, 4, 4)); }); + + test('search searching from the middle', () => { + let pieceTree = createTextBuffer([ + [ + 'def', + 'dbcabc' + ].join('\n') + ]); + pieceTree.delete(4, 1); + let ret = pieceTree.findMatchesLineByLine(new Range(2, 3, 2, 6), new SearchData(/a/gi, null, 'a'), true, 1000); + assert.equal(ret.length, 1); + assert.deepEqual(ret[0].range, new Range(2, 3, 2, 4)); + + pieceTree.delete(4, 1); + ret = pieceTree.findMatchesLineByLine(new Range(2, 2, 2, 5), new SearchData(/a/gi, null, 'a'), true, 1000); + assert.equal(ret.length, 1); + assert.deepEqual(ret[0].range, new Range(2, 2, 2, 3)); + }); }); \ No newline at end of file From c166beabe59d860c138a8aa229e6425f2a152abe Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 19 Mar 2018 20:46:02 -0700 Subject: [PATCH 2/4] Use decoration tree to search when not exceeding limit. --- src/vs/editor/contrib/find/findDecorations.ts | 44 +++++++ src/vs/editor/contrib/find/findModel.ts | 113 ++++++++++++------ 2 files changed, 121 insertions(+), 36 deletions(-) diff --git a/src/vs/editor/contrib/find/findDecorations.ts b/src/vs/editor/contrib/find/findDecorations.ts index b5b5287decb..84ddb11b262 100644 --- a/src/vs/editor/contrib/find/findDecorations.ts +++ b/src/vs/editor/contrib/find/findDecorations.ts @@ -208,6 +208,50 @@ export class FindDecorations implements IDisposable { }); } + public matchBeforePosition(position: Position): Range { + if (this._decorations.length === 0) { + return null; + } + for (let i = this._decorations.length - 1; i >= 0; i--) { + let decorationId = this._decorations[i]; + let r = this._editor.getModel().getDecorationRange(decorationId); + if (!r || r.endLineNumber > position.lineNumber) { + continue; + } + if (r.endLineNumber < position.lineNumber) { + return r; + } + if (r.endColumn > position.column) { + continue; + } + return r; + } + + return this._editor.getModel().getDecorationRange(this._decorations[this._decorations.length - 1]); + } + + public matchAfterPosition(position: Position): Range { + if (this._decorations.length === 0) { + return null; + } + for (let i = 0, len = this._decorations.length; i < len; i++) { + let decorationId = this._decorations[i]; + let r = this._editor.getModel().getDecorationRange(decorationId); + if (!r || r.startLineNumber < position.lineNumber) { + continue; + } + if (r.startLineNumber > position.lineNumber) { + return r; + } + if (r.startColumn < position.column) { + continue; + } + return r; + } + + return this._editor.getModel().getDecorationRange(this._decorations[0]); + } + private _allDecorations(): string[] { let result: string[] = []; result = result.concat(this._decorations); diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index 7f0a8ac7ab4..651fb55a460 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -215,7 +215,44 @@ export class FindModelBoundToEditorModel { this._editor.revealRangeInCenterIfOutsideViewport(match, editorCommon.ScrollType.Smooth); } + private _prevSearchPosition(before: Position) { + let isUsingLineStops = this._state.isRegex && ( + this._state.searchString.indexOf('^') >= 0 + || this._state.searchString.indexOf('$') >= 0 + ); + let { lineNumber, column } = before; + let model = this._editor.getModel(); + + if (isUsingLineStops || column === 1) { + if (lineNumber === 1) { + lineNumber = model.getLineCount(); + } else { + lineNumber--; + } + column = model.getLineMaxColumn(lineNumber); + } else { + column--; + } + + return new Position(lineNumber, column); + } + private _moveToPrevMatch(before: Position, isRecursed: boolean = false): void { + if (this._decorations.getCount() < MATCHES_LIMIT) { + let prevMatchRange = this._decorations.matchBeforePosition(before); + + if (prevMatchRange && prevMatchRange.isEmpty() && prevMatchRange.getStartPosition().equals(before)) { + before = this._prevSearchPosition(before); + prevMatchRange = this._decorations.matchBeforePosition(before); + } + + if (prevMatchRange) { + this._setCurrentFindMatch(prevMatchRange); + } + + return; + } + if (this._cannotFind()) { return; } @@ -242,24 +279,7 @@ export class FindModelBoundToEditorModel { if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) { // Looks like we're stuck at this position, unacceptable! - - let isUsingLineStops = this._state.isRegex && ( - this._state.searchString.indexOf('^') >= 0 - || this._state.searchString.indexOf('$') >= 0 - ); - - if (isUsingLineStops || column === 1) { - if (lineNumber === 1) { - lineNumber = model.getLineCount(); - } else { - lineNumber--; - } - column = model.getLineMaxColumn(lineNumber); - } else { - column--; - } - - position = new Position(lineNumber, column); + position = this._prevSearchPosition(position); prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false); } @@ -279,7 +299,45 @@ export class FindModelBoundToEditorModel { this._moveToPrevMatch(this._editor.getSelection().getStartPosition()); } + private _nextSearchPosition(after: Position) { + let isUsingLineStops = this._state.isRegex && ( + this._state.searchString.indexOf('^') >= 0 + || this._state.searchString.indexOf('$') >= 0 + ); + + let { lineNumber, column } = after; + let model = this._editor.getModel(); + + if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) { + if (lineNumber === model.getLineCount()) { + lineNumber = 1; + } else { + lineNumber++; + } + column = 1; + } else { + column++; + } + + return new Position(lineNumber, column); + } + private _moveToNextMatch(after: Position): void { + if (this._decorations.getCount() < MATCHES_LIMIT) { + let nextMatchRange = this._decorations.matchAfterPosition(after); + + if (nextMatchRange && nextMatchRange.isEmpty() && nextMatchRange.getStartPosition().equals(after)) { + // Looks like we're stuck at this position, unacceptable! + after = this._nextSearchPosition(after); + nextMatchRange = this._decorations.matchAfterPosition(after); + } + if (nextMatchRange) { + this._setCurrentFindMatch(nextMatchRange); + } + + return; + } + let nextMatch = this._getNextMatch(after, false, true); if (nextMatch) { this._setCurrentFindMatch(nextMatch.range); @@ -313,24 +371,7 @@ export class FindModelBoundToEditorModel { if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) { // Looks like we're stuck at this position, unacceptable! - - let isUsingLineStops = this._state.isRegex && ( - this._state.searchString.indexOf('^') >= 0 - || this._state.searchString.indexOf('$') >= 0 - ); - - if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) { - if (lineNumber === model.getLineCount()) { - lineNumber = 1; - } else { - lineNumber++; - } - column = 1; - } else { - column++; - } - - position = new Position(lineNumber, column); + position = this._nextSearchPosition(position); nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, captureMatches); } From 654e0683ae827f8febfa1e8b33539ba580cfd3c3 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 19 Mar 2018 20:47:23 -0700 Subject: [PATCH 3/4] For large files, we throttle research events. --- src/vs/editor/contrib/find/findModel.ts | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index 651fb55a460..7ef62988e42 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, TimeoutTimer } from 'vs/base/common/async'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/replacePattern'; import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; @@ -73,6 +73,7 @@ export const FIND_IDS = { }; export const MATCHES_LIMIT = 19999; +const RESEARCH_DELAY = 240; export class FindModelBoundToEditorModel { @@ -81,6 +82,7 @@ export class FindModelBoundToEditorModel { private _toDispose: IDisposable[]; private _decorations: FindDecorations; private _ignoreModelContentChanged: boolean; + private _startSearchingTimer: TimeoutTimer; private _updateDecorationsScheduler: RunOnceScheduler; private _isDisposed: boolean; @@ -90,6 +92,7 @@ export class FindModelBoundToEditorModel { this._state = state; this._toDispose = []; this._isDisposed = false; + this._startSearchingTimer = new TimeoutTimer(); this._decorations = new FindDecorations(editor); this._toDispose.push(this._decorations); @@ -127,6 +130,7 @@ export class FindModelBoundToEditorModel { public dispose(): void { this._isDisposed = true; + dispose(this._startSearchingTimer); this._toDispose = dispose(this._toDispose); } @@ -140,10 +144,24 @@ export class FindModelBoundToEditorModel { return; } if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) { - if (e.searchScope) { - this.research(e.moveCursor, this._state.searchScope); + let model = this._editor.getModel(); + + if (model.isTooLargeForHavingARichMode()) { + this._startSearchingTimer.cancel(); + + this._startSearchingTimer.setIfNotSet(() => { + if (e.searchScope) { + this.research(e.moveCursor, this._state.searchScope); + } else { + this.research(e.moveCursor); + } + }, RESEARCH_DELAY); } else { - this.research(e.moveCursor); + if (e.searchScope) { + this.research(e.moveCursor, this._state.searchScope); + } else { + this.research(e.moveCursor); + } } } } From e4044faa0bac6a21063ef0263e3d6347fe2c188d Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 19 Mar 2018 20:47:51 -0700 Subject: [PATCH 4/4] cmd+d can leverage chunk based searching. --- src/vs/editor/common/model/textModel.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 385c008f24b..559d12de95b 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1015,6 +1015,28 @@ export class TextModel extends Disposable implements model.ITextModel { public findNextMatch(searchString: string, rawSearchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string, captureMatches: boolean): model.FindMatch { this._assertNotDisposed(); const searchStart = this.validatePosition(rawSearchStart); + + if (!isRegex && searchString.indexOf('\n') < 0 && OPTIONS.TEXT_BUFFER_IMPLEMENTATION === TextBufferType.PieceTree) { + const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); + const searchData = searchParams.parseSearchRequest(); + const lineCount = this.getLineCount(); + let searchRange = new Range(searchStart.lineNumber, searchStart.column, lineCount, this.getLineMaxColumn(lineCount)); + let ret = this.findMatchesLineByLine(searchRange, searchData, captureMatches, 1); + TextModelSearch.findNextMatch(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchStart, captureMatches); + if (ret.length > 0) { + return ret[0]; + } + + searchRange = new Range(1, 1, searchStart.lineNumber, this.getLineMaxColumn(searchStart.lineNumber)); + ret = this.findMatchesLineByLine(searchRange, searchData, captureMatches, 1); + + if (ret.length > 0) { + return ret[0]; + } + + return null; + } + return TextModelSearch.findNextMatch(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchStart, captureMatches); }