Refactor SearchAddon state/navigation helpers for readability.

Consolidate duplicated search-state and index-stepping logic into small private helpers, and centralize decoration width/render setup to reduce drift between active/inactive paths. Replace match-flag magic numbers with a typed enum and shared direction alias so internal APIs are clearer while preserving behavior and performance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daniel Imms
2026-05-25 22:11:00 -07:00
co-authored by Cursor
parent ef84237ef3
commit 5ff85db4f3
+83 -65
View File
@@ -36,12 +36,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
private _highlightLimit = SearchAddon._defaultHighlightLimit; private _highlightLimit = SearchAddon._defaultHighlightLimit;
private _lastSearchTerm: string | undefined; private _lastSearchTerm: string | undefined;
private _lastSearchOptions: ISearchOptions | undefined; private _lastSearchOptions: ISearchOptions | undefined;
private _lastDirection: 'next' | 'previous' = 'next'; private _lastDirection: SearchDirection = 'next';
private _lastSearchKey: string | undefined; private _lastSearchKey: string | undefined;
private _resultCount = 0; private _resultCount = 0;
private _resultIndex = -1; private _resultIndex = -1;
private _pendingResultRefresh = false; private _pendingResultRefresh = false;
private readonly _matchCache = new Map<number, Map<number, Map<string, IMatch[] | undefined>>>(); private readonly _matchCache = new Map<number, Map<MatchFlags, Map<string, IMatch[] | undefined>>>();
private readonly _matchCacheOrder: IMatchCacheKey[] = []; private readonly _matchCacheOrder: IMatchCacheKey[] = [];
private _logicalLineCache: ILogicalLineCache | undefined; private _logicalLineCache: ILogicalLineCache | undefined;
private readonly _selectionIndexCache = new WeakMap<IMatch[], Map<number, Map<number, number>>>(); private readonly _selectionIndexCache = new WeakMap<IMatch[], Map<number, Map<number, number>>>();
@@ -84,7 +84,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
this._activeDecoration.clear(); this._activeDecoration.clear();
} }
private _find(term: string, searchOptions: ISearchOptions | undefined, direction: 'next' | 'previous'): boolean { private _find(term: string, searchOptions: ISearchOptions | undefined, direction: SearchDirection): boolean {
this._onBeforeSearch.fire(); this._onBeforeSearch.fire();
try { try {
const terminal = this._terminal; const terminal = this._terminal;
@@ -113,23 +113,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
}; };
if (searchOptions?.decorations) { if (searchOptions?.decorations) {
this._resultCount = matches.length; this._setDecoratedSearchState(term, searchOptions, direction, searchKey, matches.length, nextIndex);
this._resultIndex = nextIndex < this._resultCount ? nextIndex : -1;
this._lastSearchTerm = term;
this._lastSearchOptions = searchOptions;
this._lastDirection = direction;
this._lastSearchKey = searchKey;
this._registerResultRefreshListener(); this._registerResultRefreshListener();
this._refreshDecorations(matches, searchOptions.decorations, activeMatch); this._refreshDecorations(matches, searchOptions.decorations, activeMatch);
this._onDidChangeResults.fire({ resultCount: this._resultCount, resultIndex: this._resultIndex }); this._onDidChangeResults.fire({ resultCount: this._resultCount, resultIndex: this._resultIndex });
} else { } else {
this._disposeDecorations(); this._clearSearchState();
this._resultsUpdateListener.clear();
this._resultCount = 0;
this._resultIndex = -1;
this._lastSearchTerm = undefined;
this._lastSearchOptions = undefined;
this._lastSearchKey = undefined;
} }
return true; return true;
@@ -138,17 +127,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
} }
private _clearStateOnFailedSearch(term: string, searchOptions: ISearchOptions | undefined, direction: 'next' | 'previous'): void { private _clearStateOnFailedSearch(term: string, searchOptions: ISearchOptions | undefined, direction: SearchDirection): void {
this._lastResolvedNavigation = undefined; this._lastResolvedNavigation = undefined;
if (searchOptions?.decorations) { if (searchOptions?.decorations) {
this._lastSearchTerm = term; this._setDecoratedSearchState(term, searchOptions, direction, this._createSearchKey(term, searchOptions, direction), 0, -1);
this._lastSearchOptions = searchOptions;
this._lastDirection = direction;
this._lastSearchKey = this._createSearchKey(term, searchOptions, direction);
this._registerResultRefreshListener(); this._registerResultRefreshListener();
this._disposeDecorations(); this._disposeDecorations();
this._resultCount = 0;
this._resultIndex = -1;
this._onDidChangeResults.fire({ resultCount: 0, resultIndex: -1 }); this._onDidChangeResults.fire({ resultCount: 0, resultIndex: -1 });
return; return;
} }
@@ -164,13 +148,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
) { ) {
return; return;
} }
this._disposeDecorations(); this._clearSearchState();
this._resultsUpdateListener.clear();
this._resultCount = 0;
this._resultIndex = -1;
this._lastSearchTerm = undefined;
this._lastSearchOptions = undefined;
this._lastSearchKey = undefined;
} }
private _registerResultRefreshListener(): void { private _registerResultRefreshListener(): void {
@@ -484,7 +462,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
return !isWordCharAt(text, startOffset - 1) && !isWordCharAt(text, endOffset); return !isWordCharAt(text, startOffset - 1) && !isWordCharAt(text, endOffset);
} }
private _resolveResultIndex(matches: IMatch[], term: string, searchOptions: ISearchOptions | undefined, direction: 'next' | 'previous', currentSearchKey: string): number { private _resolveResultIndex(matches: IMatch[], term: string, searchOptions: ISearchOptions | undefined, direction: SearchDirection, currentSearchKey: string): number {
const previousNavigation = this._lastResolvedNavigation; const previousNavigation = this._lastResolvedNavigation;
const selection = this._terminal?.getSelectionPosition(); const selection = this._terminal?.getSelectionPosition();
if ( if (
@@ -493,10 +471,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
previousNavigation.matches === matches previousNavigation.matches === matches
) { ) {
if (!selection) { if (!selection) {
if (direction === 'next') { return this._stepResultIndex(previousNavigation.index, matches.length, direction);
return (previousNavigation.index + 1) % matches.length;
}
return (previousNavigation.index + matches.length - 1) % matches.length;
} }
if ( if (
selection.start.x === previousNavigation.selectionStartX && selection.start.x === previousNavigation.selectionStartX &&
@@ -504,10 +479,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
previousNavigation.index >= 0 && previousNavigation.index >= 0 &&
previousNavigation.index < matches.length previousNavigation.index < matches.length
) { ) {
if (direction === 'next') { return this._stepResultIndex(previousNavigation.index, matches.length, direction);
return (previousNavigation.index + 1) % matches.length;
}
return (previousNavigation.index + matches.length - 1) % matches.length;
} }
} }
const currentSelectionIndex = this._findIndexFromSelection(matches, selection); const currentSelectionIndex = this._findIndexFromSelection(matches, selection);
@@ -521,16 +493,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
if (currentSelectionIndex !== -1) { if (currentSelectionIndex !== -1) {
if (direction === 'next') { return this._stepResultIndex(currentSelectionIndex, matches.length, direction);
return (currentSelectionIndex + 1) % matches.length;
}
return (currentSelectionIndex + matches.length - 1) % matches.length;
} }
if (direction === 'next') { return this._getDirectionalDefaultIndex(matches.length, direction);
return 0;
}
return matches.length - 1;
} }
private _findIndexFromSelection(matches: IMatch[], selection: ReturnType<Terminal['getSelectionPosition']> | undefined = this._terminal?.getSelectionPosition()): number { private _findIndexFromSelection(matches: IMatch[], selection: ReturnType<Terminal['getSelectionPosition']> | undefined = this._terminal?.getSelectionPosition()): number {
@@ -586,7 +552,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
const cursorLine = terminal.buffer.active.baseY + terminal.buffer.active.cursorY; const cursorLine = terminal.buffer.active.baseY + terminal.buffer.active.cursorY;
const marker = terminal.registerMarker(activeMatch.startY - cursorLine); const marker = terminal.registerMarker(activeMatch.startY - cursorLine);
const width = Math.max(1, Math.min(activeMatch.cellLength, terminal.cols - activeMatch.startX)); const width = this._getDecorationWidth(activeMatch, terminal.cols);
const activeDecoration = terminal.registerDecoration({ const activeDecoration = terminal.registerDecoration({
marker, marker,
x: activeMatch.startX, x: activeMatch.startX,
@@ -643,7 +609,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
const inactiveOverviewRulerOptions = undefined; const inactiveOverviewRulerOptions = undefined;
for (const match of matches) { for (const match of matches) {
const marker = terminal.registerMarker(match.startY - cursorLine); const marker = terminal.registerMarker(match.startY - cursorLine);
const width = Math.max(1, Math.min(match.cellLength, terminal.cols - match.startX)); const width = this._getDecorationWidth(match, terminal.cols);
const decoration = terminal.registerDecoration({ const decoration = terminal.registerDecoration({
marker, marker,
x: match.startX, x: match.startX,
@@ -657,15 +623,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
set.decorations.push(decoration); set.decorations.push(decoration);
set.disposables.push(decoration); set.disposables.push(decoration);
if (matchBorder) { set.disposables.push(this._registerMatchDecorationRender(decoration, set, decorationOptions.matchBackground, matchBorder));
set.disposables.push(decoration.onRender(element => {
this._applyMatchDecorationStyle(element, set.isActive, decorationOptions.matchBackground, matchBorder);
}));
} else {
set.disposables.push(decoration.onRender(element => {
this._applyMatchDecorationStyle(element, set.isActive, decorationOptions.matchBackground, undefined);
}));
}
} }
return set; return set;
} }
@@ -694,6 +652,21 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
} }
private _getDecorationWidth(match: IMatch, cols: number): number {
return Math.max(1, Math.min(match.cellLength, cols - match.startX));
}
private _registerMatchDecorationRender(
decoration: IDecoration,
set: IMatchDecorationSet,
background: string | undefined,
border: string | undefined
): IDisposable {
return decoration.onRender(element => {
this._applyMatchDecorationStyle(element, set.isActive, background, border);
});
}
private _createDecorationOptionsKey(decorationOptions: ISearchDecorationOptions): string { private _createDecorationOptionsKey(decorationOptions: ISearchDecorationOptions): string {
return [ return [
decorationOptions.matchBackground, decorationOptions.matchBackground,
@@ -752,7 +725,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
this._disposeMatchDecorationSets(); this._disposeMatchDecorationSets();
} }
private _createSearchKey(term: string, searchOptions: ISearchOptions | undefined, direction: 'next' | 'previous'): string { private _createSearchKey(term: string, searchOptions: ISearchOptions | undefined, direction: SearchDirection): string {
return `${this._createMatchKey(term, searchOptions)}|${direction}`; return `${this._createMatchKey(term, searchOptions)}|${direction}`;
} }
@@ -761,12 +734,49 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
return `${term}|${flags}`; return `${term}|${flags}`;
} }
private _getMatchFlags(searchOptions: ISearchOptions | undefined): number { private _getMatchFlags(searchOptions: ISearchOptions | undefined): MatchFlags {
return ( return (
(searchOptions?.caseSensitive ? 1 : 0) | (searchOptions?.caseSensitive ? MatchFlags.CASE_SENSITIVE : 0) |
(searchOptions?.regex ? 2 : 0) | (searchOptions?.regex ? MatchFlags.REGEX : 0) |
(searchOptions?.wholeWord ? 4 : 0) (searchOptions?.wholeWord ? MatchFlags.WHOLE_WORD : 0)
); ) as MatchFlags;
}
private _setDecoratedSearchState(
term: string,
searchOptions: ISearchOptions,
direction: SearchDirection,
searchKey: string,
resultCount: number,
resultIndex: number
): void {
this._resultCount = resultCount;
this._resultIndex = resultIndex >= 0 && resultIndex < resultCount ? resultIndex : -1;
this._lastSearchTerm = term;
this._lastSearchOptions = searchOptions;
this._lastDirection = direction;
this._lastSearchKey = searchKey;
}
private _clearSearchState(): void {
this._disposeDecorations();
this._resultsUpdateListener.clear();
this._resultCount = 0;
this._resultIndex = -1;
this._lastSearchTerm = undefined;
this._lastSearchOptions = undefined;
this._lastSearchKey = undefined;
}
private _stepResultIndex(index: number, length: number, direction: SearchDirection): number {
if (direction === 'next') {
return (index + 1) % length;
}
return (index + length - 1) % length;
}
private _getDirectionalDefaultIndex(length: number, direction: SearchDirection): number {
return direction === 'next' ? 0 : length - 1;
} }
} }
@@ -809,6 +819,8 @@ interface IResolvedNavigation {
selectionStartY: number; selectionStartY: number;
} }
type SearchDirection = 'next' | 'previous';
interface IMatchDecorationSet { interface IMatchDecorationSet {
matches: IMatch[]; matches: IMatch[];
options: ISearchDecorationOptions; options: ISearchDecorationOptions;
@@ -820,10 +832,16 @@ interface IMatchDecorationSet {
interface IMatchCacheKey { interface IMatchCacheKey {
cols: number; cols: number;
flags: number; flags: MatchFlags;
term: string; term: string;
} }
const enum MatchFlags {
CASE_SENSITIVE = 1,
REGEX = 2,
WHOLE_WORD = 4
}
function escapeRegExp(value: string): string { function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
} }