perf(addon-search): optimize regex search hot path

Unify regex and whole-word matching into a single scan pass and avoid
per-line substring allocations by searching the full logical line with
lastIndex. Forward non-whole-word regex keeps a single-exec fast path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daniel Imms
2026-05-25 19:04:22 -07:00
co-authored by Cursor
parent 510a1b0d75
commit e5ffbe315a
+82 -69
View File
@@ -250,6 +250,78 @@ export class SearchEngine {
(((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
}
/**
* Finds a regex match in a line in a single pass, optionally enforcing whole-word boundaries.
*/
private _findRegexMatch(
searchStringLine: string,
searchRegex: RegExp,
offset: number,
isReverseSearch: boolean,
wholeWord: boolean
): { resultIndex: number; term: string } | undefined {
let resultIndex = -1;
let matchedTerm = '';
if (!isReverseSearch && !wholeWord) {
searchRegex.lastIndex = offset;
const foundTerm = searchRegex.exec(searchStringLine);
if (foundTerm && foundTerm[0].length > 0) {
return {
resultIndex: searchRegex.lastIndex - foundTerm[0].length,
term: foundTerm[0]
};
}
return undefined;
}
searchRegex.lastIndex = 0;
if (isReverseSearch) {
let foundTerm: RegExpExecArray | null;
while (foundTerm = searchRegex.exec(searchStringLine)) {
if (foundTerm[0].length === 0) {
if (searchRegex.lastIndex >= searchStringLine.length) {
break;
}
searchRegex.lastIndex++;
continue;
}
const candidateIndex = searchRegex.lastIndex - foundTerm[0].length;
if (candidateIndex + foundTerm[0].length > offset) {
break;
}
if (!wholeWord || this._isWholeWord(candidateIndex, searchStringLine, foundTerm[0])) {
resultIndex = candidateIndex;
matchedTerm = foundTerm[0];
}
searchRegex.lastIndex -= (foundTerm[0].length - 1);
}
} else {
searchRegex.lastIndex = offset;
let foundTerm: RegExpExecArray | null;
while (foundTerm = searchRegex.exec(searchStringLine)) {
if (foundTerm[0].length === 0) {
if (searchRegex.lastIndex >= searchStringLine.length) {
break;
}
searchRegex.lastIndex++;
continue;
}
const candidateIndex = searchRegex.lastIndex - foundTerm[0].length;
if (this._isWholeWord(candidateIndex, searchStringLine, foundTerm[0])) {
resultIndex = candidateIndex;
matchedTerm = foundTerm[0];
break;
}
}
}
if (resultIndex < 0) {
return undefined;
}
return { resultIndex, term: matchedTerm };
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line,
* which may contain subsequent terminal lines if the text is wrapped. If the provided line number
@@ -308,29 +380,16 @@ export class SearchEngine {
let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = lineSearchRegex ?? RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
searchRegex.lastIndex = 0;
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
const searchSubstring = searchStringLine.slice(0, offset);
while (foundTerm = searchRegex.exec(searchSubstring)) {
if (foundTerm[0].length === 0) {
if (searchRegex.lastIndex >= searchSubstring.length) {
break;
}
searchRegex.lastIndex++;
continue;
}
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(offset));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
const regexMatch = this._findRegexMatch(
searchStringLine,
searchRegex,
offset,
isReverseSearch,
!!searchOptions.wholeWord
);
if (regexMatch) {
resultIndex = regexMatch.resultIndex;
term = regexMatch.term;
}
} else if (isReverseSearch) {
let searchEnd = offset - searchTerm.length;
@@ -361,52 +420,6 @@ export class SearchEngine {
}
if (resultIndex >= 0) {
if (searchOptions.regex) {
if (searchOptions.wholeWord) {
const searchRegex = lineSearchRegex ?? RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');
searchRegex.lastIndex = 0;
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
resultIndex = -1;
const wholeWordSubstring = searchStringLine.slice(0, offset);
while (foundTerm = searchRegex.exec(wholeWordSubstring)) {
if (foundTerm[0].length === 0) {
if (searchRegex.lastIndex >= wholeWordSubstring.length) {
break;
}
searchRegex.lastIndex++;
continue;
}
const candidateIndex = searchRegex.lastIndex - foundTerm[0].length;
const candidateTerm = foundTerm[0];
if (this._isWholeWord(candidateIndex, searchStringLine, candidateTerm)) {
resultIndex = candidateIndex;
term = candidateTerm;
}
searchRegex.lastIndex -= (candidateTerm.length - 1);
}
} else {
resultIndex = -1;
searchRegex.lastIndex = offset;
while (foundTerm = searchRegex.exec(searchStringLine)) {
if (foundTerm[0].length === 0) {
break;
}
const candidateIndex = searchRegex.lastIndex - foundTerm[0].length;
const candidateTerm = foundTerm[0];
if (this._isWholeWord(candidateIndex, searchStringLine, candidateTerm)) {
resultIndex = candidateIndex;
term = candidateTerm;
break;
}
}
}
if (resultIndex < 0) {
return;
}
}
}
// Adjust the row number and search index if needed since a "line" of text can span multiple
// rows
let startRowOffset = 0;