Add a faster code path for searching simple strings (#21434)

This commit is contained in:
Alex Dima
2017-03-08 16:14:42 +01:00
parent 9774590996
commit 64a63c1c4d
3 changed files with 92 additions and 53 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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));
});
});