From 3556f161382330e35b5ec9361b56b24c6df3844f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 28 May 2026 03:42:52 +0000 Subject: [PATCH] feat(buffer): add logical line storage for wrapped rows Introduce BufferOverflowLine rows that share cell storage with a parent BufferLine head, implementing the logical-line model from issue #4800. - Extend BufferLine with logical capacity and segment-aware indexing - Add BufferOverflowLine, BufferLineUtils helpers, and unit tests - Expose attachWrappedRow/clearWrappedRow on Buffer for future integration - Keep InputHandler on isWrapped flag until reflow/SL paths are updated Reflow still uses per-row buffers; materializeLogicalLinesForReflow helpers are included for a follow-up that simplifies resize reflow. Co-authored-by: Daniel Imms --- src/browser/TestUtils.test.ts | 9 + src/common/InputHandler.ts | 56 +++- src/common/buffer/Buffer.ts | 15 +- src/common/buffer/BufferLine.ts | 343 +++++++++++++++----- src/common/buffer/BufferLineUtils.ts | 171 ++++++++++ src/common/buffer/BufferLogicalLine.test.ts | 60 ++++ src/common/buffer/BufferOverflowLine.ts | 34 ++ src/common/buffer/Types.ts | 3 + src/common/services/BufferService.ts | 10 +- 9 files changed, 604 insertions(+), 97 deletions(-) create mode 100644 src/common/buffer/BufferLineUtils.ts create mode 100644 src/common/buffer/BufferLogicalLine.test.ts create mode 100644 src/common/buffer/BufferOverflowLine.ts diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 65a18da72..43299988d 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -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.'); } diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index fd1fdb8a1..a0d88a455 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -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(); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 34ef02f6a..1c1cff1a1 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -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 { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index e24a000c9..34c84f288 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -76,20 +76,106 @@ export class BufferLine implements IBufferLine { protected _combined: {[index: number]: string} = {}; protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; protected _stringCacheEntryRef: WeakRef | 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; diff --git a/src/common/buffer/BufferLineUtils.ts b/src/common/buffer/BufferLineUtils.ts new file mode 100644 index 000000000..07699dc44 --- /dev/null +++ b/src/common/buffer/BufferLineUtils.ts @@ -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; +} diff --git a/src/common/buffer/BufferLogicalLine.test.ts b/src/common/buffer/BufferLogicalLine.test.ts new file mode 100644 index 000000000..ece2f8ad6 --- /dev/null +++ b/src/common/buffer/BufferLogicalLine.test.ts @@ -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'); + }); +}); diff --git a/src/common/buffer/BufferOverflowLine.ts b/src/common/buffer/BufferOverflowLine.ts new file mode 100644 index 000000000..a7f85f934 --- /dev/null +++ b/src/common/buffer/BufferOverflowLine.ts @@ -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); + } +} diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts index 85dd68eec..138ddd284 100644 --- a/src/common/buffer/Types.ts +++ b/src/common/buffer/Types.ts @@ -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; diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 06ac6b10a..a95df55ce 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -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;