Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show all matches when searching #3698

Merged
merged 63 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
60c11e8
start work
meganrogge Mar 11, 2022
9657442
get it to sort of work
meganrogge Mar 11, 2022
fa4479c
use decoration width, find all and select next
meganrogge Mar 11, 2022
372b6b6
set incremental search to false
meganrogge Mar 11, 2022
da2c59d
remove unused variable
meganrogge Mar 11, 2022
453cb00
use a class
meganrogge Mar 11, 2022
e7bbc41
start at cursor position and mandate a width gt 0
meganrogge Mar 11, 2022
b4de278
hide decorations >= bufferService rows
meganrogge Mar 11, 2022
f678f08
merge
meganrogge Mar 16, 2022
1bab173
fix problems
meganrogge Mar 16, 2022
40b8854
rm bad decoration service
meganrogge Mar 16, 2022
05eb12e
start work on selection color
meganrogge Mar 16, 2022
1939906
Merge branch 'xtermjs:master' into findAll
meganrogge Mar 16, 2022
26f8fce
update color when select next happens
meganrogge Mar 16, 2022
90f64ba
get it to work in the demo
meganrogge Mar 17, 2022
c2145d5
on dispose, clear the canvas at that position
meganrogge Mar 17, 2022
de56d04
return decoration from show result
meganrogge Mar 17, 2022
9bc5090
use a map
meganrogge Mar 18, 2022
c8dc679
Merge branch 'master' into findAll
meganrogge Mar 18, 2022
a1468d0
use start row and col
meganrogge Mar 18, 2022
6c4f8b4
append to results
meganrogge Mar 18, 2022
47ec313
support colors
meganrogge Mar 18, 2022
01fd148
clean up
meganrogge Mar 18, 2022
d2913d6
Merge branch 'master' into findAll
meganrogge Mar 18, 2022
8e68c67
applyStyles
meganrogge Mar 18, 2022
66dcd16
clean up jsdoc
meganrogge Mar 18, 2022
15c2432
jsdoc
meganrogge Mar 18, 2022
85683a4
add to col, not row
meganrogge Mar 18, 2022
dd50378
Update css/xterm.css
meganrogge Mar 18, 2022
8328fca
track disposable
meganrogge Mar 18, 2022
61c2ca7
Metrge branch 'findAll' of https://github.com/meganrogge/xterm.js int…
meganrogge Mar 18, 2022
fef30bd
revert row register marker changes
meganrogge Mar 18, 2022
61e5c10
pull apart and refactor findNext
meganrogge Mar 18, 2022
e79c21d
get it to work for previous too
meganrogge Mar 18, 2022
a0aae62
get rid of start col end col
meganrogge Mar 18, 2022
8225ecd
refactor from highlightAllMatches -> decorations
meganrogge Mar 21, 2022
12a999d
get rid of start row/col code
meganrogge Mar 21, 2022
bec4cb3
set data changed to false if true
meganrogge Mar 21, 2022
8ff34a6
update decorations dynamically when the buffer changes
meganrogge Mar 21, 2022
1098173
use set timeout
meganrogge Mar 21, 2022
faa3769
Merge branch 'master' into findAll
meganrogge Mar 21, 2022
97e4af3
only highlight all matches when decorations are requested
meganrogge Mar 21, 2022
b9331ec
Merge branch 'findAll' of https://github.com/meganrogge/xterm.js into…
meganrogge Mar 21, 2022
4cea6b3
revert a breaking change
meganrogge Mar 21, 2022
64d5af3
Update addons/xterm-addon-search/src/SearchAddon.ts
meganrogge Mar 21, 2022
e8aa4eb
IDecorationColor -> ISearchDecorationOptions
meganrogge Mar 21, 2022
90dca7d
Merge branch 'findAll' of https://github.com/meganrogge/xterm.js into…
meganrogge Mar 21, 2022
7a502c6
use const enum
meganrogge Mar 21, 2022
fefdd8a
clearDecorations
meganrogge Mar 21, 2022
866ee23
add disposeDecorations helper
meganrogge Mar 21, 2022
4645ef7
refactor
meganrogge Mar 21, 2022
7f131aa
fix error
meganrogge Mar 21, 2022
7a70db7
Fix API indentation
Tyriar Mar 21, 2022
09d1a01
Add ISearchDecorationOptions to the d.ts
Tyriar Mar 21, 2022
ef3695f
Move setting opacity into js
Tyriar Mar 21, 2022
87dd275
Undo change to Terminal.ts
Tyriar Mar 21, 2022
efda0e7
Fix indentation properly this time
Tyriar Mar 21, 2022
fe530d9
Ensure marker is disposed when decoration is
Tyriar Mar 21, 2022
23076ba
Fix duplicate decorations getting created
Tyriar Mar 21, 2022
f9deb2f
Don't set overview ruler width when using find
Tyriar Mar 21, 2022
6ef1bdd
Debounce onData listener
Tyriar Mar 21, 2022
b39477d
Fix full decorations
Tyriar Mar 21, 2022
43a014e
Fix decoration lifecycle issues
Tyriar Mar 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 203 additions & 14 deletions addons/xterm-addon-search/src/SearchAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
* @license MIT
*/

