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
3cdbc77386 fix(addon-search): advance highlight scan by buffer match size (#5994)
* fix(addon-search): advance highlight scan by buffer match size

_highlightAllMatches() stepped to the next find() using term.length
and col + 1 when staying on the same row. Search results expose size
in buffer columns (wide glyphs, wrapped matches), which select() and
decorations already use.

Advance with col + size and wrap row/col when crossing the terminal
width so highlight-all does not skip, duplicate, or stop early on
non-ASCII matches.

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

* test(addon-search): regression for highlight-all wide char advance by size

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

* test(addon-search): move highlight-all regression to integration tests

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>
2026-06-06 11:12:04 -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
10 changed files with 96 additions and 201 deletions
+10 -10
View File
@@ -436,9 +436,9 @@ export class ImageStorage implements IDisposable {
for (let col = 0; col < cols; ++col) { for (let col = 0; col < cols; ++col) {
let e: IExtendedAttrsImage; let e: IExtendedAttrsImage;
if (line.getBg(col) & BgFlags.HAS_EXTENDED) { if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
e = line._extendedAttrs?.[col] ?? EMPTY_ATTRS; e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
} else { } else {
const maybeImg = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined; const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) { if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
continue; continue;
} }
@@ -461,7 +461,7 @@ export class ImageStorage implements IDisposable {
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED. * Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
*/ */
while (++col < cols) { while (++col < cols) {
const nextE = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined; const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) { if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break; break;
} }
@@ -515,7 +515,7 @@ export class ImageStorage implements IDisposable {
for (let row = 0; row < rows; ++row) { for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row) as IBufferLineExt; const line = buffer.lines.get(row) as IBufferLineExt;
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) { if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs?.[oldCol] ?? EMPTY_ATTRS; const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
const imageId = e.imageId; const imageId = e.imageId;
if (imageId === undefined || imageId === -1) { if (imageId === undefined || imageId === -1) {
continue; continue;
@@ -560,7 +560,7 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer; const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt; const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS; const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) { if (e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig; const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) { if (window.ImageBitmap && orig instanceof ImageBitmap) {
@@ -580,7 +580,7 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer; const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt; const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS; const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) { if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId); const spec = this._images.get(e.imageId);
if (spec) { if (spec) {
@@ -611,7 +611,7 @@ export class ImageStorage implements IDisposable {
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void { private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const old = line._extendedAttrs?.[x]; const old = line._extendedAttrs[x];
if (old) { if (old) {
if (old.imageId !== undefined) { if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that // found an old ExtendedAttrsImage, since we know that
@@ -627,13 +627,13 @@ export class ImageStorage implements IDisposable {
return; return;
} }
// found a plain ExtendedAttrs instance, clone it to new entry // found a plain ExtendedAttrs instance, clone it to new entry
(line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId); line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
return; return;
} }
} }
// fall-through: always create new ExtendedAttrsImage entry // fall-through: always create new ExtendedAttrsImage entry
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED; line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
(line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(0, 0, imageId, tileId); line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
} }
private _evictOnAlternate(): void { private _evictOnAlternate(): void {
@@ -652,7 +652,7 @@ export class ImageStorage implements IDisposable {
} }
for (let x = 0; x < this._terminal.cols; ++x) { for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const imgId = line._extendedAttrs?.[x]?.imageId; const imgId = line._extendedAttrs[x]?.imageId;
if (imgId) { if (imgId) {
const spec = this._images.get(imgId); const spec = this._images.get(imgId);
if (spec) { if (spec) {
+1 -1
View File
@@ -64,7 +64,7 @@ export interface IExtendedAttrsImage extends IExtendedAttrs {
/* eslint-disable */ /* eslint-disable */
export interface IBufferLineExt extends IBufferLine { export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined} | undefined; _extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
_data: Uint32Array; _data: Uint32Array;
} }
+1 -4
View File
@@ -118,10 +118,7 @@ test.describe('ImageAddon', () => {
} }
// bufferline privates // bufferline privates
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true); strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
strictEqual(await ctx.page.evaluate(`(() => { strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
const ea = window.term._core.buffer.lines.get(0)._extendedAttrs;
return ea === undefined || (typeof ea === 'object' && ea !== null);
})()`), true);
// inputhandler privates // inputhandler privates
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), '_AttributeData'); strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), '_AttributeData');
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser'); strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
+8 -6
View File
@@ -142,12 +142,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
prevResult = result; prevResult = result;
results.push(prevResult); results.push(prevResult);
result = this._engine.find( const cols = this._terminal.cols;
term, let nextCol = prevResult.col + prevResult.size;
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row, let nextRow = prevResult.row;
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1, if (nextCol >= cols) {
searchOptions nextRow += Math.floor(nextCol / cols);
); nextCol = nextCol % cols;
}
result = this._engine.find(term, nextRow, nextCol, searchOptions);
} }
this._resultTracker.updateResults(results, this._highlightLimit); this._resultTracker.updateResults(results, this._highlightLimit);
@@ -414,6 +414,20 @@ test.describe('Search Tests', () => {
}); });
test.describe('Regression tests', () => { test.describe('Regression tests', () => {
test('should advance highlight-all scan by buffer match size for wide characters', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.resize(3, 5);
await ctx.proxy.write('𝄞𝄞𝄞');
strictEqual(await ctx.page.evaluate(`window.search.findNext('𝄞', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
]);
await ctx.proxy.resize(80, 24);
});
test.describe('#2444 wrapped line content not being found', () => { test.describe('#2444 wrapped line content not being found', () => {
let fixture: string; let fixture: string;
test.beforeAll(async () => { test.beforeAll(async () => {
+15
View File
@@ -495,6 +495,21 @@ describe('BufferLine', function(): void {
assert.deepEqual(columns, [0]); 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', () => { describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => { it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false); const line = new TestBufferLine(3, NULL_CELL_DATA, false);
+19 -67
View File
@@ -71,20 +71,12 @@ export interface IBufferLineStringCache {
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object. * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
*/ */
/**
* Shared empty sparse map for lines with no combining data.
* Uses a sentinel + copy-on-write so `copyFrom` can detect empty `_combined` with
* reference equality. `_extendedAttrs` uses `undefined` instead (addon-image writes
* the map directly without BufferLine APIs).
*/
const EMPTY_SPARSE_MAP: {[index: number]: string} = Object.create(null);
export class BufferLine implements IBufferLine { export class BufferLine implements IBufferLine {
protected _data: Uint32Array; protected _data: Uint32Array;
/** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */ /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */
protected _combined: {[index: number]: string} = EMPTY_SPARSE_MAP; protected _combined: {[index: number]: string} = {};
/** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */ /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} | undefined; protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined; protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
public length: number; public length: number;
@@ -129,7 +121,6 @@ export class BufferLine implements IBufferLine {
this._invalidateStringCache(); this._invalidateStringCache();
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
if (value[CHAR_DATA_CHAR_INDEX].length > 1) { if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
this._ensureCombinedMap();
this._combined[index] = value[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._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else { } else {
@@ -220,7 +211,7 @@ export class BufferLine implements IBufferLine {
cell.combinedData = ''; cell.combinedData = '';
} }
if (cell.bg & BgFlags.HAS_EXTENDED) { if (cell.bg & BgFlags.HAS_EXTENDED) {
cell.extended = this._extendedAttrs![index]!; cell.extended = this._extendedAttrs[index]!;
} else { } else {
// Do not mutate cell.extended in place: it may still reference this line's map entry from a // 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). // prior loadCell into a reused CellData (e.g. $workCell during insert/delete).
@@ -235,11 +226,10 @@ export class BufferLine implements IBufferLine {
public setCell(index: number, cell: ICellData): void { public setCell(index: number, cell: ICellData): void {
this._invalidateStringCache(); this._invalidateStringCache();
if (cell.content & Content.IS_COMBINED_MASK) { if (cell.content & Content.IS_COMBINED_MASK) {
this._ensureCombinedMap();
this._combined[index] = cell.combinedData; this._combined[index] = cell.combinedData;
} }
if (cell.bg & BgFlags.HAS_EXTENDED) { if (cell.bg & BgFlags.HAS_EXTENDED) {
this._ensureExtendedAttrsMap()[index] = cell.extended; this._extendedAttrs[index] = cell.extended;
} }
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content; 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.FG] = cell.fg;
@@ -254,7 +244,7 @@ export class BufferLine implements IBufferLine {
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
this._invalidateStringCache(); this._invalidateStringCache();
if (attrs.bg & BgFlags.HAS_EXTENDED) { if (attrs.bg & BgFlags.HAS_EXTENDED) {
this._ensureExtendedAttrsMap()[index] = attrs.extended; this._extendedAttrs[index] = attrs.extended;
} }
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); 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.FG] = attrs.fg;
@@ -272,14 +262,12 @@ export class BufferLine implements IBufferLine {
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT]; let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) { if (content & Content.IS_COMBINED_MASK) {
// we already have a combined string, simply add // we already have a combined string, simply add
this._ensureCombinedMap();
this._combined[index] += stringFromCodePoint(codePoint); this._combined[index] += stringFromCodePoint(codePoint);
} else { } else {
if (content & Content.CODEPOINT_MASK) { if (content & Content.CODEPOINT_MASK) {
// normal case for combining chars: // normal case for combining chars:
// - move current leading char + new one into combined string // - move current leading char + new one into combined string
// - set combined flag // - set combined flag
this._ensureCombinedMap();
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
content |= Content.IS_COMBINED_MASK; content |= Content.IS_COMBINED_MASK;
@@ -422,13 +410,11 @@ export class BufferLine implements IBufferLine {
} }
} }
// remove any cut off extended attributes // remove any cut off extended attributes
if (this._extendedAttrs) { const extKeys = Object.keys(this._extendedAttrs);
const extKeys = Object.keys(this._extendedAttrs); for (let i = 0; i < extKeys.length; i++) {
for (let i = 0; i < extKeys.length; i++) { const key = parseInt(extKeys[i], 10);
const key = parseInt(extKeys[i], 10); if (key >= cols) {
if (key >= cols) { delete this._extendedAttrs[key];
delete this._extendedAttrs[key];
}
} }
} }
} }
@@ -464,8 +450,8 @@ export class BufferLine implements IBufferLine {
} }
return; return;
} }
this._combined = EMPTY_SPARSE_MAP; this._combined = {};
this._extendedAttrs = undefined; this._extendedAttrs = {};
for (let i = 0; i < this.length; ++i) { for (let i = 0; i < this.length; ++i) {
this.setCell(i, fillCellData); this.setCell(i, fillCellData);
} }
@@ -622,57 +608,23 @@ export class BufferLine implements IBufferLine {
private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void { private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {
const srcStart = srcCol * Constants.CELL_INDICIES; const srcStart = srcCol * Constants.CELL_INDICIES;
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) { if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
this._ensureCombinedMap();
this._combined[destCol] = src._combined[srcCol]; this._combined[destCol] = src._combined[srcCol];
} else {
delete this._combined[destCol];
} }
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) { if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._ensureExtendedAttrsMap()[destCol] = src._extendedAttrs![srcCol]!; this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
} else {
delete this._extendedAttrs[destCol];
} }
} }
/** Rebuild sparse maps from another line, keyed only by `_data` flags. */ /** Rebuild sparse maps from another line, keyed only by `_data` flags. */
private _copySparseMapsFrom(line: BufferLine): void { private _copySparseMapsFrom(line: BufferLine): void {
const srcCombined = line._combined;
const srcExtended = line._extendedAttrs;
// Fast path: blank / scroll-recycle source (empty `_combined` sentinel, no extended map).
if (srcCombined === EMPTY_SPARSE_MAP && !srcExtended) {
this._combined = EMPTY_SPARSE_MAP;
this._extendedAttrs = undefined;
return;
}
this._copySparseMapsFromDense(line);
}
/** Copy sparse maps by scanning `_data` flags (inlined, no per-cell function calls). */
private _copySparseMapsFromDense(line: BufferLine): void {
this._combined = {}; this._combined = {};
this._extendedAttrs = {}; this._extendedAttrs = {};
const srcData = line._data; for (let i = 0; i < line.length; i++) {
const srcCombined = line._combined; this._copyCellMapsFrom(line, i, i);
const srcExtended = line._extendedAttrs;
const len = line.length;
const indices = Constants.CELL_INDICIES;
const combined = this._combined;
const extendedAttrs = this._extendedAttrs;
for (let i = 0; i < len; i++) {
const start = i * indices;
if (srcData[start + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
combined[i] = srcCombined[i];
}
if (srcData[start + Cell.BG] & BgFlags.HAS_EXTENDED) {
extendedAttrs[i] = srcExtended![i];
}
} }
} }
private _ensureCombinedMap(): void {
if (this._combined === EMPTY_SPARSE_MAP) {
this._combined = {};
}
}
private _ensureExtendedAttrsMap(): {[index: number]: IExtendedAttrs | undefined} {
return this._extendedAttrs ??= {};
}
} }
+23
View File
@@ -10,6 +10,7 @@ import { CellData } from './CellData';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Constants'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Constants';
import { reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; import { reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { IBufferLine } from './Types'; import { IBufferLine } from './Types';
import { createCellData, NULL_CELL_DATA } from '../TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache(); const TEST_STRING_CACHE = new BufferLineStringCache();
@@ -122,5 +123,27 @@ describe('BufferReflow', () => {
const lines = createWrappedLines('abcde'); const lines = createWrappedLines('abcde');
assert.notDeepEqual(reflowLargerGetLinesToRemove(lines, 1, 5, 10, nullCell, false), []); 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) { 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(y + wrappedLines.length - countToRemove); // index
toRemove.push(countToRemove); toRemove.push(countToRemove);
} }
-113
View File
@@ -1,113 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { CellData } from 'common/buffer/CellData';
import { BgFlags, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
const ITERATIONS = 500_000;
const stringCache = new BufferLineStringCache();
const nullCell = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
const yCell = CellData.fromCharData([0, 'y', 1, 'y'.charCodeAt(0)]);
function fillPlainY(line: BufferLine, cols: number): void {
for (let i = 0; i < cols; i++) {
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
}
}
function setupScrollRecyclePair(cols: number): { recycled: BufferLine, blank: BufferLine } {
const blank = new BufferLine(stringCache, cols, nullCell, false);
const recycled = new BufferLine(stringCache, cols, yCell, false);
fillPlainY(recycled, cols);
return { recycled, blank };
}
/** Source line with combining chars and extended attrs spread across columns. */
function buildSparseSourceLine(cols: number): BufferLine {
const line = new BufferLine(stringCache, cols, nullCell, false);
for (let i = 0; i < cols; i++) {
if (i % 10 === 0) {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.setCell(i, cell);
} else if (i % 17 === 0) {
line.setCell(i, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
} else {
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
}
}
return line;
}
function setupCopyFromSparsePair(cols: number): { dest: BufferLine, src: BufferLine } {
const src = buildSparseSourceLine(cols);
const dest = new BufferLine(stringCache, cols, yCell, false);
fillPlainY(dest, cols);
return { dest, src };
}
perfContext('BufferLine.copyFrom (scroll recycle)', () => {
perfContext('cols=80', () => {
let recycled: BufferLine;
let blank: BufferLine;
before(() => {
({ recycled, blank } = setupScrollRecyclePair(80));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
recycled.copyFrom(blank);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
perfContext('cols=279', () => {
let recycled: BufferLine;
let blank: BufferLine;
before(() => {
({ recycled, blank } = setupScrollRecyclePair(279));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
recycled.copyFrom(blank);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
});
perfContext('BufferLine.copyFrom (sparse source: combined + extended attrs)', () => {
perfContext('cols=80', () => {
let dest: BufferLine;
let src: BufferLine;
before(() => {
({ dest, src } = setupCopyFromSparsePair(80));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
dest.copyFrom(src);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
perfContext('cols=279', () => {
let dest: BufferLine;
let src: BufferLine;
before(() => {
({ dest, src } = setupCopyFromSparsePair(279));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
dest.copyFrom(src);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
});