src/vs/editor/common/model/textModelSearch.ts
555 LOC · 518 covered · 37 uncovered · 164 ranges · 1804 concepts · 63 introducers · 927 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
textModelSearch.ts ×26
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharCode } from '../../../base/common/charCode.js';
import * as strings from '../../../base/common/strings.js';
import { WordCharacterClass, WordCharacterClassifier, getMapForWordSeparators } from '../core/wordCharacterClassifier.js';
import { Position } from '../core/position.js';
import { Range } from '../core/range.js';
import { EndOfLinePreference, FindMatch, SearchData } from '../model.js';
import { TextModel } from './textModel.js';
const LIMIT_FIND_COUNT = 999;
export class SearchParams {
public readonly searchString: string;
public readonly isRegex: boolean;
public readonly matchCase: boolean;
public readonly wordSeparators: string | null;
constructor(searchString: string, isRegex: boolean, matchCase: boolean, wordSeparators: string | null) {
this.isRegex = isRegex;
this.matchCase = matchCase;
this.wordSeparators = wordSeparators;
}
public parseSearchRequest(): SearchData | null {
}
// Try to create a RegExp out of the params
let multiline: boolean;
if (this.isRegex) {
}
let regex: RegExp | null = null;
try {
regex = strings.createRegExp(this.searchString, this.isRegex, {
matchCase: this.matchCase,
wholeWord: false,
multiline: multiline,
global: true,
unicode: true
});
} catch (err) {
}
if (!regex) {
return null;
}
let canUseSimpleSearch = (!this.isRegex && !multiline);
if (canUseSimpleSearch && this.searchString.toLowerCase() !== this.searchString.toUpperCase()) {
textModelSearch.ts ×7
canUseSimpleSearch = this.matchCase;
}
return new SearchData(regex, this.wordSeparators ? getMapForWordSeparators(this.wordSeparators, []) : null, canUseSimpleSearch ? this.searchString : null);
textModelSearch.ts ×7
}
export function isMultilineRegexSource(searchString: string): boolean {
}
for (let i = 0, len = searchString.length; i < len; i++) {
const chCode = searchString.charCodeAt(i);
if (chCode === CharCode.LineFeed) {
}
if (chCode === CharCode.Backslash) {
// move to next char
i++;
if (i >= len) {
// string ends with a \
break;
}
const nextChCode = searchString.charCodeAt(i);
if (nextChCode === CharCode.n || nextChCode === CharCode.r || nextChCode === CharCode.W) {
}
return false;
}
export function createFindMatch(range: Range, rawMatches: RegExpExecArray, captureMatches: boolean): FindMatch {
}
for (let i = 0, len = rawMatches.length; i < len; i++) {
matches[i] = rawMatches[i];
}
return new FindMatch(range, matches);
}
class LineFeedCounter {
private readonly _lineFeedsOffsets: number[];
constructor(text: string) {
let lineFeedsOffsetsLen = 0;
for (let i = 0, textLen = text.length; i < textLen; i++) {
if (text.charCodeAt(i) === CharCode.LineFeed) {
lineFeedsOffsets[lineFeedsOffsetsLen++] = i;
}
}
this._lineFeedsOffsets = lineFeedsOffsets;
}
public findLineFeedCountBeforeOffset(offset: number): number {
let min = 0;
let max = lineFeedsOffsets.length - 1;
if (max === -1) {
return 0;
}
if (offset <= lineFeedsOffsets[0]) {
return 0;
}
while (min < max) {
const mid = min + ((max - min) / 2 >> 0);
if (lineFeedsOffsets[mid] >= offset) {
// bingo!
min = mid;
max = mid;
} else {
min = mid + 1;
}
}
return min + 1;
}
export class TextModelSearch {
public static findMatches(model: TextModel, searchParams: SearchParams, searchRange: Range, captureMatches: boolean, limitResultCount: number): FindMatch[] {
if (!searchData) {
return [];
}
if (searchData.regex.multiline) {
return this._doFindMatchesMultiline(model, searchRange, new Searcher(searchData.wordSeparators, searchData.regex), captureMatches, limitResultCount);
textModelSearch.ts ×1
}
return this._doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount);
textModelSearch.ts ×6
/**
* Multiline search always executes on the lines concatenated with \n.
* We must therefore compensate for the count of \n in case the model is CRLF
*/
private static _getMultilineMatchRange(model: TextModel, deltaOffset: number, text: string, lfCounter: LineFeedCounter | null, matchIndex: number, match0: string): Range {
let lineFeedCountBeforeMatch = 0;
if (lfCounter) {
lineFeedCountBeforeMatch = lfCounter.findLineFeedCountBeforeOffset(matchIndex);
textModelSearch.ts ×7
startOffset = deltaOffset + matchIndex + lineFeedCountBeforeMatch /* add as many \r as there were \n */;
}
let endOffset: number;
if (lfCounter) {
const lineFeedCountBeforeEndOfMatch = lfCounter.findLineFeedCountBeforeOffset(matchIndex + match0.length);
textModelSearch.ts ×7
const lineFeedCountInMatch = lineFeedCountBeforeEndOfMatch - lineFeedCountBeforeMatch;
endOffset = startOffset + match0.length + lineFeedCountInMatch /* add as many \r as there were \n */;
}
const startPosition = model.getPositionAt(startOffset);
const endPosition = model.getPositionAt(endOffset);
return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
}
private static _doFindMatchesMultiline(model: TextModel, searchRange: Range, searcher: Searcher, captureMatches: boolean, limitResultCount: number): FindMatch[] {
// We always execute multiline search over the lines joined with \n
// This makes it that \n will match the EOL for both CRLF and LF models
// We compensate for offset errors in `_getMultilineMatchRange`
const text = model.getValueInRange(searchRange, EndOfLinePreference.LF);
const lfCounter = (model.getEOL() === '\r\n' ? new LineFeedCounter(text) : null);
const result: FindMatch[] = [];
let counter = 0;
let m: RegExpExecArray | null;
searcher.reset(0);
while ((m = searcher.next(text))) {
result[counter++] = createFindMatch(this._getMultilineMatchRange(model, deltaOffset, text, lfCounter, m.index, m[0]), m, captureMatches);
textModelSearch.ts ×2
if (counter >= limitResultCount) {
return result;
}
return result;
}
private static _doFindMatchesLineByLine(model: TextModel, searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[] {
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);
textModelSearch.ts ×1
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);
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 && resultLen < limitResultCount; lineNumber++) {
textModelSearch.ts ×6
resultLen = this._findMatchesInLine(searchData, model.getLineContent(lineNumber), lineNumber, 0, resultLen, result, captureMatches, limitResultCount);
textModelSearch.ts ×2
}
// Collect results from last line
if (resultLen < limitResultCount) {
const text = model.getLineContent(searchRange.endLineNumber).substring(0, searchRange.endColumn - 1);
resultLen = this._findMatchesInLine(searchData, text, searchRange.endLineNumber, 0, resultLen, result, captureMatches, limitResultCount);
}
return result;
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 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;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
let m: RegExpExecArray | null;
// 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;
public static findNextMatch(model: TextModel, searchParams: SearchParams, searchStart: Position, captureMatches: boolean): FindMatch | null {
if (!searchData) {
return null;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
if (searchData.regex.multiline) {
return this._doFindNextMatchMultiline(model, searchStart, searcher, captureMatches);
textModelSearch.ts ×4
}
return this._doFindNextMatchLineByLine(model, searchStart, searcher, captureMatches);
textModelSearch.ts ×5
private static _doFindNextMatchMultiline(model: TextModel, searchStart: Position, searcher: Searcher, captureMatches: boolean): FindMatch | null {
const deltaOffset = model.getOffsetAt(searchTextStart);
const lineCount = model.getLineCount();
// We always execute multiline search over the lines joined with \n
// This makes it that \n will match the EOL for both CRLF and LF models
// We compensate for offset errors in `_getMultilineMatchRange`
const text = model.getValueInRange(new Range(searchTextStart.lineNumber, searchTextStart.column, lineCount, model.getLineMaxColumn(lineCount)), EndOfLinePreference.LF);
const lfCounter = (model.getEOL() === '\r\n' ? new LineFeedCounter(text) : null);
searcher.reset(searchStart.column - 1);
const m = searcher.next(text);
if (m) {
this._getMultilineMatchRange(model, deltaOffset, text, lfCounter, m.index, m[0]),
m,
captureMatches
);
}
// Try again from the top
return this._doFindNextMatchMultiline(model, new Position(1, 1), searcher, captureMatches);
}
return null;
private static _doFindNextMatchLineByLine(model: TextModel, searchStart: Position, searcher: Searcher, captureMatches: boolean): FindMatch | null {
const startLineNumber = searchStart.lineNumber;
// Look in first line
const text = model.getLineContent(startLineNumber);
const r = this._findFirstMatchInLine(searcher, text, startLineNumber, searchStart.column, captureMatches);
if (r) {
return r;
}
for (let i = 1; i <= lineCount; i++) {
const lineIndex = (startLineNumber + i - 1) % lineCount;
const text = model.getLineContent(lineIndex + 1);
const r = this._findFirstMatchInLine(searcher, text, lineIndex + 1, 1, captureMatches);
if (r) {
return r;
}
}
return null;
private static _findFirstMatchInLine(searcher: Searcher, text: string, lineNumber: number, fromColumn: number, captureMatches: boolean): FindMatch | null {
searcher.reset(fromColumn - 1);
const m: RegExpExecArray | null = searcher.next(text);
if (m) {
return createFindMatch(
new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length),
m,
captureMatches
);
}
public static findPreviousMatch(model: TextModel, searchParams: SearchParams, searchStart: Position, captureMatches: boolean): FindMatch | null {
if (!searchData) {
return null;
}
const searcher = new Searcher(searchData.wordSeparators, searchData.regex);
if (searchData.regex.multiline) {
return this._doFindPreviousMatchMultiline(model, searchStart, searcher, captureMatches);
textModelSearch.ts ×4
}
return this._doFindPreviousMatchLineByLine(model, searchStart, searcher, captureMatches);
textModelSearch.ts ×4
private static _doFindPreviousMatchMultiline(model: TextModel, searchStart: Position, searcher: Searcher, captureMatches: boolean): FindMatch | null {
const matches = this._doFindMatchesMultiline(model, new Range(1, 1, searchStart.lineNumber, searchStart.column), searcher, captureMatches, 10 * LIMIT_FIND_COUNT);
textModelSearch.ts ×4
if (matches.length > 0) {
return matches[matches.length - 1];
}
const lineCount = model.getLineCount();
if (searchStart.lineNumber !== lineCount || searchStart.column !== model.getLineMaxColumn(lineCount)) {
textModelSearch.ts ×4
return this._doFindPreviousMatchMultiline(model, new Position(lineCount, model.getLineMaxColumn(lineCount)), searcher, captureMatches);
}
return null;
private static _doFindPreviousMatchLineByLine(model: TextModel, searchStart: Position, searcher: Searcher, captureMatches: boolean): FindMatch | null {
const startLineNumber = searchStart.lineNumber;
// Look in first line
const text = model.getLineContent(startLineNumber).substring(0, searchStart.column - 1);
const r = this._findLastMatchInLine(searcher, text, startLineNumber, captureMatches);
if (r) {
}
for (let i = 1; i <= lineCount; i++) {
const lineIndex = (lineCount + startLineNumber - i - 1) % lineCount;
const text = model.getLineContent(lineIndex + 1);
const r = this._findLastMatchInLine(searcher, text, lineIndex + 1, captureMatches);
if (r) {
return r;
}
}
return null;
private static _findLastMatchInLine(searcher: Searcher, text: string, lineNumber: number, captureMatches: boolean): FindMatch | null {
let m: RegExpExecArray | null;
searcher.reset(0);
while ((m = searcher.next(text))) {
bestResult = createFindMatch(new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length), m, captureMatches);
}
return bestResult;
}
function leftIsWordBounday(wordSeparators: WordCharacterClassifier, text: string, textLength: number, matchStartIndex: number, matchLength: number): boolean {
textModelSearch.ts ×7
if (matchStartIndex === 0) {
return true;
}
const charBefore = text.charCodeAt(matchStartIndex - 1);
if (wordSeparators.get(charBefore) !== WordCharacterClass.Regular) {
return true;
}
if (charBefore === CharCode.CarriageReturn || charBefore === CharCode.LineFeed) {
textModelSearch.ts ×7
// 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) {
return true;
}
return false;
}
function rightIsWordBounday(wordSeparators: WordCharacterClassifier, text: string, textLength: number, matchStartIndex: number, matchLength: number): boolean {
textModelSearch.ts ×7
if (matchStartIndex + matchLength === textLength) {
return true;
}
const charAfter = text.charCodeAt(matchStartIndex + matchLength);
if (wordSeparators.get(charAfter) !== WordCharacterClass.Regular) {
return true;
}
if (charAfter === CharCode.CarriageReturn || charAfter === CharCode.LineFeed) {
textModelSearch.ts ×7
// 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) {
return true;
}
return false;
}
export function isValidMatch(wordSeparators: WordCharacterClassifier, text: string, textLength: number, matchStartIndex: number, matchLength: number): boolean {
leftIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength)
&& rightIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength)
);
}
export class Searcher {
public readonly _wordSeparators: WordCharacterClassifier | null;
private readonly _searchRegex: RegExp;
private _prevMatchStartIndex: number;
private _prevMatchLength: number;
constructor(wordSeparators: WordCharacterClassifier | null, searchRegex: RegExp,) {
this._searchRegex = searchRegex;
this._prevMatchStartIndex = -1;
this._prevMatchLength = 0;
}
public reset(lastIndex: number): void {
this._prevMatchStartIndex = -1;
this._prevMatchLength = 0;
}
public next(text: string): RegExpExecArray | null {
let m: RegExpExecArray | null;
do {
if (this._prevMatchStartIndex + this._prevMatchLength === textLength) {
return null;
}
m = this._searchRegex.exec(text);
if (!m) {
}
const matchStartIndex = m.index;
const matchLength = m[0].length;
if (matchStartIndex === this._prevMatchStartIndex && matchLength === this._prevMatchLength) {
textModelSearch.ts ×7
// the search result is an empty string and won't advance `regex.lastIndex`, so `regex.exec` will stuck here
// we attempt to recover from that by advancing by two if surrogate pair found and by one otherwise
if (strings.getNextCodePoint(text, textLength, this._searchRegex.lastIndex) > 0xFFFF) {
this._searchRegex.lastIndex += 1;
}
continue;
}
// Exit early if the regex matches the same range twice
return null;
}
this._prevMatchLength = matchLength;
if (!this._wordSeparators || isValidMatch(this._wordSeparators, text, textLength, matchStartIndex, matchLength)) {
textModelSearch.ts ×7
}
} while (m);
return null;