mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-07 17:44:05 -05:00
Add a faster code path for searching simple strings (#21434)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user