@xterm/xterm 5.4.0-beta.11 → 5.4.0-beta.13

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.11",
4
+ "version": "5.4.0-beta.13",
5
5
  "main": "lib/xterm.js",
6
6
  "style": "css/xterm.css",
7
7
  "types": "typings/xterm.d.ts",
@@ -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);
@@ -613,7 +613,14 @@ export class TextureAtlas implements ITextureAtlas {
613
613
  nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset);
614
614
  break;
615
615
  case UnderlineStyle.DASHED:
616
- this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]);
616
+ const lineRatio = 0.6;
617
+ const gapRatio = 0.3;
618
+ // End line ratio is approximately equal to 0.1
619
+ const xChWidth = xChRight - xChLeft;
620
+ const line = Math.floor(lineRatio * xChWidth);
621
+ const gap = Math.floor(gapRatio * xChWidth);
622
+ const end = xChWidth - line - gap;
623
+ this._tmpCtx.setLineDash([line, gap, end]);
617
624
  this._tmpCtx.moveTo(xChLeft, yTop);
618
625
  this._tmpCtx.lineTo(xChRight, yTop);
619
626
  break;
@@ -247,7 +247,7 @@ export interface IBufferLine {
247
247
  clone(): IBufferLine;
248
248
  getTrimmedLength(): number;
249
249
  getNoBgTrimmedLength(): number;
250
- translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
250
+ translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string;
251
251
 
252
252
  /* direct access to cell attrs */
253
253
  getWidth(index: number): number;
@@ -508,16 +508,43 @@ export class BufferLine implements IBufferLine {
508
508
  }
509
509
  }
510
510
 
511
- public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
511
+ /**
512
+ * Translates the buffer line to a string.
513
+ *
514
+ * @param trimRight Whether to trim any empty cells on the right.
515
+ * @param startCol The column to start the string (0-based inclusive).
516
+ * @param endCol The column to end the string (0-based exclusive).
517
+ * @param outColumns if specified, this array will be filled with column numbers such that
518
+ * `returnedString[i]` is displayed at `outColumns[i]` column. `outColumns[returnedString.length]`
519
+ * is where the character following `returnedString` will be displayed.
520
+ *
521
+ * When a single cell is translated to multiple UTF-16 code units (e.g. surrogate pair) in the
522
+ * returned string, the corresponding entries in `outColumns` will have the same column number.
523
+ */
524
+ public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {
525
+ startCol = startCol ?? 0;
526
+ endCol = endCol ?? this.length;
512
527
  if (trimRight) {
513
528
  endCol = Math.min(endCol, this.getTrimmedLength());
514
529
  }
530
+ if (outColumns) {
531
+ outColumns.length = 0;
532
+ }
515
533
  let result = '';
516
534
  while (startCol < endCol) {
517
535
  const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
518
536
  const cp = content & Content.CODEPOINT_MASK;
519
- result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
520
- startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1
537
+ const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
538
+ result += chars;
539
+ if (outColumns) {
540
+ for (let i = 0; i < chars.length; ++i) {
541
+ outColumns.push(startCol);
542
+ }
543
+ }
544
+ startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by at least 1
545
+ }
546
+ if (outColumns) {
547
+ outColumns.push(startCol);
521
548
  }
522
549
  return result;
523
550
  }