From 64a63c1c4d2dd7828e60e03abe74f1aa014789a2 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 8 Mar 2017 16:14:42 +0100 Subject: [PATCH] Add a faster code path for searching simple strings (#21434) --- src/vs/editor/common/model/textModelSearch.ts | 104 ++++++++++++------ .../editor/contrib/find/common/findModel.ts | 5 +- .../test/common/model/textModelSearch.test.ts | 36 +++--- 3 files changed, 92 insertions(+), 53 deletions(-) diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index 18f40e21b0b..4c6bd91c3b2 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -54,7 +54,7 @@ export class SearchParams { return false; } - public parseSearchRequest(): RegExp { + public parseSearchRequest(): SearchData { if (this.searchString === '') { return null; } @@ -83,7 +83,30 @@ export class SearchParams { return null; } - return regex; + let canUseSimpleSearch = (!this.isRegex && !this.wholeWord && !multiline); + if (canUseSimpleSearch && this.searchString.toLowerCase() !== this.searchString.toUpperCase()) { + // casing might make a difference + canUseSimpleSearch = this.matchCase; + } + + return new SearchData(regex, canUseSimpleSearch ? this.searchString : null); + } +} + +export class SearchData { + + /** + * The regex to search for. Always defined. + */ + public readonly regex: RegExp; + /** + * The simple string to search for (if possible). + */ + public readonly simpleSearch: string; + + constructor(regex: RegExp, simpleSearch: string) { + this.regex = regex; + this.simpleSearch = simpleSearch; } } @@ -101,15 +124,15 @@ function createFindMatch(range: Range, rawMatches: RegExpExecArray, captureMatch export class TextModelSearch { public static findMatches(model: TextModel, searchParams: SearchParams, searchRange: Range, captureMatches: boolean, limitResultCount: number): FindMatch[] { - const regex = searchParams.parseSearchRequest(); - if (!regex) { + const searchData = searchParams.parseSearchRequest(); + if (!searchData) { return []; } - if (regex.multiline) { - return this._doFindMatchesMultiline(model, searchRange, regex, captureMatches, limitResultCount); + if (searchData.regex.multiline) { + return this._doFindMatchesMultiline(model, searchRange, searchData.regex, captureMatches, limitResultCount); } - return this._doFindMatchesLineByLine(model, searchRange, regex, captureMatches, limitResultCount); + return this._doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount); } /** @@ -188,36 +211,52 @@ export class TextModelSearch { return result; } - private static _doFindMatchesLineByLine(model: TextModel, searchRange: Range, searchRegex: RegExp, captureMatches: boolean, limitResultCount: number): FindMatch[] { + private static _doFindMatchesLineByLine(model: TextModel, searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] { const result: FindMatch[] = []; - let counter = 0; + let resultLen = 0; // Early case for a search range that starts & stops on the same line number if (searchRange.startLineNumber === searchRange.endLineNumber) { const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1, searchRange.endColumn - 1); - counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, captureMatches, limitResultCount); + resultLen = this._findMatchesInLine(searchData, text, searchRange.startLineNumber, searchRange.startColumn - 1, resultLen, result, captureMatches, limitResultCount); return result; } // Collect results from first line const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1); - counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, captureMatches, limitResultCount); + resultLen = this._findMatchesInLine(searchData, text, searchRange.startLineNumber, searchRange.startColumn - 1, resultLen, result, captureMatches, limitResultCount); // Collect results from middle lines - for (let lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && counter < limitResultCount; lineNumber++) { - counter = this._findMatchesInLine(searchRegex, model.getLineContent(lineNumber), lineNumber, 0, counter, result, captureMatches, limitResultCount); + for (let lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && resultLen < limitResultCount; lineNumber++) { + resultLen = this._findMatchesInLine(searchData, model.getLineContent(lineNumber), lineNumber, 0, resultLen, result, captureMatches, limitResultCount); } // Collect results from last line - if (counter < limitResultCount) { + if (resultLen < limitResultCount) { const text = model.getLineContent(searchRange.endLineNumber).substring(0, searchRange.endColumn - 1); - counter = this._findMatchesInLine(searchRegex, text, searchRange.endLineNumber, 0, counter, result, captureMatches, limitResultCount); + resultLen = this._findMatchesInLine(searchData, text, searchRange.endLineNumber, 0, resultLen, result, captureMatches, limitResultCount); } return result; } - private static _findMatchesInLine(searchRegex: RegExp, text: string, lineNumber: number, deltaOffset: number, counter: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number): number { + private static _findMatchesInLine(searchData: SearchData, text: string, lineNumber: number, deltaOffset: number, resultLen: number, result: FindMatch[], captureMatches: boolean, limitResultCount: number): number { + if (!captureMatches && searchData.simpleSearch) { + const searchString = searchData.simpleSearch; + const searchStringLen = searchString.length; + + let lastMatchIndex = -searchStringLen; + while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) { + const range = new Range(lineNumber, lastMatchIndex + 1 + deltaOffset, lineNumber, lastMatchIndex + 1 + searchStringLen + deltaOffset); + result[resultLen++] = new FindMatch(range, null); + if (resultLen >= limitResultCount) { + return resultLen; + } + } + return resultLen; + } + + const searchRegex = searchData.regex; let m: RegExpExecArray; // Reset regex to search from the beginning searchRegex.lastIndex = 0; @@ -227,32 +266,31 @@ export class TextModelSearch { const range = new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset); if (result.length > 0 && range.equalsRange(result[result.length - 1].range)) { // Exit early if the regex matches the same range - return counter; + return resultLen; } - result.push(createFindMatch(range, m, captureMatches)); - counter++; - if (counter >= limitResultCount) { - return counter; + result[resultLen++] = createFindMatch(range, m, captureMatches); + if (resultLen >= limitResultCount) { + return resultLen; } if (m.index + m[0].length === text.length) { // Reached the end of the line - return counter; + return resultLen; } } } while (m); - return counter; + return resultLen; } public static findNextMatch(model: TextModel, searchParams: SearchParams, searchStart: Position, captureMatches: boolean): FindMatch { - const regex = searchParams.parseSearchRequest(); - if (!regex) { + const searchData = searchParams.parseSearchRequest(); + if (!searchData) { return null; } - if (regex.multiline) { - return this._doFindNextMatchMultiline(model, searchStart, regex, captureMatches); + if (searchData.regex.multiline) { + return this._doFindNextMatchMultiline(model, searchStart, searchData.regex, captureMatches); } - return this._doFindNextMatchLineByLine(model, searchStart, regex, captureMatches); + return this._doFindNextMatchLineByLine(model, searchStart, searchData.regex, captureMatches); } private static _doFindNextMatchMultiline(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { @@ -319,15 +357,15 @@ export class TextModelSearch { } public static findPreviousMatch(model: TextModel, searchParams: SearchParams, searchStart: Position, captureMatches: boolean): FindMatch { - const regex = searchParams.parseSearchRequest(); - if (!regex) { + const searchData = searchParams.parseSearchRequest(); + if (!searchData) { return null; } - if (regex.multiline) { - return this._doFindPreviousMatchMultiline(model, searchStart, regex, captureMatches); + if (searchData.regex.multiline) { + return this._doFindPreviousMatchMultiline(model, searchStart, searchData.regex, captureMatches); } - return this._doFindPreviousMatchLineByLine(model, searchStart, regex, captureMatches); + return this._doFindPreviousMatchLineByLine(model, searchStart, searchData.regex, captureMatches); } private static _doFindPreviousMatchMultiline(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 44847fb1b6f..7e8858821a6 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -17,6 +17,7 @@ import { ReplaceAllCommand } from './replaceAllCommand'; import { Selection } from 'vs/editor/common/core/selection'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IKeybindings } from 'vs/platform/keybinding/common/keybinding'; +import { Constants } from 'vs/editor/common/core/uint'; export const ToggleCaseSensitiveKeybinding: IKeybindings = { primary: KeyMod.Alt | KeyCode.KEY_C, @@ -385,7 +386,7 @@ export class FindModelBoundToEditorModel { let findScope = this._decorations.getFindScope(); let replacePattern = this._getReplacePattern(); // Get all the ranges (even more than the highlighted ones) - let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns, Number.MAX_VALUE); + let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns, Constants.MAX_SAFE_SMALL_INTEGER); let replaceStrings: string[] = []; for (let i = 0, len = matches.length; i < len; i++) { @@ -406,7 +407,7 @@ export class FindModelBoundToEditorModel { let findScope = this._decorations.getFindScope(); // Get all the ranges (even more than the highlighted ones) - let matches = this._findMatches(findScope, false, Number.MAX_VALUE); + let matches = this._findMatches(findScope, false, Constants.MAX_SAFE_SMALL_INTEGER); let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn)); // If one of the ranges is the editor selection, then maintain it as primary diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 331b206d0c5..278ee1539ca 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -9,7 +9,7 @@ import { Position } from 'vs/editor/common/core/position'; import { FindMatch, EndOfLineSequence } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; +import { TextModelSearch, SearchParams, SearchData } from 'vs/editor/common/model/textModelSearch'; // --------- Find suite('TextModelSearch', () => { @@ -542,7 +542,7 @@ suite('TextModelSearch', () => { model.dispose(); }); - function assertParseSearchResult(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, expected: RegExp): void { + function assertParseSearchResult(searchString: string, isRegex: boolean, matchCase: boolean, wholeWord: boolean, expected: SearchData): void { let searchParams = new SearchParams(searchString, isRegex, matchCase, wholeWord); let actual = searchParams.parseSearchRequest(); assert.deepEqual(actual, expected); @@ -555,24 +555,24 @@ suite('TextModelSearch', () => { }); test('parseSearchRequest non regex', () => { - assertParseSearchResult('foo', false, false, false, /foo/gi); - assertParseSearchResult('foo', false, false, true, /\bfoo\b/gi); - assertParseSearchResult('foo', false, true, false, /foo/g); - assertParseSearchResult('foo', false, true, true, /\bfoo\b/g); - assertParseSearchResult('foo\\n', false, false, false, /foo\\n/gi); - assertParseSearchResult('foo\\\\n', false, false, false, /foo\\\\n/gi); - assertParseSearchResult('foo\\r', false, false, false, /foo\\r/gi); - assertParseSearchResult('foo\\\\r', false, false, false, /foo\\\\r/gi); + assertParseSearchResult('foo', false, false, false, new SearchData(/foo/gi, null)); + assertParseSearchResult('foo', false, false, true, new SearchData(/\bfoo\b/gi, null)); + assertParseSearchResult('foo', false, true, false, new SearchData(/foo/g, 'foo')); + assertParseSearchResult('foo', false, true, true, new SearchData(/\bfoo\b/g, null)); + assertParseSearchResult('foo\\n', false, false, false, new SearchData(/foo\\n/gi, null)); + assertParseSearchResult('foo\\\\n', false, false, false, new SearchData(/foo\\\\n/gi, null)); + assertParseSearchResult('foo\\r', false, false, false, new SearchData(/foo\\r/gi, null)); + assertParseSearchResult('foo\\\\r', false, false, false, new SearchData(/foo\\\\r/gi, null)); }); test('parseSearchRequest regex', () => { - assertParseSearchResult('foo', true, false, false, /foo/gi); - assertParseSearchResult('foo', true, false, true, /\bfoo\b/gi); - assertParseSearchResult('foo', true, true, false, /foo/g); - assertParseSearchResult('foo', true, true, true, /\bfoo\b/g); - assertParseSearchResult('foo\\n', true, false, false, /foo\n/gim); - assertParseSearchResult('foo\\\\n', true, false, false, /foo\\n/gi); - assertParseSearchResult('foo\\r', true, false, false, /foo\r/gim); - assertParseSearchResult('foo\\\\r', true, false, false, /foo\\r/gi); + assertParseSearchResult('foo', true, false, false, new SearchData(/foo/gi, null)); + assertParseSearchResult('foo', true, false, true, new SearchData(/\bfoo\b/gi, null)); + assertParseSearchResult('foo', true, true, false, new SearchData(/foo/g, null)); + assertParseSearchResult('foo', true, true, true, new SearchData(/\bfoo\b/g, null)); + assertParseSearchResult('foo\\n', true, false, false, new SearchData(/foo\n/gim, null)); + assertParseSearchResult('foo\\\\n', true, false, false, new SearchData(/foo\\n/gi, null)); + assertParseSearchResult('foo\\r', true, false, false, new SearchData(/foo\r/gim, null)); + assertParseSearchResult('foo\\\\r', true, false, false, new SearchData(/foo\\r/gi, null)); }); });