diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index f7d434532..1e7451589 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -16,6 +16,22 @@ class TestBufferLine extends BufferLine { return this._combined; } + public get cachedCanonicalString(): string | undefined { + return this._cachedCanonicalString; + } + + public set cachedCanonicalString(value: string | undefined) { + this._cachedCanonicalString = value; + } + + public get cachedCanonicalTrimmedString(): string | undefined { + return this._cachedCanonicalTrimmedString; + } + + public set cachedCanonicalTrimmedString(value: string | undefined) { + this._cachedCanonicalTrimmedString = value; + } + public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { @@ -808,30 +824,52 @@ describe('BufferLine', function(): void { assert.equal(extendedAttributes(line, 4), extendedAttributes(initial, 4)); }); - it('should cache canonical non-trimmed string translations', () => { - const line = new TestBufferLine(3); + it('should cache canonical string translations', () => { + const line = new TestBufferLine(5); line.setCell(0, createCellData(1, 'a', 1)); line.setCell(1, createCellData(1, 'b', 1)); line.setCell(2, createCellData(1, 'c', 1)); + // Trimmed-only canonical request should cache the trimmed value. + const trimmed = line.translateToString(true, undefined, undefined, undefined); + assert.equal(trimmed, 'abc'); + assert.equal(line.cachedCanonicalTrimmedString, 'abc'); + + // Non-trimmed canonical request should cache the full value and free trimmed cache. const translated = line.translateToString(false, undefined, undefined, undefined); - assert.equal(translated, 'abc'); - assert.equal((line as any)._cachedTrimmedString, 'abc'); - (line as any)._cachedTrimmedString = 'cached'; - assert.equal(line.translateToString(false, undefined, undefined, undefined), 'cached'); + assert.equal(translated, 'abc '); + assert.equal(line.cachedCanonicalString, 'abc '); + assert.equal(line.cachedCanonicalTrimmedString, undefined); + + // Once non-trimmed is cached, trimmed should be derived from trimEnd() (not re-cached). + assert.equal(line.translateToString(true, undefined, undefined, undefined), 'abc'); + assert.equal(line.cachedCanonicalTrimmedString, undefined); + + line.cachedCanonicalString = 'cached-non-trimmed '; + line.cachedCanonicalTrimmedString = 'cached-trimmed'; + assert.equal(line.translateToString(false, undefined, undefined, undefined), 'cached-non-trimmed '); + assert.equal(line.translateToString(true, undefined, undefined, undefined), 'cached-trimmed'); + + line.cachedCanonicalTrimmedString = undefined; + assert.equal(line.translateToString(true, undefined, undefined, undefined), 'cached-non-trimmed'); + // Any optional translation argument should bypass cache. assert.equal(line.translateToString(false, 0, 2, undefined), 'ab'); - assert.equal(line.translateToString(true, undefined, undefined, undefined), 'abc'); + assert.equal(line.translateToString(true, 0, 2, undefined), 'ab'); }); - it('should invalidate cached trimmed strings on line mutations', () => { + it('should invalidate cached canonical strings on line mutations', () => { const assertCacheInvalidated = (mutate: (line: TestBufferLine) => void): void => { const line = new TestBufferLine(5); line.fill(createCellData(1, 'a', 1)); + line.translateToString(true, undefined, undefined, undefined); + assert.equal(line.cachedCanonicalTrimmedString, 'aaaaa'); line.translateToString(false, undefined, undefined, undefined); - assert.equal((line as any)._cachedTrimmedString, 'aaaaa'); + assert.equal(line.cachedCanonicalString, 'aaaaa'); + assert.equal(line.cachedCanonicalTrimmedString, undefined); mutate(line); - assert.equal((line as any)._cachedTrimmedString, undefined); + assert.equal(line.cachedCanonicalString, undefined); + assert.equal(line.cachedCanonicalTrimmedString, undefined); }; assertCacheInvalidated(line => line.set(0, [0, 'b', 1, 'b'.charCodeAt(0)])); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 40c963190..66bf124e3 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -63,7 +63,9 @@ export class BufferLine implements IBufferLine { protected _data: Uint32Array; protected _combined: {[index: number]: string} = {}; protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; - private _cachedTrimmedString: string | undefined; + // Cache for canonical full-line translations. + protected _cachedCanonicalString: string | undefined; + protected _cachedCanonicalTrimmedString: string | undefined; public length: number; constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { @@ -520,8 +522,8 @@ export class BufferLine implements IBufferLine { } /** - * Translates the buffer line to a string. Caching only applies to canonical non-trimmed - * translation requests. Using `trimRight` or any optional range/mapping arguments disables cache. + * Translates the buffer line to a string. Caching only applies to canonical full-line translation + * requests (regardless of `trimRight` value). * * @param trimRight Whether to trim any empty cells on the right. * @param startCol The column to start the string (0-based inclusive). @@ -534,9 +536,10 @@ export class BufferLine implements IBufferLine { * returned string, the corresponding entries in `outColumns` will have the same column number. */ public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string { - const isCanonicalTrimmedRequest = !trimRight && (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined; - if (isCanonicalTrimmedRequest && this._cachedTrimmedString !== undefined) { - return this._cachedTrimmedString; + const isCanonicalRequest = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined; + const cachedResult = trimRight ? this._cachedCanonicalTrimmedString ?? this._cachedCanonicalString?.trimEnd() : this._cachedCanonicalString; + if (isCanonicalRequest && cachedResult !== undefined) { + return cachedResult; } startCol = startCol ?? 0; endCol = endCol ?? this.length; @@ -562,13 +565,20 @@ export class BufferLine implements IBufferLine { if (outColumns) { outColumns.push(startCol); } - if (isCanonicalTrimmedRequest) { - this._cachedTrimmedString = result; + if (isCanonicalRequest) { + if (trimRight) { + this._cachedCanonicalTrimmedString = result; + } else { + this._cachedCanonicalString = result; + // Free the trimmed string as trimEnd() will be used to save memory + this._cachedCanonicalTrimmedString = undefined; + } } return result; } private _invalidateStringCache(): void { - this._cachedTrimmedString = undefined; + this._cachedCanonicalString = undefined; + this._cachedCanonicalTrimmedString = undefined; } } diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 22501b9cf..279e9391e 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "lib": [ "es2015", - "es2016.Array.Include" + "es2016.Array.Include", + "es2019.string" ], "outDir": "../../out", "types": [