@xterm/xterm 6.1.0-beta.214 → 6.1.0-beta.215

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xterm/xterm",
3
3
  "description": "Full xterm terminal, in your browser",
4
- "version": "6.1.0-beta.214",
4
+ "version": "6.1.0-beta.215",
5
5
  "main": "lib/xterm.js",
6
6
  "module": "lib/xterm.mjs",
7
7
  "style": "css/xterm.css",
@@ -119,5 +119,5 @@
119
119
  "ws": "^8.2.3",
120
120
  "xterm-benchmark": "^0.3.1"
121
121
  },
122
- "commit": "a2e51e8d36114119d5f2c174eb45be1fbc92cbfa"
122
+ "commit": "1023e404281ce625b469bd3614c38df26bcf885d"
123
123
  }
@@ -6,4 +6,4 @@
6
6
  /**
7
7
  * The xterm.js version. This is updated by the publish script from package.json.
8
8
  */
9
- export const XTERM_VERSION = '6.1.0-beta.214';
9
+ export const XTERM_VERSION = '6.1.0-beta.215';
@@ -4,10 +4,12 @@
4
4
  */
5
5
 
6
6
  import { CircularList, IInsertEvent } from 'common/CircularList';
7
+ import { Disposable, toDisposable } from 'common/Lifecycle';
7
8
  import { IdleTaskQueue } from 'common/TaskQueue';
8
9
  import { IAttributeData, IBufferLine, ICellData, ICharset } from 'common/Types';
9
10
  import { ExtendedAttrs } from 'common/buffer/AttributeData';
10
11
  import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
12
+ import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
11
13
  import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
12
14
  import { CellData } from 'common/buffer/CellData';
13
15
  import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from 'common/buffer/Constants';
@@ -25,7 +27,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
25
27
  * - cursor position
26
28
  * - scroll position
27
29
  */
28
- export class Buffer implements IBuffer {
30
+ export class Buffer extends Disposable implements IBuffer {
29
31
  public lines: CircularList<IBufferLine>;
30
32
  public ydisp: number = 0;
31
33
  public ybase: number = 0;
@@ -50,6 +52,7 @@ export class Buffer implements IBuffer {
50
52
  private _isClearing: boolean = false;
51
53
  private _memoryCleanupQueue: InstanceType<typeof IdleTaskQueue>;
52
54
  private _memoryCleanupPosition = 0;
55
+ private readonly _stringCache: BufferLineStringCache;
53
56
 
54
57
  constructor(
55
58
  private _hasScrollback: boolean,
@@ -57,6 +60,7 @@ export class Buffer implements IBuffer {
57
60
  private _bufferService: IBufferService,
58
61
  private readonly _logService: ILogService
59
62
  ) {
63
+ super();
60
64
  this._cols = this._bufferService.cols;
61
65
  this._rows = this._bufferService.rows;
62
66
  this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows));
@@ -64,6 +68,9 @@ export class Buffer implements IBuffer {
64
68
  this.scrollBottom = this._rows - 1;
65
69
  this.setupTabStops();
66
70
  this._memoryCleanupQueue = new IdleTaskQueue(this._logService);
71
+ this._register(toDisposable(() => this._memoryCleanupQueue.clear()));
72
+ this._register(toDisposable(() => this.clearAllMarkers()));
73
+ this._stringCache = this._register(new BufferLineStringCache());
67
74
  }
68
75
 
69
76
  public getNullCell(attr?: IAttributeData): ICellData {
@@ -93,7 +100,7 @@ export class Buffer implements IBuffer {
93
100
  }
94
101
 
95
102
  public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
96
- return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);
103
+ return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), isWrapped);
97
104
  }
98
105
 
