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 <Tyriar@users.noreply.github.com>
This commit is contained in:
co-authored by
Daniel Imms
parent
c2ecfe7446
commit
3556f16138
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user