@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.
- package/lib/xterm.js +1 -1
- package/lib/xterm.js.map +1 -1
- package/lib/xterm.mjs +8 -8
- package/lib/xterm.mjs.map +4 -4
- package/package.json +2 -2
- package/src/browser/AccessibilityManager.ts +5 -3
- package/src/browser/renderer/dom/DomRenderer.ts +41 -39
- package/src/browser/scrollable/scrollableElement.ts +7 -5
- package/src/browser/services/SelectionService.ts +26 -27
- package/src/common/CoreTerminal.ts +3 -3
- package/src/common/InputHandler.ts +18 -20
- package/src/common/Version.ts +1 -1
- package/src/common/buffer/Buffer.ts +14 -5
- package/src/common/buffer/BufferLine.ts +129 -60
- package/src/common/buffer/BufferLineStringCache.ts +69 -0
- package/src/common/buffer/BufferSet.ts +5 -1
- package/src/common/input/WriteBuffer.ts +30 -30
- package/src/common/parser/Params.ts +14 -8
- package/src/common/services/BufferService.ts +6 -4
- package/src/common/services/ServiceRegistry.ts +9 -7
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
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 *
|
|
29
|
-
* `fg = data[column *
|
|
30
|
-
* `bg = data[column *
|
|
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
|
-
|
|
45
|
-
|
|
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(
|
|
69
|
-
|
|
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 *
|
|
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 *
|
|
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.
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
207
|
-
this._data[index *
|
|
208
|
-
this._data[index *
|
|
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 *
|
|
221
|
-
this._data[index *
|
|
222
|
-
this._data[index *
|
|
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
|
-
|
|
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 *
|
|
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 *
|
|
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 *
|
|
463
|
-
return i + (this._data[i *
|
|
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 *
|
|
472
|
-
return i + (this._data[i *
|
|
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 <
|
|
483
|
-
this._data[(destCol + cell) *
|
|
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) *
|
|
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 <
|
|
492
|
-
this._data[(destCol + cell) *
|
|
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) *
|
|
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 *
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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;
|