@xterm/xterm 5.4.0-beta.10 → 5.4.0-beta.12

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.10",
4
+ "version": "5.4.0-beta.12",
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);
@@ -519,7 +519,7 @@ export class InputHandler extends Disposable implements IInputHandler {
519
519
 
520
520
  // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char
521
521
  if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {
522
- bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
522
+ bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr);
523
523
  }
524
524
 
525
525
  let precedingJoinState = this._parser.precedingJoinState;
@@ -581,7 +581,7 @@ export class InputHandler extends Disposable implements IInputHandler {
581
581
  }
582
582
  // clear left over cells to the right
583
583
  while (oldCol < cols) {
584
- oldRow.setCellFromCodePoint(oldCol++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
584
+ oldRow.setCellFromCodepoint(oldCol++, 0, 1, curAttr);
585
585
  }
586
586
  } else {
587
587
  this._activeBuffer.x = cols - 1;
@@ -605,7 +605,7 @@ export class InputHandler extends Disposable implements IInputHandler {
605
605
  bufferRow.addCodepointToCell(this._activeBuffer.x - offset,
606
606
  code, chWidth);
607
607
  for (let delta = chWidth - oldWidth; --delta >= 0; ) {
608
- bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended);
608
+ bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);
609
609
  }
610
610
  continue;
611
611
  }
@@ -613,17 +613,17 @@ export class InputHandler extends Disposable implements IInputHandler {
613
613
  // insert mode: move characters to right
614
614
  if (insertMode) {
615
615
  // right shift cells according to the width
616
- bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr), curAttr);
616
+ bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr));
617
617
  // test last cell - since the last cell has only room for
618
618
  // a halfwidth char any fullwidth shifted there is lost
619
619
  // and will be set to empty cell
620
620
  if (bufferRow.getWidth(cols - 1) === 2) {
621
- bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg, curAttr.extended);
621
+ bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr);
622
622
  }
623
623
  }
624
624
 
625
625
  // write current char to buffer and advance cursor
626
- bufferRow.setCellFromCodePoint(this._activeBuffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended);
626
+ bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr);
627
627
 
628
628
  // fullwidth char - also set next cell to placeholder stub and advance cursor
629
629
  // for graphemes bigger than fullwidth we can simply loop to zero
@@ -631,7 +631,7 @@ export class InputHandler extends Disposable implements IInputHandler {
631
631
  if (chWidth > 0) {
632
632
  while (--chWidth) {
633
633
  // other than a regular empty cell a cell following a wide char has no width
634
- bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended);
634
+ bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr);
635
635
  }
636
636
  }
637
637
  }
@@ -640,7 +640,7 @@ export class InputHandler extends Disposable implements IInputHandler {
640
640
 
641
641
  // handle wide chars: reset cell to the right if it is second cell of a wide char
642
642
  if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) {
643
- bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
643
+ bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr);
644
644
  }
645
645
 
646
646
  this._dirtyRowTracker.markDirty(this._activeBuffer.y);
@@ -1145,7 +1145,6 @@ export class InputHandler extends Disposable implements IInputHandler {
1145
1145
  start,
1146
1146
  end,
1147
1147
  this._activeBuffer.getNullCell(this._eraseAttrData()),
1148
- this._eraseAttrData(),
1149
1148
  respectProtect
1150
1149
  );
1151
1150
  if (clearWrap) {
@@ -1366,8 +1365,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1366
1365
  line.insertCells(
1367
1366
  this._activeBuffer.x,
1368
1367
  params.params[0] || 1,
1369
- this._activeBuffer.getNullCell(this._eraseAttrData()),
1370
- this._eraseAttrData()
1368
+ this._activeBuffer.getNullCell(this._eraseAttrData())
1371
1369
  );
1372
1370
  this._dirtyRowTracker.markDirty(this._activeBuffer.y);
1373
1371
  }
@@ -1393,8 +1391,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1393
1391
  line.deleteCells(
1394
1392
  this._activeBuffer.x,
1395
1393
  params.params[0] || 1,
1396
- this._activeBuffer.getNullCell(this._eraseAttrData()),
1397
- this._eraseAttrData()
1394
+ this._activeBuffer.getNullCell(this._eraseAttrData())
1398
1395
  );
1399
1396
  this._dirtyRowTracker.markDirty(this._activeBuffer.y);
1400
1397
  }
@@ -1461,7 +1458,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1461
1458
  const param = params.params[0] || 1;
1462
1459
  for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
1463
1460
  const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
1464
- line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
1461
+ line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
1465
1462
  line.isWrapped = false;
1466
1463
  }
1467
1464
  this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
@@ -1494,7 +1491,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1494
1491
  const param = params.params[0] || 1;
1495
1492
  for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
1496
1493
  const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
1497
- line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
1494
+ line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
1498
1495
  line.isWrapped = false;
1499
1496
  }
1500
1497
  this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
@@ -1517,7 +1514,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1517
1514
  const param = params.params[0] || 1;
1518
1515
  for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
1519
1516
  const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
1520
- line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
1517
+ line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
1521
1518
  line.isWrapped = false;
1522
1519
  }
1523
1520
  this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
@@ -1540,7 +1537,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1540
1537
  const param = params.params[0] || 1;
1541
1538
  for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
1542
1539
  const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
1543
- line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
1540
+ line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
1544
1541
  line.isWrapped = false;
