@xterm/xterm 5.4.0-beta.8 → 5.4.0

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": "5.4.0-beta.8",
4
+ "version": "5.4.0",
5
5
  "main": "lib/xterm.js",
6
6
  "style": "css/xterm.css",
7
7
  "types": "typings/xterm.d.ts",
@@ -78,8 +78,8 @@
78
78
  "chai": "^4.3.4",
79
79
  "cross-env": "^7.0.3",
80
80
  "deep-equal": "^2.0.5",
81
- "eslint": "^8.45.0",
82
- "eslint-plugin-jsdoc": "^39.3.6",
81
+ "eslint": "^8.56.0",
82
+ "eslint-plugin-jsdoc": "^46.9.1",
83
83
  "express": "^4.17.1",
84
84
  "express-ws": "^5.0.2",
85
85
  "glob": "^7.2.0",
@@ -10,6 +10,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
10
10
  import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
11
11
  import { IBuffer } from 'common/buffer/Types';
12
12
  import { IInstantiationService } from 'common/services/Services';
13
+ import { addDisposableDomListener } from 'browser/Lifecycle';
13
14
 
14
15
  const MAX_ROWS_TO_READ = 20;
15
16
 
@@ -18,11 +19,17 @@ const enum BoundaryPosition {
18
19
  BOTTOM
19
20
  }
20
21
 
22
+ // Turn this on to unhide the accessibility tree and display it under
23
+ // (instead of overlapping with) the terminal.
24
+ const DEBUG = false;
25
+
21
26
  export class AccessibilityManager extends Disposable {
27
+ private _debugRootContainer: HTMLElement | undefined;
22
28
  private _accessibilityContainer: HTMLElement;
23
29
 
24
30
  private _rowContainer: HTMLElement;
25
31
  private _rowElements: HTMLElement[];
32
+ private _rowColumns: WeakMap<HTMLElement, number[]> = new WeakMap();
26
33
 
27
34
  private _liveRegion: HTMLElement;
28
35
  private _liveRegionLineCount: number = 0;
@@ -80,7 +87,23 @@ export class AccessibilityManager extends Disposable {
80
87
  if (!this._terminal.element) {
81
88
  throw new Error('Cannot enable accessibility before Terminal.open');
82
89
  }
83
- this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
90
+
91
+ if (DEBUG) {
92
+ this._accessibilityContainer.classList.add('debug');
93
+ this._rowContainer.classList.add('debug');
94
+
95
+ // Use a `<div class="xterm">` container so that the css will still apply.
96
+ this._debugRootContainer = document.createElement('div');
97
+ this._debugRootContainer.classList.add('xterm');
98
+
99
+ this._debugRootContainer.appendChild(document.createTextNode('------start a11y------'));
100
+ this._debugRootContainer.appendChild(this._accessibilityContainer);
101
+ this._debugRootContainer.appendChild(document.createTextNode('------end a11y------'));
102
+
103
+ this._terminal.element.insertAdjacentElement('afterend', this._debugRootContainer);
104
+ } else {
105
+ this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
106
+ }
84
107
 
85
108
  this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
86
109
  this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
@@ -92,11 +115,16 @@ export class AccessibilityManager extends Disposable {
92
115
  this.register(this._terminal.onKey(e => this._handleKey(e.key)));
93
116
  this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
94
117
  this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
118
+ this.register(addDisposableDomListener(document, 'selectionchange', () => this._handleSelectionChange()));
95
119
  this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions()));
96
120
 
97
121
  this._refreshRows();
98
122
  this.register(toDisposable(() => {
99
- this._accessibilityContainer.remove();
123
+ if (DEBUG) {
124
+ this._debugRootContainer!.remove();
125
+ } else {
126
+ this._accessibilityContainer.remove();
127
+ }
100
128
  this._rowElements.length = 0;
101
129
  }));
102
130
  }
