Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3556f16138 |
@@ -258,6 +258,15 @@ export class MockBuffer implements IBuffer {
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return Buffer.prototype.getBlankLine.apply(this, arguments as any);
|
||||
}
|
||||
public attachWrappedRow(y: number, fillCellData: ICellData): IBufferLine {
|
||||
return Buffer.prototype.attachWrappedRow.apply(this, arguments as any);
|
||||
}
|
||||
public clearWrappedRow(y: number, fillCellData: ICellData, preserveContent?: boolean): void {
|
||||
Buffer.prototype.clearWrappedRow.apply(this, arguments as any);
|
||||
}
|
||||
public createScrollWrappedLine(lineAbove: IBufferLine | undefined, attr: IAttributeData): IBufferLine {
|
||||
return Buffer.prototype.createScrollWrappedLine.apply(this, arguments as any);
|
||||
}
|
||||
public getNullCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
+38
-18
@@ -11,6 +11,8 @@ import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { getWrappedRangeEnd } from 'common/buffer/BufferLineUtils';
|
||||
import { BufferOverflowLine } from 'common/buffer/BufferOverflowLine';
|
||||
import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
@@ -590,8 +592,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (this._activeBuffer.y >= this._bufferService.rows) {
|
||||
this._activeBuffer.y = this._bufferService.rows - 1;
|
||||
}
|
||||
// The line already exists (eg. the initial viewport), mark it as a
|
||||
// wrapped line
|
||||
// The line already exists (eg. the initial viewport), mark it as a wrapped line
|
||||
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;
|
||||
}
|
||||
// row changed, get it again
|
||||
@@ -756,7 +757,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics
|
||||
// can mess with this so windowsMode should be disabled, which is recommended on Windows build
|
||||
// 21376 and above.
|
||||
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + this._activeBuffer.y, false);
|
||||
}
|
||||
// If the end of the line is hit, prevent this action from wrapping around to the next line.
|
||||
if (this._activeBuffer.x >= this._bufferService.cols) {
|
||||
@@ -820,7 +821,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
&& this._activeBuffer.y > this._activeBuffer.scrollTop
|
||||
&& this._activeBuffer.y <= this._activeBuffer.scrollBottom
|
||||
&& this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {
|
||||
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + this._activeBuffer.y, false);
|
||||
this._activeBuffer.y--;
|
||||
this._activeBuffer.x = this._bufferService.cols - 1;
|
||||
// find last taken cell - last cell can have 3 different states:
|
||||
@@ -1184,21 +1185,43 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
respectProtect
|
||||
);
|
||||
if (clearWrap) {
|
||||
line.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _unwrapBufferRow(y: number, preserveContent: boolean): void {
|
||||
const line = this._activeBuffer.lines.get(y);
|
||||
if (!line?.isWrapped) {
|
||||
return;
|
||||
}
|
||||
if (line instanceof BufferOverflowLine) {
|
||||
if (preserveContent) {
|
||||
this._activeBuffer.lines.set(y, line.materializeToStandalone(this._activeBuffer.getNullCell(this._eraseAttrData())));
|
||||
} else {
|
||||
this._activeBuffer.clearWrappedRow(y, this._activeBuffer.getNullCell(this._eraseAttrData()), false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
line.isWrapped = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of
|
||||
* the terminal and the isWrapped property is set to false.
|
||||
* @param y row index
|
||||
*/
|
||||
private _resetBufferLine(y: number, respectProtect: boolean = false): void {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);
|
||||
const absY = this._activeBuffer.ybase + y;
|
||||
const nullCell = this._activeBuffer.getNullCell(this._eraseAttrData());
|
||||
const last = getWrappedRangeEnd(this._activeBuffer.lines, absY);
|
||||
for (let i = last; i > absY; i--) {
|
||||
this._activeBuffer.clearWrappedRow(i, nullCell);
|
||||
}
|
||||
const line = this._activeBuffer.lines.get(absY);
|
||||
if (line) {
|
||||
line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);
|
||||
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
|
||||
line.isWrapped = false;
|
||||
line.fill(nullCell, respectProtect);
|
||||
this._bufferService.buffer.clearMarkers(absY);
|
||||
this._activeBuffer.clearWrappedRow(absY, nullCell);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1246,10 +1269,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);
|
||||
if (this._activeBuffer.x + 1 >= this._bufferService.cols) {
|
||||
// Deleted entire previous line. This next line can no longer be wrapped.
|
||||
const nextLine = this._activeBuffer.lines.get(j + 1);
|
||||
if (nextLine) {
|
||||
nextLine.isWrapped = false;
|
||||
}
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + j + 1, true);
|
||||
}
|
||||
while (j--) {
|
||||
this._resetBufferLine(j, respectProtect);
|
||||
@@ -1513,7 +1533,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
|
||||
line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
|
||||
line.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
|
||||
}
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
@@ -1546,7 +1566,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
|
||||
line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
|
||||
line.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
|
||||
}
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
@@ -1569,7 +1589,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
|
||||
line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
|
||||
line.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
|
||||
}
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
@@ -1592,7 +1612,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
|
||||
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
|
||||
line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
|
||||
line.isWrapped = false;
|
||||
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
|
||||
}
|
||||
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
|
||||
return true;
|
||||
@@ -3476,7 +3496,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
const line = this._activeBuffer.lines.get(row);
|
||||
if (line) {
|
||||
line.fill(cell);
|
||||
line.isWrapped = false;
|
||||
this._unwrapBufferRow(row, true);
|
||||
}
|
||||
}
|
||||
this._dirtyRowTracker.markAllDirty();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
import { IAttributeData, IBufferLine, ICellData, ICharset } from 'common/Types';
|
||||
import { ExtendedAttrs } from 'common/buffer/AttributeData';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { attachWrappedRow, clearWrappedRow, createScrollWrappedLine } from 'common/buffer/BufferLineUtils';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
@@ -100,7 +101,19 @@ export class Buffer extends Disposable implements IBuffer {
|
||||
}
|
||||
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), isWrapped);
|
||||
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), !!isWrapped);
|
||||
}
|
||||
|
||||
public attachWrappedRow(y: number, fillCellData: ICellData): IBufferLine {
|
||||
return attachWrappedRow(this.lines, y, this._stringCache, this._cols, fillCellData);
|
||||
}
|
||||
|
||||
public clearWrappedRow(y: number, fillCellData: ICellData, preserveContent: boolean = true): void {
|
||||
clearWrappedRow(this.lines, y, fillCellData, this._stringCache, this._cols, preserveContent);
|
||||
}
|
||||
|
||||
public createScrollWrappedLine(lineAbove: IBufferLine | undefined, attr: IAttributeData): IBufferLine {
|
||||
return createScrollWrappedLine(lineAbove, this._stringCache, this._cols, this.getNullCell(attr), attr);
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
|
||||
+270
-73
@@ -76,20 +76,106 @@ export class BufferLine implements IBufferLine {
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
|
||||
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
|
||||
/** When set, this row is a viewport slice of the logical line owned by this buffer line. */
|
||||
protected _logicalHead: BufferLine | undefined;
|
||||
protected _segmentStart: number = 0;
|
||||
protected _logicalCellCount: number = 0;
|
||||
protected _overflowRowCount: number = 0;
|
||||
protected _fillCellData: ICellData;
|
||||
public length: number;
|
||||
public isWrapped: boolean = false;
|
||||
|
||||
constructor(
|
||||
protected readonly _stringCache: IBufferLineStringCache,
|
||||
cols: number,
|
||||
fillCellData?: ICellData,
|
||||
public isWrapped: boolean = false
|
||||
isWrapped: boolean = false,
|
||||
logicalHead?: BufferLine,
|
||||
segmentStart?: number
|
||||
) {
|
||||
this._data = new Uint32Array(cols * Constants.CELL_INDICIES);
|
||||
const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._fillCellData = cell;
|
||||
if (logicalHead) {
|
||||
this._logicalHead = logicalHead;
|
||||
this._segmentStart = segmentStart ?? 0;
|
||||
this._data = logicalHead._data;
|
||||
this._combined = logicalHead._combined;
|
||||
this._extendedAttrs = logicalHead._extendedAttrs;
|
||||
this.length = cols;
|
||||
this.isWrapped = true;
|
||||
return;
|
||||
}
|
||||
this._data = new Uint32Array(cols * Constants.CELL_INDICIES);
|
||||
this._logicalCellCount = cols;
|
||||
for (let i = 0; i < cols; ++i) {
|
||||
this.setCell(i, cell);
|
||||
}
|
||||
this.length = cols;
|
||||
this.isWrapped = isWrapped;
|
||||
}
|
||||
|
||||
public get logicalCellCount(): number {
|
||||
return this._logicalHead ? this._logicalHead._logicalCellCount : this._logicalCellCount;
|
||||
}
|
||||
|
||||
public registerOverflowRow(): void {
|
||||
if (this._logicalHead) {
|
||||
this._logicalHead.registerOverflowRow();
|
||||
} else {
|
||||
this._overflowRowCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterOverflowRow(): void {
|
||||
if (this._logicalHead) {
|
||||
this._logicalHead.unregisterOverflowRow();
|
||||
} else if (this._overflowRowCount > 0) {
|
||||
this._overflowRowCount--;
|
||||
}
|
||||
}
|
||||
|
||||
public clearOverflowRows(): void {
|
||||
if (!this._logicalHead) {
|
||||
this._overflowRowCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** After reflow merges wrapped rows into the head, reset overflow tracking. */
|
||||
public clearReflowMergedState(): void {
|
||||
if (!this._logicalHead) {
|
||||
this._overflowRowCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public ensureLogicalCapacity(minCells: number, fillCellData: ICellData): void {
|
||||
const head = this._logicalHead ?? this;
|
||||
if (minCells <= head._logicalCellCount) {
|
||||
return;
|
||||
}
|
||||
head._invalidateStringCache();
|
||||
const uint32Cells = minCells * Constants.CELL_INDICIES;
|
||||
if (head._data.buffer.byteLength >= uint32Cells * 4) {
|
||||
head._data = new Uint32Array(head._data.buffer, 0, uint32Cells);
|
||||
} else {
|
||||
const data = new Uint32Array(uint32Cells);
|
||||
data.set(head._data);
|
||||
head._data = data;
|
||||
}
|
||||
for (let i = head._logicalCellCount; i < minCells; ++i) {
|
||||
const start = i * Constants.CELL_INDICIES;
|
||||
head._data[start + Cell.CONTENT] = fillCellData.content;
|
||||
head._data[start + Cell.FG] = fillCellData.fg;
|
||||
head._data[start + Cell.BG] = fillCellData.bg;
|
||||
}
|
||||
head._logicalCellCount = minCells;
|
||||
}
|
||||
|
||||
protected _absoluteColumn(column: number): number {
|
||||
return this._segmentStart + column;
|
||||
}
|
||||
|
||||
protected _cellStartIndex(column: number): number {
|
||||
return this._absoluteColumn(column) * Constants.CELL_INDICIES;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,16 +183,18 @@ export class BufferLine implements IBufferLine {
|
||||
* @deprecated
|
||||
*/
|
||||
public get(index: number): CharData {
|
||||
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const abs = this._absoluteColumn(index);
|
||||
const start = this._cellStartIndex(index);
|
||||
const content = this._data[start + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
return [
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG],
|
||||
this._data[start + Cell.FG],
|
||||
(content & Content.IS_COMBINED_MASK)
|
||||
? this._combined[index]
|
||||
? this._combined[abs]
|
||||
: (cp) ? stringFromCodePoint(cp) : '',
|
||||
content >> Content.WIDTH_SHIFT,
|
||||
(content & Content.IS_COMBINED_MASK)
|
||||
? this._combined[index].charCodeAt(this._combined[index].length - 1)
|
||||
? this._combined[abs].charCodeAt(this._combined[abs].length - 1)
|
||||
: cp
|
||||
];
|
||||
}
|
||||
@@ -117,12 +205,14 @@ export class BufferLine implements IBufferLine {
|
||||
*/
|
||||
public set(index: number, value: CharData): void {
|
||||
this._invalidateStringCache();
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
const abs = this._absoluteColumn(index);
|
||||
const start = this._cellStartIndex(index);
|
||||
this._data[start + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
this._combined[abs] = value[1];
|
||||
this._data[start + Cell.CONTENT] = abs | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
} else {
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
this._data[start + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,22 +221,22 @@ export class BufferLine implements IBufferLine {
|
||||
* use these when only one value is needed, otherwise use `loadCell`
|
||||
*/
|
||||
public getWidth(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;
|
||||
return this._data[this._cellStartIndex(index) + Cell.CONTENT] >> Content.WIDTH_SHIFT;
|
||||
}
|
||||
|
||||
/** Test whether content has width. */
|
||||
public hasWidth(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;
|
||||
return this._data[this._cellStartIndex(index) + Cell.CONTENT] & Content.WIDTH_MASK;
|
||||
}
|
||||
|
||||
/** Get FG cell component. */
|
||||
public getFg(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.FG];
|
||||
return this._data[this._cellStartIndex(index) + Cell.FG];
|
||||
}
|
||||
|
||||
/** Get BG cell component. */
|
||||
public getBg(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.BG];
|
||||
return this._data[this._cellStartIndex(index) + Cell.BG];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +245,7 @@ export class BufferLine implements IBufferLine {
|
||||
* from real empty cells.
|
||||
*/
|
||||
public hasContent(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
|
||||
return this._data[this._cellStartIndex(index) + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,23 +254,25 @@ export class BufferLine implements IBufferLine {
|
||||
* a single UTF32 codepoint or the last codepoint of a combined string.
|
||||
*/
|
||||
public getCodePoint(index: number): number {
|
||||
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const abs = this._absoluteColumn(index);
|
||||
const content = this._data[this._cellStartIndex(index) + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
return this._combined[index].charCodeAt(this._combined[index].length - 1);
|
||||
return this._combined[abs].charCodeAt(this._combined[abs].length - 1);
|
||||
}
|
||||
return content & Content.CODEPOINT_MASK;
|
||||
}
|
||||
|
||||
/** Test whether the cell contains a combined string. */
|
||||
public isCombined(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;
|
||||
return this._data[this._cellStartIndex(index) + Cell.CONTENT] & Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
/** Returns the string content of the cell. */
|
||||
public getString(index: number): string {
|
||||
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const abs = this._absoluteColumn(index);
|
||||
const content = this._data[this._cellStartIndex(index) + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
return this._combined[index];
|
||||
return this._combined[abs];
|
||||
}
|
||||
if (content & Content.CODEPOINT_MASK) {
|
||||
return stringFromCodePoint(content & Content.CODEPOINT_MASK);
|
||||
@@ -191,7 +283,7 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** Get state of protected flag. */
|
||||
public isProtected(index: number): number {
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;
|
||||
return this._data[this._cellStartIndex(index) + Cell.BG] & BgFlags.PROTECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,15 +291,16 @@ export class BufferLine implements IBufferLine {
|
||||
* to GC as it significantly reduced the amount of new objects/references needed.
|
||||
*/
|
||||
public loadCell(index: number, cell: ICellData): ICellData {
|
||||
$startIndex = index * Constants.CELL_INDICIES;
|
||||
const abs = this._absoluteColumn(index);
|
||||
$startIndex = this._cellStartIndex(index);
|
||||
cell.content = this._data[$startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[$startIndex + Cell.FG];
|
||||
cell.bg = this._data[$startIndex + Cell.BG];
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
cell.combinedData = this._combined[index];
|
||||
cell.combinedData = this._combined[abs];
|
||||
}
|
||||
if (cell.bg & BgFlags.HAS_EXTENDED) {
|
||||
cell.extended = this._extendedAttrs[index]!;
|
||||
cell.extended = this._extendedAttrs[abs]!;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
@@ -217,15 +310,17 @@ export class BufferLine implements IBufferLine {
|
||||
*/
|
||||
public setCell(index: number, cell: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
const abs = this._absoluteColumn(index);
|
||||
const start = this._cellStartIndex(index);
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
this._combined[index] = cell.combinedData;
|
||||
this._combined[abs] = cell.combinedData;
|
||||
}
|
||||
if (cell.bg & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[index] = cell.extended;
|
||||
this._extendedAttrs[abs] = cell.extended;
|
||||
}
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;
|
||||
this._data[start + Cell.CONTENT] = cell.content;
|
||||
this._data[start + Cell.FG] = cell.fg;
|
||||
this._data[start + Cell.BG] = cell.bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,12 +330,14 @@ export class BufferLine implements IBufferLine {
|
||||
*/
|
||||
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
|
||||
this._invalidateStringCache();
|
||||
const abs = this._absoluteColumn(index);
|
||||
const start = this._cellStartIndex(index);
|
||||
if (attrs.bg & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[index] = attrs.extended;
|
||||
this._extendedAttrs[abs] = attrs.extended;
|
||||
}
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.BG] = attrs.bg;
|
||||
this._data[start + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[start + Cell.FG] = attrs.fg;
|
||||
this._data[start + Cell.BG] = attrs.bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,16 +348,18 @@ export class BufferLine implements IBufferLine {
|
||||
*/
|
||||
public addCodepointToCell(index: number, codePoint: number, width: number): void {
|
||||
this._invalidateStringCache();
|
||||
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const abs = this._absoluteColumn(index);
|
||||
const start = this._cellStartIndex(index);
|
||||
let content = this._data[start + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
// we already have a combined string, simply add
|
||||
this._combined[index] += stringFromCodePoint(codePoint);
|
||||
this._combined[abs] += stringFromCodePoint(codePoint);
|
||||
} else {
|
||||
if (content & Content.CODEPOINT_MASK) {
|
||||
// normal case for combining chars:
|
||||
// - move current leading char + new one into combined string
|
||||
// - set combined flag
|
||||
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
|
||||
this._combined[abs] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
|
||||
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
|
||||
content |= Content.IS_COMBINED_MASK;
|
||||
} else {
|
||||
@@ -273,7 +372,7 @@ export class BufferLine implements IBufferLine {
|
||||
content &= ~Content.WIDTH_MASK;
|
||||
content |= width << Content.WIDTH_SHIFT;
|
||||
}
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;
|
||||
this._data[start + Cell.CONTENT] = content;
|
||||
}
|
||||
|
||||
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
@@ -373,20 +472,28 @@ export class BufferLine implements IBufferLine {
|
||||
*/
|
||||
public resize(cols: number, fillCellData: ICellData): boolean {
|
||||
this._invalidateStringCache();
|
||||
if (this._logicalHead) {
|
||||
if (cols > this.length) {
|
||||
this._logicalHead.ensureLogicalCapacity(this._segmentStart + cols, fillCellData);
|
||||
}
|
||||
this.length = cols;
|
||||
return false;
|
||||
}
|
||||
if (this._overflowRowCount > 0) {
|
||||
if (cols > this.length) {
|
||||
this.ensureLogicalCapacity(Math.max(this._logicalCellCount, this._segmentStart + cols), fillCellData);
|
||||
}
|
||||
this.length = cols;
|
||||
return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
}
|
||||
if (cols === this.length) {
|
||||
return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
}
|
||||
const uint32Cells = cols * Constants.CELL_INDICIES;
|
||||
if (cols > this.length) {
|
||||
if (this._data.buffer.byteLength >= uint32Cells * 4) {
|
||||
// optimization: avoid alloc and data copy if buffer has enough room
|
||||
this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);
|
||||
} else {
|
||||
// slow path: new alloc and full data copy
|
||||
const data = new Uint32Array(uint32Cells);
|
||||
data.set(this._data);
|
||||
this._data = data;
|
||||
}
|
||||
const data = new Uint32Array(uint32Cells);
|
||||
data.set(this._data.subarray(0, this.length * Constants.CELL_INDICIES));
|
||||
this._data = data;
|
||||
for (let i = this.length; i < cols; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
}
|
||||
@@ -411,6 +518,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
}
|
||||
this.length = cols;
|
||||
this._logicalCellCount = cols;
|
||||
return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
}
|
||||
|
||||
@@ -421,10 +529,12 @@ export class BufferLine implements IBufferLine {
|
||||
* Returns 0 or 1 indicating whether a cleanup happened.
|
||||
*/
|
||||
public cleanupMemory(): number {
|
||||
if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
|
||||
const data = new Uint32Array(this._data.length);
|
||||
data.set(this._data);
|
||||
this._data = data;
|
||||
const head = this._logicalHead ?? this;
|
||||
const usedCells = head._logicalCellCount * Constants.CELL_INDICIES;
|
||||
if (usedCells * 4 * Constants.CLEANUP_THRESHOLD < head._data.buffer.byteLength) {
|
||||
const data = new Uint32Array(usedCells);
|
||||
data.set(head._data.subarray(0, usedCells));
|
||||
head._data = data;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
@@ -442,8 +552,10 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._combined = {};
|
||||
this._extendedAttrs = {};
|
||||
if (!this._logicalHead) {
|
||||
this._combined = {};
|
||||
this._extendedAttrs = {};
|
||||
}
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
this.setCell(i, fillCellData);
|
||||
}
|
||||
@@ -451,30 +563,72 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** alter to a full copy of line */
|
||||
public copyFrom(line: BufferLine): void {
|
||||
if (this._logicalHead) {
|
||||
this.materializeFromLogicalHead(this._fillCellData);
|
||||
}
|
||||
this._invalidateStringCache();
|
||||
this._logicalHead = undefined;
|
||||
this._segmentStart = 0;
|
||||
this._overflowRowCount = 0;
|
||||
if (line._logicalHead) {
|
||||
const standalone = new BufferLine(this._stringCache, line.length, this._fillCellData, false);
|
||||
standalone.copyCellsFrom(line, 0, 0, line.length, false);
|
||||
line = standalone;
|
||||
}
|
||||
if (this.length !== line.length) {
|
||||
this._data = new Uint32Array(line._data);
|
||||
this._data = new Uint32Array(line._data.subarray(0, line.length * Constants.CELL_INDICIES));
|
||||
} else {
|
||||
// use high speed copy if lengths are equal
|
||||
this._data.set(line._data);
|
||||
this._data.set(line._data.subarray(0, line.length * Constants.CELL_INDICIES));
|
||||
}
|
||||
this.length = line.length;
|
||||
this._logicalCellCount = line._logicalCellCount || line.length;
|
||||
this._combined = {};
|
||||
for (const el in line._combined) {
|
||||
this._combined[el] = line._combined[el];
|
||||
const key = parseInt(el, 10);
|
||||
if (key < this._logicalCellCount) {
|
||||
this._combined[key] = line._combined[el];
|
||||
}
|
||||
}
|
||||
this._extendedAttrs = {};
|
||||
for (const el in line._extendedAttrs) {
|
||||
this._extendedAttrs[el] = line._extendedAttrs[el];
|
||||
const key = parseInt(el, 10);
|
||||
if (key < this._logicalCellCount) {
|
||||
this._extendedAttrs[key] = line._extendedAttrs[el];
|
||||
}
|
||||
}
|
||||
this.isWrapped = line.isWrapped;
|
||||
}
|
||||
|
||||
public materializeFromLogicalHead(fillCellData: ICellData): BufferLine {
|
||||
if (!this._logicalHead) {
|
||||
return this;
|
||||
}
|
||||
const standalone = new BufferLine(this._stringCache, this.length, fillCellData, false);
|
||||
standalone.copyCellsFrom(this._logicalHead, this._segmentStart, 0, this.length, false);
|
||||
this._logicalHead.unregisterOverflowRow();
|
||||
this._data = standalone._data;
|
||||
this._combined = standalone._combined;
|
||||
this._extendedAttrs = standalone._extendedAttrs;
|
||||
this._logicalHead = undefined;
|
||||
this._segmentStart = 0;
|
||||
this._logicalCellCount = this.length;
|
||||
this.isWrapped = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** create a new clone */
|
||||
public clone(): IBufferLine {
|
||||
if (this._logicalHead) {
|
||||
const newLine = new BufferLine(this._stringCache, 0, undefined, true, this._logicalHead, this._segmentStart);
|
||||
newLine.length = this.length;
|
||||
return newLine;
|
||||
}
|
||||
const newLine = new BufferLine(this._stringCache, 0, undefined, false);
|
||||
newLine._data = new Uint32Array(this._data);
|
||||
newLine._data = new Uint32Array(this._data.subarray(0, this._logicalCellCount * Constants.CELL_INDICIES));
|
||||
newLine.length = this.length;
|
||||
newLine._logicalCellCount = this._logicalCellCount;
|
||||
newLine._overflowRowCount = this._overflowRowCount;
|
||||
for (const el in this._combined) {
|
||||
newLine._combined[el] = this._combined[el];
|
||||
}
|
||||
@@ -487,8 +641,34 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
public getTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
|
||||
return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
const start = this._cellStartIndex(i);
|
||||
if ((this._data[start + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
|
||||
return i + (this._data[start + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Creates a standalone line containing the full logical line content. */
|
||||
public toStandaloneLogicalLine(cols: number, fillCellData: ICellData, stringCache: IBufferLineStringCache): BufferLine {
|
||||
const head = this._logicalHead ?? this;
|
||||
const trimmed = head.getLogicalTrimmedLength();
|
||||
const flat = new BufferLine(stringCache, cols, fillCellData, false);
|
||||
if (trimmed > 0) {
|
||||
flat.copyCellsFrom(head, 0, 0, trimmed, false);
|
||||
flat._logicalCellCount = trimmed;
|
||||
}
|
||||
head.clearReflowMergedState();
|
||||
return flat;
|
||||
}
|
||||
|
||||
/** Trimmed length across the entire logical line (all segments). */
|
||||
public getLogicalTrimmedLength(): number {
|
||||
const head = this._logicalHead ?? this;
|
||||
for (let i = head._logicalCellCount - 1; i >= 0; --i) {
|
||||
const start = i * Constants.CELL_INDICIES;
|
||||
if ((head._data[start + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
|
||||
return i + (head._data[start + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -496,8 +676,9 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
public getNoBgTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {
|
||||
return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
const start = this._cellStartIndex(i);
|
||||
if ((this._data[start + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[start + Cell.BG] & Attributes.CM_MASK)) {
|
||||
return i + (this._data[start + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -506,22 +687,32 @@ export class BufferLine implements IBufferLine {
|
||||
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
|
||||
this._invalidateStringCache();
|
||||
const srcData = src._data;
|
||||
const srcOffset = src._segmentStart;
|
||||
const destOffset = this._segmentStart;
|
||||
if (applyInReverse) {
|
||||
for (let cell = length - 1; cell >= 0; cell--) {
|
||||
const srcStart = (srcOffset + srcCol + cell) * Constants.CELL_INDICIES;
|
||||
const destStart = (destOffset + destCol + cell) * Constants.CELL_INDICIES;
|
||||
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
|
||||
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
|
||||
this._data[destStart + i] = srcData[srcStart + i];
|
||||
}
|
||||
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
|
||||
if (srcData[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const absSrc = srcOffset + srcCol + cell;
|
||||
const absDest = destOffset + destCol + cell;
|
||||
this._extendedAttrs[absDest] = src._extendedAttrs[absSrc];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let cell = 0; cell < length; cell++) {
|
||||
const srcStart = (srcOffset + srcCol + cell) * Constants.CELL_INDICIES;
|
||||
const destStart = (destOffset + destCol + cell) * Constants.CELL_INDICIES;
|
||||
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
|
||||
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
|
||||
this._data[destStart + i] = srcData[srcStart + i];
|
||||
}
|
||||
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
|
||||
if (srcData[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const absSrc = srcOffset + srcCol + cell;
|
||||
const absDest = destOffset + destCol + cell;
|
||||
this._extendedAttrs[absDest] = src._extendedAttrs[absSrc];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,8 +721,9 @@ export class BufferLine implements IBufferLine {
|
||||
const srcCombinedKeys = Object.keys(src._combined);
|
||||
for (let i = 0; i < srcCombinedKeys.length; i++) {
|
||||
const key = parseInt(srcCombinedKeys[i], 10);
|
||||
if (key >= srcCol) {
|
||||
this._combined[key - srcCol + destCol] = src._combined[key];
|
||||
const srcAbsCol = srcOffset + srcCol;
|
||||
if (key >= srcAbsCol && key < srcAbsCol + length) {
|
||||
this._combined[key - srcCol + destOffset + destCol] = src._combined[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,9 +766,13 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
$translateToStringBuilder.reset();
|
||||
while (startCol < endCol) {
|
||||
const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const abs = this._absoluteColumn(startCol);
|
||||
const start = this._cellStartIndex(startCol);
|
||||
const content = this._data[start + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
|
||||
const chars = (content & Content.IS_COMBINED_MASK)
|
||||
? (this._combined[abs] ?? '')
|
||||
: (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
|
||||
$translateToStringBuilder.append(chars);
|
||||
if (outColumns) {
|
||||
for (let i = 0; i < chars.length; ++i) {
|
||||
@@ -613,8 +809,9 @@ export class BufferLine implements IBufferLine {
|
||||
return cacheEntry;
|
||||
}
|
||||
|
||||
private _invalidateStringCache(): void {
|
||||
const cacheEntry = this._getStringCacheEntry(false);
|
||||
protected _invalidateStringCache(): void {
|
||||
const head = this._logicalHead ?? this;
|
||||
const cacheEntry = head._getStringCacheEntry(false);
|
||||
if (cacheEntry) {
|
||||
cacheEntry.value = undefined;
|
||||
cacheEntry.isTrimmed = false;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { BufferOverflowLine } from 'common/buffer/BufferOverflowLine';
|
||||
import { IAttributeData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { IBufferLineStringCache } from 'common/buffer/BufferLine';
|
||||
|
||||
export function getLogicalHead(line: IBufferLine): BufferLine {
|
||||
if (line instanceof BufferOverflowLine) {
|
||||
return line.head;
|
||||
}
|
||||
return line as BufferLine;
|
||||
}
|
||||
|
||||
export function isBufferOverflowLine(line: IBufferLine): line is BufferOverflowLine {
|
||||
return line instanceof BufferOverflowLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logical cell offset for the start of a buffer row.
|
||||
*/
|
||||
export function getSegmentStart(line: IBufferLine): number {
|
||||
if (line instanceof BufferOverflowLine) {
|
||||
return line.segmentStart;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the row at `y` as a wrapped continuation of the logical line above.
|
||||
* Extends the logical line storage and replaces the row with a {@link BufferOverflowLine}.
|
||||
*/
|
||||
export function attachWrappedRow(
|
||||
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void },
|
||||
y: number,
|
||||
stringCache: IBufferLineStringCache,
|
||||
cols: number,
|
||||
fillCellData: ICellData
|
||||
): IBufferLine {
|
||||
const headY = y > 0 ? getWrappedRangeStart(lines, y - 1) : y;
|
||||
const head = getLogicalHead(lines.get(headY)!);
|
||||
const segmentStart = head.logicalCellCount;
|
||||
head.ensureLogicalCapacity(segmentStart + cols, fillCellData);
|
||||
|
||||
const existingLine = lines.get(y);
|
||||
if (existingLine && !(existingLine instanceof BufferOverflowLine) && existingLine.getTrimmedLength() > 0) {
|
||||
head.copyCellsFrom(existingLine as BufferLine, 0, segmentStart, existingLine.length, false);
|
||||
}
|
||||
|
||||
const overflowLine = new BufferOverflowLine(head, segmentStart, stringCache, cols, fillCellData);
|
||||
lines.set(y, overflowLine);
|
||||
head.registerOverflowRow();
|
||||
return overflowLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the wrapped state for a row, detaching overflow rows from their logical line.
|
||||
*/
|
||||
export function clearWrappedRow(
|
||||
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void },
|
||||
y: number,
|
||||
fillCellData: ICellData,
|
||||
stringCache?: IBufferLineStringCache,
|
||||
cols?: number,
|
||||
preserveContent: boolean = true
|
||||
): void {
|
||||
const line = lines.get(y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
if (line.isWrapped) {
|
||||
if (line instanceof BufferOverflowLine) {
|
||||
if (preserveContent) {
|
||||
lines.set(y, line.materializeToStandalone(fillCellData));
|
||||
} else if (stringCache !== undefined && cols !== undefined) {
|
||||
line.head.unregisterOverflowRow();
|
||||
lines.set(y, new BufferLine(stringCache, cols, fillCellData, false));
|
||||
} else {
|
||||
lines.set(y, line.materializeToStandalone(fillCellData));
|
||||
}
|
||||
} else if (line instanceof BufferLine) {
|
||||
(line as BufferLine).materializeFromLogicalHead(fillCellData);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const head = line as BufferLine;
|
||||
head.clearOverflowRows();
|
||||
}
|
||||
|
||||
export function getWrappedRangeStart(lines: { get(index: number): IBufferLine | undefined }, y: number): number {
|
||||
let first = y;
|
||||
while (first > 0 && lines.get(first)?.isWrapped) {
|
||||
first--;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
export function getWrappedRangeEnd(lines: { get(index: number): IBufferLine | undefined; length: number }, y: number): number {
|
||||
let last = y;
|
||||
while (last + 1 < lines.length && lines.get(last + 1)?.isWrapped) {
|
||||
last++;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a logical line group into a single standalone buffer row and removes overflow rows.
|
||||
* Used before reflow so existing reflow algorithms can operate on independent lines.
|
||||
*/
|
||||
export function flattenLogicalGroup(
|
||||
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void; length: number },
|
||||
headY: number,
|
||||
cols: number,
|
||||
fillCellData: ICellData,
|
||||
stringCache: IBufferLineStringCache
|
||||
): void {
|
||||
const last = getWrappedRangeEnd(lines, headY);
|
||||
if (last <= headY) {
|
||||
return;
|
||||
}
|
||||
const head = getLogicalHead(lines.get(headY)!);
|
||||
lines.set(headY, head.toStandaloneLogicalLine(cols, fillCellData, stringCache));
|
||||
for (let i = headY + 1; i <= last; i++) {
|
||||
const line = lines.get(i);
|
||||
if (line instanceof BufferOverflowLine) {
|
||||
const standalone = line.materializeToStandalone(fillCellData);
|
||||
standalone.isWrapped = true;
|
||||
lines.set(i, standalone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function materializeLogicalLinesForReflow(
|
||||
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void; length: number },
|
||||
cols: number,
|
||||
fillCellData: ICellData,
|
||||
stringCache: IBufferLineStringCache
|
||||
): void {
|
||||
let y = 0;
|
||||
while (y < lines.length) {
|
||||
const line = lines.get(y);
|
||||
const next = lines.get(y + 1);
|
||||
if (line && next instanceof BufferOverflowLine && next.head === getLogicalHead(line)) {
|
||||
flattenLogicalGroup(lines, y, cols, fillCellData, stringCache);
|
||||
y++;
|
||||
continue;
|
||||
}
|
||||
y++;
|
||||
}
|
||||
}
|
||||
|
||||
export function createScrollWrappedLine(
|
||||
lineAbove: IBufferLine | undefined,
|
||||
stringCache: IBufferLineStringCache,
|
||||
cols: number,
|
||||
fillCellData: ICellData,
|
||||
attr: IAttributeData
|
||||
): IBufferLine {
|
||||
if (!lineAbove) {
|
||||
return new BufferLine(stringCache, cols, fillCellData, true);
|
||||
}
|
||||
const head = getLogicalHead(lineAbove);
|
||||
const segmentStart = head.logicalCellCount;
|
||||
head.ensureLogicalCapacity(segmentStart + cols, fillCellData);
|
||||
const overflowLine = new BufferOverflowLine(head, segmentStart, stringCache, cols, fillCellData);
|
||||
head.registerOverflowRow();
|
||||
return overflowLine;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { attachWrappedRow } from 'common/buffer/BufferLineUtils';
|
||||
import { BufferOverflowLine } from 'common/buffer/BufferOverflowLine';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH } from 'common/buffer/Constants';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
const NULL_CELL = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
|
||||
class LineList {
|
||||
private _lines: IBufferLine[] = [];
|
||||
public get(index: number): IBufferLine | undefined { return this._lines[index]; }
|
||||
public set(index: number, line: IBufferLine): void { this._lines[index] = line; }
|
||||
public get length(): number { return this._lines.length; }
|
||||
}
|
||||
|
||||
describe('BufferLogicalLine', () => {
|
||||
it('should share storage across wrapped rows', () => {
|
||||
const cols = 5;
|
||||
const lines = new LineList();
|
||||
const head = new BufferLine(TEST_STRING_CACHE, cols, NULL_CELL, false);
|
||||
lines.set(0, head);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
head.setCellFromCodepoint(i, 'a'.charCodeAt(0) + i, 1, NULL_CELL);
|
||||
}
|
||||
attachWrappedRow(lines, 1, TEST_STRING_CACHE, cols, NULL_CELL);
|
||||
const overflow = lines.get(1)!;
|
||||
assert.instanceOf(overflow, BufferOverflowLine);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
(overflow as BufferLine).setCellFromCodepoint(i, 'f'.charCodeAt(0) + i, 1, NULL_CELL);
|
||||
}
|
||||
assert.equal(head.translateToString(true), 'abcde');
|
||||
assert.equal(overflow.translateToString(true), 'fghij');
|
||||
assert.equal(head.getLogicalTrimmedLength(), 10);
|
||||
});
|
||||
|
||||
it('should reflow unwrap without copying when logical content fits', () => {
|
||||
const lines = new LineList();
|
||||
const head = new BufferLine(TEST_STRING_CACHE, 5, NULL_CELL, false);
|
||||
lines.set(0, head);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
head.setCellFromCodepoint(i, 'f'.charCodeAt(0) + i, 1, NULL_CELL);
|
||||
}
|
||||
attachWrappedRow(lines, 1, TEST_STRING_CACHE, 2, NULL_CELL);
|
||||
const overflow = lines.get(1)! as BufferOverflowLine;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
overflow.setCellFromCodepoint(i, 'k'.charCodeAt(0) + i, 1, NULL_CELL);
|
||||
}
|
||||
assert.equal(head.getLogicalTrimmedLength(), 7);
|
||||
assert.equal(overflow.translateToString(true), 'kl');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICellData } from 'common/Types';
|
||||
import { BufferLine, IBufferLineStringCache } from 'common/buffer/BufferLine';
|
||||
|
||||
/**
|
||||
* A viewport row that is a view into a slice of a parent {@link BufferLine}'s logical storage.
|
||||
*/
|
||||
export class BufferOverflowLine extends BufferLine {
|
||||
public readonly head: BufferLine;
|
||||
public readonly segmentStart: number;
|
||||
|
||||
constructor(
|
||||
head: BufferLine,
|
||||
segmentStart: number,
|
||||
stringCache: IBufferLineStringCache,
|
||||
cols: number,
|
||||
fillCellData: ICellData
|
||||
) {
|
||||
super(stringCache, cols, fillCellData, true, head, segmentStart);
|
||||
this.head = head;
|
||||
this.segmentStart = segmentStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies this row's segment into a standalone {@link BufferLine}.
|
||||
*/
|
||||
public materializeToStandalone(fillCellData: ICellData): BufferLine {
|
||||
return this.materializeFromLogicalHead(fillCellData);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ export interface IBuffer {
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
|
||||
attachWrappedRow(y: number, fillCellData: ICellData): IBufferLine;
|
||||
clearWrappedRow(y: number, fillCellData: ICellData, preserveContent?: boolean): void;
|
||||
createScrollWrappedLine(lineAbove: IBufferLine | undefined, attr: IAttributeData): IBufferLine;
|
||||
getNullCell(attr?: IAttributeData): ICellData;
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData;
|
||||
addMarker(y: number): IMarker;
|
||||
|
||||
@@ -69,17 +69,17 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
|
||||
const buffer = this.buffer;
|
||||
|
||||
let newLine: IBufferLine | undefined;
|
||||
newLine = this._cachedBlankLine;
|
||||
const topRow = buffer.ybase + buffer.scrollTop;
|
||||
const bottomRow = buffer.ybase + buffer.scrollBottom;
|
||||
|
||||
let newLine: IBufferLine;
|
||||
newLine = this._cachedBlankLine!;
|
||||
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
|
||||
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
|
||||
this._cachedBlankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
|
||||
const topRow = buffer.ybase + buffer.scrollTop;
|
||||
const bottomRow = buffer.ybase + buffer.scrollBottom;
|
||||
|
||||
if (buffer.scrollTop === 0) {
|
||||
// Determine whether the buffer is going to be trimmed after insertion.
|
||||
const willBufferBeTrimmed = buffer.lines.isFull;
|
||||
|
||||
Reference in New Issue
Block a user