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

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.
@@ -9,25 +9,26 @@ import { CellData } from 'common/buffer/CellData';
9
9
  import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
10
10
  import { stringFromCodePoint } from 'common/input/TextDecoder';
11
11
 
12
- /**
13
- * buffer memory layout:
14
- *
15
- * | uint32_t | uint32_t | uint32_t |
16
- * | `content` | `FG` | `BG` |
17
- * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
18
- */
19
-
20
-
21
- /** typed array slots taken by one cell */
22
- const CELL_SIZE = 3;
12
+ // Buffer memory layout:
13
+ //
14
+ // [0]: content `uint32_t` - wcwidth(2) comb(1) codepoint(21)
15
+ // [1]: fg `uint32_t` - flags(8) r(8) g(8) b(8)
16
+ // [2]: bg `uint32_t` - flags(8) r(8) g(8) b(8)
17
+
18
+ const enum Constants {
19
+ /** The number of 32 bit array indices taken by one cell. */
20
+ CELL_INDICIES = 3,
21
+ /** Factor when to cleanup underlying array buffer after shrinking. */
22
+ CLEANUP_THRESHOLD = 2
23
+ }
23
24
 
24
25
  /**
25
26
  * Cell member indices.
26
27
  *
27
28
  * Direct access:
28
- * `content = data[column * CELL_SIZE + Cell.CONTENT];`
29
- * `fg = data[column * CELL_SIZE + Cell.FG];`
30
- * `bg = data[column * CELL_SIZE + Cell.BG];`
29
+ * `content = data[column * Constants.CELL_INDICIES + Cell.CONTENT];`
30
+ * `fg = data[column * Constants.CELL_INDICIES + Cell.FG];`
31
+ * `bg = data[column * Constants.CELL_INDICIES + Cell.BG];`
31
32
  */