1545
1542
  }
1546
1543
  this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
@@ -1562,8 +1559,7 @@ export class InputHandler extends Disposable implements IInputHandler {
1562
1559
  line.replaceCells(
1563
1560
  this._activeBuffer.x,
1564
1561
  this._activeBuffer.x + (params.params[0] || 1),
1565
- this._activeBuffer.getNullCell(this._eraseAttrData()),
1566
- this._eraseAttrData()
1562
+ this._activeBuffer.getNullCell(this._eraseAttrData())
1567
1563
  );
1568
1564
  this._dirtyRowTracker.markDirty(this._activeBuffer.y);
1569
1565
  }
@@ -235,11 +235,11 @@ export interface IBufferLine {
235
235
  set(index: number, value: CharData): void;
236
236
  loadCell(index: number, cell: ICellData): ICellData;
237
237
  setCell(index: number, cell: ICellData): void;
238
- setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void;
238
+ setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void;
239
239
  addCodepointToCell(index: number, codePoint: number, width: number): void;
240
- insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void;
241
- deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void;
242
- replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData, respectProtect?: boolean): void;
240
+ insertCells(pos: number, n: number, ch: ICellData): void;
241
+ deleteCells(pos: number, n: number, fill: ICellData): void;
242
+ replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void;
243
243
  resize(cols: number, fill: ICellData): boolean;
244
244
  cleanupMemory(): number;
245
245
  fill(fillCellData: ICellData, respectProtect?: boolean): void;
@@ -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;
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types';
7
- import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
7
+ import { AttributeData } from 'common/buffer/AttributeData';
8
8
  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';
@@ -212,13 +212,13 @@ export class BufferLine implements IBufferLine {
212
212
  * Since the input handler see the incoming chars as UTF32 codepoints,
213
213
  * it gets an optimized access method.
214
214
  */
215
- public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void {
216
- if (bg & BgFlags.HAS_EXTENDED) {
217
- this._extendedAttrs[index] = eAttrs;
215
+ public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
216
+ if (attrs.bg & BgFlags.HAS_EXTENDED) {
217
+ this._extendedAttrs[index] = attrs.extended;
218
218
  }
219
219
  this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
220
- this._data[index * CELL_SIZE + Cell.FG] = fg;
221
- this._data[index * CELL_SIZE + Cell.BG] = bg;
220
+ this._data[index * CELL_SIZE + Cell.FG] = attrs.fg;
221
+ this._data[index * CELL_SIZE + Cell.BG] = attrs.bg;
222
222
  }
223
223
 
224
224
  /**
@@ -253,12 +253,12 @@ export class BufferLine implements IBufferLine {
253
253
  this._data[index * CELL_SIZE + Cell.CONTENT] = content;
254
254
  }
255
255
 
256
- public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void {
256
+ public insertCells(pos: number, n: number, fillCellData: ICellData): void {
257
257
  pos %= this.length;
258
258
 
259
259
  // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
260
260
  if (pos && this.getWidth(pos - 1) === 2) {
261
- this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
261
+ this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);
262
262
  }
263
263
 
264
264
  if (n < this.length - pos) {
@@ -277,11 +277,11 @@ export class BufferLine implements IBufferLine {
277
277
 
278
278
  // handle fullwidth at line end: reset last cell if it is first cell of a wide char
279
279
  if (this.getWidth(this.length - 1) === 2) {
280
- this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
280
+ this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData);
281
281
  }
282
282
  }
283
283
 
284
- public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void {
284
+ public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
285
285
  pos %= this.length;
286
286
  if (n < this.length - pos) {
287
287
  const cell = new CellData();
@@ -301,21 +301,21 @@ export class BufferLine implements IBufferLine {
301
301
  // - reset pos-1 if wide char
302
302
  // - reset pos if width==0 (previous second cell of a wide char)
303
303
  if (pos && this.getWidth(pos - 1) === 2) {
304
- this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
304
+ this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData);
305
305
  }
306
306
  if (this.getWidth(pos) === 0 && !this.hasContent(pos)) {
307
- this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
307
+ this.setCellFromCodepoint(pos, 0, 1, fillCellData);
308
308
  }
309
309
  }
310
310
 
311
- public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData, respectProtect: boolean = false): void {
311
+ public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {
312
312
  // full branching on respectProtect==true, hopefully getting fast JIT for standard case
313
313
  if (respectProtect) {
314
314
  if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {
315
- this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
315
+ this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);
316
316
  }
317
317
  if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) {
318
- this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
318
+ this.setCellFromCodepoint(end, 0, 1, fillCellData);
319
319
  }
320
320
  while (start < end && start < this.length) {
321
321
  if (!this.isProtected(start)) {
@@ -328,11 +328,11 @@ export class BufferLine implements IBufferLine {
328
328
 
329
329
  // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char
330
330
  if (start && this.getWidth(start - 1) === 2) {
331
- this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
331
+ this.setCellFromCodepoint(start - 1, 0, 1, fillCellData);
332
332
  }
333
333
  // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char
334
334
  if (end < this.length && this.getWidth(end - 1) === 2) {
335
- this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs());
335
+ this.setCellFromCodepoint(end, 0, 1, fillCellData);
336
336
  }
337
337
 
338
338
  while (start < end && start < this.length) {
@@ -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
  }