159 lines
5.8 KiB
TypeScript
159 lines
5.8 KiB
TypeScript
/**
|
|
* 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 '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],
|
|
layer: isActiveResult ? 'top' : 'bottom',
|
|
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;
|
|
}
|
|
}
|
|
|
|
|