import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';

export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
incremental?: boolean;
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
decorations?: ISearchDecorationOptions;
}

interface ISearchDecorationOptions {
matchColor: string;
selectedColor: string;
}

export interface ISearchPosition {
Expand Down Expand Up @@ -40,7 +46,14 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs

export class SearchAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;

private _dataChanged: boolean = false;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
private _resultDecorations: Map<number, IDecoration[]> = new Map<number, IDecoration[]>();
private _searchResults: Map<string, ISearchResult> = new Map();
private _onDataDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
private _highlightTimeout: number | undefined;
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
Expand All @@ -53,9 +66,41 @@ export class SearchAddon implements ITerminalAddon {

public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this._terminal.onData(() => {
this._dataChanged = true;
if (this._highlightTimeout) {
window.clearTimeout(this._highlightTimeout);
}
this._highlightTimeout = setTimeout(() => {
if (this._lastSearchOptions?.decorations && this._cachedSearchTerm && this._resultDecorations.size > 0 && this._lastSearchOptions) {
this._highlightAllMatches(this._cachedSearchTerm, this._lastSearchOptions);
}
}, 200);
});
}

public dispose(): void { }
public dispose(): void {
this.clearDecorations();
this._onDataDisposable?.dispose();
}

public clearDecorations(): void {
this._selectedDecoration?.dispose();
this._terminal?.clearSelection();
this._searchResults.clear();
this._disposeDecorations();
this._cachedSearchTerm = undefined;
this._dataChanged = true;
}

private _disposeDecorations(): void {
this._resultDecorations.forEach(decorations => {
for (const d of decorations) {
d.dispose();
}
});
this._resultDecorations.clear();
}

/**
* Find the next instance of the term, then scroll to and select it. If it
Expand All @@ -68,9 +113,90 @@ export class SearchAddon implements ITerminalAddon {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
this._lastSearchOptions = searchOptions;
const findNextResult = this._findNextAndSelect(term, searchOptions);
if (searchOptions?.decorations) {
this._highlightAllMatches(term, searchOptions);
}
return findNextResult;
}

private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
if (!term || term.length === 0) {
this._terminal.clearSelection();
this.clearDecorations();
return;
}
searchOptions = searchOptions || {};
if (term === this._cachedSearchTerm && !this._dataChanged) {
return;
}
// new search, clear out the old decorations
this._disposeDecorations();
this._searchResults.clear();
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);
result = this._find(term, result.row, result.col + 1, searchOptions);
}
this._searchResults.forEach(result => {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
if (resultDecoration) {
const decorationsForLine = this._resultDecorations.get(resultDecoration.marker.line) || [];
decorationsForLine.push(resultDecoration);
this._resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
}
});
if (this._dataChanged) {
this._dataChanged = false;
}
if (this._searchResults.size > 0) {
this._cachedSearchTerm = term;
}
}

private _find(term: string, startRow?: number, startCol?: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
return undefined;
}
let result: ISearchResult | undefined = undefined;
startCol = startCol || 0;
startRow = startRow ?? 0;

this._initLinesCache();

const searchPosition: ISearchPosition = {
startRow,
startCol
};

// Search startRow
result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {

for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
// If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index
result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
return result;
}

private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
return false;
}

Expand All @@ -95,7 +221,6 @@ export class SearchAddon implements ITerminalAddon {

// Search startRow
let result = this._findInLine(term, searchPosition, searchOptions);

// Search from startRow + 1 to end
if (!result) {

Expand Down Expand Up @@ -130,9 +255,8 @@ export class SearchAddon implements ITerminalAddon {
}

// Set selection and scroll if a result was found
return this._selectResult(result);
return this._selectResult(result, searchOptions?.decorations);
}

/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
Expand All @@ -144,16 +268,30 @@ export class SearchAddon implements ITerminalAddon {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
this._lastSearchOptions = searchOptions;
const findPreviousResult = this._findAndSelectPrevious(term, searchOptions);
if (searchOptions?.decorations) {
this._highlightAllMatches(term, searchOptions);
}
return findPreviousResult;
}

if (!term || term.length === 0) {
this._terminal.clearSelection();
private _findAndSelectPrevious(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
let result: ISearchResult | undefined;
if (!this._terminal || !term || term.length === 0) {
result = undefined;
this._terminal?.clearSelection();
this.clearDecorations();
return false;
}

const isReverseSearch = true;
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
let startCol = this._terminal.cols;
let result: ISearchResult | undefined;
const isReverseSearch = true;

const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
Expand Down Expand Up @@ -211,7 +349,7 @@ export class SearchAddon implements ITerminalAddon {
if (!result && currentSelection) return true;

// Set selection and scroll if a result was found
return this._selectResult(result);
return this._selectResult(result, searchOptions?.decorations);
}

/**
Expand Down Expand Up @@ -446,15 +584,25 @@ export class SearchAddon implements ITerminalAddon {
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whethera result was selected.
* @return Whether a result was selected.
*/
private _selectResult(result: ISearchResult | undefined): boolean {
private _selectResult(result: ISearchResult | undefined, decorations?: ISearchDecorationOptions): boolean {
const terminal = this._terminal!;
this._selectedDecoration?.dispose();
if (!result) {
terminal.clearSelection();
return false;
}
terminal.select(result.col, result.row, result.size);
if (decorations?.selectedColor) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
if (marker) {
this._selectedDecoration = terminal.registerDecoration({ marker, overviewRulerOptions: { color: decorations.selectedColor } });
this._selectedDecoration?.onRender((e) => this._applyStyles(e, decorations.selectedColor, result));
this._selectedDecoration?.onDispose(() => marker.dispose());
}
}

// If it is not in the viewport then we scroll else it just gets selected
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
let scroll = result.row - terminal.buffer.active.viewportY;
Expand All @@ -463,4 +611,45 @@ export class SearchAddon implements ITerminalAddon {
}
return true;
}

/**
* Applies styles to the decoration when it is rendered
* @param element the decoration's element
* @param color the color to apply
* @param result the search result associated with the decoration
* @returns
*/
private _applyStyles(element: HTMLElement, color: string, result: ISearchResult): void {
if (element.clientWidth <= 0) {
return;
}
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
element.style.left = `${element.clientWidth * result.col}px`;
element.style.width = `${element.clientWidth * result.term.length}px`;
element.style.backgroundColor = color;
element.style.opacity = '0.6';
}
}

