Files
xterm.js/src/common/buffer/BufferLine.test.ts
T
Cursor AgentandDaniel Imms eacf90b2b7 Split common into composite subprojects with project references
- common/tsconfig.json is a solution root referencing layer tsconfigs
- Each layer has composite tsconfig with correct references and outDir
- Break buffer/services cycle via buffer-local service interfaces
- Move cell types, keyboard/unicode types, TaskQueue, and TerminalOptions
- Exclude *.test.ts from composite builds; fix parser IDisposable imports

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-30 17:32:03 +00:00

911 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
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, ICellData } from 'common/buffer/CellTypes';
import { assert } from 'chai';
import { AttributeData } from 'common/buffer/AttributeData';
import { createCellData, NULL_CELL_DATA, extendedAttributes } from 'common/terminal/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._getStringCacheEntry(false)?.value;
}
public set cachedString(value: string | undefined) {
this._getStringCacheEntry(true)!.value = value;
}
public get isCachedStringTrimmed(): boolean {
return this._getStringCacheEntry(false)?.isTrimmed ?? false;
}
public set isCachedStringTrimmed(value: boolean) {
this._getStringCacheEntry(true)!.isTrimmed = value;
}
public toArray(): CharData[] {
const result = [];
for (let i = 0; i < this.length; ++i) {
result.push(this.loadCell(i, new CellData()).getAsCharData());
}
return result;
}
}
describe('AttributeData', () => {
describe('extended attributes', () => {
it('hasExtendedAttrs', () => {
const attrs = new AttributeData();
assert.equal(!!attrs.hasExtendedAttrs(), false);
attrs.bg |= BgFlags.HAS_EXTENDED;
assert.equal(!!attrs.hasExtendedAttrs(), true);
});
it('getUnderlineColor - P256', () => {
const attrs = new AttributeData();
// set a P256 color
attrs.extended.underlineColor = Attributes.CM_P256 | 45;
// should use FG color if BgFlags.HAS_EXTENDED is not set
assert.equal(attrs.getUnderlineColor(), -1);
// should use underlineColor if BgFlags.HAS_EXTENDED is set and underlineColor holds a value
attrs.bg |= BgFlags.HAS_EXTENDED;
assert.equal(attrs.getUnderlineColor(), 45);
// should use FG color if underlineColor holds no value
attrs.extended.underlineColor = 0;
attrs.fg |= Attributes.CM_P256 | 123;
assert.equal(attrs.getUnderlineColor(), 123);
});
it('getUnderlineColor - RGB', () => {
const attrs = new AttributeData();
// set a P256 color
attrs.extended.underlineColor = Attributes.CM_RGB | (1 << 16) | (2 << 8) | 3;
// should use FG color if BgFlags.HAS_EXTENDED is not set
assert.equal(attrs.getUnderlineColor(), -1);
// should use underlineColor if BgFlags.HAS_EXTENDED is set and underlineColor holds a value
attrs.bg |= BgFlags.HAS_EXTENDED;
assert.equal(attrs.getUnderlineColor(), (1 << 16) | (2 << 8) | 3);
// should use FG color if underlineColor holds no value
attrs.extended.underlineColor = 0;
attrs.fg |= Attributes.CM_P256 | 123;
assert.equal(attrs.getUnderlineColor(), 123);
});
it('getUnderlineColorMode / isUnderlineColorRGB / isUnderlineColorPalette / isUnderlineColorDefault', () => {
const attrs = new AttributeData();
// should always return color mode of fg
for (const mode of [Attributes.CM_DEFAULT, Attributes.CM_P16, Attributes.CM_P256, Attributes.CM_RGB]) {
attrs.extended.underlineColor = mode;
assert.equal(attrs.getUnderlineColorMode(), attrs.getFgColorMode());
assert.equal(attrs.isUnderlineColorDefault(), true);
}
attrs.fg = Attributes.CM_RGB;
for (const mode of [Attributes.CM_DEFAULT, Attributes.CM_P16, Attributes.CM_P256, Attributes.CM_RGB]) {
attrs.extended.underlineColor = mode;
assert.equal(attrs.getUnderlineColorMode(), attrs.getFgColorMode());
assert.equal(attrs.isUnderlineColorDefault(), false);
assert.equal(attrs.isUnderlineColorRGB(), true);
}
// should return own mode
attrs.bg |= BgFlags.HAS_EXTENDED;
attrs.extended.underlineColor = Attributes.CM_DEFAULT;
assert.equal(attrs.getUnderlineColorMode(), Attributes.CM_DEFAULT);
attrs.extended.underlineColor = Attributes.CM_P16;
assert.equal(attrs.getUnderlineColorMode(), Attributes.CM_P16);
assert.equal(attrs.isUnderlineColorPalette(), true);
attrs.extended.underlineColor = Attributes.CM_P256;
assert.equal(attrs.getUnderlineColorMode(), Attributes.CM_P256);
assert.equal(attrs.isUnderlineColorPalette(), true);
attrs.extended.underlineColor = Attributes.CM_RGB;
assert.equal(attrs.getUnderlineColorMode(), Attributes.CM_RGB);
assert.equal(attrs.isUnderlineColorRGB(), true);
});
it('getUnderlineStyle', () => {
const attrs = new AttributeData();
// defaults to no underline style
assert.equal(attrs.getUnderlineStyle(), UnderlineStyle.NONE);
// should return NONE if UNDERLINE is not set
attrs.extended.underlineStyle = UnderlineStyle.CURLY;
assert.equal(attrs.getUnderlineStyle(), UnderlineStyle.NONE);
// should return SINGLE style if UNDERLINE is set and HAS_EXTENDED is false
attrs.fg |= FgFlags.UNDERLINE;
assert.equal(attrs.getUnderlineStyle(), UnderlineStyle.SINGLE);
// should return correct style if both is set
attrs.bg |= BgFlags.HAS_EXTENDED;
assert.equal(attrs.getUnderlineStyle(), UnderlineStyle.CURLY);
// should return NONE if UNDERLINE is not set, but HAS_EXTENDED is true
attrs.fg &= ~FgFlags.UNDERLINE;
assert.equal(attrs.getUnderlineStyle(), UnderlineStyle.NONE);
});
it('getUnderlineVariantOffset', () => {
const attrs = new AttributeData();
// defaults to no offset
assert.equal(attrs.getUnderlineVariantOffset(), 0);
// should return 0 - 7
for (let i = 0; i < 8; ++i) {
attrs.extended.underlineVariantOffset = i;
assert.equal(attrs.getUnderlineVariantOffset(), i);
}
});
});
});
describe('CellData', () => {
it('CharData <--> CellData equality', () => {
const cell = new CellData();
// ASCII
cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]);
assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]);
assert.equal(cell.isCombined(), 0);
// combining
cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
// surrogate
cell.setFromCharData([123, '𝄞', 1, 0x1D11E]);
assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]);
assert.equal(cell.isCombined(), 0);
// surrogate + combining
cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
// wide char
cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]);
assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]);
assert.equal(cell.isCombined(), 0);
});
});
describe('BufferLine', function(): void {
it('ctor', function(): void {
let line: IBufferLine = new TestBufferLine(0);
assert.equal(line.length, 0);
assert.equal(line.isWrapped, false);
line = new TestBufferLine(10);
assert.equal(line.length, 10);
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
assert.equal(line.isWrapped, false);
line = new TestBufferLine(10, undefined, true);
assert.equal(line.length, 10);
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
assert.equal(line.isWrapped, true);
line = new TestBufferLine(10, createCellData(123, 'a', 456), true);
assert.equal(line.length, 10);
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 456, 'a'.charCodeAt(0)]);
assert.equal(line.isWrapped, true);
});
it('insertCells', function(): void {
const line = new TestBufferLine(3);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.insertCells(1, 3, createCellData(4, 'd', 1));
assert.deepEqual(line.toArray(), [
[1, 'a', 1, 'a'.charCodeAt(0)],
[4, 'd', 1, 'd'.charCodeAt(0)],
[4, 'd', 1, 'd'.charCodeAt(0)]
]);
});
it('deleteCells', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
line.deleteCells(1, 2, createCellData(6, 'f', 1));
assert.deepEqual(line.toArray(), [
[1, 'a', 1, 'a'.charCodeAt(0)],
[4, 'd', 1, 'd'.charCodeAt(0)],
[5, 'e', 1, 'e'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)]
]);
});
it('replaceCells', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
line.replaceCells(2, 4, createCellData(6, 'f', 1));
assert.deepEqual(line.toArray(), [
[1, 'a', 1, 'a'.charCodeAt(0)],
[2, 'b', 1, 'b'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)],
[5, 'e', 1, 'e'.charCodeAt(0)]
]);
});
it('fill', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
line.fill(createCellData(123, 'z', 1));
assert.deepEqual(line.toArray(), [
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)]
]);
});
it('clone', function(): void {
const line = new TestBufferLine(5, undefined, true);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
const line2 = line.clone();
assert.deepEqual(TestBufferLine.prototype.toArray.apply(line2), line.toArray());
assert.equal(line2.length, line.length);
assert.equal(line2.isWrapped, line.isWrapped);
});
it('copyFrom', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
const line2 = new TestBufferLine(5, createCellData(1, 'a', 1), true);
line2.copyFrom(line);
assert.deepEqual(line2.toArray(), line.toArray());
assert.equal(line2.length, line.length);
assert.equal(line2.isWrapped, line.isWrapped);
});
it('should support combining chars', function(): void {
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
// --> set code to the last charCodeAt value of the string
// Note: needs to be fixed once the string pointer is in place
const line = new TestBufferLine(2, createCellData(1, 'e\u0301', 1));
assert.deepEqual(line.toArray(), [[1, 'e\u0301', 1, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, createCellData(1, 'a', 1), true);
line2.copyFrom(line);
assert.deepEqual(line2.toArray(), line.toArray());
const line3 = line.clone();
assert.deepEqual(TestBufferLine.prototype.toArray.apply(line3), line.toArray());
});
describe('resize', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, createCellData(1, 'a', 1), false);
line.resize(10, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, createCellData(1, 'a', 1), false);
line.resize(10, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, createCellData(1, 'a', 1), false);
line.resize(5, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(5) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, createCellData(1, 'a', 1), false);
line.resize(0, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(0) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
const line = new TestBufferLine(10, createCellData(1, 'a', 1), false);
line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
assert.equal(line.translateToString(), 'aa😁aaaaaa😁');
assert.equal(Object.keys(line.combined).length, 2);
line.resize(5, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aa😁aa');
line.resize(10, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aa😁aaaaaaa');
assert.equal(Object.keys(line.combined).length, 1);
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
assert.equal(line.getTrimmedLength(), 0);
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'a', 1));
assert.equal(line.getTrimmedLength(), 3);
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '𝄞', 1));
assert.equal(line.getTrimmedLength(), 3);
});
it('combining', function(): void {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'e\u0301', 1));
assert.equal(line.getTrimmedLength(), 3);
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '1', 2));
line.setCell(3, createCellData(0, '', 0));
assert.equal(line.getTrimmedLength(), 4); // also counts null cell after fullwidth
});
});
describe('translateToString with and w\'o trimming', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
const columns: number[] = [];
assert.equal(line.translateToString(false, undefined, undefined, columns), ' ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), '');
assert.deepEqual(columns, [0]);
});
it('ASCII', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'a', 1));
line.setCell(4, createCellData(1, 'a', 1));
line.setCell(5, createCellData(1, 'a', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a a aa ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a a aa');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6]);
for (const trimRight of [true, false]) {
assert.equal(line.translateToString(trimRight, 0, 5, columns), 'a a a');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5]);
assert.equal(line.translateToString(trimRight, 0, 4, columns), 'a a ');
assert.deepEqual(columns, [0, 1, 2, 3, 4]);
assert.equal(line.translateToString(trimRight, 0, 3, columns), 'a a');
assert.deepEqual(columns, [0, 1, 2, 3]);
}
});
it('surrogate', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '𝄞', 1));
line.setCell(4, createCellData(1, '𝄞', 1));
line.setCell(5, createCellData(1, '𝄞', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a 𝄞 𝄞𝄞 ');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a 𝄞 𝄞𝄞');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5, 5, 6]);
for (const trimRight of [true, false]) {
assert.equal(line.translateToString(trimRight, 0, 5, columns), 'a 𝄞 𝄞');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5]);
assert.equal(line.translateToString(trimRight, 0, 4, columns), 'a 𝄞 ');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4]);
assert.equal(line.translateToString(trimRight, 0, 3, columns), 'a 𝄞');
assert.deepEqual(columns, [0, 1, 2, 2, 3]);
}
});
it('combining', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'e\u0301', 1));
line.setCell(4, createCellData(1, 'e\u0301', 1));
line.setCell(5, createCellData(1, 'e\u0301', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a e\u0301 e\u0301e\u0301 ');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a e\u0301 e\u0301e\u0301');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5, 5, 6]);
for (const trimRight of [true, false]) {
assert.equal(line.translateToString(trimRight, 0, 5, columns), 'a e\u0301 e\u0301');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5]);
assert.equal(line.translateToString(trimRight, 0, 4, columns), 'a e\u0301 ');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4]);
assert.equal(line.translateToString(trimRight, 0, 3, columns), 'a e\u0301');
assert.deepEqual(columns, [0, 1, 2, 2, 3]);
}
});
it('fullwidth', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '1', 2));
line.setCell(3, createCellData(0, '', 0));
line.setCell(5, createCellData(1, '1', 2));
line.setCell(6, createCellData(0, '', 0));
line.setCell(7, createCellData(1, '1', 2));
line.setCell(8, createCellData(0, '', 0));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a 1 11 ');
assert.deepEqual(columns, [0, 1, 2, 4, 5, 7, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a 1 11');
assert.deepEqual(columns, [0, 1, 2, 4, 5, 7, 9]);
for (const trimRight of [true, false]) {
assert.equal(line.translateToString(trimRight, 0, 7, columns), 'a 1 1');
assert.deepEqual(columns, [0, 1, 2, 4, 5, 7]);
assert.equal(line.translateToString(trimRight, 0, 6, columns), 'a 1 1');
assert.deepEqual(columns, [0, 1, 2, 4, 5, 7]);
assert.equal(line.translateToString(trimRight, 0, 5, columns), 'a 1 ');
assert.deepEqual(columns, [0, 1, 2, 4, 5]);
assert.equal(line.translateToString(trimRight, 0, 4, columns), 'a 1');
assert.deepEqual(columns, [0, 1, 2, 4]);
assert.equal(line.translateToString(trimRight, 0, 3, columns), 'a 1');
assert.deepEqual(columns, [0, 1, 2, 4]);
assert.equal(line.translateToString(trimRight, 0, 2, columns), 'a ');
assert.deepEqual(columns, [0, 1, 2]);
}
});
it('space at end', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'a', 1));
line.setCell(4, createCellData(1, 'a', 1));
line.setCell(5, createCellData(1, 'a', 1));
line.setCell(6, createCellData(1, ' ', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a a aa ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a a aa ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7]);
});
it('should always return some sane value', function(): void {
const columns: number[] = [];
// sanity check - broken line with invalid out of bound null width cells
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
assert.equal(line.translateToString(false, undefined, undefined, columns), ' ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), '');
assert.deepEqual(columns, [0]);
});
it('should work with endCol=0', () => {
const columns: number[] = [];
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
assert.equal(line.translateToString(true, 0, 0, columns), '');
assert.deepEqual(columns, [0]);
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0);
const cell = line.loadCell(0, new CellData());
// chars contains single combining char
// width is set to 1
assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]);
// do not account a single combining char as combined
assert.equal(cell.isCombined(), 0);
});
it('should add char to combining string in cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]);
line.setCell(0, cell);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0);
line.loadCell(0, cell);
// chars contains 3 chars
// width is set to 1
assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]);
// do not account a single combining char as combined
assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
it('should create combining string on taken cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]);
line.setCell(0, cell);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0);
line.loadCell(0, cell);
// chars contains 2 chars
// width is set to 1
assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]);
// do not account a single combining char as combined
assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
});
describe('correct fullwidth handling', () => {
function populate(line: BufferLine): void {
const cell = createCellData(1, '¥', 2);
for (let i = 0; i < line.length; i += 2) {
line.setCell(i, cell);
}
}
it('insert - wide char at pos', () => {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.insertCells(9, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), '¥¥¥¥ a');
line.insertCells(8, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), '¥¥¥¥a ');
line.insertCells(1, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' a ¥¥¥a');
});
it('insert - wide char at end', () => {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.insertCells(0, 3, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa¥¥¥ ');
line.insertCells(4, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa a ¥¥');
line.insertCells(4, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa aa ¥ ');
});
it('delete', () => {
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.deleteCells(0, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' ¥¥¥¥a');
line.deleteCells(5, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' ¥¥¥aaa');
line.deleteCells(0, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' ¥¥aaaaa');
});
it('replace - start at 0', () => {
let line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'a ¥¥¥¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aa¥¥¥¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 3, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa ¥¥¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 8, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaaaaaaa¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 9, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaaaaaaaa ');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 10, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaaaaaaaaa');
});
it('replace - start at 1', () => {
let line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' a¥¥¥¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 3, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aa ¥¥¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 4, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaa¥¥¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 8, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaaaaaa¥');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 9, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaaaaaaa ');
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 10, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaaaaaaaa');
});
});
describe('extended attributes', () => {
it('setCells', function(): void {
const line = new TestBufferLine(5);
const cell = createCellData(1, 'a', 1);
// no eAttrs
line.setCell(0, cell);
// some underline style
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.setCell(1, cell);
// same eAttr, different codepoint
cell.content = createCellData(1, 'A', 1).content;
line.setCell(2, cell);
// different eAttr
cell.extended = cell.extended.clone();
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
line.setCell(3, cell);
// no eAttrs again
cell.bg &= ~BgFlags.HAS_EXTENDED;
line.setCell(4, cell);
assert.deepEqual(line.toArray(), [
[1, 'a', 1, 'a'.charCodeAt(0)],
[1, 'a', 1, 'a'.charCodeAt(0)],
[1, 'A', 1, 'A'.charCodeAt(0)],
[1, 'A', 1, 'A'.charCodeAt(0)],
[1, 'A', 1, 'A'.charCodeAt(0)]
]);
assert.equal(extendedAttributes(line, 0), undefined);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, undefined);
// should be ref to the same object
assert.equal(extendedAttributes(line, 1), extendedAttributes(line, 2));
// should be a different obj
assert.notEqual(extendedAttributes(line, 1), extendedAttributes(line, 3));
});
it('loadCell', () => {
const line = new TestBufferLine(5);
const cell = createCellData(1, 'a', 1);
// no eAttrs
line.setCell(0, cell);
// some underline style
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.setCell(1, cell);
// same eAttr, different codepoint
cell.content = 65; // 'A'
line.setCell(2, cell);
// different eAttr
cell.extended = cell.extended.clone();
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
line.setCell(3, cell);
// no eAttrs again
cell.bg &= ~BgFlags.HAS_EXTENDED;
line.setCell(4, cell);
const cell0 = new CellData();
line.loadCell(0, cell0);
const cell1 = new CellData();
line.loadCell(1, cell1);
const cell2 = new CellData();
line.loadCell(2, cell2);
const cell3 = new CellData();
line.loadCell(3, cell3);
const cell4 = new CellData();
line.loadCell(4, cell4);
assert.equal(cell0.extended.underlineStyle, UnderlineStyle.NONE);
assert.equal(cell1.extended.underlineStyle, UnderlineStyle.CURLY);
assert.equal(cell2.extended.underlineStyle, UnderlineStyle.CURLY);
assert.equal(cell3.extended.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(cell4.extended.underlineStyle, UnderlineStyle.NONE);
assert.equal(cell1.extended, cell2.extended);
assert.notEqual(cell2.extended, cell3.extended);
});
it('fill', () => {
const line = new TestBufferLine(3);
const cell = createCellData(1, 'a', 1);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.fill(cell);
assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY);
});
it('insertCells', () => {
const line = new TestBufferLine(5);
const cell = createCellData(1, 'a', 1);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.insertCells(1, 3, cell);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 4), undefined);
cell.extended = cell.extended.clone();
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
line.insertCells(2, 2, cell);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY);
});
it('deleteCells', () => {
const line = new TestBufferLine(5);
const fillCell = createCellData(1, 'a', 1);
fillCell.extended.underlineStyle = UnderlineStyle.CURLY;
fillCell.bg |= BgFlags.HAS_EXTENDED;
line.fill(fillCell);
fillCell.extended = fillCell.extended.clone();
fillCell.extended.underlineStyle = UnderlineStyle.DOUBLE;
line.deleteCells(1, 3, fillCell);
assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.DOUBLE);
});
it('replaceCells', () => {
const line = new TestBufferLine(5);
const fillCell = createCellData(1, 'a', 1);
fillCell.extended.underlineStyle = UnderlineStyle.CURLY;
fillCell.bg |= BgFlags.HAS_EXTENDED;
line.fill(fillCell);
fillCell.extended = fillCell.extended.clone();
fillCell.extended.underlineStyle = UnderlineStyle.DOUBLE;
line.replaceCells(1, 3, fillCell);
assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY);
});
it('clone', () => {
const line = new TestBufferLine(5);
const cell = createCellData(1, 'a', 1);
// no eAttrs
line.setCell(0, cell);
// some underline style
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.setCell(1, cell);
// same eAttr, different codepoint
cell.content = 65; // 'A'
line.setCell(2, cell);
// different eAttr
cell.extended = cell.extended.clone();
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
line.setCell(3, cell);
// no eAttrs again
cell.bg &= ~BgFlags.HAS_EXTENDED;
line.setCell(4, cell);
const nLine = line.clone();
assert.equal(extendedAttributes(nLine, 0), extendedAttributes(line, 0));
assert.equal(extendedAttributes(nLine, 1), extendedAttributes(line, 1));
assert.equal(extendedAttributes(nLine, 2), extendedAttributes(line, 2));
assert.equal(extendedAttributes(nLine, 3), extendedAttributes(line, 3));
assert.equal(extendedAttributes(nLine, 4), extendedAttributes(line, 4));
});
it('copyFrom', () => {
const initial = new TestBufferLine(5);
const cell = createCellData(1, 'a', 1);
// no eAttrs
initial.setCell(0, cell);
// some underline style
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
initial.setCell(1, cell);
// same eAttr, different codepoint
cell.content = 65; // 'A'
initial.setCell(2, cell);
// different eAttr
cell.extended = cell.extended.clone();
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
initial.setCell(3, cell);
// no eAttrs again
cell.bg &= ~BgFlags.HAS_EXTENDED;
initial.setCell(4, cell);
const line = new TestBufferLine(5);
line.fill(createCellData(1, 'b', 1));
line.copyFrom(initial);
assert.equal(extendedAttributes(line, 0), extendedAttributes(initial, 0));
assert.equal(extendedAttributes(line, 1), extendedAttributes(initial, 1));
assert.equal(extendedAttributes(line, 2), extendedAttributes(initial, 2));
assert.equal(extendedAttributes(line, 3), extendedAttributes(initial, 3));
assert.equal(extendedAttributes(line, 4), extendedAttributes(initial, 4));
});
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.cachedString, 'abc');
assert.equal(line.isCachedStringTrimmed, true);
// Non-trimmed canonical request should refresh cache with the full value.
const translated = line.translateToString(false, undefined, undefined, undefined);
assert.equal(translated, 'abc ');
assert.equal(line.cachedString, 'abc ');
assert.equal(line.isCachedStringTrimmed, false);
// Once non-trimmed is cached, trimmed should be derived via trimEnd().
assert.equal(line.translateToString(true, undefined, undefined, undefined), 'abc');
assert.equal(line.cachedString, 'abc ');
assert.equal(line.isCachedStringTrimmed, false);
line.cachedString = 'cached-non-trimmed ';
line.isCachedStringTrimmed = false;
assert.equal(line.translateToString(false, undefined, undefined, undefined), 'cached-non-trimmed ');
assert.equal(line.translateToString(true, undefined, undefined, undefined), 'cached-non-trimmed');
line.cachedString = 'cached-trimmed';
line.isCachedStringTrimmed = true;
assert.equal(line.translateToString(true, undefined, undefined, undefined), 'cached-trimmed');
assert.equal(line.translateToString(false, undefined, undefined, undefined), 'abc ');
assert.equal(line.cachedString, 'abc ');
assert.equal(line.isCachedStringTrimmed, false);
// Any optional translation argument should bypass cache.
assert.equal(line.translateToString(false, 0, 2, undefined), 'ab');
assert.equal(line.translateToString(true, 0, 2, undefined), 'ab');
});
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.cachedString, 'aaaaa');
assert.equal(line.isCachedStringTrimmed, true);
line.translateToString(false, undefined, undefined, undefined);
assert.equal(line.cachedString, 'aaaaa');
assert.equal(line.isCachedStringTrimmed, false);
mutate(line);
assert.equal(line.cachedString, undefined);
assert.equal(line.isCachedStringTrimmed, false);
};
assertCacheInvalidated(line => line.set(0, [0, 'b', 1, 'b'.charCodeAt(0)]));
assertCacheInvalidated(line => line.setCell(0, createCellData(1, 'b', 1)));
assertCacheInvalidated(line => line.setCellFromCodepoint(0, 'b'.charCodeAt(0), 1, createCellData(1, 'b', 1)));
assertCacheInvalidated(line => line.addCodepointToCell(0, 0x301, 0));
assertCacheInvalidated(line => line.insertCells(1, 1, createCellData(1, 'b', 1)));
assertCacheInvalidated(line => line.deleteCells(1, 1, createCellData(1, 'b', 1)));
assertCacheInvalidated(line => line.replaceCells(1, 3, createCellData(1, 'b', 1)));
assertCacheInvalidated(line => line.resize(6, createCellData(1, 'b', 1)));
assertCacheInvalidated(line => line.fill(createCellData(1, 'b', 1)));
assertCacheInvalidated(line => {
const src = new TestBufferLine(5);
src.fill(createCellData(1, 'x', 1));
line.copyFrom(src);
});
assertCacheInvalidated(line => {
const src = new TestBufferLine(5);
src.fill(createCellData(1, 'x', 1));
line.copyCellsFrom(src, 0, 0, 2, false);
});
});
});
});