Skip to content

Commit a1d8e96

Browse files
authored
Merge pull request #5406 from Tyriar/tyriar/search_refactor
Refactor search addon into multiple files, add unit tests
2 parents f03dfbb + c4780e5 commit a1d8e96

File tree

10 files changed

+1861
-524
lines changed

10 files changed

+1861
-524
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
7+
import type { ISearchDecorationOptions } from '@xterm/addon-search';
8+
import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
9+
import type { ISearchResult } from './SearchEngine';
10+
11+
/**
12+
* Interface for managing a highlight decoration.
13+
*/
14+
interface IHighlight extends IDisposable {
15+
decoration: IDecoration;
16+
match: ISearchResult;
17+
}
18+
19+
/**
20+
* Interface for managing multiple decorations for a single match.
21+
*/
22+
interface IMultiHighlight extends IDisposable {
23+
decorations: IDecoration[];
24+
match: ISearchResult;
25+
}
26+
27+
/**
28+
* Manages visual decorations for search results including highlighting and active selection
29+
* indicators. This class handles the creation, styling, and disposal of search-related decorations.
30+
*/
31+
export class DecorationManager extends Disposable {
32+
private _highlightDecorations: IHighlight[] = [];
33+
private _highlightedLines: Set<number> = new Set();
34+
35+
constructor(private readonly _terminal: Terminal) {
36+
super();
37+
this._register(toDisposable(() => this.clearHighlightDecorations()));
38+
}
39+
40+
/**
41+
* Creates decorations for all provided search results.
42+
* @param results The search results to create decorations for.
43+
* @param options The decoration options.
44+
*/
45+
public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {
46+
this.clearHighlightDecorations();
47+
48+
for (const match of results) {
49+
const decorations = this._createResultDecorations(match, options, false);
50+
if (decorations) {
51+
for (const decoration of decorations) {
52+
this._storeDecoration(decoration, match);
53+
}
54+
}
55+
}
56+
}
57+
58+
/**
59+
* Creates decorations for the currently active search result.
60+
* @param result The active search result.
61+
* @param options The decoration options.
62+
* @returns The multi-highlight decoration or undefined if creation failed.
63+
*/
64+
public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {
65+
const decorations = this._createResultDecorations(result, options, true);
66+
if (decorations) {
67+
return { decorations, match: result, dispose() { dispose(decorations); } };
68+
}
69+
return undefined;
70+
}
71+
72+
/**
73+
* Clears all highlight decorations.
74+
*/
75+
public clearHighlightDecorations(): void {
76+
dispose(this._highlightDecorations);
77+
this._highlightDecorations = [];
78+
this._highlightedLines.clear();
79+
}
80+
81+
/**
82+
* Stores a decoration and tracks it for management.
83+
* @param decoration The decoration to store.
84+
* @param match The search result this decoration represents.
85+
*/
86+
private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
87+
this._highlightedLines.add(decoration.marker.line);
88+
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
89+
}
90+
91+
/**
92+
* Applies styles to the decoration when it is rendered.
93+
* @param element The decoration's element.
94+
* @param borderColor The border color to apply.
95+
* @param isActiveResult Whether the element is part of the active search result.
96+
*/
97+
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
98+
if (!element.classList.contains('xterm-find-result-decoration')) {
99+
element.classList.add('xterm-find-result-decoration');
100+
if (borderColor) {
101+
element.style.outline = `1px solid ${borderColor}`;
102+
}
103+
}
104+
if (isActiveResult) {
105+
element.classList.add('xterm-find-active-result-decoration');
106+
}
107+
}
108+
109+
/**
110+
* Creates a decoration for the result and applies styles
111+
* @param result the search result for which to create the decoration
112+
* @param options the options for the decoration
113+
* @param isActiveResult whether this is the currently active result
114+
* @returns the decorations or undefined if the marker has already been disposed of
115+
*/
116+
private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {
117+
// Gather decoration ranges for this match as it could wrap
118+
const decorationRanges: [number, number, number][] = [];
119+
let currentCol = result.col;
120+
let remainingSize = result.size;
121+
let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;
122+
while (remainingSize > 0) {
123+
const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);
124+
decorationRanges.push([markerOffset, currentCol, amountThisRow]);
125+
currentCol = 0;
126+
remainingSize -= amountThisRow;
127+
markerOffset++;
128+
}
129+
130+
// Create the decorations
131+
const decorations: IDecoration[] = [];
132+
for (const range of decorationRanges) {
133+
const marker = this._terminal.registerMarker(range[0]);
134+
const decoration = this._terminal.registerDecoration({
135+
marker,
136+
x: range[1],
137+
width: range[2],
138+
backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
139+
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
140+
color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
141+
position: 'center'
142+
}
143+
});
144+
if (decoration) {
145+
const disposables: IDisposable[] = [];
146+
disposables.push(marker);
147+
disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));
148+
disposables.push(decoration.onDispose(() => dispose(disposables)));
149+
decorations.push(decoration);
150+
}
151+
}
152+
153+
return decorations.length === 0 ? undefined : decorations;
154+
}
155+
}
156+
157+

0 commit comments

Comments
 (0)