Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
157 changes: 157 additions & 0 deletions addons/addon-search/src/DecorationManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
import type { ISearchDecorationOptions } from '@xterm/addon-search';
import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import type { ISearchResult } from './SearchEngine';

/**
* Interface for managing a highlight decoration.
*/
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
}

/**
* Interface for managing multiple decorations for a single match.
*/
interface IMultiHighlight extends IDisposable {
decorations: IDecoration[];
match: ISearchResult;
}

/**
* Manages visual decorations for search results including highlighting and active selection
* indicators. This class handles the creation, styling, and disposal of search-related decorations.
*/
export class DecorationManager extends Disposable {
private _highlightDecorations: IHighlight[] = [];
private _highlightedLines: Set<number> = new Set();

constructor(private readonly _terminal: Terminal) {
super();
this._register(toDisposable(() => this.clearHighlightDecorations()));
}

/**
* Creates decorations for all provided search results.
* @param results The search results to create decorations for.
* @param options The decoration options.
*/
public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {
this.clearHighlightDecorations();

for (const match of results) {
const decorations = this._createResultDecorations(match, options, false);
if (decorations) {
for (const decoration of decorations) {
this._storeDecoration(decoration, match);
}
}
}
}

/**
* Creates decorations for the currently active search result.
* @param result The active search result.
* @param options The decoration options.
* @returns The multi-highlight decoration or undefined if creation failed.
*/
public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {
const decorations = this._createResultDecorations(result, options, true);
if (decorations) {
return { decorations, match: result, dispose() { dispose(decorations); } };
}
return undefined;
}

/**
* Clears all highlight decorations.
*/
public clearHighlightDecorations(): void {
dispose(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
}

/**
* Stores a decoration and tracks it for management.
* @param decoration The decoration to store.
* @param match The search result this decoration represents.
*/
private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
this._highlightedLines.add(decoration.marker.line);
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
}

/**
* Applies styles to the decoration when it is rendered.
* @param element The decoration's element.
* @param borderColor The border color to apply.
* @param isActiveResult Whether the element is part of the active search result.
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (borderColor) {
element.style.outline = `1px solid ${borderColor}`;
}
}
if (isActiveResult) {
element.classList.add('xterm-find-active-result-decoration');
}
}

/**
* Creates a decoration for the result and applies styles
* @param result the search result for which to create the decoration
* @param options the options for the decoration
* @param isActiveResult whether this is the currently active result
* @returns the decorations or undefined if the marker has already been disposed of
*/
private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {
// Gather decoration ranges for this match as it could wrap
const decorationRanges: [number, number, number][] = [];
let currentCol = result.col;
let remainingSize = result.size;
let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;
while (remainingSize > 0) {
const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);
decorationRanges.push([markerOffset, currentCol, amountThisRow]);
currentCol = 0;
remainingSize -= amountThisRow;
markerOffset++;
}

// Create the decorations
const decorations: IDecoration[] = [];
for (const range of decorationRanges) {
const marker = this._terminal.registerMarker(range[0]);
const decoration = this._terminal.registerDecoration({
marker,
x: range[1],
width: range[2],
backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
position: 'center'
}
});
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));
disposables.push(decoration.onDispose(() => dispose(disposables)));
decorations.push(decoration);
}
}

return decorations.length === 0 ? undefined : decorations;
}
}


Loading
Loading