perf(addon-search): memoize repeated no-match searches

Track buffer cache generation and skip full-buffer scans when the same
term, options, and start position previously produced no match until the
line cache is invalidated.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daniel Imms
2026-05-25 19:12:47 -07:00
co-authored by Cursor
parent 73c99a8412
commit 0440861b84
2 changed files with 78 additions and 1 deletions
+72 -1
View File
@@ -15,6 +15,15 @@ interface ISearchPosition {
startRow: number;
}
interface INoMatchMemo {
term: string;
optionsKey: string;
bufferGeneration: number;
startRow: number;
startCol: number;
isReverse: boolean;
}
/**
* Represents a search result with its position and content.
*/
@@ -42,11 +51,45 @@ const enum Constants {
* This class is responsible for the actual search algorithms and position calculations.
*/
export class SearchEngine {
private _noMatchMemo: INoMatchMemo | undefined;
constructor(
private readonly _terminal: Terminal,
private readonly _lineCache: SearchLineCache
) {}
private _searchOptionsKey(searchOptions?: ISearchOptions): string {
return `${searchOptions?.caseSensitive ? 1 : 0}:${searchOptions?.regex ? 1 : 0}:${searchOptions?.wholeWord ? 1 : 0}`;
}
private _matchesNoMatchMemo(term: string, startRow: number, startCol: number, isReverse: boolean, searchOptions?: ISearchOptions): boolean {
const memo = this._noMatchMemo;
if (!memo) {
return false;
}
return memo.term === term &&
memo.optionsKey === this._searchOptionsKey(searchOptions) &&
memo.bufferGeneration === this._lineCache.cacheGeneration &&
memo.startRow === startRow &&
memo.startCol === startCol &&
memo.isReverse === isReverse;
}
private _recordNoMatchMemo(term: string, startRow: number, startCol: number, isReverse: boolean, searchOptions?: ISearchOptions): void {
this._noMatchMemo = {
term,
optionsKey: this._searchOptionsKey(searchOptions),
bufferGeneration: this._lineCache.cacheGeneration,
startRow,
startCol,
isReverse
};
}
private _clearNoMatchMemo(): void {
this._noMatchMemo = undefined;
}
private _createLineSearchRegex(term: string, searchOptions?: ISearchOptions): RegExp | undefined {
if (!searchOptions?.regex) {
return undefined;
@@ -72,8 +115,17 @@ export class SearchEngine {
}
this._lineCache.initLinesCache();
if (this._matchesNoMatchMemo(term, startRow, startCol, false, searchOptions)) {
return undefined;
}
const lineSearchRegex = this._createLineSearchRegex(term, searchOptions);
return this._findFromPosition(term, startRow, startCol, searchOptions, lineSearchRegex);
const result = this._findFromPosition(term, startRow, startCol, searchOptions, lineSearchRegex);
if (result) {
this._clearNoMatchMemo();
} else {
this._recordNoMatchMemo(term, startRow, startCol, false, searchOptions);
}
return result;
}
/**
@@ -169,6 +221,9 @@ export class SearchEngine {
}
this._lineCache.initLinesCache();
if (this._matchesNoMatchMemo(term, startRow, startCol, false, searchOptions)) {
return undefined;
}
const searchPosition: ISearchPosition = {
startRow,
@@ -209,6 +264,12 @@ export class SearchEngine {
result = this._findInLine(term, searchPosition, searchOptions, false, lineSearchRegex);
}
if (result) {
this._clearNoMatchMemo();
} else {
this._recordNoMatchMemo(term, startRow, startCol, false, searchOptions);
}
return result;
}
@@ -233,6 +294,10 @@ export class SearchEngine {
const isReverseSearch = true;
this._lineCache.initLinesCache();
if (this._matchesNoMatchMemo(term, startRow, startCol, true, searchOptions)) {
return undefined;
}
const searchPosition: ISearchPosition = {
startRow,
startCol
@@ -279,6 +344,12 @@ export class SearchEngine {
}
}
if (result) {
this._clearNoMatchMemo();
} else {
this._recordNoMatchMemo(term, startRow, startCol, true, searchOptions);
}
return result;
}
@@ -40,6 +40,7 @@ export class SearchLineCache extends Disposable {
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: LineCacheEntry[] | undefined;
private _cacheGeneration = 0;
private _linesCacheTimeout = this._register(new MutableDisposable());
private _linesCacheDisposables = this._register(new MutableDisposable());
// Track access to avoid recreating a timeout on every init call which occurs once per search
@@ -70,7 +71,12 @@ export class SearchLineCache extends Disposable {
}
}
public get cacheGeneration(): number {
return this._cacheGeneration;
}
private _destroyLinesCache(): void {
this._cacheGeneration++;
this._linesCache = undefined;
this._lastAccessTimestamp = 0;
this._linesCacheDisposables.clear();