@@ -149,14 +177,18 @@ export class AccessibilityManager extends Disposable {
149
177
  const buffer: IBuffer = this._terminal.buffer;
150
178
  const setSize = buffer.lines.length.toString();
151
179
  for (let i = start; i <= end; i++) {
152
- const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
180
+ const line = buffer.lines.get(buffer.ydisp + i);
181
+ const columns: number[] = [];
182
+ const lineData = line?.translateToString(true, undefined, undefined, columns) || '';
153
183
  const posInSet = (buffer.ydisp + i + 1).toString();
154
184
  const element = this._rowElements[i];
155
185
  if (element) {
156
186
  if (lineData.length === 0) {
157
187
  element.innerText = '\u00a0';
188
+ this._rowColumns.set(element, [0, 1]);
158
189
  } else {
159
190
  element.textContent = lineData;
191
+ this._rowColumns.set(element, columns);
160
192
  }
161
193
  element.setAttribute('aria-posinset', posInSet);
162
194
  element.setAttribute('aria-setsize', setSize);
@@ -233,6 +265,103 @@ export class AccessibilityManager extends Disposable {
233
265
  e.stopImmediatePropagation();
234
266
  }
235
267
 
268
+ private _handleSelectionChange(): void {
269
+ if (this._rowElements.length === 0) {
270
+ return;
271
+ }
272
+
273
+ const selection = document.getSelection();
274
+ if (!selection) {
275
+ return;
276
+ }
277
+
278
+ if (selection.isCollapsed) {
279
+ // Only do something when the anchorNode is inside the row container. This
280
+ // behavior mirrors what we do with mouse --- if the mouse clicks
281
+ // somewhere outside of the terminal, we don't clear the selection.
282
+ if (this._rowContainer.contains(selection.anchorNode)) {
283
+ this._terminal.clearSelection();
284
+ }
285
+ return;
286
+ }
287
+
288
+ if (!selection.anchorNode || !selection.focusNode) {
289
+ console.error('anchorNode and/or focusNode are null');
290
+ return;
291
+ }
292
+
293
+ // Sort the two selection points in document order.
294
+ let begin = { node: selection.anchorNode, offset: selection.anchorOffset };
295
+ let end = { node: selection.focusNode, offset: selection.focusOffset };
296
+ if ((begin.node.compareDocumentPosition(end.node) & Node.DOCUMENT_POSITION_PRECEDING) || (begin.node === end.node && begin.offset > end.offset) ) {
297
+ [begin, end] = [end, begin];
298
+ }
299
+
300
+ // Clamp begin/end to the inside of the row container.
301
+ if (begin.node.compareDocumentPosition(this._rowElements[0]) & (Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING)) {
302
+ begin = { node: this._rowElements[0].childNodes[0], offset: 0 };
303
+ }
304
+ if (!this._rowContainer.contains(begin.node)) {
305
+ // This happens when `begin` is below the last row.
306
+ return;
307
+ }
308
+ const lastRowElement = this._rowElements.slice(-1)[0];
309
+ if (end.node.compareDocumentPosition(lastRowElement) & (Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_PRECEDING)) {
310
+ end = {
311
+ node: lastRowElement,
312
+ offset: lastRowElement.textContent?.length ?? 0
313
+ };
314
+ }
315
+ if (!this._rowContainer.contains(end.node)) {
316
+ // This happens when `end` is above the first row.
317
+ return;
318
+ }
319
+
320
+ const toRowColumn = ({ node, offset }: typeof begin): {row: number, column: number} | null => {
321
+ // `node` is either the row element or the Text node inside it.
322
+ const rowElement: any = node instanceof Text ? node.parentNode : node;
323
+ let row = parseInt(rowElement?.getAttribute('aria-posinset'), 10) - 1;
324
+ if (isNaN(row)) {
325
+ console.warn('row is invalid. Race condition?');
326
+ return null;
327
+ }
328
+
329
+ const columns = this._rowColumns.get(rowElement);
330
+ if (!columns) {
331
+ console.warn('columns is null. Race condition?');
332
+ return null;
333
+ }
334
+
335
+ let column = offset < columns.length ? columns[offset] : columns.slice(-1)[0] + 1;
336
+ if (column >= this._terminal.cols) {
337
+ ++row;
338
+ column = 0;
339
+ }
340
+ return {
341
+ row,
342
+ column
343
+ };
344
+ };
345
+
346
+ const beginRowColumn = toRowColumn(begin);
347
+ const endRowColumn = toRowColumn(end);
348
+
349
+ if (!beginRowColumn || !endRowColumn) {
350
+ return;
351
+ }
352
+
353
+ if (beginRowColumn.row > endRowColumn.row || (beginRowColumn.row === endRowColumn.row && beginRowColumn.column >= endRowColumn.column)) {
354
+ // This should not happen unless we have some bugs.
355
+ throw new Error('invalid range');
356
+ }
357
+
358
+ this._terminal.select(
359
+ beginRowColumn.column,
360
+ beginRowColumn.row,
361
+ (endRowColumn.row - beginRowColumn.row) * this._terminal.cols - beginRowColumn.column + endRowColumn.column
362
+ );
363
+ }
364
+
236
365
  private _handleResize(rows: number): void {
237
366
  // Remove bottom boundary listener
238
367
  this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
@@ -4,18 +4,14 @@
4
4
  */
5
5
 
6
6
  import { addDisposableDomListener } from 'browser/Lifecycle';
7
- import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
7
+ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
8
8
  import { EventEmitter } from 'common/EventEmitter';
9
9
  import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
10
10
  import { IDisposable } from 'common/Types';
11
11
  import { IBufferService } from 'common/services/Services';
12
- import { IMouseService, IRenderService } from './services/Services';
12
+ import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
13
13
 
14
- export class Linkifier2 extends Disposable implements ILinkifier2 {
15
- private _element: HTMLElement | undefined;
16
- private _mouseService: IMouseService | undefined;
17
- private _renderService: IRenderService | undefined;
18
- private _linkProviders: ILinkProvider[] = [];
14
+ export class Linkifier extends Disposable implements ILinkifier2 {
19
15
  public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
20
16
  protected _currentLink: ILinkWithState | undefined;
21
17
  private _mouseDownLink: ILinkWithState | undefined;
@@ -33,39 +29,24 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
33
29
  public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
34
30
 
35
31
  constructor(
36
- @IBufferService private readonly _bufferService: IBufferService
32
+ private readonly _element: HTMLElement,
33
+ @IMouseService private readonly _mouseService: IMouseService,
34
+ @IRenderService private readonly _renderService: IRenderService,
35
+ @IBufferService private readonly _bufferService: IBufferService,
36
+ @ILinkProviderService private readonly _linkProviderService: ILinkProviderService
37
37
  ) {
38
38
  super();
39
39
  this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
40
40
  this.register(toDisposable(() => {
41
41
  this._lastMouseEvent = undefined;
42
+ // Clear out link providers as they could easily cause an embedder memory leak
43
+ this._activeProviderReplies?.clear();
42
44
  }));
43
45
  // Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
44
46
  this.register(this._bufferService.onResize(() => {
45
47
  this._clearCurrentLink();
46
48
  this._wasResized = true;
47
49
  }));
48
- }
49
-
50
- public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
51
- this._linkProviders.push(linkProvider);
52
- return {
53
- dispose: () => {
54
- // Remove the link provider from the list
55
- const providerIndex = this._linkProviders.indexOf(linkProvider);
56
-
57
- if (providerIndex !== -1) {
58
- this._linkProviders.splice(providerIndex, 1);
59
- }
60
- }
61
- };
62
- }
63
-
64
- public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
65
- this._element = element;
66
- this._mouseService = mouseService;
67
- this._renderService = renderService;
68
-
69
50
  this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
70
51
  this._isMouseOut = true;
71
52
  this._clearCurrentLink();
@@ -78,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
78
59
  private _handleMouseMove(event: MouseEvent): void {
79
60
  this._lastMouseEvent = event;
80
61
 
81
- if (!this._element || !this._mouseService) {
82
- return;
83
- }
84
-
85
62
  const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
86
63
  if (!position) {
87
64
  return;
@@ -142,7 +119,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
142
119
  let linkProvided = false;
143
120
 
144
121
  // There is no link cached, so ask for one
145
- for (const [i, linkProvider] of this._linkProviders.entries()) {
122
+ for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) {
146
123
  if (useLineCache) {
147
124
  const existingReply = this._activeProviderReplies?.get(i);
148
125
  // If there isn't a reply, the provider hasn't responded yet.
@@ -164,7 +141,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
164
141
 
165
142
  // If all providers have responded, remove lower priority links that intersect ranges of
166
143
  // higher priority links
167
- if (this._activeProviderReplies?.size === this._linkProviders.length) {
144
+ if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) {
168
145
  this._removeIntersectingLinks(position.y, this._activeProviderReplies);
169
146
  }
170
147
  });
@@ -220,7 +197,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
220
197
  }
221
198
 
222
199
  // Check if all the providers have responded
223
- if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
200
+ if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) {
224
201
  // Respect the order of the link providers
225
202
  for (let j = 0; j < this._activeProviderReplies.size; j++) {
226
203
  const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
@@ -240,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
240
217
  }
241
218
 
242
219
  private _handleMouseUp(event: MouseEvent): void {
243
- if (!this._element || !this._mouseService || !this._currentLink) {
220
+ if (!this._currentLink) {
244
221
  return;
245
222
  }
246
223
 
@@ -255,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
255
232
  }
256
233
 
257
234
  private _clearCurrentLink(startRow?: number, endRow?: number): void {
258
- if (!this._element || !this._currentLink || !this._lastMouseEvent) {
235
+ if (!this._currentLink || !this._lastMouseEvent) {
259
236
  return;
260
237
  }
261
238
 
@@ -268,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
268
245
  }
269
246
 
270
247
  private _handleNewLink(linkWithState: ILinkWithState): void {
271
- if (!this._element || !this._lastMouseEvent || !this._mouseService) {
248
+ if (!this._lastMouseEvent) {
272
249
  return;
273
250
  }
274
251
 
@@ -299,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
299
276
  if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
300
277
  this._currentLink.state.decorations.pointerCursor = v;
301
278
  if (this._currentLink.state.isHovered) {
302
- this._element?.classList.toggle('xterm-cursor-pointer', v);
279
+ this._element.classList.toggle('xterm-cursor-pointer', v);
303
280
  }
304
281
  }
305
282
  }
@@ -319,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
319
296
 
320
297
  // Listen to viewport changes to re-render the link under the cursor (only when the line the
321
298
  // link is on changes)
322
- if (this._renderService) {
323
- this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
324
- // Sanity check, this shouldn't happen in practice as this listener would be disposed
325
- if (!this._currentLink) {
326
- return;
327
- }
328
- // When start is 0 a scroll most likely occurred, make sure links above the fold also get
329
- // cleared.
330
- const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
331
- const end = this._bufferService.buffer.ydisp + 1 + e.end;
332
- // Only clear the link if the viewport change happened on this line
333
- if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
334
- this._clearCurrentLink(start, end);
335
- if (this._lastMouseEvent && this._element) {
336
- // re-eval previously active link after changes
337
- const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
338
- if (position) {
339
- this._askForLink(position, false);
340
- }
299
+ this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
300
+ // Sanity check, this shouldn't happen in practice as this listener would be disposed
301
+ if (!this._currentLink) {
302
+ return;
303
+ }
304
+ // When start is 0 a scroll most likely occurred, make sure links above the fold also get
305
+ // cleared.
306
+ const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
307
+ const end = this._bufferService.buffer.ydisp + 1 + e.end;
308
+ // Only clear the link if the viewport change happened on this line
309
+ if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
310
+ this._clearCurrentLink(start, end);
311
+ if (this._lastMouseEvent) {
312
+ // re-eval previously active link after changes
313
+ const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
314
+ if (position) {
315
+ this._askForLink(position, false);
341
316
  }
342
317
  }
343
- }));
344
- }
318
+ }
319
+ }));
345
320
  }
346
321
  }
