Address feedback

This commit is contained in:
Jean Pierre
2023-05-12 16:08:11 -05:00
parent a1e9105ce7
commit cc5136aeb3
3 changed files with 79 additions and 18 deletions
+30 -16
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm'; import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter'; import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle'; import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
@@ -30,6 +30,10 @@ export interface ISearchPosition {
startRow: number; startRow: number;
} }
export interface ISearchAddonOptions {
highlightLimit: number;
}
export interface ISearchResult { export interface ISearchResult {
term: string; term: string;
col: number; col: number;
@@ -48,15 +52,22 @@ type LineCacheEntry = [
lineOffsets: number[] lineOffsets: number[]
]; ];
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
}
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'; const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
export class SearchAddon extends Disposable implements ITerminalAddon { export class SearchAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined; private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined; private _cachedSearchTerm: string | undefined;
private _highlightedLines: Set<number> = new Set(); private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: (IDecoration & { match: ISearchResult })[] = []; private _highlightDecorations: IHighlight[] = [];
private _selectedDecoration: IDecoration & { match: ISearchResult } | undefined; private _selectedDecoration: IHighlight | undefined;
private _highlightLimit: number;
private _onDataDisposable: IDisposable | undefined; private _onDataDisposable: IDisposable | undefined;
private _onResizeDisposable: IDisposable | undefined; private _onResizeDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined; private _lastSearchOptions: ISearchOptions | undefined;
@@ -74,7 +85,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>()); private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event; public readonly onDidChangeResults = this._onDidChangeResults.event;
public readonly MATCHES_LIMIT = 1000; constructor(options?: Partial<ISearchAddonOptions>) {
super();
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
}
public activate(terminal: Terminal): void { public activate(terminal: Terminal): void {
this._terminal = terminal; this._terminal = terminal;
@@ -93,14 +108,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
} }
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) { if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => { this._highlightTimeout = setTimeout(() => {
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true }); const term = this._cachedSearchTerm;
this._fireResults(this._lastSearchOptions); this._cachedSearchTerm = undefined;
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
}, 200); }, 200);
} }
} }
public clearDecorations(retainCachedSearchTerm?: boolean): void { public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration?.dispose(); this.clearActiveDecoration();
disposeArray(this._highlightDecorations); disposeArray(this._highlightDecorations);
this._highlightDecorations = []; this._highlightDecorations = [];
this._highlightedLines.clear(); this._highlightedLines.clear();
@@ -156,7 +172,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
let prevResult: ISearchResult | undefined = undefined; let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions); let result = this._find(term, 0, 0, searchOptions);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) { while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this.MATCHES_LIMIT) { if (searchResultsWithHighlight.length >= this._highlightLimit) {
break; break;
} }
prevResult = result; prevResult = result;
@@ -168,12 +184,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
searchOptions searchOptions
); );
} }
for (const result of searchResultsWithHighlight) { for (const match of searchResultsWithHighlight) {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!); const decoration = this._createResultDecoration(match, searchOptions.decorations!);
if (resultDecoration) { if (decoration) {
this._highlightedLines.add(resultDecoration.marker.line); this._highlightedLines.add(decoration.marker.line);
(resultDecoration as unknown as { match: ISearchResult }).match = result; this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
this._highlightDecorations.push(resultDecoration as (IDecoration & { match: ISearchResult }));
} }
} }
} }
@@ -654,8 +669,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
disposables.push(marker); disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true))); disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
disposables.push(decoration.onDispose(() => disposeArray(disposables))); disposables.push(decoration.onDispose(() => disposeArray(disposables)));
(decoration as unknown as { match: ISearchResult }).match = result; this._selectedDecoration = { decoration, match: result, dispose() { decoration.dispose(); } };
this._selectedDecoration = decoration as (IDecoration & { match: ISearchResult });
} }
} }
} }
@@ -242,6 +242,23 @@ describe('Search Tests', function (): void {
{ resultCount: 1000, resultIndex: 1 } { resultCount: 1000, resultIndex: 1 }
]); ]);
}); });
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 3, resultIndex: 0 }
]);
});
}); });
describe('findPrevious', () => { describe('findPrevious', () => {
it('should not fire unless the decorations option is set', async () => { it('should not fire unless the decorations option is set', async () => {
@@ -359,6 +376,23 @@ describe('Search Tests', function (): void {
{ resultCount: 1000, resultIndex: -1 } { resultCount: 1000, resultIndex: -1 }
]); ]);
}); });
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 3, resultIndex: 1 }
]);
});
}); });
}); });
+15 -2
View File
@@ -75,14 +75,27 @@ declare module 'xterm-addon-search' {
activeMatchColorOverviewRuler: string; activeMatchColorOverviewRuler: string;
} }
/**
* Options for the search addon.
*/
export interface ISearchAddonOptions {
/**
* Max number of matches highlighted when decorations are enabled.
* Defaults to 1000 highlighted matches
*/
highlightLimit: number
}
/** /**
* An xterm.js addon that provides search functionality. * An xterm.js addon that provides search functionality.
*/ */
export class SearchAddon implements ITerminalAddon { export class SearchAddon implements ITerminalAddon {
/** /**
* Max number of matches when decorations are enabled * Creates a new search addon.
* @param options Options for the search addon.
*/ */
public readonly MATCHES_LIMIT: number; constructor(options?: Partial<ISearchAddonOptions>);
/** /**
* Activates the addon * Activates the addon