@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/css/xterm.css +10 -1
- package/lib/xterm.js +1 -1
- package/lib/xterm.js.map +1 -1
- package/package.json +1 -1
- package/src/browser/AccessibilityManager.ts +132 -3
- package/src/browser/renderer/shared/TextureAtlas.ts +8 -1
- package/src/common/Types.d.ts +1 -1
- package/src/common/buffer/BufferLine.ts +30 -3
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/src/common/Types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
}
|