137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
/**
|
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import type { Terminal } from '@xterm/xterm';
|
|
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
|
import { disposableTimeout } from 'common/Async';
|
|
|
|
export type LineCacheEntry = [
|
|
/**
|
|
* The string representation of a line (as opposed to the buffer cell representation).
|
|
*/
|
|
lineAsString: string,
|
|
/**
|
|
* The offsets where each line starts when the entry describes a wrapped line.
|
|
*/
|
|
lineOffsets: number[]
|
|
];
|
|
|
|
/**
|
|
* Configuration constants for the search line cache functionality.
|
|
*/
|
|
const enum Constants {
|
|
/**
|
|
* Time-to-live for cached search results in milliseconds. After this duration, cached search
|
|
* results will be invalidated to ensure they remain consistent with terminal content changes.
|
|
*/
|
|
LINES_CACHE_TIME_TO_LIVE = 15000
|
|
}
|
|
|
|
export class SearchLineCache extends Disposable {
|
|
/**
|
|
* translateBufferLineToStringWithWrap is a fairly expensive call.
|
|
* We memoize the calls into an array that has a time based ttl.
|
|
* _linesCache is also invalidated when the terminal cursor moves.
|
|
*/
|
|
private _linesCache: LineCacheEntry[] | undefined;
|
|
private _linesCacheTimeout = this._register(new MutableDisposable());
|
|
private _linesCacheDisposables = this._register(new MutableDisposable());
|
|
// Track access to avoid recreating a timeout on every init call which occurs once per search
|
|
// result (findNext/findPrevious -> _highlightAllMatches -> find loop).
|
|
private _lastAccessTimestamp = 0;
|
|
|
|
constructor(private readonly _terminal: Terminal) {
|
|
super();
|
|
this._register(toDisposable(() => this._destroyLinesCache()));
|
|
}
|
|
|
|
/**
|
|
* Sets up a line cache with a ttl
|
|
*/
|
|
public initLinesCache(): void {
|
|
if (!this._linesCache) {
|
|
this._linesCache = new Array(this._terminal.buffer.active.length);
|
|
this._linesCacheDisposables.value = combinedDisposable(
|
|
this._terminal.onLineFeed(() => this._destroyLinesCache()),
|
|
this._terminal.onCursorMove(() => this._destroyLinesCache()),
|
|
this._terminal.onResize(() => this._destroyLinesCache())
|
|
);
|
|
}
|
|
|
|
this._lastAccessTimestamp = Date.now();
|
|
if (!this._linesCacheTimeout.value) {
|
|
this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);
|
|
}
|
|
}
|
|
|
|
private _destroyLinesCache(): void {
|
|
this._linesCache = undefined;
|
|
this._lastAccessTimestamp = 0;
|
|
this._linesCacheDisposables.clear();
|
|
this._linesCacheTimeout.clear();
|
|
}
|
|
|
|
private _scheduleLinesCacheTimeout(delay: number): void {
|
|
this._linesCacheTimeout.value = disposableTimeout(() => {
|
|
if (!this._linesCache) {
|
|
return;
|
|
}
|
|
const now = Date.now();
|
|
const elapsed = now - this._lastAccessTimestamp;
|
|
if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {
|
|
this._destroyLinesCache();
|
|
return;
|
|
}
|
|
this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);
|
|
}, delay);
|
|
}
|
|
|
|
public getLineFromCache(row: number): LineCacheEntry | undefined {
|
|
return this._linesCache?.[row];
|
|
}
|
|
|
|
public setLineInCache(row: number, entry: LineCacheEntry): void {
|
|
if (this._linesCache) {
|
|
this._linesCache[row] = entry;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Translates a buffer line to a string, including subsequent lines if they are wraps.
|
|
* Wide characters will count as two columns in the resulting string. This
|
|
* function is useful for getting the actual text underneath the raw selection
|
|
* position.
|
|
* @param lineIndex The index of the line being translated.
|
|
* @param trimRight Whether to trim whitespace to the right.
|
|
*/
|
|
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
|
|
const strings = [];
|
|
const lineOffsets = [0];
|
|
let line = this._terminal.buffer.active.getLine(lineIndex);
|
|
while (line) {
|
|
const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);
|
|
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
|
let string = line.translateToString(!lineWrapsToNext && trimRight);
|
|
if (lineWrapsToNext && nextLine) {
|
|
const lastCell = line.getCell(line.length - 1);
|
|
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
|
|
// a wide character wrapped to the next line
|
|
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
|
|
string = string.slice(0, -1);
|
|
}
|
|
}
|
|
strings.push(string);
|
|
if (lineWrapsToNext) {
|
|
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
|
|
} else {
|
|
break;
|
|
}
|
|
lineIndex++;
|
|
line = nextLine;
|
|
}
|
|
return [strings.join(''), lineOffsets];
|
|
}
|
|
}
|