347
322
 
@@ -3,7 +3,8 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
6
+ import { IBufferRange, ILink } from 'browser/Types';
7
+ import { ILinkProvider } from 'browser/services/Services';
7
8
  import { CellData } from 'common/buffer/CellData';
8
9
  import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
9
10
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IRenderDebouncerWithCallback } from 'browser/Types';
7
+ import { ICoreBrowserService } from 'browser/services/Services';
7
8
 
8
9
  /**
9
10
  * Debounces calls to render terminal rows using animation frames.
@@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
16
17
  private _refreshCallbacks: FrameRequestCallback[] = [];
17
18
 
18
19
  constructor(
19
- private _parentWindow: Window,
20
- private _renderCallback: (start: number, end: number) => void
20
+ private _renderCallback: (start: number, end: number) => void,
21
+ private readonly _coreBrowserService: ICoreBrowserService
21
22
  ) {
22
23
  }
23
24
 
24
25
  public dispose(): void {
25
26
  if (this._animationFrame) {
26
- this._parentWindow.cancelAnimationFrame(this._animationFrame);
27
+ this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
27
28
  this._animationFrame = undefined;
28
29
  }
29
30
  }
@@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
31
32
  public addRefreshCallback(callback: FrameRequestCallback): number {
32
33
  this._refreshCallbacks.push(callback);
33
34
  if (!this._animationFrame) {
34
- this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
35
+ this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
35
36
  }
36
37
  return this._animationFrame;
37
38
  }
@@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
49
50
  return;
50
51
  }
51
52
 
52
- this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
53
+ this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
53
54
  }
54
55
 
55
56
  private _innerRefresh(): void {