/**
* Creates a decoration for the result and applies styles
* @param result the search result for which to create the decoration
* @param color the color to use for the decoration
* @returns the {@link IDecoration} or undefined if the marker has already been disposed of
*/
private _createResultDecoration(result: ISearchResult, decorations: ISearchDecorationOptions): IDecoration | undefined {
const terminal = this._terminal!;
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
if (!marker || !decorations?.matchColor) {
return undefined;
}
const findResultDecoration = terminal.registerDecoration(
{ marker,
overviewRulerOptions: this._resultDecorations.get(marker.line) && !this._dataChanged ? undefined : { color: decorations.matchColor, position: 'center' }
});
findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchColor, result));
findResultDecoration?.onDispose(() => marker.dispose());
return findResultDecoration;
}
}
26 changes: 26 additions & 0 deletions addons/xterm-addon-search/typings/xterm-addon-search.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ declare module 'xterm-addon-search' {
* `findNext`, not `findPrevious`.
*/
incremental?: boolean;

/**
* When set, will highlight all instances of the word on search and show
* them in the overview ruler if it's enabled.
*/
decorations?: ISearchDecorationOptions;
}

/**
* Options for showing decorations when searching.
*/
interface ISearchDecorationOptions {
/**
* The color of a match.
*/
matchColor: string;

/**
* The color for the currently selected match.
*/
selectedColor: string;
}

/**
Expand Down Expand Up @@ -64,5 +85,10 @@ declare module 'xterm-addon-search' {
* @param searchOptions The options for the search.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean;

/**
* Clears the decorations and selection
*/
public clearDecorations(): void;
}
}
4 changes: 3 additions & 1 deletion demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {

const terminalContainer = document.getElementById('terminal-container');
const actionElements = {
find: <HTMLInputElement>document.querySelector('#find'),
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
};
Expand All @@ -107,7 +108,8 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
incremental: e.key !== `Enter`
incremental: e.key !== `Enter`,
decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { matchColor: '#555753', selectedColor: '#ef2929' } : undefined
};
}

Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ <h4>SearchAddon</h4>
<label><input type="checkbox" id="regex"/>Use regex</label>
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
<label><input type="checkbox" id="whole-word"/>Whole word</label>
<label><input type="checkbox" id="highlight-all-matches"/>Highlight All Matches</label>
</div>
<h4>SerializeAddon</h4>
<div>
Expand Down
2 changes: 1 addition & 1 deletion src/browser/Decorations/BufferDecorationRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class BufferDecorationRenderer extends Disposable {

private _refreshStyle(decoration: IInternalDecoration, element: HTMLElement): void {
const line = decoration.marker.line - this._bufferService.buffers.active.ydisp;
if (line < 0 || line > this._bufferService.rows) {
if (line < 0 || line >= this._bufferService.rows) {
// outside of viewport
element.style.display = 'none';
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/browser/Decorations/OverviewRulerRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ export class OverviewRulerRenderer extends Disposable {
}
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
for (const decoration of this._decorationService.decorations) {
if (decoration.options.overviewRulerOptions!.position !== 'full') {
if (decoration.options.overviewRulerOptions && decoration.options.overviewRulerOptions.position !== 'full') {
this._renderDecoration(decoration, updateAnchor);
}
}
for (const decoration of this._decorationService.decorations) {
if (decoration.options.overviewRulerOptions!.position === 'full') {
if (decoration.options.overviewRulerOptions && decoration.options.overviewRulerOptions.position === 'full') {
this._renderDecoration(decoration, updateAnchor);
}
}
Expand Down
Loading