@xterm/xterm 6.1.0-beta.220 → 6.1.0-beta.221

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.220",
4
+ "version": "6.1.0-beta.221",
5
5
  "main": "lib/xterm.js",
6
6
  "module": "lib/xterm.mjs",
7
7
  "style": "css/xterm.css",
@@ -111,5 +111,5 @@
111
111
  "ws": "^8.2.3",
112
112
  "xterm-benchmark": "^0.3.1"
113
113
  },
114
- "commit": "e749cb61253b9ca886a0ad9bf15f5d95c70eadf6"
114
+ "commit": "4e5994d8a2ae575eaac2c355c54f97a9f4c3b928"
115
115
  }
@@ -73,6 +73,42 @@ export class TimeoutTimer implements IDisposable {
73
73
  }
74
74
  }
75
75
 
76
+ /**
77
+ * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled
78
+ * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not
79
+ * run yet.
80
+ */
81
+ export class MicrotaskTimer implements IDisposable {
82
+ private _isScheduled = false;
83
+ private _isDisposed = false;
84
+
85
+ public dispose(): void {
86
+ this.cancel();
87
+ this._isDisposed = true;
88
+ }
89
+
90
+ public cancel(): void {
91
+ this._isScheduled = false;
92
+ }
93
+
94
+ public set(runner: () => void): void {
95
+ if (this._isDisposed) {
96
+ throw new Error('Calling set on a disposed MicrotaskTimer');
97
+ }
98
+ if (this._isScheduled) {
99
+ return;
100
+ }
101
+ this._isScheduled = true;
102
+ queueMicrotask(() => {
103
+ if (!this._isScheduled) {
104
+ return;
105
+ }
106
+ this._isScheduled = false;
107
+ runner();
108
+ });
109
+ }
110
+ }
111
+
76
112
  export class IntervalTimer implements IDisposable {
77
113
  private _disposable: IDisposable | undefined;
78
114
  private _isDisposed = false;
@@ -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.220';
9
+ export const XTERM_VERSION = '6.1.0-beta.221';
@@ -3,19 +3,19 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
+ import type { IDeleteEvent, IInsertEvent } from 'common/CircularList';
7
+ import { MicrotaskTimer } from 'common/Async';
6
8
  import { css } from 'common/Color';
7
- import { Disposable, DisposableStore, toDisposable } from 'common/Lifecycle';
8
- import { IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
9
+ import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'common/Lifecycle';
10
+ import { IBufferService, IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
9
11
  import { SortedList } from 'common/SortedList';
10
- import { IColor } from 'common/Types';
12
+ import { IColor, ICircularList } from 'common/Types';
11
13
  import { IDecoration, IDecorationOptions, IMarker } from '@xterm/xterm';
12
14
  import { Emitter } from 'common/Event';
13
15
 
14
16
  // Work variables to avoid garbage collection
15
17
  let $xmin = 0;
16
18
  let $xmax = 0;
17
- let $ymin = 0;
18
- let $ymax = 0;
19
19
 
20
20
  export class DecorationService extends Disposable implements IDecorationService {
21
21
  public serviceBrand: any;
@@ -27,6 +27,8 @@ export class DecorationService extends Disposable implements IDecorationService
27
27
  */
28
28
  private readonly _decorations: SortedList<IInternalDecoration>;
29
29
 
30
+ private readonly _lineCache = this._register(new DecorationLineCache());
31
+
30
32
  private readonly _onDecorationRegistered = this._register(new Emitter<IInternalDecoration>());
31
33
  public readonly onDecorationRegistered = this._onDecorationRegistered.event;
32
34
  private readonly _onDecorationRemoved = this._register(new Emitter<IInternalDecoration>());
@@ -34,12 +36,19 @@ export class DecorationService extends Disposable implements IDecorationService
34
36
 
35
37
  public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
36
38
 
37
- constructor(@ILogService private readonly _logService: ILogService) {
39
+ constructor(
40
+ @ILogService private readonly _logService: ILogService,
41
+ @IBufferService private readonly _bufferService: IBufferService
42
+ ) {
38
43
  super();
39
44
 
40
45
  this._decorations = new SortedList(e => e?.marker.line, this._logService);
41
46
 
42
47
  this._register(toDisposable(() => this.reset()));
48
+ this._register(this._bufferService.buffers.onBufferActivate(() => {
49
+ this._lineCache.attachToBufferLines(this._bufferService.buffer.lines);
50
+ }));
51
+ this._lineCache.attachToBufferLines(this._bufferService.buffer.lines);
43
52
  }
44
53
 
45
54
  public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
@@ -53,12 +62,14 @@ export class DecorationService extends Disposable implements IDecorationService
53
62
  listener.dispose();
54
63
  if (decoration) {
55
64
  if (this._decorations.delete(decoration)) {
65
+ this._lineCache.remove(decoration);
56
66
  this._onDecorationRemoved.fire(decoration);
57
67
  }
58
68
  markerDispose.dispose();
59
69
  }
60
70
  });
61
71
  this._decorations.insert(decoration);
72
+ this._lineCache.add(decoration);
62
73
  this._onDecorationRegistered.fire(decoration);
63
74
  }
64
75
  return decoration;
@@ -69,19 +80,17 @@ export class DecorationService extends Disposable implements IDecorationService
69
80
  d.dispose();
70
81
  }
71
82
  this._decorations.clear();
83
+ this._lineCache.clear();
72
84
  }
