fix(buffer): clear stale attrs and combined data in BufferLine (#5972)

* fix(buffer): bound combined-cell copy in copyCellsFrom

When copying a column range, combined-string metadata was copied for
every source column >= srcCol with no upper limit. Partial copies could
pull _combined entries from outside [srcCol, srcCol + length) into the
destination and corrupt cells beyond the copied span.

Only copy combined keys within the same range as the cell data loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear stale _combined entry in BufferLine.setCell

setCell updated combined data when IS_COMBINED_MASK was set but left
orphaned _combined[index] entries when writing a non-combined cell.
Delete the map entry in that case so later combined copies and lookups
stay aligned with cell content flags.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear stale extended attrs in BufferLine.setCell

setCell stored extended attributes when HAS_EXTENDED was set but did
not remove orphaned _extendedAttrs[index] when overwriting with a
cell without extended background data. Delete the entry in that case,
matching the combined-cell cleanup added for setCell earlier.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear dest extended attrs in copyCellsFrom

When copying a column range, extended attributes were copied from
source cells with HAS_EXTENDED but stale _extendedAttrs on the
destination were left for cells without extended data. Delete dest
entries in that case, matching setCell behavior and the bounded
_combined copy loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear extended attrs in setCellFromCodepoint

The input-handler fast path set extended data when HAS_EXTENDED was
present but left orphaned _extendedAttrs entries when overwriting
with non-extended cells. Delete the map entry in that case, matching
setCell and copyCellsFrom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear stale combined data in setCellFromCodepoint

When the input handler overwrites a cell that previously held a
grapheme cluster (IS_COMBINED_MASK), setCellFromCodepoint updated
_data and _extendedAttrs but left this._combined[index] intact.
setCell already deletes _combined for non-combined cells; this path
did not, so getString and renderers could show the old cluster string
after a normal codepoint write.

Add delete this._combined[index] at the start of setCellFromCodepoint,
matching setCell's non-combined branch and the extended-attrs cleanup
already performed in the same method.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(buffer): only delete stale combined/extended map entries when present

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* fix(buffer): treat combined/extended maps as flag-gated caches

_flags in _data are authoritative; sparse maps are only read when
IS_COMBINED_MASK or HAS_EXTENDED is set. setCellFromCodepoint no longer
touches _combined. loadCell clears CellData side fields when flags are off.
copyCellsFrom copies map entries per-cell from src flags only.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
Daniel Imms
2026-06-02 03:04:25 +00:00
committed by GitHub
co-authored by Daniel Imms Cursor
parent 1f054097f3
commit b3de9fa6db
+32 -29
View File
@@ -73,7 +73,9 @@ export interface IBufferLineStringCache {
*/
export class BufferLine implements IBufferLine {
protected _data: Uint32Array;
/** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */
protected _combined: {[index: number]: string} = {};
/** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
public length: number;
@@ -205,9 +207,15 @@ export class BufferLine implements IBufferLine {
cell.bg = this._data[$startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
} else {
cell.combinedData = '';
}
if (cell.bg & BgFlags.HAS_EXTENDED) {
cell.extended = this._extendedAttrs[index]!;
} else {
// Do not mutate cell.extended in place: it may still reference this line's map entry from a
// prior loadCell into a reused CellData (e.g. $workCell during insert/delete).
cell.extended = DEFAULT_ATTR_DATA.extended.clone();
}
return cell;
}
@@ -459,14 +467,7 @@ export class BufferLine implements IBufferLine {
this._data.set(line._data);
}
this.length = line.length;
this._combined = {};
for (const el in line._combined) {
this._combined[el] = line._combined[el];
}
this._extendedAttrs = {};
for (const el in line._extendedAttrs) {
this._extendedAttrs[el] = line._extendedAttrs[el];
}
this._copySparseMapsFrom(line);
this.isWrapped = line.isWrapped;
}
@@ -475,12 +476,7 @@ export class BufferLine implements IBufferLine {
const newLine = new BufferLine(this._stringCache, 0, undefined, false);
newLine._data = new Uint32Array(this._data);
newLine.length = this.length;
for (const el in this._combined) {
newLine._combined[el] = this._combined[el];
}
for (const el in this._extendedAttrs) {
newLine._extendedAttrs[el] = this._extendedAttrs[el];
}
newLine._copySparseMapsFrom(this);
newLine.isWrapped = this.isWrapped;
return newLine;
}
@@ -511,27 +507,14 @@ export class BufferLine implements IBufferLine {
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
}
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
}
this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);
}
} else {
for (let cell = 0; cell < length; cell++) {
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
}
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
}
}
}
// Move any combined data over as needed, FIXME: repeat for extended attrs
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];
this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);
}
}
}
@@ -620,4 +603,24 @@ export class BufferLine implements IBufferLine {
cacheEntry.isTrimmed = false;
}
}
/** Copy sparse map entries for a single cell when `_data` flags require them. */
private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {
const srcStart = srcCol * Constants.CELL_INDICIES;
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
this._combined[destCol] = src._combined[srcCol];
}
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
}
}
/** Rebuild sparse maps from another line, keyed only by `_data` flags. */
private _copySparseMapsFrom(line: BufferLine): void {
this._combined = {};
this._extendedAttrs = {};
for (let i = 0; i < line.length; i++) {
this._copyCellMapsFrom(line, i, i);
}
}
}