99
106
  public get hasScrollback(): boolean {
@@ -138,6 +145,7 @@ export class Buffer implements IBuffer {
138
145
  * Clears the buffer to it's initial state, discarding all previous data.
139
146
  */
140
147
  public clear(): void {
148
+ this._stringCache.clear();
141
149
  this.ydisp = 0;
142
150
  this.ybase = 0;
143
151
  this.y = 0;
@@ -156,6 +164,7 @@ export class Buffer implements IBuffer {
156
164
  public resize(newCols: number, newRows: number): void {
157
165
  // store reference to null cell with default attrs
158
166
  const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
167
+ this._stringCache.clear();
159
168
 
160
169
  // count bufferlines with overly big memory to be cleaned afterwards
161
170
  let dirtyMemoryLines = 0;
@@ -190,7 +199,7 @@ export class Buffer implements IBuffer {
190
199
  if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
191
200
  // Just add the new missing rows on Windows as conpty reprints the screen with it's
192
201
  // view of the world. Once a line enters scrollback for conpty it remains there
193
- this.lines.push(new BufferLine(newCols, nullCell));
202
+ this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
194
203
  } else {
195
204
  if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
196
205
  // There is room above the buffer and there are no empty elements below the line,
@@ -204,7 +213,7 @@ export class Buffer implements IBuffer {
204
213
  } else {
205
214
  // Add a blank line if there is no buffer left at the top to scroll to, or if there
206
215
  // are blank lines after the cursor
207
- this.lines.push(new BufferLine(newCols, nullCell));
216
+ this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
208
217
  }
209
218
  }
210
219
  }
@@ -345,7 +354,7 @@ export class Buffer implements IBuffer {
345
354
  }
346
355
  if (this.lines.length < newRows) {
347
356
  // Add an extra row at the bottom of the viewport
348
- this.lines.push(new BufferLine(newCols, nullCell));
357
+ this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
349
358
  }
350
359
  } else {
351
360
  if (this.ydisp === this.ybase) {
@@ -44,6 +44,18 @@ const $workCell = new CellData();
44
44
  /** Factor when to cleanup underlying array buffer after shrinking. */
45
45
  const CLEANUP_THRESHOLD = 2;
46
46
 
47
+ export interface IBufferLineStringCacheEntry {
48
+ value: string | undefined;
49
+ isTrimmed: boolean;
50
+ generation: number;
51
+ }
52
+
53
+ export interface IBufferLineStringCache {
54
+ generation: number;
55
+ allocateEntry(): IBufferLineStringCacheEntry;
56
+ touch?(): void;
57
+ }
58
+
47
59
  /**
48
60
  * Typed array based bufferline implementation.
49
61
  *
@@ -63,9 +75,15 @@ export class BufferLine implements IBufferLine {
63
75
  protected _data: Uint32Array;
64
76
  protected _combined: {[index: number]: string} = {};
65
77
  protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
78
+ protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
66
79
  public length: number;
67
80
 
68
- constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
81
+ constructor(
82
+ protected readonly _stringCache: IBufferLineStringCache,
83
+ cols: number,
84
+ fillCellData?: ICellData,
85
+ public isWrapped: boolean = false
86
+ ) {
69
87
  this._data = new Uint32Array(cols * CELL_SIZE);
70
88
  const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
71
89
  for (let i = 0; i < cols; ++i) {
@@ -98,6 +116,7 @@ export class BufferLine implements IBufferLine {
98
116
  * @deprecated
99
117
  */
100
118
  public set(index: number, value: CharData): void {
119
+ this._invalidateStringCache();
101
120
  this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
102
121
  if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
103
122
  this._combined[index] = value[1];
@@ -197,6 +216,7 @@ export class BufferLine implements IBufferLine {
197
216
  * Set data at `index` to `cell`.
198
217
  */
199
218
  public setCell(index: number, cell: ICellData): void {
219
+ this._invalidateStringCache();
200
220
  if (cell.content & Content.IS_COMBINED_MASK) {
201
221
  this._combined[index] = cell.combinedData;
202
222
  }
@@ -214,6 +234,7 @@ export class BufferLine implements IBufferLine {
214
234
  * it gets an optimized access method.
215
235
  */
216
236
  public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
237
+ this._invalidateStringCache();
217
238
  if (attrs.bg & BgFlags.HAS_EXTENDED) {
218
239
  this._extendedAttrs[index] = attrs.extended;
219
240
  }
@@ -229,6 +250,7 @@ export class BufferLine implements IBufferLine {
229
250
  * by the previous `setDataFromCodePoint` call, we can omit it here.
230
251
  */
231
252
  public addCodepointToCell(index: number, codePoint: number, width: number): void {
253
+ this._invalidateStringCache();
232
254
  let content = this._data[index * CELL_SIZE + Cell.CONTENT];
233
255
  if (content & Content.IS_COMBINED_MASK) {
234
256
  // we already have a combined string, simply add
@@ -255,6 +277,7 @@ export class BufferLine implements IBufferLine {
255
277
  }
256
278
 
257
279
  public insertCells(pos: number, n: number, fillCellData: ICellData): void {
280
+ this._invalidateStringCache();
258
281
  pos %= this.length;
259
282
 
260
283
  // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
@@ -282,6 +305,7 @@ export class BufferLine implements IBufferLine {
282
305
  }
283
306
 
284
307
  public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
308
+ this._invalidateStringCache();
285
309
  pos %= this.length;
286
310
  if (n < this.length - pos) {
287
311
  for (let i = 0; i < this.length - pos - n; ++i) {
@@ -308,6 +332,7 @@ export class BufferLine implements IBufferLine {
308
332
  }
309
333
 
310
334
  public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {
335
+ this._invalidateStringCache();
311
336
  // full branching on respectProtect==true, hopefully getting fast JIT for standard case
312
337
  if (respectProtect) {
313
338
  if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {
@@ -347,6 +372,7 @@ export class BufferLine implements IBufferLine {
347
372
  * excess memory (true after shrinking > CLEANUP_THRESHOLD).
348
373
  */
349
374
  public resize(cols: number, fillCellData: ICellData): boolean {
375
+ this._invalidateStringCache();
350
376
  if (cols === this.length) {
351
377
  return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
352
378
  }
@@ -406,6 +432,7 @@ export class BufferLine implements IBufferLine {
406
432
 
407
433
  /** fill a line with fillCharData */
408
434
  public fill(fillCellData: ICellData, respectProtect: boolean = false): void {
435
+ this._invalidateStringCache();
409
436
  // full branching on respectProtect==true, hopefully getting fast JIT for standard case
410
437
  if (respectProtect) {
411
438
  for (let i = 0; i < this.length; ++i) {
@@ -424,6 +451,7 @@ export class BufferLine implements IBufferLine {
424
451
 
425
452
  /** alter to a full copy of line */
426
453
  public copyFrom(line: BufferLine): void {
454
+ this._invalidateStringCache();
427
455
  if (this.length !== line.length) {
428
456
  this._data = new Uint32Array(line._data);
429
457
  } else {
@@ -444,7 +472,7 @@ export class BufferLine implements IBufferLine {
444
472
 
445
473
  /** create a new clone */
446
474
  public clone(): IBufferLine {
447
- const newLine = new BufferLine(0);
475
+ const newLine = new BufferLine(this._stringCache, 0, undefined, false);
448
476
  newLine._data = new Uint32Array(this._data);
449
477
  newLine.length = this.length;
450
478
  for (const el in this._combined) {
@@ -476,6 +504,7 @@ export class BufferLine implements IBufferLine {
476
504
  }
477
505
 
478
506
  public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
507
+ this._invalidateStringCache();
479
508
  const srcData = src._data;
480
509
  if (applyInReverse) {
481
510
  for (let cell = length - 1; cell >= 0; cell--) {
@@ -508,7 +537,8 @@ export class BufferLine implements IBufferLine {
508
537
  }
509
538
 
510
539
  /**
511
- * Translates the buffer line to a string.
540
+ * Translates the buffer line to a string. Caching only applies to canonical full-line translation
541
+ * requests (regardless of `trimRight` value).
512
542
  *
513
543
  * @param trimRight Whether to trim any empty cells on the right.
514
544
  * @param startCol The column to start the string (0-based inclusive).
@@ -521,6 +551,19 @@ export class BufferLine implements IBufferLine {
521
551
  * returned string, the corresponding entries in `outColumns` will have the same column number.
522
552
  */
523
553
  public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {
554
+ const isCanonicalRequest = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;
555
+ if (isCanonicalRequest) {
556
+ this._stringCache.touch?.();
557
+ }
558
+ const stringCacheEntry = isCanonicalRequest ? this._getStringCacheEntry(false) : undefined;
559
+ if (isCanonicalRequest && stringCacheEntry?.value !== undefined) {
560
+ if (trimRight) {
561
+ return stringCacheEntry.isTrimmed ? stringCacheEntry.value : stringCacheEntry.value.trimEnd();
562
+ }
563
+ if (!stringCacheEntry.isTrimmed) {
564
+ return stringCacheEntry.value;
565
+ }
566
+ }
524
567
  startCol = startCol ?? 0;
525
568
  endCol = endCol ?? this.length;
526
569
  if (trimRight) {
@@ -545,6 +588,34 @@ export class BufferLine implements IBufferLine {
545
588
  if (outColumns) {
546
589
  outColumns.push(startCol);
547
590
  }
591
+ if (isCanonicalRequest) {
592
+ const cacheEntry = this._getStringCacheEntry(true)!;
593
+ cacheEntry.value = result;
594
+ cacheEntry.isTrimmed = !!trimRight;
595
+ }
548
596
  return result;
549
597
  }
598
+
599
+ protected _getStringCacheEntry(createIfNeeded: boolean): IBufferLineStringCacheEntry | undefined {
600
+ const cachedEntry = this._stringCacheEntryRef?.deref();
601
+ if (cachedEntry) {
602
+ if (cachedEntry.generation === this._stringCache.generation) {
603
+ return cachedEntry;
604
+ }
605
+ }
606
+ if (!createIfNeeded) {
607
+ return undefined;
608
+ }
609
+ const cacheEntry = this._stringCache.allocateEntry();
610
+ this._stringCacheEntryRef = new WeakRef(cacheEntry);
611
+ return cacheEntry;
612
+ }
613
+
614
+ private _invalidateStringCache(): void {
615
+ const cacheEntry = this._getStringCacheEntry(false);
616
+ if (cacheEntry) {
617
+ cacheEntry.value = undefined;
618
+ cacheEntry.isTrimmed = false;
619
+ }
620
+ }
550
621
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import type { IBufferLineStringCache, IBufferLineStringCacheEntry } from 'common/buffer/BufferLine';
7
+ import { disposableTimeout } from 'common/Async';
8
+ import { Disposable, MutableDisposable, toDisposable, type IDisposable } from 'common/Lifecycle';
9
+
10
+ const enum Constants {
11
+ CACHE_TTL_MS = 15000
12
+ }
13
+
14
+ export class BufferLineStringCache extends Disposable implements IBufferLineStringCache {
15
+ public generation: number = 0;
16
+ public readonly entries: Set<IBufferLineStringCacheEntry> = new Set();
17
+ private readonly _clearTimeout = this._register(new MutableDisposable<IDisposable>());
18
+ private _lastAccessTimestamp: number = 0;
19
+
20
+ constructor() {
21
+ super();
22
+ this._register(toDisposable(() => this.entries.clear()));
23
+ }
24
+
25
+ public touch(): void {
26
+ this._scheduleClear();
27
+ }
28
+
29
+ public allocateEntry(): IBufferLineStringCacheEntry {
30
+ const entry: IBufferLineStringCacheEntry = {
31
+ value: undefined,
32
+ isTrimmed: false,
33
+ generation: this.generation
34
+ };
35
+ this.entries.add(entry);
36
+ this._scheduleClear();
37
+ return entry;
38
+ }
39
+
40
+ public clear(): void {
41
+ this._clearTimeout.clear();
42
+ this._lastAccessTimestamp = 0;
43
+ this.generation++;
44
+ for (const entry of this.entries) {
45
+ entry.value = undefined;
46
+ entry.isTrimmed = false;
47
+ }
48
+ this.entries.clear();
49
+ }
50
+
51
+ private _scheduleClear(): void {
52
+ this._lastAccessTimestamp = Date.now();
53
+ if (this._clearTimeout.value) {
54
+ return;
55
+ }
56
+ this._scheduleClearTimeout(Constants.CACHE_TTL_MS);
57
+ }
58
+
59
+ private _scheduleClearTimeout(timeoutMs: number): void {
60
+ this._clearTimeout.value = disposableTimeout(() => {
61
+ const elapsed = Date.now() - this._lastAccessTimestamp;
62
+ if (elapsed >= Constants.CACHE_TTL_MS) {
63
+ this.clear();
64
+ return;
65
+ }
66
+ this._scheduleClearTimeout(Constants.CACHE_TTL_MS - elapsed);
67
+ }, timeoutMs);
68
+ }
69
+ }
@@ -3,7 +3,7 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { Disposable } from 'common/Lifecycle';
6
+ import { Disposable, MutableDisposable } from 'common/Lifecycle';
7
7
  import { IAttributeData } from 'common/Types';
8
8
  import { Buffer } from 'common/buffer/Buffer';
9
9
  import { IBuffer, IBufferSet } from 'common/buffer/Types';
@@ -18,6 +18,8 @@ export class BufferSet extends Disposable implements IBufferSet {
18
18
  private _normal!: Buffer;
19
19
  private _alt!: Buffer;
20
20
  private _activeBuffer!: Buffer;
21
+ private readonly _normalBuffer = this._register(new MutableDisposable<Buffer>());
22
+ private readonly _altBuffer = this._register(new MutableDisposable<Buffer>());
21
23
 
22
24
  private readonly _onBufferActivate = this._register(new Emitter<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>());
23
25
  public readonly onBufferActivate = this._onBufferActivate.event;
@@ -38,11 +40,13 @@ export class BufferSet extends Disposable implements IBufferSet {
38
40
 
39
41
  public reset(): void {
40
42
  this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);
43
+ this._normalBuffer.value = this._normal;
41
44
  this._normal.fillViewportRows();
42
45
 
43
46
  // The alt buffer should never have scrollback.
44
47
  // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
45
48
  this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);
49
+ this._altBuffer.value = this._alt;
46
50
  this._activeBuffer = this._normal;
47
51
  this._onBufferActivate.fire({
48
52
  activeBuffer: this._normal,