Improve search addon behavior when there are > 1000 results

This commit is contained in:
Jean Pierre
2023-05-07 15:19:23 -05:00
parent 913cb25863
commit 508589aa80
2 changed files with 79 additions and 88 deletions
+72 -84
View File
@@ -5,7 +5,7 @@
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -54,9 +54,9 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
export class SearchAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
private _resultDecorations: Map<number, IDecoration[]> | undefined;
private _searchResults: Map<string, ISearchResult> | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: (IDecoration & { match: ISearchResult })[] = [];
private _selectedDecoration: IDecoration & { match: ISearchResult } | undefined;
private _onDataDisposable: IDisposable | undefined;
private _onResizeDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
@@ -71,11 +71,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
private _resultIndex: number | undefined;
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>());
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
public readonly MATCHES_LIMIT = 1000;
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
@@ -94,23 +94,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => {
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
this._fireResults(this._lastSearchOptions);
}, 200);
}
}
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration?.dispose();
this._searchResults?.clear();
this._resultDecorations?.forEach(decorations => {
for (const d of decorations) {
d.dispose();
}
});
this._resultDecorations?.clear();
this._searchResults = undefined;
this._resultDecorations = undefined;
disposeArray(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
}
@@ -134,11 +127,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
const found = this._findNextAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
@@ -153,32 +151,31 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
// new search, clear out the old decorations
this.clearDecorations(true);
this._searchResults = new Map<string, ISearchResult>();
this._resultDecorations = new Map<number, IDecoration[]>();
const resultDecorations = this._resultDecorations;
const searchResultsWithHighlight: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
this._searchResults.set(`${result.row}-${result.col}`, result);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this.MATCHES_LIMIT) {
break;
}
prevResult = result;
searchResultsWithHighlight.push(prevResult);
result = this._find(
term,
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
if (this._searchResults.size > 1000) {
this.clearDecorations();
this._resultIndex = undefined;
return;
}
}
this._searchResults.forEach(result => {
for (const result of searchResultsWithHighlight) {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
if (resultDecoration) {
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
decorationsForLine.push(resultDecoration);
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
this._highlightedLines.add(resultDecoration.marker.line);
(resultDecoration as unknown as { match: ISearchResult }).match = result;
this._highlightDecorations.push(resultDecoration as (IDecoration & { match: ISearchResult }));
}
});
}
}
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
@@ -224,12 +221,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
this._terminal?.clearSelection();
this.clearDecorations();
this._cachedSearchTerm = undefined;
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
@@ -287,18 +282,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
result = this._findInLine(term, searchPosition, searchOptions);
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined) {
this._resultIndex = 0;
} else {
this._resultIndex++;
if (this._resultIndex >= this._searchResults.size) {
this._resultIndex = 0;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -315,23 +298,33 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
const found = this._findPreviousAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
private _fireResults(searchOptions?: ISearchOptions): void {
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
} else {
this._onDidChangeResults.fire(undefined);
let resultIndex = -1;
if (this._selectedDecoration) {
const selectedMatch = this._selectedDecoration.match;
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
}
this._cachedSearchTerm = term;
return found;
}
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
@@ -343,16 +336,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
result = undefined;
this._terminal?.clearSelection();
this.clearDecorations();
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const isReverseSearch = true;
@@ -399,8 +390,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
}
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
@@ -409,19 +400,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
this._resultIndex = this._searchResults.size - 1;
} else {
this._resultIndex--;
if (this._resultIndex === -1) {
this._resultIndex = this._searchResults.size - 1;
}
}
}
// If there is only one result, return true.
if (!result && currentSelection) return true;
@@ -675,7 +653,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
if (options) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
if (marker) {
this._selectedDecoration = terminal.registerDecoration({
const decoration = terminal.registerDecoration({
marker,
x: result.col,
width: result.size,
@@ -685,8 +663,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
color: options.activeMatchColorOverviewRuler
}
});
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
this._selectedDecoration?.onDispose(() => marker.dispose());
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
(decoration as unknown as { match: ISearchResult }).match = result;
this._selectedDecoration = decoration as (IDecoration & { match: ISearchResult });
}
}
}
@@ -740,13 +724,17 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
x: result.col,
width: result.size,
backgroundColor: options.matchBackground,
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: options.matchOverviewRuler,
position: 'center'
}
});
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
findResultDecoration?.onDispose(() => marker.dispose());
if (findResultDecoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
}
return findResultDecoration;
}
}
+7 -4
View File
@@ -79,6 +79,11 @@ declare module 'xterm-addon-search' {
* An xterm.js addon that provides search functionality.
*/
export class SearchAddon implements ITerminalAddon {
/**
* Max number of matches when decorations are enabled
*/
public readonly MATCHES_LIMIT: number;
/**
* Activates the addon
* @param terminal The terminal the addon is being loaded in.
@@ -121,10 +126,8 @@ declare module 'xterm-addon-search' {
/**
* When decorations are enabled, fires when
* the search results change.
* @returns -1 for resultIndex for a resultCount of 0
* and @returns undefined when the threshold of 1k results
* is exceeded and decorations are disposed of.
* @returns -1 for resultIndex when the threshold of matches is exceeded.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
}
}