32
33
  const enum Cell {
33
34
  CONTENT = 0,
@@ -41,8 +42,17 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
41
42
  let $startIndex = 0;
42
43
  const $workCell = new CellData();
43
44
 
44
- /** Factor when to cleanup underlying array buffer after shrinking. */
45
- const CLEANUP_THRESHOLD = 2;
45
+ export interface IBufferLineStringCacheEntry {
46
+ value: string | undefined;
47
+ isTrimmed: boolean;
48
+ generation: number;
49
+ }
50
+
51
+ export interface IBufferLineStringCache {
52
+ generation: number;
53
+ allocateEntry(): IBufferLineStringCacheEntry;
54
+ touch?(): void;
55
+ }
46
56
 
47
57
  /**
48
58
  * Typed array based bufferline implementation.
@@ -63,10 +73,16 @@ export class BufferLine implements IBufferLine {
63
73
  protected _data: Uint32Array;
64
74
  protected _combined: {[index: number]: string} = {};
65
75
  protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
76
+ protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
66
77
  public length: number;
67
78
 
68
- constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
69
- this._data = new Uint32Array(cols * CELL_SIZE);
79
+ constructor(
80
+ protected readonly _stringCache: IBufferLineStringCache,
81
+ cols: number,
82
+ fillCellData?: ICellData,
83
+ public isWrapped: boolean = false
84
+ ) {
85
+ this._data = new Uint32Array(cols * Constants.CELL_INDICIES);
70
86
  const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
71
87
  for (let i = 0; i < cols; ++i) {
72
88
  this.setCell(i, cell);
@@ -79,10 +95,10 @@ export class BufferLine implements IBufferLine {
79
95
  * @deprecated
80
96
  */
81
97
  public get(index: number): CharData {
82
- const content = this._data[index * CELL_SIZE + Cell.CONTENT];
98
+ const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
83
99
  const cp = content & Content.CODEPOINT_MASK;
84
100
  return [
85
- this._data[index * CELL_SIZE + Cell.FG],
101
+ this._data[index * Constants.CELL_INDICIES + Cell.FG],
86
102
  (content & Content.IS_COMBINED_MASK)
87
103
  ? this._combined[index]
88
104
  : (cp) ? stringFromCodePoint(cp) : '',
@@ -98,12 +114,13 @@ export class BufferLine implements IBufferLine {
98
114
  * @deprecated
99
115
  */
100
116
  public set(index: number, value: CharData): void {
101
- this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
117
+ this._invalidateStringCache();
118
+ this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
102
119
  if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
103
120
  this._combined[index] = value[1];
104
- this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
121
+ this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
105
122
  } else {
106
- this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
123
+ this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
107
124
  }
108
125
  }
109
126
 
@@ -112,22 +129,22 @@ export class BufferLine implements IBufferLine {
112
129
  * use these when only one value is needed, otherwise use `loadCell`
113
130
  */
114
131
  public getWidth(index: number): number {
115
- return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
132
+ return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;
116
133
  }
117
134
 
118
135
  /** Test whether content has width. */
119
136
  public hasWidth(index: number): number {
120
- return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
137
+ return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;
121
138
  }
122
139
 
123
140
  /** Get FG cell component. */
124
141
  public getFg(index: number): number {
125
- return this._data[index * CELL_SIZE + Cell.FG];
142
+ return this._data[index * Constants.CELL_INDICIES + Cell.FG];
126
143
  }
127
144
 
128
145
  /** Get BG cell component. */
129
146
  public getBg(index: number): number {
130
- return this._data[index * CELL_SIZE + Cell.BG];
147
+ return this._data[index * Constants.CELL_INDICIES + Cell.BG];
131
148
  }
132
149
 
133
150
  /**
@@ -136,7 +153,7 @@ export class BufferLine implements IBufferLine {
136
153
  * from real empty cells.
137
154
  */
138
155
  public hasContent(index: number): number {
139
- return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
156
+ return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
140
157
  }
141
158
 
142
159
  /**
@@ -145,7 +162,7 @@ export class BufferLine implements IBufferLine {
145
162
  * a single UTF32 codepoint or the last codepoint of a combined string.
146
163
  */
147
164
  public getCodePoint(index: number): number {
148
- const content = this._data[index * CELL_SIZE + Cell.CONTENT];
165
+ const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
149
166
  if (content & Content.IS_COMBINED_MASK) {
150
167
  return this._combined[index].charCodeAt(this._combined[index].length - 1);
151
168
  }
@@ -154,12 +171,12 @@ export class BufferLine implements IBufferLine {
154
171
 
155
172
  /** Test whether the cell contains a combined string. */
156
173
  public isCombined(index: number): number {
157
- return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK;
174
+ return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;
158
175
  }
159
176
 
160
177
  /** Returns the string content of the cell. */
161
178
  public getString(index: number): string {
162
- const content = this._data[index * CELL_SIZE + Cell.CONTENT];
179
+ const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
163
180
  if (content & Content.IS_COMBINED_MASK) {
164
181
  return this._combined[index];
165
182
  }
@@ -172,7 +189,7 @@ export class BufferLine implements IBufferLine {
172
189
 
173
190
  /** Get state of protected flag. */
174
191
  public isProtected(index: number): number {
175
- return this._data[index * CELL_SIZE + Cell.BG] & BgFlags.PROTECTED;
192
+ return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;
176
193
  }
177
194
 
178
195
  /**
@@ -180,7 +197,7 @@ export class BufferLine implements IBufferLine {
180
197
  * to GC as it significantly reduced the amount of new objects/references needed.
181
198
  */
182
199
  public loadCell(index: number, cell: ICellData): ICellData {
183
- $startIndex = index * CELL_SIZE;
200
+ $startIndex = index * Constants.CELL_INDICIES;
184
201
  cell.content = this._data[$startIndex + Cell.CONTENT];
185
202
  cell.fg = this._data[$startIndex + Cell.FG];
186
203
  cell.bg = this._data[$startIndex + Cell.BG];
@@ -197,15 +214,16 @@ export class BufferLine implements IBufferLine {
197
214
  * Set data at `index` to `cell`.
198
215
  */
199
216
  public setCell(index: number, cell: ICellData): void {
217
+ this._invalidateStringCache();
200
218
  if (cell.content & Content.IS_COMBINED_MASK) {
201
219
  this._combined[index] = cell.combinedData;
202
220
  }
203
221
  if (cell.bg & BgFlags.HAS_EXTENDED) {
204
222
  this._extendedAttrs[index] = cell.extended;
205
223
  }
206
- this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
207
- this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
208
- this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
224
+ this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;
225
+ this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;
226
+ this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;
209
227
  }
210
228
 
211
229
  /**
@@ -214,12 +232,13 @@ export class BufferLine implements IBufferLine {
214
232
  * it gets an optimized access method.
215
233
  */
216
234
  public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
235
+ this._invalidateStringCache();
217
236
  if (attrs.bg & BgFlags.HAS_EXTENDED) {
218
237
  this._extendedAttrs[index] = attrs.extended;
219
238
  }
220
- this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
221
- this._data[index * CELL_SIZE + Cell.FG] = attrs.fg;
222
- this._data[index * CELL_SIZE + Cell.BG] = attrs.bg;
239
+ this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
240
+ this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg;
241
+ this._data[index * Constants.CELL_INDICIES + Cell.BG] = attrs.bg;
223
242
  }
224
243
 
225
244
  /**
@@ -229,7 +248,8 @@ export class BufferLine implements IBufferLine {
229
248
  * by the previous `setDataFromCodePoint` call, we can omit it here.
230
249
  */
231
250
  public addCodepointToCell(index: number, codePoint: number, width: number): void {
232
- let content = this._data[index * CELL_SIZE + Cell.CONTENT];
251
+ this._invalidateStringCache();
252
+ let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
233
253
  if (content & Content.IS_COMBINED_MASK) {
234
254
  // we already have a combined string, simply add
235
255
  this._combined[index] += stringFromCodePoint(codePoint);
@@ -251,10 +271,11 @@ export class BufferLine implements IBufferLine {
251
271
  content &= ~Content.WIDTH_MASK;
252
272
  content |= width << Content.WIDTH_SHIFT;
253
273
  }
254
- this._data[index * CELL_SIZE + Cell.CONTENT] = content;
274
+ this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;
255
275
  }
256
276
 
257
277
  public insertCells(pos: number, n: number, fillCellData: ICellData): void {
278
+ this._invalidateStringCache();
258
279
  pos %= this.length;
259
280
 
260
281
  // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
@@ -282,6 +303,7 @@ export class BufferLine implements IBufferLine {
282
303
  }
283
304
 
284
305
  public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
306
+ this._invalidateStringCache();
285
307
  pos %= this.length;
286
308
  if (n < this.length - pos) {
287
309
  for (let i = 0; i < this.length - pos - n; ++i) {
@@ -308,6 +330,7 @@ export class BufferLine implements IBufferLine {
308
330
  }
309
331
 
310
332
  public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {
333
+ this._invalidateStringCache();
311
334
  // full branching on respectProtect==true, hopefully getting fast JIT for standard case
312
335
  if (respectProtect) {
313
336
  if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {
@@ -344,13 +367,14 @@ export class BufferLine implements IBufferLine {
344
367
  * The underlying array buffer will not change if there is still enough space
345
368
  * to hold the new buffer line data.
346
369
  * Returns a boolean indicating, whether a `cleanupMemory` call would free
347
- * excess memory (true after shrinking > CLEANUP_THRESHOLD).
370
+ * excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).
348
371
  */
349
372
  public resize(cols: number, fillCellData: ICellData): boolean {
373
+ this._invalidateStringCache();
350
374
  if (cols === this.length) {
351
- return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
375
+ return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
352
376
  }
353
- const uint32Cells = cols * CELL_SIZE;
377
+ const uint32Cells = cols * Constants.CELL_INDICIES;
354
378
  if (cols > this.length) {
355
379
  if (this._data.buffer.byteLength >= uint32Cells * 4) {
356
380
  // optimization: avoid alloc and data copy if buffer has enough room
@@ -385,17 +409,17 @@ export class BufferLine implements IBufferLine {
385
409
  }
386
410
  }
387
411
  this.length = cols;
388
- return uint32Cells * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
412
+ return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
389
413
  }
390
414
 
391
415
  /**
392
416
  * Cleanup underlying array buffer.
393
417
  * A cleanup will be triggered if the array buffer exceeds the actual used
394
- * memory by a factor of CLEANUP_THRESHOLD.
418
+ * memory by a factor of Constants.CLEANUP_THRESHOLD.
395
419
  * Returns 0 or 1 indicating whether a cleanup happened.
396
420
  */
397
421
  public cleanupMemory(): number {
398
- if (this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
422
+ if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
399
423
  const data = new Uint32Array(this._data.length);
400
424
  data.set(this._data);
401
425
  this._data = data;
@@ -406,6 +430,7 @@ export class BufferLine implements IBufferLine {
406
430
 
407
431
  /** fill a line with fillCharData */
408
432
  public fill(fillCellData: ICellData, respectProtect: boolean = false): void {
433
+ this._invalidateStringCache();
409
434
  // full branching on respectProtect==true, hopefully getting fast JIT for standard case
410
435
  if (respectProtect) {
411
436
  for (let i = 0; i < this.length; ++i) {
@@ -424,6 +449,7 @@ export class BufferLine implements IBufferLine {
424
449
 
425
450
  /** alter to a full copy of line */
426
451
  public copyFrom(line: BufferLine): void {
452
+ this._invalidateStringCache();
427
453
  if (this.length !== line.length) {
428
454
  this._data = new Uint32Array(line._data);
429
455
  } else {
@@ -444,7 +470,7 @@ export class BufferLine implements IBufferLine {
444
470
 
445
471
  /** create a new clone */
446
472
  public clone(): IBufferLine {
447
- const newLine = new BufferLine(0);
473
+ const newLine = new BufferLine(this._stringCache, 0, undefined, false);
448
474
  newLine._data = new Uint32Array(this._data);
449
475
  newLine.length = this.length;
450
476
  for (const el in this._combined) {
@@ -459,8 +485,8 @@ export class BufferLine implements IBufferLine {
459
485
 
460
486
  public getTrimmedLength(): number {
461
487
  for (let i = this.length - 1; i >= 0; --i) {
462
- if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
463
- return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
488
+ if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
489
+ return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
464
490
  }
465
491
  }
466
492
  return 0;
@@ -468,30 +494,31 @@ export class BufferLine implements IBufferLine {
468
494
 
469
495
  public getNoBgTrimmedLength(): number {
470
496
  for (let i = this.length - 1; i >= 0; --i) {
471
- if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) {
472
- return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
497
+ if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {
498
+ return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
473
499
  }
474
500
  }
475
501
  return 0;
476
502
  }
477
503
 
478
504
  public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
505
+ this._invalidateStringCache();
479
506
  const srcData = src._data;
480
507
  if (applyInReverse) {
481
508
  for (let cell = length - 1; cell >= 0; cell--) {
482
- for (let i = 0; i < CELL_SIZE; i++) {
483
- this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
509
+ for (let i = 0; i < Constants.CELL_INDICIES; i++) {
510
+ this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
484
511
  }
485
- if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
512
+ if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
486
513
  this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
487
514
  }
488
515
  }
489
516
  } else {
490
517
  for (let cell = 0; cell < length; cell++) {
491
- for (let i = 0; i < CELL_SIZE; i++) {
492
- this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
518
+ for (let i = 0; i < Constants.CELL_INDICIES; i++) {
519
+ this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
493
520
  }
494
- if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
521
+ if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
495
522
  this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
496
523
  }
497
524
  }
@@ -508,7 +535,8 @@ export class BufferLine implements IBufferLine {
508
535
  }
509
536
 
510
537
  /**
511
- * Translates the buffer line to a string.
538
+ * Translates the buffer line to a string. Caching only applies to canonical full-line translation
539
+ * requests (regardless of `trimRight` value).
512
540
  *
513
541
  * @param trimRight Whether to trim any empty cells on the right.
514
542
  * @param startCol The column to start the string (0-based inclusive).
@@ -521,6 +549,19 @@ export class BufferLine implements IBufferLine {
521
549
  * returned string, the corresponding entries in `outColumns` will have the same column number.
522
550
  */
523
551
  public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {
552
+ const isCanonicalRequest = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;
553
+ if (isCanonicalRequest) {
554
+ this._stringCache.touch?.();
555
+ }
556
+ const stringCacheEntry = isCanonicalRequest ? this._getStringCacheEntry(false) : undefined;
557
+ if (isCanonicalRequest && stringCacheEntry?.value !== undefined) {
558
+ if (trimRight) {
559
+ return stringCacheEntry.isTrimmed ? stringCacheEntry.value : stringCacheEntry.value.trimEnd();
560
+ }
561
+ if (!stringCacheEntry.isTrimmed) {
562
+ return stringCacheEntry.value;
563
+ }
564
+ }
524
565
  startCol = startCol ?? 0;
525
566
  endCol = endCol ?? this.length;
526
567
  if (trimRight) {
@@ -531,7 +572,7 @@ export class BufferLine implements IBufferLine {
531
572
  }
532
573
  let result = '';
533
574
  while (startCol < endCol) {
534
- const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
575
+ const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];
535
576
  const cp = content & Content.CODEPOINT_MASK;
536
577
  const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
537
578
  result += chars;
@@ -545,6 +586,34 @@ export class BufferLine implements IBufferLine {
545
586
  if (outColumns) {
546
587
  outColumns.push(startCol);
547
588
  }
589
+ if (isCanonicalRequest) {
590
+ const cacheEntry = this._getStringCacheEntry(true)!;
591
+ cacheEntry.value = result;
592
+ cacheEntry.isTrimmed = !!trimRight;
593
+ }
548
594
  return result;
549
595
  }
596
+
597
+ protected _getStringCacheEntry(createIfNeeded: boolean): IBufferLineStringCacheEntry | undefined {
598
+ const cachedEntry = this._stringCacheEntryRef?.deref();
599
+ if (cachedEntry) {
600
+ if (cachedEntry.generation === this._stringCache.generation) {
601
+ return cachedEntry;
602
+ }
603
+ }
604
+ if (!createIfNeeded) {
605
+ return undefined;
606
+ }
607
+ const cacheEntry = this._stringCache.allocateEntry();
608
+ this._stringCacheEntryRef = new WeakRef(cacheEntry);
609
+ return cacheEntry;
610
+ }
611
+
612
+ private _invalidateStringCache(): void {
613
+ const cacheEntry = this._getStringCacheEntry(false);
614
+ if (cacheEntry) {
615
+ cacheEntry.value = undefined;
616
+ cacheEntry.isTrimmed = false;
617
+ }
618
+ }
550
619
  }
@@ -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,
@@ -9,30 +9,30 @@ import { Emitter } from 'common/Event';
9
9
 
10
10
  declare const setTimeout: (handler: () => void, timeout?: number) => void;
11
11
 
12
- /**
13
- * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
14
- * Enable flow control to avoid this limit and make sure that your backend correctly
15
- * propagates this to the underlying pty. (see docs for further instructions)
16
- * Since this limit is meant as a safety parachute to prevent browser crashs,
17
- * it is set to a very high number. Typically xterm.js gets unresponsive with
18
- * a 100 times lower number (>500 kB).
19
- */
20
- const DISCARD_WATERMARK = 50000000; // ~50 MB
21
-
22
- /**
23
- * The max number of ms to spend on writes before allowing the renderer to
24
- * catch up with a 0ms setTimeout. A value of < 33 to keep us close to
25
- * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
26
- * depends on the time it takes for the renderer to draw the frame.
27
- */
28
- const WRITE_TIMEOUT_MS = 12;
29
-
30
- /**
31
- * Threshold of max held chunks in the write buffer, that were already processed.
32
- * This is a tradeoff between extensive write buffer shifts (bad runtime) and high
33
- * memory consumption by data thats not used anymore.
34
- */
35
- const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
12
+ const enum Constants {
13
+ /**
14
+ * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
15
+ * Enable flow control to avoid this limit and make sure that your backend correctly
16
+ * propagates this to the underlying pty. (see docs for further instructions)
17
+ * Since this limit is meant as a safety parachute to prevent browser crashs,
18
+ * it is set to a very high number. Typically xterm.js gets unresponsive with
19
+ * a 100 times lower number (>500 kB).
20
+ */
21
+ DISCARD_WATERMARK = 50000000, // ~50 MB
22
+ /**
23
+ * The max number of ms to spend on writes before allowing the renderer to
24
+ * catch up with a 0ms setTimeout. A value of < 33 to keep us close to
25
+ * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
26
+ * depends on the time it takes for the renderer to draw the frame.
27
+ */
28
+ WRITE_TIMEOUT_MS = 12,
29
+ /**
30
+ * Threshold of max held chunks in the write buffer, that were already processed.
31
+ * This is a tradeoff between extensive write buffer shifts (bad runtime) and high
32
+ * memory consumption by data thats not used anymore.
33
+ */
34
+ WRITE_BUFFER_LENGTH_THRESHOLD = 50
35
+ }
36
36
 
37
37
  export class WriteBuffer extends Disposable {
38
38
  private _writeBuffer: (string | Uint8Array)[] = [];
@@ -133,7 +133,7 @@ export class WriteBuffer extends Disposable {
133
133
  }
134
134
 
135
135
  public write(data: string | Uint8Array, callback?: () => void): void {
136
- if (this._pendingData > DISCARD_WATERMARK) {
136
+ if (this._pendingData > Constants.DISCARD_WATERMARK) {
137
137
  throw new Error('write data discarded, use flow control to avoid losing data');
138
138
  }
139
139
 
@@ -169,7 +169,7 @@ export class WriteBuffer extends Disposable {
169
169
  * effectively lowering the redrawing needs, schematically:
170
170
  *
171
171
  * macroTask _innerWrite:
172
- * if (performance.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
172
+ * if (performance.now() - (lastTime | 0) < Constants.WRITE_TIMEOUT_MS):
173
173
  * schedule microTask _innerWrite(lastTime)
174
174
  * else:
175
175
  * schedule macroTask _innerWrite(0)
@@ -218,7 +218,7 @@ export class WriteBuffer extends Disposable {
218
218
  * responsibility to slice hard work), but we can at least schedule a screen update as we
219
219
  * gain control.
220
220
  */
221
- const continuation: (r: boolean) => void = (r: boolean) => performance.now() - startTime >= WRITE_TIMEOUT_MS
221
+ const continuation: (r: boolean) => void = (r: boolean) => performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS
222
222
  ? setTimeout(() => this._innerWrite(0, r))
223
223
  : this._innerWrite(startTime, r);
224
224
 
@@ -235,7 +235,7 @@ export class WriteBuffer extends Disposable {
235
235
  * current microtask queue (executed before setTimeout).
236
236
  */
237
237
  // const continuation: (r: boolean) => void = performance.now() - startTime >=
238
- // WRITE_TIMEOUT_MS
238
+ // Constants.WRITE_TIMEOUT_MS
239
239
  // ? r => setTimeout(() => this._innerWrite(0, r))
240
240
  // : r => this._innerWrite(startTime, r);
241
241
 
@@ -255,14 +255,14 @@ export class WriteBuffer extends Disposable {
255
255
  this._bufferOffset++;
256
256
  this._pendingData -= data.length;
257
257
 
258
- if (performance.now() - startTime >= WRITE_TIMEOUT_MS) {
258
+ if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {
259
259
  break;
260
260
  }
261
261
  }
262
262
  if (this._writeBuffer.length > this._bufferOffset) {
263
263
  // Allow renderer to catch up before processing the next batch
264
264
  // trim already processed chunks if we are above threshold
265
- if (this._bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
265
+ if (this._bufferOffset > Constants.WRITE_BUFFER_LENGTH_THRESHOLD) {
266
266
  this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);
267
267
  this._callbacks = this._callbacks.slice(this._bufferOffset);
268
268
  this._bufferOffset = 0;