73
85
 
74
86
  public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
87
+ const bucket = this._lineCache.getDecorationsOnLine(line);
88
+ if (!bucket) {
89
+ return;
90
+ }
75
91
  let xmin = 0;
76
92
  let xmax = 0;
77
- let ymin = 0;
78
- let ymax = 0;
79
- for (const d of this._decorations.values()) {
80
- ymin = d.marker.line;
81
- ymax = ymin + (d.options.height ?? 1);
82
- if (line < ymin || line >= ymax) {
83
- continue;
84
- }
93
+ for (const d of bucket) {
85
94
  xmin = d.options.x ?? 0;
86
95
  xmax = xmin + (d.options.width ?? 1);
87
96
  if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
@@ -91,12 +100,11 @@ export class DecorationService extends Disposable implements IDecorationService
91
100
  }
92
101
 
93
102
  public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
94
- for (const d of this._decorations.values()) {
95
- $ymin = d.marker.line;
96
- $ymax = $ymin + (d.options.height ?? 1);
97
- if (line < $ymin || line >= $ymax) {
98
- continue;
99
- }
103
+ const bucket = this._lineCache.getDecorationsOnLine(line);
104
+ if (!bucket) {
105
+ return;
106
+ }
107
+ for (const d of bucket) {
100
108
  $xmin = d.options.x ?? 0;
101
109
  $xmax = $xmin + (d.options.width ?? 1);
102
110
  if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
@@ -106,10 +114,235 @@ export class DecorationService extends Disposable implements IDecorationService
106
114
  }
107
115
  }
108
116
 
117
+ /**
118
+ * Per-logical-line index of decorations for fast cell lookup.
119
+ *
120
+ * Keys are marker.line coordinates (logical buffer lines), not CircularList ring slots.
121
+ * Multi-line decorations appear in every line bucket they span. The index is kept aligned
122
+ * with marker.line updates via buffer line trim/insert/delete events.
123
+ */
124
+ export class DecorationLineCache extends Disposable {
125
+ private readonly _decorationsByLine: Map<number, IInternalDecoration[]> = new Map();
126
+ private readonly _decorations = new Set<IInternalDecoration>();
127
+ private readonly _bufferLineListeners = this._register(new MutableDisposable<DisposableStore>());
128
+ private readonly _lineIndexSyncTimer = this._register(new MicrotaskTimer());
129
+ private _lineIndexSyncCallbacks: (() => void)[] = [];
130
+
131
+ public clear(): void {
132
+ this._lineIndexSyncCallbacks.length = 0;
133
+ this._lineIndexSyncTimer.cancel();
134
+ this._decorationsByLine.clear();
135
+ this._decorations.clear();
136
+ }
137
+
138
+ public add(decoration: IInternalDecoration): void {
139
+ this._decorations.add(decoration);
140
+ this._addToLineBuckets(decoration);
141
+ }
142
+
143
+ public remove(decoration: IInternalDecoration): void {
144
+ this._decorations.delete(decoration);
145
+ this._removeFromLineBuckets(decoration);
146
+ }
147
+
148
+ public getDecorationsOnLine(line: number): ReadonlyArray<IInternalDecoration> | undefined {
149
+ return this._decorationsByLine.get(line);
150
+ }
151
+
152
+ public attachToBufferLines(lines: ICircularList<unknown>): void {
153
+ const store = new DisposableStore();
154
+ this._bufferLineListeners.value = store;
155
+ store.add(lines.onTrim(amount => this._handleBufferLinesTrim(amount)));
156
+ store.add(lines.onInsert(event => this._handleBufferLinesInsert(event)));
157
+ store.add(lines.onDelete(event => this._handleBufferLinesDelete(event)));
158
+ }
159
+
160
+ private _getDecorationHeight(decoration: IInternalDecoration): number {
161
+ return decoration.options.height ?? 1;
162
+ }
163
+
164
+ private _addToLineBuckets(decoration: IInternalDecoration): void {
165
+ const start = decoration.marker.line;
166
+ if (start < 0) {
167
+ return;
168
+ }
169
+ decoration._indexedStartLine = start;
170
+ const height = this._getDecorationHeight(decoration);
171
+ for (let line = start; line < start + height; line++) {
172
+ let bucket = this._decorationsByLine.get(line);
173
+ if (!bucket) {
174
+ bucket = [];
175
+ this._decorationsByLine.set(line, bucket);
176
+ }
177
+ bucket.push(decoration);
178
+ }
179
+ }
180
+
181
+ private _removeFromLineBuckets(decoration: IInternalDecoration): void {
182
+ const start = decoration._indexedStartLine;
183
+ const height = this._getDecorationHeight(decoration);
184
+ for (let line = start; line < start + height; line++) {
185
+ const bucket = this._decorationsByLine.get(line);
186
+ if (!bucket) {
187
+ continue;
188
+ }
189
+ const index = bucket.indexOf(decoration);
190
+ if (index !== -1) {
191
+ bucket.splice(index, 1);
192
+ }
193
+ if (bucket.length === 0) {
194
+ this._decorationsByLine.delete(line);
195
+ }
196
+ }
197
+ }
198
+
199
+ private _reindexDecoration(decoration: IInternalDecoration): void {
200
+ this._removeFromLineBuckets(decoration);
201
+ if (!decoration.marker.isDisposed && decoration.marker.line >= 0) {
202
+ this._addToLineBuckets(decoration);
203
+ }
204
+ }
205
+
206
+ /** Re-index after marker line updates (buffer listeners may run before markers). */
207
+ private _scheduleLineIndexSync(callback: () => void): void {
208
+ this._lineIndexSyncCallbacks.push(callback);
209
+ this._lineIndexSyncTimer.set(() => {
210
+ const callbacks = this._lineIndexSyncCallbacks;
211
+ this._lineIndexSyncCallbacks = [];
212
+ for (const cb of callbacks) {
213
+ cb();
214
+ }
215
+ });
216
+ }
217
+
218
+ private _handleBufferLinesTrim(amount: number): void {
219
+ if (amount <= 0) {
220
+ return;
221
+ }
222
+ const newMap = new Map<number, IInternalDecoration[]>();
223
+ for (const [line, bucket] of this._decorationsByLine) {
224
+ const newLine = line - amount;
225
+ if (newLine < 0) {
226
+ continue;
227
+ }
228
+ const existing = newMap.get(newLine);
229
+ if (existing) {
230
+ existing.push(...bucket);
231
+ } else {
232
+ newMap.set(newLine, bucket.slice());
233
+ }
234
+ }
235
+ this._decorationsByLine.clear();
236
+ for (const [line, bucket] of newMap) {
237
+ this._decorationsByLine.set(line, bucket);
238
+ }
239
+ for (const d of this._decorations) {
240
+ if (!d.marker.isDisposed) {
241
+ d._indexedStartLine -= amount;
242
+ }
243
+ }
244
+ }
245
+
246
+ private _handleBufferLinesInsert(event: IInsertEvent): void {
247
+ this._scheduleLineIndexSync(() => this._applyBufferLinesInsert(event));
248
+ }
249
+
250
+ private _handleBufferLinesDelete(event: IDeleteEvent): void {
251
+ this._scheduleLineIndexSync(() => this._applyBufferLinesDelete(event));
252
+ }
253
+
254
+ private _mergeLineBucket(newMap: Map<number, IInternalDecoration[]>, line: number, bucket: IInternalDecoration[]): void {
255
+ const existing = newMap.get(line);
256
+ if (existing) {
257
+ existing.push(...bucket);
258
+ } else {
259
+ newMap.set(line, bucket.slice());
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Shift indexed line keys and sync start lines. O(unique indexed lines), not O(decoration count).
265
+ * Decorations that span the insert point are re-indexed individually (rare vs single-line hits).
266
+ */
267
+ private _applyBufferLinesInsert(event: IInsertEvent): void {
268
+ const { index, amount } = event;
269
+ const spanCrossers: IInternalDecoration[] = [];
270
+ for (const d of this._decorations) {
271
+ if (d.marker.isDisposed) {
272
+ continue;
273
+ }
274
+ const start = d._indexedStartLine;
275
+ if (start < index && start + this._getDecorationHeight(d) > index) {
276
+ spanCrossers.push(d);
277
+ this._removeFromLineBuckets(d);
278
+ }
279
+ }
280
+ const newMap = new Map<number, IInternalDecoration[]>();
281
+ for (const [line, bucket] of this._decorationsByLine) {
282
+ const newLine = line >= index ? line + amount : line;
283
+ this._mergeLineBucket(newMap, newLine, bucket);
284
+ }
285
+ this._decorationsByLine.clear();
286
+ for (const [line, bucket] of newMap) {
287
+ this._decorationsByLine.set(line, bucket);
288
+ }
289
+ for (const d of this._decorations) {
290
+ if (d.marker.isDisposed) {
291
+ continue;
292
+ }
293
+ if (d._indexedStartLine >= index) {
294
+ d._indexedStartLine = d.marker.line;
295
+ }
296
+ }
297
+ for (const d of spanCrossers) {
298
+ this._addToLineBuckets(d);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Drop deleted line keys, shift keys below, sync start lines. Full re-index only when a
304
+ * multi-line decoration spans across the deleted range but survives.
305
+ */
306
+ private _applyBufferLinesDelete(event: IDeleteEvent): void {
307
+ const deleteEnd = event.index + event.amount;
308
+ const newMap = new Map<number, IInternalDecoration[]>();
309
+ for (const [line, bucket] of this._decorationsByLine) {
310
+ if (line >= event.index && line < deleteEnd) {
311
+ continue;
312
+ }
313
+ const newLine = line >= deleteEnd ? line - event.amount : line;
314
+ this._mergeLineBucket(newMap, newLine, bucket);
315
+ }
316
+ this._decorationsByLine.clear();
317
+ for (const [line, bucket] of newMap) {
318
+ this._decorationsByLine.set(line, bucket);
319
+ }
320
+ const toReindex: IInternalDecoration[] = [];
321
+ for (const d of this._decorations) {
322
+ if (d.marker.isDisposed) {
323
+ continue;
324
+ }
325
+ const start = d._indexedStartLine;
326
+ const height = this._getDecorationHeight(d);
327
+ if (start >= deleteEnd) {
328
+ d._indexedStartLine = d.marker.line;
329
+ } else if (start < event.index && start + height > deleteEnd) {
330
+ toReindex.push(d);
331
+ }
332
+ }
333
+ for (const d of toReindex) {
334
+ this._reindexDecoration(d);
335
+ }
336
+ }
337
+ }
338
+
109
339
  class Decoration extends DisposableStore implements IInternalDecoration {
110
340
  public readonly marker: IMarker;
111
341
  public element: HTMLElement | undefined;
112
342
 
343
+ /** Start line used for line-index removal when marker.line is cleared on dispose. */
344
+ public _indexedStartLine: number;
345
+
113
346
  public readonly onRenderEmitter = this.add(new Emitter<HTMLElement>());
114
347
  public readonly onRender = this.onRenderEmitter.event;
115
348
  private readonly _onDispose = this.add(new Emitter<void>());
@@ -144,6 +377,7 @@ class Decoration extends DisposableStore implements IInternalDecoration {
144
377
  ) {
145
378
  super();
146
379
  this.marker = options.marker;
380
+ this._indexedStartLine = options.marker.line;
147
381
  if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
148
382
  this.options.overviewRulerOptions.position = 'full';
149
383
  }
@@ -396,4 +396,6 @@ export interface IInternalDecoration extends IDecoration {
396
396
  readonly backgroundColorRGB: IColor | undefined;
397
397
  readonly foregroundColorRGB: IColor | undefined;
398
398
  readonly onRenderEmitter: Emitter<HTMLElement>;
399
+ /** @internal Start line for line-index removal; kept in sync on buffer line shifts. */
400
+ _indexedStartLine: number;
399
401
  }