Move cache into Buffer owned object, use WeakRefs

This allows O(1) clearing of the cache, a single TTL timer and we
can avoid making BufferLine's Disposable
This commit is contained in:
Daniel Imms
2026-05-02 05:32:43 -07:00
parent 7615cd210a
commit 52d4b2719c
10 changed files with 219 additions and 40 deletions
@@ -8,6 +8,7 @@ import { assert } from 'chai';
import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from 'common/TestUtils.test';
@@ -15,6 +16,7 @@ import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService }
import { TestWidthCache } from 'browser/renderer/dom/WidthCache.test';
const dom = new jsdom.JSDOM('');
const TEST_STRING_CACHE = new BufferLineStringCache();
describe('DomRendererRowFactory', () => {
@@ -517,7 +519,7 @@ describe('DomRendererRowFactory', () => {
}
function createEmptyLineData(cols: number): IBufferLine {
const lineData = new BufferLine(cols);
const lineData = new BufferLine(TEST_STRING_CACHE, cols);
for (let i = 0; i < cols; i++) {
lineData.setCell(i, NULL_CELL_DATA);
}
@@ -7,10 +7,13 @@ import { assert } from 'chai';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockBufferService, createCellData } from 'common/TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
describe('CharacterJoinerService', () => {
let service: ICharacterJoinerService;
@@ -22,7 +25,7 @@ describe('CharacterJoinerService', () => {
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
lines.set(3, lineData([['no joined ranges']]));
lines.set(4, new BufferLine(0));
lines.set(4, new BufferLine(TEST_STRING_CACHE, 0));
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
const line6 = lineData([['wi']]);
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
@@ -267,7 +270,7 @@ describe('CharacterJoinerService', () => {
type IPartialLineData = ([string] | [string, number]);
function lineData(data: IPartialLineData[]): IBufferLine {
const tline = new BufferLine(0);
const tline = new BufferLine(TEST_STRING_CACHE, 0);
for (let i = 0; i < data.length; ++i) {
const line = data[i][0];
const attr = (data[i][1] || 0) as number;
@@ -9,12 +9,15 @@ import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferLine } from 'common/Types';
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test';
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test';
import { CellData } from 'common/buffer/CellData';
import { IBuffer } from 'common/buffer/Types';
import { IRenderService } from 'browser/services/Services';
const TEST_STRING_CACHE = new BufferLineStringCache();
class TestSelectionService extends SelectionService {
constructor(
bufferService: IBufferService,
@@ -55,7 +58,7 @@ describe('SelectionService', () => {
});
function stringToRow(text: string): IBufferLine {
const result = new BufferLine(text.length);
const result = new BufferLine(TEST_STRING_CACHE, text.length);
for (let i = 0; i < text.length; i++) {
result.setCell(i, createCellData(0, text.charAt(i), 1));
}
@@ -63,7 +66,7 @@ describe('SelectionService', () => {
}
function stringArrayToRow(chars: string[]): IBufferLine {
const line = new BufferLine(chars.length);
const line = new BufferLine(TEST_STRING_CACHE, chars.length);
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
return line;
}
@@ -118,7 +121,7 @@ describe('SelectionService', () => {
[0, 'o', 1, 'o'.charCodeAt(0)],
[0, 'o', 1, 'o'.charCodeAt(0)]
];
const line = new BufferLine(data.length);
const line = new BufferLine(TEST_STRING_CACHE, data.length);
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
buffer.lines.set(0, line);
// Ensure wide characters take up 2 columns
+61 -6
View File
@@ -8,12 +8,14 @@ import { Buffer } from 'common/buffer/Buffer';
import { CircularList } from 'common/CircularList';
import { MockOptionsService, MockBufferService, MockLogService, createCellData } from 'common/TestUtils.test';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { CellData } from 'common/buffer/CellData';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
const INIT_COLS = 80;
const INIT_ROWS = 24;
const INIT_SCROLLBACK = 1000;
const TEST_STRING_CACHE = new BufferLineStringCache();
describe('Buffer', () => {
let optionsService: MockOptionsService;
@@ -1104,7 +1106,7 @@ describe('Buffer', () => {
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
const line = new BufferLine(4);
const line = new BufferLine(TEST_STRING_CACHE, 4);
line.setCell(0, createCellData(0, 'a', 1));
line.setCell(1, createCellData(0, 'b', 1));
line.setCell(2, createCellData(0, 'c', 1));
@@ -1116,7 +1118,7 @@ describe('Buffer', () => {
});
it('should handle a cut-off double width character by including it', () => {
const line = new BufferLine(3);
const line = new BufferLine(TEST_STRING_CACHE, 3);
line.setCell(0, createCellData(0, '語', 2));
line.setCell(1, createCellData(0, '', 0));
line.setCell(2, createCellData(0, 'a', 1));
@@ -1127,7 +1129,7 @@ describe('Buffer', () => {
});
it('should handle a zero width character in the middle of the string by not including it', () => {
const line = new BufferLine(3);
const line = new BufferLine(TEST_STRING_CACHE, 3);
line.setCell(0, createCellData(0, '語', 2));
line.setCell(1, createCellData(0, '', 0));
line.setCell(2, createCellData(0, 'a', 1));
@@ -1144,7 +1146,7 @@ describe('Buffer', () => {
});
it('should handle single width emojis', () => {
const line = new BufferLine(2);
const line = new BufferLine(TEST_STRING_CACHE, 2);
line.setCell(0, createCellData(0, '😁', 1));
line.setCell(1, createCellData(0, 'a', 1));
buffer.lines.set(0, line);
@@ -1157,7 +1159,7 @@ describe('Buffer', () => {
});
it('should handle double width emojis', () => {
const line = new BufferLine(2);
const line = new BufferLine(TEST_STRING_CACHE, 2);
line.setCell(0, createCellData(0, '😁', 2));
line.setCell(1, createCellData(0, '', 0));
buffer.lines.set(0, line);
@@ -1168,7 +1170,7 @@ describe('Buffer', () => {
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
const line2 = new BufferLine(3);
const line2 = new BufferLine(TEST_STRING_CACHE, 3);
line2.setCell(0, createCellData(0, '😁', 2));
line2.setCell(1, createCellData(0, '', 0));
line2.setCell(2, createCellData(0, 'a', 1));
@@ -1179,6 +1181,59 @@ describe('Buffer', () => {
});
});
describe('line string cache cleanup', () => {
it('should clear shared cache entries with a single timer', async () => {
buffer.fillViewportRows();
buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 1));
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
assert.equal(buffer.translateBufferLineToString(1, false), `b${' '.repeat(INIT_COLS - 1)}`);
const cache = (buffer as any)._stringCache;
assert.equal(cache.entries.size, 2);
const previousTimer = (buffer as any)._stringCacheClearTimeout;
assert.notEqual(previousTimer, undefined);
// Another translation should refresh the same shared timer, not create a second queue.
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
assert.notEqual((buffer as any)._stringCacheClearTimeout, undefined);
assert.notEqual((buffer as any)._stringCacheClearTimeout, previousTimer);
await new Promise(r => setTimeout(r, 70));
assert.equal(cache.entries.size, 0);
assert.equal((buffer as any)._stringCacheClearTimeout, undefined);
// Cache entries should be recreated lazily after a timer-based reset.
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
assert.equal(cache.entries.size, 1);
});
it('should reset line string cache state on clear and resize', () => {
buffer.fillViewportRows();
buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
buffer.translateBufferLineToString(0, false);
const cache = (buffer as any)._stringCache;
assert.equal(cache.entries.size, 1);
assert.notEqual((buffer as any)._stringCacheClearTimeout, undefined);
buffer.clear();
assert.equal(cache.entries.size, 0);
assert.equal((buffer as any)._stringCacheClearTimeout, undefined);
buffer.fillViewportRows();
buffer.lines.get(0)!.setCell(0, createCellData(0, 'b', 1));
buffer.translateBufferLineToString(0, false);
assert.equal(cache.entries.size, 1);
buffer.resize(INIT_COLS - 1, INIT_ROWS);
assert.equal(cache.entries.size, 0);
assert.equal((buffer as any)._stringCacheClearTimeout, undefined);
});
});
describe('memory cleanup after shrinking', () => {
it('should realign memory from idle task execution', async () => {
buffer.fillViewportRows();
+33 -4
View File
@@ -8,6 +8,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 { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
import { CellData } from 'common/buffer/CellData';
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from 'common/buffer/Constants';
@@ -17,6 +18,7 @@ import { DEFAULT_CHARSET } from 'common/data/Charsets';
import { IBufferService, ILogService, IOptionsService } from 'common/services/Services';
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
const STRING_CACHE_CLEAR_DELAY_MS = 50;
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
@@ -50,6 +52,8 @@ export class Buffer implements IBuffer {
private _isClearing: boolean = false;
private _memoryCleanupQueue: InstanceType<typeof IdleTaskQueue>;
private _memoryCleanupPosition = 0;
private readonly _stringCache: BufferLineStringCache;
private _stringCacheClearTimeout: ReturnType<typeof setTimeout> | undefined;
constructor(
private _hasScrollback: boolean,
@@ -64,6 +68,7 @@ export class Buffer implements IBuffer {
this.scrollBottom = this._rows - 1;
this.setupTabStops();
this._memoryCleanupQueue = new IdleTaskQueue(this._logService);
this._stringCache = new BufferLineStringCache(() => this._scheduleStringCacheCleanup());
}
public getNullCell(attr?: IAttributeData): ICellData {
@@ -93,7 +98,7 @@ export class Buffer implements IBuffer {
}
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), isWrapped);
}
public get hasScrollback(): boolean {
@@ -138,6 +143,7 @@ export class Buffer implements IBuffer {
* Clears the buffer to it's initial state, discarding all previous data.
*/
public clear(): void {
this._resetStringCache();
this.ydisp = 0;
this.ybase = 0;
this.y = 0;
@@ -156,6 +162,7 @@ export class Buffer implements IBuffer {
public resize(newCols: number, newRows: number): void {
// store reference to null cell with default attrs
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
this._resetStringCache();
// count bufferlines with overly big memory to be cleaned afterwards
let dirtyMemoryLines = 0;
@@ -190,7 +197,7 @@ export class Buffer implements IBuffer {
if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
// Just add the new missing rows on Windows as conpty reprints the screen with it's
// view of the world. Once a line enters scrollback for conpty it remains there
this.lines.push(new BufferLine(newCols, nullCell));
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
} else {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
@@ -204,7 +211,7 @@ export class Buffer implements IBuffer {
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this.lines.push(new BufferLine(newCols, nullCell));
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
}
}
}
@@ -303,6 +310,28 @@ export class Buffer implements IBuffer {
return normalRun;
}
private _scheduleStringCacheCleanup(): void {
if (this._stringCacheClearTimeout !== undefined) {
clearTimeout(this._stringCacheClearTimeout);
}
this._stringCacheClearTimeout = setTimeout(() => {
this._stringCacheClearTimeout = undefined;
this._clearStringCache();
}, STRING_CACHE_CLEAR_DELAY_MS);
}
private _resetStringCache(): void {
if (this._stringCacheClearTimeout !== undefined) {
clearTimeout(this._stringCacheClearTimeout);
this._stringCacheClearTimeout = undefined;
}
this._clearStringCache();
}
private _clearStringCache(): void {
this._stringCache.clear();
}
private get _isReflowEnabled(): boolean {
const windowsPty = this._optionsService.rawOptions.windowsPty;
if (windowsPty && windowsPty.buildNumber) {
@@ -345,7 +374,7 @@ export class Buffer implements IBuffer {
}
if (this.lines.length < newRows) {
// Add an extra row at the bottom of the viewport
this.lines.push(new BufferLine(newCols, nullCell));
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
}
} else {
if (this.ydisp === this.ybase) {
+12 -5
View File
@@ -4,32 +4,39 @@
*/
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from 'common/buffer/Constants';
import { BufferLine } from 'common/buffer//BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { CellData } from 'common/buffer/CellData';
import { CharData, IBufferLine } from '../Types';
import { CharData, IBufferLine, ICellData } from '../Types';
import { assert } from 'chai';
import { AttributeData } from 'common/buffer/AttributeData';
import { createCellData, NULL_CELL_DATA, extendedAttributes } from 'common/TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
class TestBufferLine extends BufferLine {
constructor(cols: number, fillCellData?: ICellData, isWrapped: boolean = false) {
super(TEST_STRING_CACHE, cols, fillCellData, isWrapped);
}
public get combined(): {[index: number]: string} {
return this._combined;
}
public get cachedString(): string | undefined {
return this._cachedString;
return this._getStringCacheEntry(false)?.value;
}
public set cachedString(value: string | undefined) {
this._cachedString = value;
this._getStringCacheEntry(true)!.value = value;
}
public get isCachedStringTrimmed(): boolean {
return this._isCachedStringTrimmed;
return this._getStringCacheEntry(false)?.isTrimmed ?? false;
}
public set isCachedStringTrimmed(value: boolean) {
this._isCachedStringTrimmed = value;
this._getStringCacheEntry(true)!.isTrimmed = value;
}
public toArray(): CharData[] {
+51 -12
View File
@@ -44,6 +44,18 @@ const $workCell = new CellData();
/** Factor when to cleanup underlying array buffer after shrinking. */
const CLEANUP_THRESHOLD = 2;
export interface IBufferLineStringCacheEntry {
value: string | undefined;
isTrimmed: boolean;
generation: number;
}
export interface IBufferLineStringCache {
generation: number;
allocateEntry(): IBufferLineStringCacheEntry;
touch?(): void;
}
/**
* Typed array based bufferline implementation.
*
@@ -63,11 +75,15 @@ export class BufferLine implements IBufferLine {
protected _data: Uint32Array;
protected _combined: {[index: number]: string} = {};
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
protected _cachedString: string | undefined;
protected _isCachedStringTrimmed: boolean = false;
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
public length: number;
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
constructor(
protected readonly _stringCache: IBufferLineStringCache,
cols: number,
fillCellData?: ICellData,
public isWrapped: boolean = false
) {
this._data = new Uint32Array(cols * CELL_SIZE);
const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
for (let i = 0; i < cols; ++i) {
@@ -456,7 +472,7 @@ export class BufferLine implements IBufferLine {
/** create a new clone */
public clone(): IBufferLine {
const newLine = new BufferLine(0);
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) {
@@ -536,12 +552,16 @@ export class BufferLine implements IBufferLine {
*/
public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {
const isCanonicalRequest = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;
if (isCanonicalRequest && this._cachedString !== undefined) {
if (isCanonicalRequest) {
this._stringCache.touch?.();
}
const stringCacheEntry = isCanonicalRequest ? this._getStringCacheEntry(false) : undefined;
if (isCanonicalRequest && stringCacheEntry?.value !== undefined) {
if (trimRight) {
return this._isCachedStringTrimmed ? this._cachedString : this._cachedString.trimEnd();
return stringCacheEntry.isTrimmed ? stringCacheEntry.value : stringCacheEntry.value.trimEnd();
}
if (!this._isCachedStringTrimmed) {
return this._cachedString;
if (!stringCacheEntry.isTrimmed) {
return stringCacheEntry.value;
}
}
startCol = startCol ?? 0;
@@ -569,14 +589,33 @@ export class BufferLine implements IBufferLine {
outColumns.push(startCol);
}
if (isCanonicalRequest) {
this._cachedString = result;
this._isCachedStringTrimmed = !!trimRight;
const cacheEntry = this._getStringCacheEntry(true)!;
cacheEntry.value = result;
cacheEntry.isTrimmed = !!trimRight;
}
return result;
}
protected _getStringCacheEntry(createIfNeeded: boolean): IBufferLineStringCacheEntry | undefined {
const cachedEntry = this._stringCacheEntryRef?.deref();
if (cachedEntry) {
if (cachedEntry.generation === this._stringCache.generation) {
return cachedEntry;
}
}
if (!createIfNeeded) {
return undefined;
}
const cacheEntry = this._stringCache.allocateEntry();
this._stringCacheEntryRef = new WeakRef(cacheEntry);
return cacheEntry;
}
private _invalidateStringCache(): void {
this._cachedString = undefined;
this._isCachedStringTrimmed = false;
const cacheEntry = this._getStringCacheEntry(false);
if (cacheEntry) {
cacheEntry.value = undefined;
cacheEntry.isTrimmed = false;
}
}
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IBufferLineStringCache, IBufferLineStringCacheEntry } from 'common/buffer/BufferLine';
export class BufferLineStringCache implements IBufferLineStringCache {
public generation: number = 0;
public readonly entries: Set<IBufferLineStringCacheEntry> = new Set();
constructor(private readonly _onCacheUsed: () => void = () => { }) { }
public touch(): void {
this._onCacheUsed();
}
public allocateEntry(): IBufferLineStringCacheEntry {
const entry: IBufferLineStringCacheEntry = {
value: undefined,
isTrimmed: false,
generation: this.generation
};
this.entries.add(entry);
this._onCacheUsed();
return entry;
}
public clear(): void {
this.generation++;
for (const entry of this.entries) {
entry.value = undefined;
entry.isTrimmed = false;
}
this.entries.clear();
}
}
+9 -6
View File
@@ -4,13 +4,16 @@
*/
import { assert } from 'chai';
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'common/buffer/Constants';
import { reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
const TEST_STRING_CACHE = new BufferLineStringCache();
describe('BufferReflow', () => {
describe('reflowSmallerGetNewLineLengths', () => {
it('should return correct line lengths for a small line with wide characters', () => {
const line = new BufferLine(4);
const line = new BufferLine(TEST_STRING_CACHE, 4);
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [0, '', 0, 0]);
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
@@ -20,7 +23,7 @@ describe('BufferReflow', () => {
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
});
it('should return correct line lengths for a large line with wide characters', () => {
const line = new BufferLine(12);
const line = new BufferLine(TEST_STRING_CACHE, 12);
for (let i = 0; i < 12; i += 4) {
line.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]);
@@ -42,7 +45,7 @@ describe('BufferReflow', () => {
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语');
});
it('should return correct line lengths for a string with wide and single characters', () => {
const line = new BufferLine(6);
const line = new BufferLine(TEST_STRING_CACHE, 6);
line.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
line.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(2, [0, '', 0, 0]);
@@ -56,14 +59,14 @@ describe('BufferReflow', () => {
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b');
});
it('should return correct line lengths for a wrapped line with wide and single characters', () => {
const line1 = new BufferLine(6);
const line1 = new BufferLine(TEST_STRING_CACHE, 6);
line1.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
line1.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
line1.set(2, [0, '', 0, 0]);
line1.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
line1.set(4, [0, '', 0, 0]);
line1.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
const line2 = new BufferLine(6, undefined, true);
const line2 = new BufferLine(TEST_STRING_CACHE, 6, undefined, true);
line2.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
line2.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
line2.set(2, [0, '', 0, 0]);
@@ -78,7 +81,7 @@ describe('BufferReflow', () => {
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b');
});
it('should work on lines ending in null space', () => {
const line = new BufferLine(5);
const line = new BufferLine(TEST_STRING_CACHE, 5);
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [0, '', 0, 0]);
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
+2 -1
View File
@@ -4,7 +4,8 @@
"lib": [
"es2015",
"es2016.Array.Include",
"es2019.string"
"es2019.string",
"es2021.weakref"
],
"outDir": "../../out",
"types": [