Skip to content

Commit 15d9082

Browse files
authored
Merge pull request #5403 from Tyriar/tyriar/76c0_search_line_cache
Add SearchLineCache for improved search performance
2 parents 02b3704 + 304143a commit 15d9082

File tree

2 files changed

+130
-93
lines changed

2 files changed

+130
-93
lines changed

addons/addon-search/src/SearchAddon.ts

Lines changed: 13 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
77
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchDecorationOptions } from '@xterm/addon-search';
88
import { Emitter, Event } from 'vs/base/common/event';
9-
import { combinedDisposable, Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
9+
import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
10+
import { SearchLineCache } from './SearchLineCache';
1011

1112
interface IInternalSearchOptions {
1213
noScroll?: boolean;
@@ -33,16 +34,7 @@ export interface ISearchResult {
3334
size: number;
3435
}
3536

36-
type LineCacheEntry = [
37-
/**
38-
* The string representation of a line (as opposed to the buffer cell representation).
39-
*/
40-
lineAsString: string,
41-
/**
42-
* The offsets where each line starts when the entry describes a wrapped line.
43-
*/
44-
lineOffsets: number[]
45-
];
37+
4638

4739
interface IHighlight extends IDisposable {
4840
decoration: IDecoration;
@@ -65,11 +57,7 @@ const enum Constants {
6557
*/
6658
NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?',
6759

68-
/**
69-
* Time-to-live for cached search results in milliseconds. After this duration, cached search
70-
* results will be invalidated to ensure they remain consistent with terminal content changes.
71-
*/
72-
LINES_CACHE_TIME_TO_LIVE = 15000,
60+
7361

7462
/**
7563
* Default maximum number of search results to highlight simultaneously. This limit prevents
@@ -89,14 +77,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
8977
private _highlightLimit: number;
9078
private _lastSearchOptions: ISearchOptions | undefined;
9179
private _highlightTimeout: number | undefined;
92-
/**
93-
* translateBufferLineToStringWithWrap is a fairly expensive call.
94-
* We memoize the calls into an array that has a time based ttl.
95-
* _linesCache is also invalidated when the terminal cursor moves.
96-
*/
97-
private _linesCache: LineCacheEntry[] | undefined;
98-
private _linesCacheTimeoutId = 0;
99-
private _linesCacheDisposables = new MutableDisposable();
80+
private _lineCache = this._register(new MutableDisposable<SearchLineCache>());
10081

10182
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
10283
public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
@@ -109,6 +90,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
10990

11091
public activate(terminal: Terminal): void {
11192
this._terminal = terminal;
93+
this._lineCache.value = new SearchLineCache(terminal);
11294
this._register(this._terminal.onWriteParsed(() => this._updateMatches()));
11395
this._register(this._terminal.onResize(() => this._updateMatches()));
11496
this._register(toDisposable(() => this.clearDecorations()));
@@ -223,7 +205,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
223205

224206
let result: ISearchResult | undefined = undefined;
225207

226-
this._initLinesCache();
208+
this._lineCache.value!.initLinesCache();
227209

228210
const searchPosition: ISearchPosition = {
229211
startRow,
@@ -271,7 +253,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
271253
}
272254
}
273255

274-
this._initLinesCache();
256+
this._lineCache.value!.initLinesCache();
275257

276258
const searchPosition: ISearchPosition = {
277259
startRow,
@@ -392,7 +374,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
392374
let startCol = this._terminal.cols;
393375
const isReverseSearch = true;
394376

395-
this._initLinesCache();
377+
this._lineCache.value!.initLinesCache();
396378
const searchPosition: ISearchPosition = {
397379
startRow,
398380
startCol
@@ -443,32 +425,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
443425
return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);
444426
}
445427

446-
/**
447-
* Sets up a line cache with a ttl
448-
*/
449-
private _initLinesCache(): void {
450-
const terminal = this._terminal!;
451-
if (!this._linesCache) {
452-
this._linesCache = new Array(terminal.buffer.active.length);
453-
this._linesCacheDisposables.value = combinedDisposable(
454-
terminal.onLineFeed(() => this._destroyLinesCache()),
455-
terminal.onCursorMove(() => this._destroyLinesCache()),
456-
terminal.onResize(() => this._destroyLinesCache())
457-
);
458-
}
459428

460-
window.clearTimeout(this._linesCacheTimeoutId);
461-
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
462-
}
463-
464-
private _destroyLinesCache(): void {
465-
this._linesCache = undefined;
466-
this._linesCacheDisposables.clear();
467-
if (this._linesCacheTimeoutId) {
468-
window.clearTimeout(this._linesCacheTimeoutId);
469-
this._linesCacheTimeoutId = 0;
470-
}
471-
}
472429

473430
/**
474431
* A found substring is a whole word if it doesn't have an alphanumeric character directly
@@ -513,12 +470,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
513470
searchPosition.startCol += terminal.cols;
514471
return this._findInLine(term, searchPosition, searchOptions);
515472
}
516-
let cache = this._linesCache?.[row];
473+
let cache = this._lineCache.value!.getLineFromCache(row);
517474
if (!cache) {
518-
cache = this._translateBufferLineToStringWithWrap(row, true);
519-
if (this._linesCache) {
520-
this._linesCache[row] = cache;
521-
}
475+
cache = this._lineCache.value!.translateBufferLineToStringWithWrap(row, true);
476+
this._lineCache.value!.setLineInCache(row, cache);
522477
}
523478
const [stringLine, offsets] = cache;
524479

@@ -639,42 +594,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
639594
return offset;
640595
}
641596

642-
/**
643-
* Translates a buffer line to a string, including subsequent lines if they are wraps.
644-
* Wide characters will count as two columns in the resulting string. This
645-
* function is useful for getting the actual text underneath the raw selection
646-
* position.
647-
* @param lineIndex The index of the line being translated.
648-
* @param trimRight Whether to trim whitespace to the right.
649-
*/
650-
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
651-
const terminal = this._terminal!;
652-
const strings = [];
653-
const lineOffsets = [0];
654-
let line = terminal.buffer.active.getLine(lineIndex);
655-
while (line) {
656-
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
657-
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
658-
let string = line.translateToString(!lineWrapsToNext && trimRight);
659-
if (lineWrapsToNext && nextLine) {
660-
const lastCell = line.getCell(line.length - 1);
661-
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
662-
// a wide character wrapped to the next line
663-
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
664-
string = string.slice(0, -1);
665-
}
666-
}
667-
strings.push(string);
668-
if (lineWrapsToNext) {
669-
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
670-
} else {
671-
break;
672-
}
673-
lineIndex++;
674-
line = nextLine;
675-
}
676-
return [strings.join(''), lineOffsets];
677-
}
597+
678598

679599
/**
680600
* Selects and scrolls to a result.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
3+
* @license MIT
4+
*/
5+
6+
import type { Terminal } from '@xterm/xterm';
7+
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
8+
9+
export type LineCacheEntry = [
10+
/**
11+
* The string representation of a line (as opposed to the buffer cell representation).
12+
*/
13+
lineAsString: string,
14+
/**
15+
* The offsets where each line starts when the entry describes a wrapped line.
16+
*/
17+
lineOffsets: number[]
18+
];
19+
20+
/**
21+
* Configuration constants for the search line cache functionality.
22+
*/
23+
const enum Constants {
24+
/**
25+
* Time-to-live for cached search results in milliseconds. After this duration, cached search
26+
* results will be invalidated to ensure they remain consistent with terminal content changes.
27+
*/
28+
LINES_CACHE_TIME_TO_LIVE = 15000
29+
}
30+
31+
export class SearchLineCache extends Disposable {
32+
/**
33+
* translateBufferLineToStringWithWrap is a fairly expensive call.
34+
* We memoize the calls into an array that has a time based ttl.
35+
* _linesCache is also invalidated when the terminal cursor moves.
36+
*/
37+
private _linesCache: LineCacheEntry[] | undefined;
38+
private _linesCacheTimeoutId = 0;
39+
private _linesCacheDisposables = this._register(new MutableDisposable());
40+
41+
constructor(private _terminal: Terminal) {
42+
super();
43+
this._register(toDisposable(() => this._destroyLinesCache()));
44+
}
45+
46+
/**
47+
* Sets up a line cache with a ttl
48+
*/
49+
public initLinesCache(): void {
50+
if (!this._linesCache) {
51+
this._linesCache = new Array(this._terminal.buffer.active.length);
52+
this._linesCacheDisposables.value = combinedDisposable(
53+
this._terminal.onLineFeed(() => this._destroyLinesCache()),
54+
this._terminal.onCursorMove(() => this._destroyLinesCache()),
55+
this._terminal.onResize(() => this._destroyLinesCache())
56+
);
57+
}
58+
59+
window.clearTimeout(this._linesCacheTimeoutId);
60+
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
61+
}
62+
63+
private _destroyLinesCache(): void {
64+
this._linesCache = undefined;
65+
this._linesCacheDisposables.clear();
66+
if (this._linesCacheTimeoutId) {
67+
window.clearTimeout(this._linesCacheTimeoutId);
68+
this._linesCacheTimeoutId = 0;
69+
}
70+
}
71+
72+
public getLineFromCache(row: number): LineCacheEntry | undefined {
73+
return this._linesCache?.[row];
74+
}
75+
76+
public setLineInCache(row: number, entry: LineCacheEntry): void {
77+
if (this._linesCache) {
78+
this._linesCache[row] = entry;
79+
}
80+
}
81+
82+
/**
83+
* Translates a buffer line to a string, including subsequent lines if they are wraps.
84+
* Wide characters will count as two columns in the resulting string. This
85+
* function is useful for getting the actual text underneath the raw selection
86+
* position.
87+
* @param lineIndex The index of the line being translated.
88+
* @param trimRight Whether to trim whitespace to the right.
89+
*/
90+
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
91+
const strings = [];
92+
const lineOffsets = [0];
93+
let line = this._terminal.buffer.active.getLine(lineIndex);
94+
while (line) {
95+
const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);
96+
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
97+
let string = line.translateToString(!lineWrapsToNext && trimRight);
98+
if (lineWrapsToNext && nextLine) {
99+
const lastCell = line.getCell(line.length - 1);
100+
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
101+
// a wide character wrapped to the next line
102+
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
103+
string = string.slice(0, -1);
104+
}
105+
}
106+
strings.push(string);
107+
if (lineWrapsToNext) {
108+
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
109+
} else {
110+
break;
111+
}
112+
lineIndex++;
113+
line = nextLine;
114+
}
115+
return [strings.join(''), lineOffsets];
116+
}
117+
}

0 commit comments

Comments
 (0)