From fd5f5e3204843263c13bda850502d1d77be2b3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 8 Mar 2020 18:57:05 +0100 Subject: [PATCH] extend AttributeData with eAttrs, implement SGR 4 subparams, tests --- src/InputHandler.ts | 47 ++- src/common/Types.d.ts | 15 +- src/common/buffer/AttributeData.ts | 53 ++- src/common/buffer/Buffer.ts | 5 + src/common/buffer/BufferLine.test.ts | 503 ++++++++++++++++++++------- src/common/buffer/BufferLine.ts | 42 ++- src/common/buffer/CellData.ts | 13 +- src/common/buffer/Constants.ts | 14 +- 8 files changed, 540 insertions(+), 152 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index fb37f4ab1..1841f0110 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -497,7 +497,7 @@ export class InputHandler extends Disposable implements IInputHandler { // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char if (buffer.x && end - start > 0 && bufferRow.getWidth(buffer.x - 1) === 2) { - bufferRow.setCellFromCodePoint(buffer.x - 1, 0, 1, curAttr.fg, curAttr.bg); + bufferRow.setCellFromCodePoint(buffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } for (let pos = start; pos < end; ++pos) { @@ -577,12 +577,12 @@ export class InputHandler extends Disposable implements IInputHandler { // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell if (bufferRow.getWidth(cols - 1) === 2) { - bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg); + bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg, curAttr.extended); } } // write current char to buffer and advance cursor - bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg); + bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero @@ -590,7 +590,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg); + bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); } } } @@ -611,7 +611,7 @@ export class InputHandler extends Disposable implements IInputHandler { // handle wide chars: reset cell to the right if it is second cell of a wide char if (buffer.x < cols && end - start > 0 && bufferRow.getWidth(buffer.x) === 0 && !bufferRow.hasContent(buffer.x)) { - bufferRow.setCellFromCodePoint(buffer.x, 0, 1, curAttr.fg, curAttr.bg); + bufferRow.setCellFromCodePoint(buffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } this._dirtyRowService.markDirty(buffer.y); @@ -2098,6 +2098,40 @@ export class InputHandler extends Disposable implements IInputHandler { return advance; } + /** + * SGR 4 subparams: + * 4:0 - equal to SGR 24 (turn off all underline) + * 4:1 - equal to SGR 4 (single underline) + * 4:2 - equal to SGR 21 (double underline) + * 4:3 - curly underline + * 4:4 - dotted underline + * 4:5 - dashed underline + */ + private _processUnderline(subparams: Int32Array, attr: IAttributeData): void { + // treat extended attrs as immutable, thus always clone from old one + // this is needed since the buffer only holds references to it + attr.extended = attr.extended.clone(); + let style = subparams[0]; + + // default to 1 == single underline + if (!~style || style > 5) { + style = 1; + } + attr.extended.underlineStyle = style; + + // 0 deactivates underline + if (style === 0) { + attr.fg &= ~FgFlags.UNDERLINE; + } + + // update HAS_EXTENDED in BG + if (attr.extended.isEmpty()) { + attr.bg &= ~BgFlags.HAS_EXTENDED; + } else { + attr.bg |= BgFlags.HAS_EXTENDED; + } + } + /** * CSI Pm m Character Attributes (SGR). * @@ -2210,6 +2244,9 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 4) { // underlined text attr.fg |= FgFlags.UNDERLINE; + if (params.hasSubParams(i)) { + this._processUnderline(params.getSubParams(i), attr); + } } else if (p === 5) { // blink attr.fg |= FgFlags.BLINK; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index b250b45ea..bc76efdaa 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -68,10 +68,18 @@ export interface ICharset { export type CharData = [number, string, number, number]; export type IColorRGB = [number, number, number]; +export interface IExtendedAttrs { + underlineStyle: number; + underlineColor: number; + clone(): IExtendedAttrs; + isEmpty(): boolean; +} + /** Attribute data */ export interface IAttributeData { fg: number; bg: number; + extended: IExtendedAttrs; clone(): IAttributeData; @@ -98,6 +106,11 @@ export interface IAttributeData { // colors getFgColor(): number; getBgColor(): number; + + // extended attrs + hasExtendedAttrs(): number; + getUnderlineColor(): number; + getUnderlineStyle(): number; } /** Cell data */ @@ -122,7 +135,7 @@ export interface IBufferLine { set(index: number, value: CharData): void; loadCell(index: number, cell: ICellData): ICellData; setCell(index: number, cell: ICellData): void; - setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; + setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void; addCodepointToCell(index: number, codePoint: number): void; insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 0b2679ee4..b8b64169e 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IAttributeData, IColorRGB } from 'common/Types'; -import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; +import { IAttributeData, IColorRGB, IExtendedAttrs } from 'common/Types'; +import { Attributes, FgFlags, BgFlags, UnderlineStyle } from 'common/buffer/Constants'; export class AttributeData implements IAttributeData { static toColorRGB(value: number): IColorRGB { @@ -22,12 +22,14 @@ export class AttributeData implements IAttributeData { const newObj = new AttributeData(); newObj.fg = this.fg; newObj.bg = this.bg; + newObj.extended = this.extended.clone(); return newObj; } // data - public fg: number = 0; - public bg: number = 0; + public fg = 0; + public bg = 0; + public extended = new ExtendedAttrs(); // flags public isInverse(): number { return this.fg & FgFlags.INVERSE; } @@ -66,4 +68,47 @@ export class AttributeData implements IAttributeData { default: return -1; // CM_DEFAULT defaults to -1 } } + + // extended attrs + public hasExtendedAttrs(): number { + return this.bg & BgFlags.HAS_EXTENDED; + } + public getUnderlineColor(): number { + if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) { + return this.extended.underlineColor; + } + return this.getFgColor(); + } + public getUnderlineStyle(): UnderlineStyle { + return this.fg & FgFlags.UNDERLINE + ? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE) + : UnderlineStyle.NONE; + } +} + + +/** + * Extended attributes for a cell. + * Holds information about different underline styles and color. + */ +export class ExtendedAttrs implements IExtendedAttrs { + constructor( + // underline style, NONE is empty + public underlineStyle: UnderlineStyle = UnderlineStyle.NONE, + // underline color, -1 is empty (same as FG) + public underlineColor: number = -1 + ) {} + + public clone(): IExtendedAttrs { + return new ExtendedAttrs(this.underlineStyle, this.underlineColor); + } + + /** + * Convenient method to indicate whether the object holds no additional information + * and can be removed from the global attr object. + */ + public isEmpty(): boolean { + // needs to test for every single attribute stored + return !(this.underlineStyle !== UnderlineStyle.NONE || ~this.underlineColor); + } } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 152bab7a3..6e1536ab6 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -13,6 +13,7 @@ import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGe import { Marker } from 'common/buffer/Marker'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { DEFAULT_CHARSET } from 'common/data/Charsets'; +import { ExtendedAttrs } from 'common/buffer/AttributeData'; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 @@ -60,9 +61,11 @@ export class Buffer implements IBuffer { if (attr) { this._nullCell.fg = attr.fg; this._nullCell.bg = attr.bg; + this._nullCell.extended = attr.extended; } else { this._nullCell.fg = 0; this._nullCell.bg = 0; + this._nullCell.extended = new ExtendedAttrs(); } return this._nullCell; } @@ -71,9 +74,11 @@ export class Buffer implements IBuffer { if (attr) { this._whitespaceCell.fg = attr.fg; this._whitespaceCell.bg = attr.bg; + this._whitespaceCell.extended = attr.extended; } else { this._whitespaceCell.fg = 0; this._whitespaceCell.bg = 0; + this._whitespaceCell.extended = new ExtendedAttrs(); } return this._whitespaceCell; } diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 686371f25..1b4561073 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -2,11 +2,13 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import * as chai from 'chai'; -import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content } from 'common/buffer/Constants'; +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 { CellData } from 'common/buffer/CellData'; import { CharData, IBufferLine } from '../Types'; +import { assert, expect } from 'chai'; +import { AttributeData } from 'common/buffer/AttributeData'; + class TestBufferLine extends BufferLine { public get combined(): {[index: number]: string} { @@ -22,49 +24,98 @@ class TestBufferLine extends BufferLine { } } +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', () => { + const attrs = new AttributeData(); + attrs.extended.underlineColor = (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 = -1; + attrs.fg |= Attributes.CM_P256 | 123; + assert.equal(attrs.getUnderlineColor(), 123); + }); + 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); + }); + }); +}); + describe('CellData', () => { it('CharData <--> CellData equality', () => { const cell = new CellData(); // ASCII cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), 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)]); - chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); + 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]); - chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); - chai.assert.equal(cell.isCombined(), 0); + assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); + assert.equal(cell.isCombined(), 0); // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); + 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)]); - chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), 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); - chai.expect(line.length).equals(0); - chai.expect(line.isWrapped).equals(false); + expect(line.length).equals(0); + expect(line.isWrapped).equals(false); line = new TestBufferLine(10); - chai.expect(line.length).equals(10); - chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - chai.expect(line.isWrapped).equals(false); + expect(line.length).equals(10); + expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + expect(line.isWrapped).equals(false); line = new TestBufferLine(10, undefined, true); - chai.expect(line.length).equals(10); - chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - chai.expect(line.isWrapped).equals(true); + expect(line.length).equals(10); + expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + expect(line.isWrapped).equals(true); line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); - chai.expect(line.length).equals(10); - chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]); - chai.expect(line.isWrapped).equals(true); + expect(line.length).equals(10); + expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]); + expect(line.isWrapped).equals(true); }); it('insertCells', function(): void { const line = new TestBufferLine(3); @@ -72,7 +123,7 @@ describe('BufferLine', function(): void { line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); - chai.expect(line.toArray()).eql([ + expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)] @@ -86,7 +137,7 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); - chai.expect(line.toArray()).eql([ + expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], [5, 'e', 0, 'e'.charCodeAt(0)], @@ -102,7 +153,7 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); - chai.expect(line.toArray()).eql([ + expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [2, 'b', 0, 'b'.charCodeAt(0)], [6, 'f', 0, 'f'.charCodeAt(0)], @@ -118,7 +169,7 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)])); - chai.expect(line.toArray()).eql([ + expect(line.toArray()).eql([ [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], @@ -134,9 +185,9 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = line.clone(); - chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray()); - chai.expect(line2.length).equals(line.length); - chai.expect(line2.isWrapped).equals(line.isWrapped); + expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray()); + expect(line2.length).equals(line.length); + expect(line2.isWrapped).equals(line.isWrapped); }); it('copyFrom', function(): void { const line = new TestBufferLine(5); @@ -147,92 +198,92 @@ describe('BufferLine', function(): void { line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); line2.copyFrom(line); - chai.expect(line2.toArray()).eql(line.toArray()); - chai.expect(line2.length).equals(line.length); - chai.expect(line2.isWrapped).equals(line.isWrapped); + expect(line2.toArray()).eql(line.toArray()); + expect(line2.length).equals(line.length); + expect(line2.isWrapped).equals(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, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)])); - chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); + expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true); line2.copyFrom(line); - chai.expect(line2.toArray()).eql(line.toArray()); + expect(line2.toArray()).eql(line.toArray()); const line3 = line.clone(); - chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray()); + expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray()); }); describe('resize', function(): void { it('enlarge(false)', function(): void { const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink to 0 length', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('should remove combining data on replaced cells after shrinking then enlarging', () => { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); - chai.expect(line.translateToString()).eql('aa😁aaaaaa😁'); - chai.expect(Object.keys(line.combined).length).eql(2); + expect(line.translateToString()).eql('aa😁aaaaaa😁'); + expect(Object.keys(line.combined).length).eql(2); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.translateToString()).eql('aa😁aa'); + expect(line.translateToString()).eql('aa😁aa'); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.translateToString()).eql('aa😁aaaaaaa'); - chai.expect(Object.keys(line.combined).length).eql(1); + expect(line.translateToString()).eql('aa😁aaaaaaa'); + expect(Object.keys(line.combined).length).eql(1); }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - chai.expect(line.getTrimmedLength()).equal(0); + expect(line.getTrimmedLength()).equal(0); }); it('ASCII', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.expect(line.getTrimmedLength()).equal(3); + expect(line.getTrimmedLength()).equal(3); }); it('surrogate', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); - chai.expect(line.getTrimmedLength()).equal(3); + expect(line.getTrimmedLength()).equal(3); }); it('combining', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); - chai.expect(line.getTrimmedLength()).equal(3); + expect(line.getTrimmedLength()).equal(3); }); it('fullwidth', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([0, '', 0, 0])); - chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth + expect(line.getTrimmedLength()).equal(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, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - chai.expect(line.translateToString(false)).equal(' '); - chai.expect(line.translateToString(true)).equal(''); + expect(line.translateToString(false)).equal(' '); + expect(line.translateToString(true)).equal(''); }); it('ASCII', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -240,14 +291,14 @@ describe('BufferLine', function(): void { line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.expect(line.translateToString(false)).equal('a a aa '); - chai.expect(line.translateToString(true)).equal('a a aa'); - chai.expect(line.translateToString(false, 0, 5)).equal('a a a'); - chai.expect(line.translateToString(false, 0, 4)).equal('a a '); - chai.expect(line.translateToString(false, 0, 3)).equal('a a'); - chai.expect(line.translateToString(true, 0, 5)).equal('a a a'); - chai.expect(line.translateToString(true, 0, 4)).equal('a a '); - chai.expect(line.translateToString(true, 0, 3)).equal('a a'); + expect(line.translateToString(false)).equal('a a aa '); + expect(line.translateToString(true)).equal('a a aa'); + expect(line.translateToString(false, 0, 5)).equal('a a a'); + expect(line.translateToString(false, 0, 4)).equal('a a '); + expect(line.translateToString(false, 0, 3)).equal('a a'); + expect(line.translateToString(true, 0, 5)).equal('a a a'); + expect(line.translateToString(true, 0, 4)).equal('a a '); + expect(line.translateToString(true, 0, 3)).equal('a a'); }); it('surrogate', function(): void { @@ -256,14 +307,14 @@ describe('BufferLine', function(): void { line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); - chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 '); - chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞'); - chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞'); - chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 '); - chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞'); - chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞'); - chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 '); - chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞'); + expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 '); + expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞'); + expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞'); + expect(line.translateToString(false, 0, 4)).equal('a 𝄞 '); + expect(line.translateToString(false, 0, 3)).equal('a 𝄞'); + expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞'); + expect(line.translateToString(true, 0, 4)).equal('a 𝄞 '); + expect(line.translateToString(true, 0, 3)).equal('a 𝄞'); }); it('combining', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -271,14 +322,14 @@ describe('BufferLine', function(): void { line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); - chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 '); - chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301'); - chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301'); - chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 '); - chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301'); - chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301'); - chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 '); - chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301'); + expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 '); + expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301'); + expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301'); + expect(line.translateToString(false, 0, 4)).equal('a e\u0301 '); + expect(line.translateToString(false, 0, 3)).equal('a e\u0301'); + expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301'); + expect(line.translateToString(true, 0, 4)).equal('a e\u0301 '); + expect(line.translateToString(true, 0, 3)).equal('a e\u0301'); }); it('fullwidth', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -289,20 +340,20 @@ describe('BufferLine', function(): void { line.setCell(6, CellData.fromCharData([0, '', 0, 0])); line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); line.setCell(8, CellData.fromCharData([0, '', 0, 0])); - chai.expect(line.translateToString(false)).equal('a 1 11 '); - chai.expect(line.translateToString(true)).equal('a 1 11'); - chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1'); - chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1'); - chai.expect(line.translateToString(false, 0, 5)).equal('a 1 '); - chai.expect(line.translateToString(false, 0, 4)).equal('a 1'); - chai.expect(line.translateToString(false, 0, 3)).equal('a 1'); - chai.expect(line.translateToString(false, 0, 2)).equal('a '); - chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1'); - chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1'); - chai.expect(line.translateToString(true, 0, 5)).equal('a 1 '); - chai.expect(line.translateToString(true, 0, 4)).equal('a 1'); - chai.expect(line.translateToString(true, 0, 3)).equal('a 1'); - chai.expect(line.translateToString(true, 0, 2)).equal('a '); + expect(line.translateToString(false)).equal('a 1 11 '); + expect(line.translateToString(true)).equal('a 1 11'); + expect(line.translateToString(false, 0, 7)).equal('a 1 1'); + expect(line.translateToString(false, 0, 6)).equal('a 1 1'); + expect(line.translateToString(false, 0, 5)).equal('a 1 '); + expect(line.translateToString(false, 0, 4)).equal('a 1'); + expect(line.translateToString(false, 0, 3)).equal('a 1'); + expect(line.translateToString(false, 0, 2)).equal('a '); + expect(line.translateToString(true, 0, 7)).equal('a 1 1'); + expect(line.translateToString(true, 0, 6)).equal('a 1 1'); + expect(line.translateToString(true, 0, 5)).equal('a 1 '); + expect(line.translateToString(true, 0, 4)).equal('a 1'); + expect(line.translateToString(true, 0, 3)).equal('a 1'); + expect(line.translateToString(true, 0, 2)).equal('a '); }); it('space at end', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -311,21 +362,21 @@ describe('BufferLine', function(): void { line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)])); - chai.expect(line.translateToString(false)).equal('a a aa '); - chai.expect(line.translateToString(true)).equal('a a aa '); + expect(line.translateToString(false)).equal('a a aa '); + expect(line.translateToString(true)).equal('a a aa '); }); it('should always return some sane value', function(): void { // 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, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); - chai.expect(line.translateToString(false)).equal(' '); - chai.expect(line.translateToString(true)).equal(''); + expect(line.translateToString(false)).equal(' '); + expect(line.translateToString(true)).equal(''); }); it('should work with endCol=0', () => { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.expect(line.translateToString(true, 0, 0)).equal(''); + expect(line.translateToString(true, 0, 0)).equal(''); }); }); describe('addCharToCell', () => { @@ -335,9 +386,9 @@ describe('BufferLine', function(): void { const cell = line.loadCell(0, new CellData()); // chars contains single combining char // width is set to 1 - chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]); + assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), 0); + assert.equal(cell.isCombined(), 0); }); it('should add char to combining string in cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -348,9 +399,9 @@ describe('BufferLine', function(): void { line.loadCell(0, cell); // chars contains 3 chars // width is set to 1 - chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); + assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); + assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -361,9 +412,9 @@ describe('BufferLine', function(): void { line.loadCell(0, cell); // chars contains 2 chars // width is set to 1 - chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); + assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); + assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); }); describe('correct fullwidth handling', () => { @@ -377,83 +428,291 @@ describe('BufferLine', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.insertCells(9, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), '¥¥¥¥ a'); + assert.equal(line.translateToString(), '¥¥¥¥ a'); line.insertCells(8, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), '¥¥¥¥a '); + assert.equal(line.translateToString(), '¥¥¥¥a '); line.insertCells(1, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' a ¥¥¥a'); + assert.equal(line.translateToString(), ' a ¥¥¥a'); }); it('insert - wide char at end', () => { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.insertCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaa¥¥¥ '); + assert.equal(line.translateToString(), 'aaa¥¥¥ '); line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaa a ¥¥'); + assert.equal(line.translateToString(), 'aaa a ¥¥'); line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaa aa ¥ '); + assert.equal(line.translateToString(), 'aaa aa ¥ '); }); it('delete', () => { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.deleteCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' ¥¥¥¥a'); + assert.equal(line.translateToString(), ' ¥¥¥¥a'); line.deleteCells(5, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' ¥¥¥aaa'); + assert.equal(line.translateToString(), ' ¥¥¥aaa'); line.deleteCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' ¥¥aaaaa'); + assert.equal(line.translateToString(), ' ¥¥aaaaa'); }); it('replace - start at 0', () => { let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'a ¥¥¥¥'); + assert.equal(line.translateToString(), 'a ¥¥¥¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aa¥¥¥¥'); + assert.equal(line.translateToString(), 'aa¥¥¥¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaa ¥¥¥'); + assert.equal(line.translateToString(), 'aaa ¥¥¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaaaaaaa¥'); + assert.equal(line.translateToString(), 'aaaaaaaa¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaaaaaaaa '); + assert.equal(line.translateToString(), 'aaaaaaaaa '); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), 'aaaaaaaaaa'); + assert.equal(line.translateToString(), 'aaaaaaaaaa'); }); it('replace - start at 1', () => { let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' a¥¥¥¥'); + assert.equal(line.translateToString(), ' a¥¥¥¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' aa ¥¥¥'); + assert.equal(line.translateToString(), ' aa ¥¥¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' aaa¥¥¥'); + assert.equal(line.translateToString(), ' aaa¥¥¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' aaaaaaa¥'); + assert.equal(line.translateToString(), ' aaaaaaa¥'); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' aaaaaaaa '); + assert.equal(line.translateToString(), ' aaaaaaaa '); line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - chai.assert.equal(line.translateToString(), ' aaaaaaaaa'); + assert.equal(line.translateToString(), ' aaaaaaaaa'); + }); + }); + describe('extended attributes', () => { + it('setCells', function(): void { + const line = new TestBufferLine(5); + const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + // 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); + + assert.deepEqual(line.toArray(), [ + [1, 'a', 0, 'a'.charCodeAt(0)], + [1, 'a', 0, 'a'.charCodeAt(0)], + [1, 'A', 0, 'A'.charCodeAt(0)], + [1, 'A', 0, 'A'.charCodeAt(0)], + [1, 'A', 0, 'A'.charCodeAt(0)] + ]); + assert.equal((line as any)._extendedAttrs[0], undefined); + assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOTTED); + assert.equal((line as any)._extendedAttrs[4], undefined); + // should be ref to the same object + assert.equal((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[2]); + // should be a different obj + assert.notEqual((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[3]); + }); + it('loadCell', () => { + const line = new TestBufferLine(5); + const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + // 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 = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + cell.extended.underlineStyle = UnderlineStyle.CURLY; + cell.bg |= BgFlags.HAS_EXTENDED; + line.fill(cell); + assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY); + }); + it('insertCells', () => { + const line = new TestBufferLine(5); + const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + cell.extended.underlineStyle = UnderlineStyle.CURLY; + cell.bg |= BgFlags.HAS_EXTENDED; + line.insertCells(1, 3, cell); + assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[4], undefined); + cell.extended = cell.extended.clone(); + cell.extended.underlineStyle = UnderlineStyle.DOTTED; + line.insertCells(2, 2, cell); + assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOTTED); + assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOTTED); + assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.CURLY); + }); + it('deleteCells', () => { + const line = new TestBufferLine(5); + const fillCell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + 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((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOUBLE); + assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOUBLE); + assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.DOUBLE); + }); + it('replaceCells', () => { + const line = new TestBufferLine(5); + const fillCell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + 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((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.DOUBLE); + assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOUBLE); + assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.CURLY); + assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.CURLY); + }); + it('clone', () => { + const line = new TestBufferLine(5); + const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + // 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((nLine as any)._extendedAttrs[0], (line as any)._extendedAttrs[0]); + assert.equal((nLine as any)._extendedAttrs[1], (line as any)._extendedAttrs[1]); + assert.equal((nLine as any)._extendedAttrs[2], (line as any)._extendedAttrs[2]); + assert.equal((nLine as any)._extendedAttrs[3], (line as any)._extendedAttrs[3]); + assert.equal((nLine as any)._extendedAttrs[4], (line as any)._extendedAttrs[4]); + }); + it('copyFrom', () => { + const initial = new TestBufferLine(5); + const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + // 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(CellData.fromCharData([1, 'b', 0, 'b'.charCodeAt(0)])); + line.copyFrom(initial); + assert.equal((line as any)._extendedAttrs[0], (initial as any)._extendedAttrs[0]); + assert.equal((line as any)._extendedAttrs[1], (initial as any)._extendedAttrs[1]); + assert.equal((line as any)._extendedAttrs[2], (initial as any)._extendedAttrs[2]); + assert.equal((line as any)._extendedAttrs[3], (initial as any)._extendedAttrs[3]); + assert.equal((line as any)._extendedAttrs[4], (initial as any)._extendedAttrs[4]); }); }); }); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index d54b59aaf..f0bf4fcb6 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { CharData, IBufferLine, ICellData, IAttributeData } from 'common/Types'; +import { CharData, IBufferLine, ICellData, IAttributeData, IExtendedAttrs } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, BgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { AttributeData } from 'common/buffer/AttributeData'; +import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; /** * buffer memory layout: @@ -55,6 +55,7 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); export class BufferLine implements IBufferLine { protected _data: Uint32Array; protected _combined: {[index: number]: string} = {}; + protected _extendedAttrs: {[index: number]: ExtendedAttrs} = {}; public length: number; constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { @@ -174,6 +175,9 @@ export class BufferLine implements IBufferLine { if (cell.content & Content.IS_COMBINED_MASK) { cell.combinedData = this._combined[index]; } + if (cell.bg & BgFlags.HAS_EXTENDED) { + cell.extended = this._extendedAttrs[index]; + } return cell; } @@ -184,6 +188,9 @@ export class BufferLine implements IBufferLine { if (cell.content & Content.IS_COMBINED_MASK) { this._combined[index] = cell.combinedData; } + if (cell.bg & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[index] = cell.extended; + } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; this._data[index * CELL_SIZE + Cell.FG] = cell.fg; this._data[index * CELL_SIZE + Cell.BG] = cell.bg; @@ -194,7 +201,10 @@ export class BufferLine implements IBufferLine { * Since the input handler see the incoming chars as UTF32 codepoints, * it gets an optimized access method. */ - public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { + public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + if (bg & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[index] = eAttrs; + } this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; @@ -233,7 +243,7 @@ export class BufferLine implements IBufferLine { // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } if (n < this.length - pos) { @@ -252,7 +262,7 @@ export class BufferLine implements IBufferLine { // handle fullwidth at line end: reset last cell if it is first cell of a wide char if (this.getWidth(this.length - 1) === 2) { - this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } } @@ -276,21 +286,21 @@ export class BufferLine implements IBufferLine { // - reset pos-1 if wide char // - reset pos if width==0 (previous second cell of a wide char) if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { - this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } } public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char if (start && this.getWidth(start - 1) === 2) { - this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char if (end < this.length && this.getWidth(end - 1) === 2) { - this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } while (start < end && start < this.length) { @@ -320,7 +330,7 @@ export class BufferLine implements IBufferLine { const data = new Uint32Array(cols * CELL_SIZE); data.set(this._data.subarray(0, cols * CELL_SIZE)); this._data = data; - // Remove any cut off combined data + // Remove any cut off combined data, FIXME: repeat this for extended attrs const keys = Object.keys(this._combined); for (let i = 0; i < keys.length; i++) { const key = parseInt(keys[i], 10); @@ -339,6 +349,7 @@ export class BufferLine implements IBufferLine { /** fill a line with fillCharData */ public fill(fillCellData: ICellData): void { this._combined = {}; + this._extendedAttrs = {}; for (let i = 0; i < this.length; ++i) { this.setCell(i, fillCellData); } @@ -357,6 +368,10 @@ export class BufferLine implements IBufferLine { for (const el in line._combined) { this._combined[el] = line._combined[el]; } + this._extendedAttrs = {}; + for (const el in line._extendedAttrs) { + this._extendedAttrs[el] = line._extendedAttrs[el]; + } this.isWrapped = line.isWrapped; } @@ -368,6 +383,9 @@ export class BufferLine implements IBufferLine { for (const el in this._combined) { newLine._combined[el] = this._combined[el]; } + for (const el in this._extendedAttrs) { + newLine._extendedAttrs[el] = this._extendedAttrs[el]; + } newLine.isWrapped = this.isWrapped; return newLine; } @@ -397,7 +415,7 @@ export class BufferLine implements IBufferLine { } } - // Move any combined data over as needed + // Move any combined data over as needed, FIXME: repeat for extended attrs const srcCombinedKeys = Object.keys(src._combined); for (let i = 0; i < srcCombinedKeys.length; i++) { const key = parseInt(srcCombinedKeys[i], 10); diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 21ad2ee5c..a87b5795d 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { CharData, ICellData } from 'common/Types'; +import { CharData, ICellData, IExtendedAttrs } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants'; -import { AttributeData } from 'common/buffer/AttributeData'; +import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; /** * CellData - represents a single Cell in the terminal buffer. @@ -19,10 +19,11 @@ export class CellData extends AttributeData implements ICellData { return obj; } /** Primitives from terminal buffer. */ - public content: number = 0; - public fg: number = 0; - public bg: number = 0; - public combinedData: string = ''; + public content = 0; + public fg = 0; + public bg = 0; + public extended: IExtendedAttrs = new ExtendedAttrs(); + public combinedData = ''; /** Whether cell contains a combined string. */ public isCombined(): number { return this.content & Content.IS_COMBINED_MASK; diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 276a5c543..ee86a804d 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -121,8 +121,18 @@ export const enum FgFlags { export const enum BgFlags { /** - * bit 27..32 (upper 4 unused) + * bit 27..32 (upper 3 unused) */ ITALIC = 0x4000000, - DIM = 0x8000000 + DIM = 0x8000000, + HAS_EXTENDED = 0x10000000 +} + +export const enum UnderlineStyle { + NONE = 0, + SINGLE = 1, + DOUBLE = 2, + CURLY = 3, + DOTTED = 4, + DASHED = 5 }