Compare commits

...
Author SHA1 Message Date
Daniel Imms c2f5c424a8 Merge branch 'master' into fix/buffer-copy-and-reflow-wrap 2026-06-06 11:16:13 -07:00
Daniel ImmsandCursor e9c648f519 fix(BufferReflow): clear stale isWrapped after larger reflow
When widening the terminal unwraps a multi-row soft wrap and removes
trailing buffer lines, the last retained row could still have
isWrapped set, and the following line could still think it continued
a removed row. That broke wrapped range translation.

Clear isWrapped on the last retained row in the group and on the
first line after the group when rows are removed during reflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:37:20 +00:00
Daniel ImmsandCursor 60bae9528d fix(BufferLine): limit copyCellsFrom metadata to copied range
copyCellsFrom copied Uint32 cell data only for the requested column
range but migrated every combined entry with key >= srcCol, which could
attach grapheme metadata to unrelated destination columns during
partial line copies in reflow and wraparound.

Copy combined and extended attributes only per copied cell and clear
stale entries on the destination when the source cell does not use
them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:36:39 +00:00
4 changed files with 47 additions and 0 deletions
+15
View File
@@ -495,6 +495,21 @@ describe('BufferLine', function(): void {
assert.deepEqual(columns, [0]);
});
});
describe('copyCellsFrom', () => {
it('should only move combined data within the copied range', () => {
const src = new TestBufferLine(10, NULL_CELL_DATA, false);
src.setCell(1, createCellData(1, 'a', 1));
src.addCodepointToCell(1, '\u0301'.charCodeAt(0), 0);
src.setCell(5, createCellData(1, 'z', 1));
src.addCodepointToCell(5, '\u0301'.charCodeAt(0), 0);
const dest = new TestBufferLine(10, NULL_CELL_DATA, false);
dest.copyCellsFrom(src, 0, 0, 2, false);
assert.property(dest.combined, '1');
assert.notProperty(dest.combined, '5');
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
+4
View File
@@ -609,9 +609,13 @@ export class BufferLine implements IBufferLine {
const srcStart = srcCol * Constants.CELL_INDICIES;
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
this._combined[destCol] = src._combined[srcCol];
} else {
delete this._combined[destCol];
}
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
} else {
delete this._extendedAttrs[destCol];
}
}
+23
View File
@@ -10,6 +10,7 @@ import { CellData } from './CellData';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Constants';
import { reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { IBufferLine } from './Types';
import { createCellData, NULL_CELL_DATA } from '../TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
@@ -122,5 +123,27 @@ describe('BufferReflow', () => {
const lines = createWrappedLines('abcde');
assert.notDeepEqual(reflowLargerGetLinesToRemove(lines, 1, 5, 10, nullCell, false), []);
});
it('should clear isWrapped on the last retained row after unwrap', () => {
const lines = new CircularList<BufferLine>(10);
const set2 = (line: BufferLine, s: string) => {
for (let i = 0; i < s.length; i++) {
line.setCell(i, createCellData(0, s[i], 1));
}
};
const l0 = new BufferLine(TEST_STRING_CACHE, 2);
set2(l0, 'ab');
const l1 = new BufferLine(TEST_STRING_CACHE, 2, undefined, true);
set2(l1, 'cd');
const l2 = new BufferLine(TEST_STRING_CACHE, 2, undefined, true);
set2(l2, 'ef');
lines.push(l0);
lines.push(l1);
lines.push(l2);
reflowLargerGetLinesToRemove(lines, 2, 6, 99, NULL_CELL_DATA, false);
assert.equal(l1.isWrapped, false);
});
});
});
+5
View File
@@ -99,6 +99,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, o
}
if (countToRemove > 0) {
wrappedLines[destLineIndex].isWrapped = false;
const lineAfterGroup = lines.get(y + wrappedLines.length);
if (lineAfterGroup) {
lineAfterGroup.isWrapped = false;
}
toRemove.push(y + wrappedLines.length - countToRemove); // index
toRemove.push(countToRemove);
}