Merge pull request #6114 from xtermjs/fix_bufferline
bufferline perf fixes
This commit is contained in:
@@ -6,16 +6,14 @@
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from '../../../common/buffer/Constants';
|
||||
import { 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/buffer/Types';
|
||||
import { CellData } from '../../../common/buffer/CellData';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from '../../../common/TestUtils.test';
|
||||
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from '../../TestUtils.test';
|
||||
import { TestWidthCache } from './WidthCache.test';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
@@ -517,7 +515,7 @@ describe('DomRendererRowFactory', () => {
|
||||
}
|
||||
|
||||
function createEmptyLineData(cols: number): IBufferLine {
|
||||
const lineData = new BufferLine(TEST_STRING_CACHE, cols);
|
||||
const lineData = new BufferLine(cols);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
lineData.setCell(i, NULL_CELL_DATA);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,10 @@ import { assert } from 'chai';
|
||||
import { ICharacterJoinerService } from './Services';
|
||||
import { CharacterJoinerService } from './CharacterJoinerService';
|
||||
import { BufferLine } from '../../common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from '../../common/buffer/BufferLineStringCache';
|
||||
import { IBufferLine } from '../../common/buffer/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;
|
||||
@@ -25,7 +23,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(TEST_STRING_CACHE, 0));
|
||||
lines.set(4, new BufferLine(0));
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
|
||||
@@ -270,7 +268,7 @@ describe('CharacterJoinerService', () => {
|
||||
type IPartialLineData = ([string] | [string, number]);
|
||||
|
||||
function lineData(data: IPartialLineData[]): IBufferLine {
|
||||
const tline = new BufferLine(TEST_STRING_CACHE, 0);
|
||||
const tline = new BufferLine(0);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
const line = data[i][0];
|
||||
const attr = (data[i][1] || 0) as number;
|
||||
|
||||
@@ -9,13 +9,11 @@ import { SelectionModel } from '../selection/SelectionModel';
|
||||
import { IBuffer, IBufferLine } from '../../common/buffer/Types';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService, MockMouseStateService, 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 '../TestUtils.test';
|
||||
import { CellData } from '../../common/buffer/CellData';
|
||||
import { IRenderService } from './Services';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
class TestSelectionService extends SelectionService {
|
||||
constructor(
|
||||
@@ -60,7 +58,7 @@ describe('SelectionService', () => {
|
||||
});
|
||||
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(TEST_STRING_CACHE, text.length);
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.setCell(i, createCellData(0, text.charAt(i), 1));
|
||||
}
|
||||
@@ -68,7 +66,7 @@ describe('SelectionService', () => {
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, chars.length);
|
||||
const line = new BufferLine(chars.length);
|
||||
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
|
||||
return line;
|
||||
}
|
||||
@@ -123,7 +121,7 @@ describe('SelectionService', () => {
|
||||
[0, 'o', 1, 'o'.charCodeAt(0)],
|
||||
[0, 'o', 1, 'o'.charCodeAt(0)]
|
||||
];
|
||||
const line = new BufferLine(TEST_STRING_CACHE, data.length);
|
||||
const line = new BufferLine(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
|
||||
|
||||
@@ -8,24 +8,14 @@ import { Buffer } from './Buffer';
|
||||
import { CircularList } from '../CircularList';
|
||||
import { MockOptionsService, MockBufferService, MockLogService, createCellData } from '../TestUtils.test';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from './BufferLine';
|
||||
import { BufferLineStringCache } from './BufferLineStringCache';
|
||||
import { CellData } from './CellData';
|
||||
import { ExtendedAttrs } from './AttributeData';
|
||||
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
const INIT_SCROLLBACK = 1000;
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
class TestBuffer extends Buffer {
|
||||
public getStringCache(): BufferLineStringCache {
|
||||
return (this as unknown as { _stringCache: BufferLineStringCache })._stringCache;
|
||||
}
|
||||
|
||||
public getStringCacheClearTimeout(): unknown {
|
||||
return (this.getStringCache() as unknown as { _clearTimeout: { value: unknown } })._clearTimeout.value;
|
||||
}
|
||||
}
|
||||
class TestBuffer extends Buffer {}
|
||||
|
||||
describe('Buffer', () => {
|
||||
let optionsService: MockOptionsService;
|
||||
@@ -1202,7 +1192,7 @@ describe('Buffer', () => {
|
||||
|
||||
describe ('translateBufferLineToString', () => {
|
||||
it('should handle selecting a section of ascii text', () => {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 4);
|
||||
const line = new BufferLine(4);
|
||||
line.setCell(0, createCellData(0, 'a', 1));
|
||||
line.setCell(1, createCellData(0, 'b', 1));
|
||||
line.setCell(2, createCellData(0, 'c', 1));
|
||||
@@ -1214,7 +1204,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle a cut-off double width character by including it', () => {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 3);
|
||||
const line = new BufferLine(3);
|
||||
line.setCell(0, createCellData(0, '語', 2));
|
||||
line.setCell(1, createCellData(0, '', 0));
|
||||
line.setCell(2, createCellData(0, 'a', 1));
|
||||
@@ -1225,7 +1215,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 3);
|
||||
const line = new BufferLine(3);
|
||||
line.setCell(0, createCellData(0, '語', 2));
|
||||
line.setCell(1, createCellData(0, '', 0));
|
||||
line.setCell(2, createCellData(0, 'a', 1));
|
||||
@@ -1242,7 +1232,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle single width emojis', () => {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 2);
|
||||
const line = new BufferLine(2);
|
||||
line.setCell(0, createCellData(0, '😁', 1));
|
||||
line.setCell(1, createCellData(0, 'a', 1));
|
||||
buffer.lines.set(0, line);
|
||||
@@ -1255,7 +1245,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle double width emojis', () => {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 2);
|
||||
const line = new BufferLine(2);
|
||||
line.setCell(0, createCellData(0, '😁', 2));
|
||||
line.setCell(1, createCellData(0, '', 0));
|
||||
buffer.lines.set(0, line);
|
||||
@@ -1266,7 +1256,7 @@ describe('Buffer', () => {
|
||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str2, '😁');
|
||||
|
||||
const line2 = new BufferLine(TEST_STRING_CACHE, 3);
|
||||
const line2 = new BufferLine(3);
|
||||
line2.setCell(0, createCellData(0, '😁', 2));
|
||||
line2.setCell(1, createCellData(0, '', 0));
|
||||
line2.setCell(2, createCellData(0, 'a', 1));
|
||||
@@ -1277,99 +1267,6 @@ describe('Buffer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('line string cache cleanup', () => {
|
||||
it('should clear shared cache entries with a single timer', () => {
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const originalClearTimeout = globalThis.clearTimeout;
|
||||
const originalDateNow = Date.now;
|
||||
let timeoutId = 0;
|
||||
let now = 0;
|
||||
const clearedTimeouts: number[] = [];
|
||||
const scheduledTimeouts = new Map<number, { delay: number, fire: () => void }>();
|
||||
(globalThis as any).setTimeout = ((handler: (...args: any[]) => void, timeout?: number) => {
|
||||
const id = ++timeoutId;
|
||||
scheduledTimeouts.set(id, {
|
||||
delay: timeout ?? 0,
|
||||
fire: () => {
|
||||
scheduledTimeouts.delete(id);
|
||||
handler();
|
||||
}
|
||||
});
|
||||
return id as ReturnType<typeof setTimeout>;
|
||||
}) as typeof setTimeout;
|
||||
(globalThis as any).clearTimeout = ((id: ReturnType<typeof setTimeout>) => {
|
||||
const numericId = id as unknown as number;
|
||||
clearedTimeouts.push(numericId);
|
||||
scheduledTimeouts.delete(numericId);
|
||||
}) as typeof clearTimeout;
|
||||
Date.now = () => now;
|
||||
try {
|
||||
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.getStringCache();
|
||||
assert.equal(cache.entries.size, 2);
|
||||
assert.ok(buffer.getStringCacheClearTimeout() !== undefined);
|
||||
assert.equal(scheduledTimeouts.size, 1);
|
||||
assert.equal([...scheduledTimeouts.values()][0].delay, 15000);
|
||||
const initialTimerCreationCount = timeoutId;
|
||||
|
||||
now = 5000;
|
||||
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
|
||||
assert.equal(timeoutId, initialTimerCreationCount);
|
||||
assert.equal(scheduledTimeouts.size, 1);
|
||||
assert.deepEqual(clearedTimeouts, []);
|
||||
|
||||
now = 15000;
|
||||
[...scheduledTimeouts.values()][0].fire();
|
||||
assert.equal(timeoutId, initialTimerCreationCount + 1);
|
||||
assert.ok(buffer.getStringCacheClearTimeout() !== undefined);
|
||||
assert.equal(scheduledTimeouts.size, 1);
|
||||
assert.equal([...scheduledTimeouts.values()][0].delay, 5000);
|
||||
|
||||
now = 20000;
|
||||
[...scheduledTimeouts.values()][0].fire();
|
||||
|
||||
assert.equal(cache.entries.size, 0);
|
||||
assert.equal(buffer.getStringCacheClearTimeout(), undefined);
|
||||
|
||||
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
|
||||
assert.equal(cache.entries.size, 1);
|
||||
} finally {
|
||||
Date.now = originalDateNow;
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
globalThis.clearTimeout = originalClearTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
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.getStringCache();
|
||||
assert.equal(cache.entries.size, 1);
|
||||
assert.ok(buffer.getStringCacheClearTimeout() !== undefined);
|
||||
|
||||
buffer.clear();
|
||||
assert.equal(cache.entries.size, 0);
|
||||
assert.equal(buffer.getStringCacheClearTimeout(), 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.getStringCacheClearTimeout(), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('memory cleanup after shrinking', () => {
|
||||
it('should realign memory from idle task execution', async () => {
|
||||
buffer.fillViewportRows();
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ICharset } from '../Types';
|
||||
import { IAttributeData, IBuffer, IBufferLine, ICellData } from './Types';
|
||||
import { ExtendedAttrs } from './AttributeData';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from './BufferLine';
|
||||
import { BufferLineStringCache } from './BufferLineStringCache';
|
||||
import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
|
||||
import { CellData } from './CellData';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from './Constants';
|
||||
@@ -52,7 +51,6 @@ export class Buffer extends Disposable implements IBuffer {
|
||||
private _isClearing: boolean = false;
|
||||
private _memoryCleanupQueue: InstanceType<typeof IdleTaskQueue>;
|
||||
private _memoryCleanupPosition = 0;
|
||||
private readonly _stringCache: BufferLineStringCache;
|
||||
|
||||
constructor(
|
||||
private _hasScrollback: boolean,
|
||||
@@ -70,7 +68,6 @@ export class Buffer extends Disposable implements IBuffer {
|
||||
this._memoryCleanupQueue = new IdleTaskQueue(this._logService);
|
||||
this._register(toDisposable(() => this._memoryCleanupQueue.clear()));
|
||||
this._register(toDisposable(() => this.clearAllMarkers()));
|
||||
this._stringCache = this._register(new BufferLineStringCache());
|
||||
}
|
||||
|
||||
public getNullCell(attr?: IAttributeData): ICellData {
|
||||
@@ -100,7 +97,7 @@ export class Buffer extends Disposable implements IBuffer {
|
||||
}
|
||||
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), isWrapped);
|
||||
return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
@@ -145,7 +142,6 @@ export class Buffer extends Disposable implements IBuffer {
|
||||
* Clears the buffer to its initial state, discarding all previous data.
|
||||
*/
|
||||
public clear(): void {
|
||||
this._stringCache.clear();
|
||||
this.ydisp = 0;
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
@@ -164,7 +160,6 @@ export class Buffer extends Disposable 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._stringCache.clear();
|
||||
|
||||
// count bufferlines with overly big memory to be cleaned afterwards
|
||||
let dirtyMemoryLines = 0;
|
||||
@@ -199,7 +194,7 @@ export class Buffer extends Disposable 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 its
|
||||
// view of the world. Once a line enters scrollback for conpty it remains there
|
||||
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
|
||||
this.lines.push(new BufferLine(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,
|
||||
@@ -213,7 +208,7 @@ export class Buffer extends Disposable 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(this._stringCache, newCols, nullCell, false));
|
||||
this.lines.push(new BufferLine(newCols, nullCell, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,7 +349,7 @@ export class Buffer extends Disposable implements IBuffer {
|
||||
}
|
||||
if (this.lines.length < newRows) {
|
||||
// Add an extra row at the bottom of the viewport
|
||||
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
|
||||
this.lines.push(new BufferLine(newCols, nullCell, false));
|
||||
}
|
||||
} else {
|
||||
if (this.ydisp === this.ybase) {
|
||||
|
||||
@@ -4,19 +4,16 @@
|
||||
*/
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from './Constants';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { BufferLineStringCache } from './BufferLineStringCache';
|
||||
import { CellData } from './CellData';
|
||||
import { CharData, IBufferLine, ICellData } from './Types';
|
||||
import { assert } from 'chai';
|
||||
import { AttributeData } from './AttributeData';
|
||||
import { createCellData, NULL_CELL_DATA, extendedAttributes } from '../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);
|
||||
super(cols, fillCellData, isWrapped);
|
||||
}
|
||||
|
||||
public get combined(): {[index: number]: string} {
|
||||
@@ -24,19 +21,19 @@ class TestBufferLine extends BufferLine {
|
||||
}
|
||||
|
||||
public get cachedString(): string | undefined {
|
||||
return this._getStringCacheEntry(false)?.value;
|
||||
return this._cacheValid ? this._cache : undefined;
|
||||
}
|
||||
|
||||
public set cachedString(value: string | undefined) {
|
||||
this._getStringCacheEntry(true)!.value = value;
|
||||
this._cache = value ?? '';
|
||||
}
|
||||
|
||||
public get isCachedStringTrimmed(): boolean {
|
||||
return this._getStringCacheEntry(false)?.isTrimmed ?? false;
|
||||
return this._cacheTrimmed;
|
||||
}
|
||||
|
||||
public set isCachedStringTrimmed(value: boolean) {
|
||||
this._getStringCacheEntry(true)!.isTrimmed = value;
|
||||
this._cacheTrimmed = value;
|
||||
}
|
||||
|
||||
public toArray(): CharData[] {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AttributeData } from './AttributeData';
|
||||
import { CellData } from './CellData';
|
||||
import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from './Constants';
|
||||
import { stringFromCodePoint } from '../input/TextDecoder';
|
||||
import { StringBuilder } from '../StringBuilder';
|
||||
|
||||
// Buffer memory layout:
|
||||
//
|
||||
@@ -37,24 +36,20 @@ const enum Cell {
|
||||
BG = 2 // currently unused
|
||||
}
|
||||
|
||||
|
||||
interface IExtendedAttrsExt extends IExtendedAttrs {
|
||||
_ext: number;
|
||||
_urlId: number;
|
||||
}
|
||||
|
||||
|
||||
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $startIndex = 0;
|
||||
const $workCell = new CellData();
|
||||
const $translateToStringBuilder = new StringBuilder();
|
||||
const $extended = DEFAULT_ATTR_DATA.extended.clone() as IExtendedAttrsExt;
|
||||
|
||||
export interface IBufferLineStringCacheEntry {
|
||||
value: string | undefined;
|
||||
isTrimmed: boolean;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
export interface IBufferLineStringCache {
|
||||
generation: number;
|
||||
allocateEntry(): IBufferLineStringCacheEntry;
|
||||
touch?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
@@ -77,11 +72,14 @@ export class BufferLine implements IBufferLine {
|
||||
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;
|
||||
|
||||
/** line text cache */
|
||||
protected _cacheValid = false;
|
||||
protected _cache: string = '';
|
||||
protected _cacheTrimmed = false;
|
||||
|
||||
constructor(
|
||||
protected readonly _stringCache: IBufferLineStringCache,
|
||||
cols: number,
|
||||
fillCellData?: ICellData,
|
||||
public isWrapped: boolean = false
|
||||
@@ -118,7 +116,7 @@ export class BufferLine implements IBufferLine {
|
||||
* @deprecated
|
||||
*/
|
||||
public set(index: number, value: CharData): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
@@ -215,7 +213,11 @@ export class BufferLine implements IBufferLine {
|
||||
} 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();
|
||||
// We use $extended as blueprint and reset the internals
|
||||
// mimicking the ctor to avoid a new allocation.
|
||||
$extended._ext = 0;
|
||||
$extended._urlId = 0;
|
||||
cell.extended = $extended;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
@@ -224,7 +226,7 @@ export class BufferLine implements IBufferLine {
|
||||
* Set data at `index` to `cell`.
|
||||
*/
|
||||
public setCell(index: number, cell: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
this._combined[index] = cell.combinedData;
|
||||
}
|
||||
@@ -242,13 +244,14 @@ export class BufferLine implements IBufferLine {
|
||||
* it gets an optimized access method.
|
||||
*/
|
||||
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
if (attrs.bg & BgFlags.HAS_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.FG] = attrs.fg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.BG] = attrs.bg;
|
||||
const $idx = index * Constants.CELL_INDICIES;
|
||||
this._data[$idx + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[$idx + Cell.FG] = attrs.fg;
|
||||
this._data[$idx + Cell.BG] = attrs.bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +261,7 @@ export class BufferLine implements IBufferLine {
|
||||
* by the previous `setDataFromCodePoint` call, we can omit it here.
|
||||
*/
|
||||
public addCodepointToCell(index: number, codePoint: number, width: number): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
// we already have a combined string, simply add
|
||||
@@ -285,7 +288,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
pos %= this.length;
|
||||
|
||||
// handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
|
||||
@@ -313,7 +316,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
pos %= this.length;
|
||||
if (n < this.length - pos) {
|
||||
for (let i = 0; i < this.length - pos - n; ++i) {
|
||||
@@ -340,7 +343,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
// full branching on respectProtect==true, hopefully getting fast JIT for standard case
|
||||
if (respectProtect) {
|
||||
if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {
|
||||
@@ -380,7 +383,7 @@ export class BufferLine implements IBufferLine {
|
||||
* excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).
|
||||
*/
|
||||
public resize(cols: number, fillCellData: ICellData): boolean {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
if (cols === this.length) {
|
||||
return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
}
|
||||
@@ -440,7 +443,7 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** fill a line with fillCharData */
|
||||
public fill(fillCellData: ICellData, respectProtect: boolean = false): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
// full branching on respectProtect==true, hopefully getting fast JIT for standard case
|
||||
if (respectProtect) {
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
@@ -458,8 +461,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
/** alter to a full copy of line */
|
||||
public copyFrom(line: BufferLine): void {
|
||||
this._invalidateStringCache();
|
||||
public copyFrom(line: BufferLine, blank?: boolean): void {
|
||||
if (this.length !== line.length) {
|
||||
this._data = new Uint32Array(line._data);
|
||||
} else {
|
||||
@@ -467,16 +469,29 @@ export class BufferLine implements IBufferLine {
|
||||
this._data.set(line._data);
|
||||
}
|
||||
this.length = line.length;
|
||||
this._copySparseMapsFrom(line);
|
||||
if (blank) {
|
||||
// a blank line may never hold combined or extended attrs,
|
||||
// thus we can skip handling them
|
||||
this._combined = {};
|
||||
this._extendedAttrs = {};
|
||||
} else {
|
||||
this._copySparseMapsFrom(line);
|
||||
}
|
||||
this._cache = '';
|
||||
this._cacheValid = false;
|
||||
this.isWrapped = line.isWrapped;
|
||||
}
|
||||
|
||||
/** create a new clone */
|
||||
public clone(): IBufferLine {
|
||||
const newLine = new BufferLine(this._stringCache, 0, undefined, false);
|
||||
public clone(blank?: boolean): IBufferLine {
|
||||
const newLine = new BufferLine(0, undefined, false);
|
||||
newLine._data = new Uint32Array(this._data);
|
||||
newLine.length = this.length;
|
||||
newLine._copySparseMapsFrom(this);
|
||||
if (!blank) {
|
||||
// a blank line may never hold combined or extended attrs,
|
||||
// thus we can skip handling them
|
||||
newLine._copySparseMapsFrom(this);
|
||||
}
|
||||
newLine.isWrapped = this.isWrapped;
|
||||
return newLine;
|
||||
}
|
||||
@@ -500,7 +515,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
|
||||
this._invalidateStringCache();
|
||||
this._cacheValid = false;
|
||||
const srcData = src._data;
|
||||
if (applyInReverse) {
|
||||
for (let cell = length - 1; cell >= 0; cell--) {
|
||||
@@ -534,17 +549,13 @@ 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 isCanonicalRequest = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;
|
||||
if (isCanonicalRequest) {
|
||||
this._stringCache.touch?.();
|
||||
}
|
||||
const stringCacheEntry = isCanonicalRequest ? this._getStringCacheEntry(false) : undefined;
|
||||
if (isCanonicalRequest && stringCacheEntry?.value !== undefined) {
|
||||
const isCanonical = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;
|
||||
if (isCanonical && this._cacheValid) {
|
||||
if (trimRight) {
|
||||
return stringCacheEntry.isTrimmed ? stringCacheEntry.value : stringCacheEntry.value.trimEnd();
|
||||
return this._cacheTrimmed ? this._cache : this._cache.trimEnd();
|
||||
}
|
||||
if (!stringCacheEntry.isTrimmed) {
|
||||
return stringCacheEntry.value;
|
||||
if (!this._cacheTrimmed) {
|
||||
return this._cache;
|
||||
}
|
||||
}
|
||||
startCol = startCol ?? 0;
|
||||
@@ -555,12 +566,12 @@ export class BufferLine implements IBufferLine {
|
||||
if (outColumns) {
|
||||
outColumns.length = 0;
|
||||
}
|
||||
$translateToStringBuilder.reset();
|
||||
const cellContents: string[] = [];
|
||||
while (startCol < endCol) {
|
||||
const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
|
||||
$translateToStringBuilder.append(chars);
|
||||
cellContents.push(chars);
|
||||
if (outColumns) {
|
||||
for (let i = 0; i < chars.length; ++i) {
|
||||
outColumns.push(startCol);
|
||||
@@ -571,39 +582,15 @@ export class BufferLine implements IBufferLine {
|
||||
if (outColumns) {
|
||||
outColumns.push(startCol);
|
||||
}
|
||||
const result = $translateToStringBuilder.toString();
|
||||
$translateToStringBuilder.reset();
|
||||
if (isCanonicalRequest) {
|
||||
const cacheEntry = this._getStringCacheEntry(true)!;
|
||||
cacheEntry.value = result;
|
||||
cacheEntry.isTrimmed = !!trimRight;
|
||||
const result = cellContents.join('');
|
||||
if (isCanonical) {
|
||||
this._cache = result;
|
||||
this._cacheValid = true;
|
||||
this._cacheTrimmed = !!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 {
|
||||
const cacheEntry = this._getStringCacheEntry(false);
|
||||
if (cacheEntry) {
|
||||
cacheEntry.value = undefined;
|
||||
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;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { IBufferLineStringCache, IBufferLineStringCacheEntry } from './BufferLine';
|
||||
import { disposableTimeout } from '../Async';
|
||||
import { Disposable, MutableDisposable, toDisposable, type IDisposable } from '../Lifecycle';
|
||||
|
||||
const enum Constants {
|
||||
CACHE_TTL_MS = 15000
|
||||
}
|
||||
|
||||
export class BufferLineStringCache extends Disposable implements IBufferLineStringCache {
|
||||
public generation: number = 0;
|
||||
public readonly entries: Set<IBufferLineStringCacheEntry> = new Set();
|
||||
private readonly _clearTimeout = this._register(new MutableDisposable<IDisposable>());
|
||||
private _lastAccessTimestamp: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._register(toDisposable(() => this.entries.clear()));
|
||||
}
|
||||
|
||||
public touch(): void {
|
||||
this._scheduleClear();
|
||||
}
|
||||
|
||||
public allocateEntry(): IBufferLineStringCacheEntry {
|
||||
const entry: IBufferLineStringCacheEntry = {
|
||||
value: undefined,
|
||||
isTrimmed: false,
|
||||
generation: this.generation
|
||||
};
|
||||
this.entries.add(entry);
|
||||
this._scheduleClear();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._clearTimeout.clear();
|
||||
this._lastAccessTimestamp = 0;
|
||||
this.generation++;
|
||||
for (const entry of this.entries) {
|
||||
entry.value = undefined;
|
||||
entry.isTrimmed = false;
|
||||
}
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
private _scheduleClear(): void {
|
||||
this._lastAccessTimestamp = Date.now();
|
||||
if (this._clearTimeout.value) {
|
||||
return;
|
||||
}
|
||||
this._scheduleClearTimeout(Constants.CACHE_TTL_MS);
|
||||
}
|
||||
|
||||
private _scheduleClearTimeout(timeoutMs: number): void {
|
||||
this._clearTimeout.value = disposableTimeout(() => {
|
||||
const elapsed = Date.now() - this._lastAccessTimestamp;
|
||||
if (elapsed >= Constants.CACHE_TTL_MS) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
this._scheduleClearTimeout(Constants.CACHE_TTL_MS - elapsed);
|
||||
}, timeoutMs);
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,16 @@
|
||||
import { assert } from 'chai';
|
||||
import { CircularList } from '../CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { BufferLineStringCache } from './BufferLineStringCache';
|
||||
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';
|
||||
|
||||
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(TEST_STRING_CACHE, 4);
|
||||
const line = new BufferLine(4);
|
||||
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(1, [0, '', 0, 0]);
|
||||
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
@@ -26,7 +24,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(TEST_STRING_CACHE, 12);
|
||||
const line = new BufferLine(12);
|
||||
for (let i = 0; i < 12; i += 4) {
|
||||
line.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
@@ -48,7 +46,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(TEST_STRING_CACHE, 6);
|
||||
const line = new BufferLine(6);
|
||||
line.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(2, [0, '', 0, 0]);
|
||||
@@ -62,14 +60,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(TEST_STRING_CACHE, 6);
|
||||
const line1 = new BufferLine(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(TEST_STRING_CACHE, 6, undefined, true);
|
||||
const line2 = new BufferLine(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]);
|
||||
@@ -84,7 +82,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(TEST_STRING_CACHE, 5);
|
||||
const line = new BufferLine(5);
|
||||
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(1, [0, '', 0, 0]);
|
||||
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
@@ -102,7 +100,7 @@ describe('BufferReflow', () => {
|
||||
function createWrappedLines(chars: string): CircularList<IBufferLine> {
|
||||
const lines = new CircularList<IBufferLine>(chars.length);
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 1);
|
||||
const line = new BufferLine(1);
|
||||
line.set(0, [0, chars[i], 1, chars.charCodeAt(i)]);
|
||||
line.isWrapped = i > 0;
|
||||
lines.push(line);
|
||||
|
||||
@@ -82,45 +82,4 @@ describe('BufferSet', () => {
|
||||
assert.equal(bufferSet.alt.markers.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('should dispose previous buffers on reset', () => {
|
||||
const oldNormal = bufferSet.normal as any;
|
||||
oldNormal.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
|
||||
oldNormal.translateBufferLineToString(0, false);
|
||||
|
||||
const oldCache = oldNormal._stringCache;
|
||||
assert.equal(oldCache.entries.size, 1);
|
||||
assert.notEqual(oldCache._clearTimeout.value, undefined);
|
||||
|
||||
bufferSet.reset();
|
||||
|
||||
assert.notEqual(bufferSet.normal, oldNormal);
|
||||
assert.equal(oldCache.entries.size, 0);
|
||||
assert.equal(oldCache._clearTimeout.value, undefined);
|
||||
});
|
||||
|
||||
it('should dispose both buffers when disposed', () => {
|
||||
const normal = bufferSet.normal as any;
|
||||
normal.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
|
||||
normal.translateBufferLineToString(0, false);
|
||||
|
||||
bufferSet.activateAltBuffer();
|
||||
const alt = bufferSet.alt as any;
|
||||
alt.lines.get(0)!.setCell(0, createCellData(0, 'b', 1));
|
||||
alt.translateBufferLineToString(0, false);
|
||||
|
||||
const normalCache = normal._stringCache;
|
||||
const altCache = alt._stringCache;
|
||||
assert.notEqual(normalCache._clearTimeout.value, undefined);
|
||||
assert.notEqual(altCache._clearTimeout.value, undefined);
|
||||
|
||||
bufferSet.dispose();
|
||||
|
||||
assert.equal(normalCache.entries.size, 0);
|
||||
assert.equal(altCache.entries.size, 0);
|
||||
assert.equal(normalCache._clearTimeout.value, undefined);
|
||||
assert.equal(altCache._clearTimeout.value, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,8 +131,8 @@ export interface IBufferLine {
|
||||
resize(cols: number, fill: ICellData): boolean;
|
||||
cleanupMemory(): number;
|
||||
fill(fillCellData: ICellData, respectProtect?: boolean): void;
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
copyFrom(line: IBufferLine, blank?: boolean): void;
|
||||
clone(blank?: boolean): IBufferLine;
|
||||
getTrimmedLength(): number;
|
||||
getNoBgTrimmedLength(): number;
|
||||
translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string;
|
||||
|
||||
@@ -86,12 +86,12 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
// Insert the line using the fastest method
|
||||
if (bottomRow === buffer.lines.length - 1) {
|
||||
if (willBufferBeTrimmed) {
|
||||
buffer.lines.recycle().copyFrom(newLine);
|
||||
buffer.lines.recycle().copyFrom(newLine, true);
|
||||
} else {
|
||||
buffer.lines.push(newLine.clone());
|
||||
buffer.lines.push(newLine.clone(true));
|
||||
}
|
||||
} else {
|
||||
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
|
||||
buffer.lines.splice(bottomRow + 1, 0, newLine.clone(true));
|
||||
}
|
||||
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
@@ -113,7 +113,7 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
// scrollback, instead we can just shift them in-place.
|
||||
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
|
||||
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
|
||||
buffer.lines.set(bottomRow, newLine.clone());
|
||||
buffer.lines.set(bottomRow, newLine.clone(true));
|
||||
}
|
||||
|
||||
// Move the viewport to the bottom of the buffer unless the user is
|
||||
|
||||
Reference in New Issue
Block a user