Address feedback
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user