diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 6118859048..46f8eaccc7 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -8,7 +8,7 @@ import { InputHandler } from 'common/InputHandler'; import { IBufferLine, IAttributeData } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; -import { Attributes } from 'common/buffer/Constants'; +import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test'; @@ -1468,6 +1468,181 @@ describe('InputHandler', () => { }); }); + describe('extended underline style support (SGR 4)', () => { + beforeEach(() => { + bufferService.resize(10, 5); + }); + it('4 | 24', () => { + inputHandler.parse('\x1b[4m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('21 | 24', () => { + inputHandler.parse('\x1b[21m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('4:1 | 4:0', () => { + inputHandler.parse('\x1b[4:1m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); + inputHandler.parse('\x1b[4:0m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + inputHandler.parse('\x1b[4:1m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('4:2 | 4:0', () => { + inputHandler.parse('\x1b[4:2m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); + inputHandler.parse('\x1b[4:0m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + inputHandler.parse('\x1b[4:2m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('4:3 | 4:0', () => { + inputHandler.parse('\x1b[4:3m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY); + inputHandler.parse('\x1b[4:0m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + inputHandler.parse('\x1b[4:3m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('4:4 | 4:0', () => { + inputHandler.parse('\x1b[4:4m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED); + inputHandler.parse('\x1b[4:0m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + inputHandler.parse('\x1b[4:4m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('4:5 | 4:0', () => { + inputHandler.parse('\x1b[4:5m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); + inputHandler.parse('\x1b[4:0m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + inputHandler.parse('\x1b[4:5m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); + inputHandler.parse('\x1b[24m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); + }); + it('4:x --> 4 should revert to single underline', () => { + inputHandler.parse('\x1b[4:5m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); + inputHandler.parse('\x1b[4m'); + assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); + }); + }); + describe('underline colors (SGR 58 & SGR 59)', () => { + beforeEach(() => { + bufferService.resize(10, 5); + }); + it('defaults to FG color', () => { + for (const s of ['', '\x1b[30m', '\x1b[38;510m', '\x1b[38;2;1;2;3m']) { + inputHandler.parse(s); + assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); + assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); + assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); + assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); + assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); + } + }); + it('correctly sets P256/RGB colors', () => { + inputHandler.parse('\x1b[4m'); + inputHandler.parse('\x1b[58;5;123m'); + assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123); + assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false); + assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true); + assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); + inputHandler.parse('\x1b[58;2::1:2:3m'); + assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); + assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB); + assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true); + assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false); + assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); + }); + it('P256/RGB persistence', () => { + const cell = new CellData(); + inputHandler.parse('\x1b[4m'); + inputHandler.parse('\x1b[58;5;123m'); + assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123); + assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false); + assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true); + assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); + inputHandler.parse('ab'); + bufferService.buffer!.lines.get(0)!.loadCell(1, cell); + assert.equal(cell.getUnderlineColor(), 123); + assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); + assert.equal(cell.isUnderlineColorRGB(), false); + assert.equal(cell.isUnderlineColorPalette(), true); + assert.equal(cell.isUnderlineColorDefault(), false); + + inputHandler.parse('\x1b[4:0m'); + assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); + assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); + assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); + assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); + assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); + inputHandler.parse('a'); + bufferService.buffer!.lines.get(0)!.loadCell(1, cell); + assert.equal(cell.getUnderlineColor(), 123); + assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); + assert.equal(cell.isUnderlineColorRGB(), false); + assert.equal(cell.isUnderlineColorPalette(), true); + assert.equal(cell.isUnderlineColorDefault(), false); + bufferService.buffer!.lines.get(0)!.loadCell(2, cell); + assert.equal(cell.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); + assert.equal(cell.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); + assert.equal(cell.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); + assert.equal(cell.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); + assert.equal(cell.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); + + inputHandler.parse('\x1b[4m'); + inputHandler.parse('\x1b[58;2::1:2:3m'); + assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); + assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB); + assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true); + assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false); + assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); + inputHandler.parse('a'); + inputHandler.parse('\x1b[24m'); + bufferService.buffer!.lines.get(0)!.loadCell(1, cell); + assert.equal(cell.getUnderlineColor(), 123); + assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); + assert.equal(cell.isUnderlineColorRGB(), false); + assert.equal(cell.isUnderlineColorPalette(), true); + assert.equal(cell.isUnderlineColorDefault(), false); + bufferService.buffer!.lines.get(0)!.loadCell(3, cell); + assert.equal(cell.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); + assert.equal(cell.getUnderlineColorMode(), Attributes.CM_RGB); + assert.equal(cell.isUnderlineColorRGB(), true); + assert.equal(cell.isUnderlineColorPalette(), false); + assert.equal(cell.isUnderlineColorDefault(), false); + + // eAttrs in buffer pos 0 and 1 should be the same object + assert.equal( + (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[0], + (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[1] + ); + // should not have written eAttr for pos 2 in the buffer + assert.equal((bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[2], undefined); + // eAttrs in buffer pos 1 and pos 3 must be different objs + assert.notEqual( + (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[1], + (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[3] + ); + }); + }); describe('DECSTR', () => { beforeEach(() => { bufferService.resize(10, 5); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b66b038b8f..dbf695e607 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -14,7 +14,7 @@ import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from ' import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services'; @@ -509,7 +509,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) { @@ -558,7 +558,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (wraparoundMode) { // clear left over cells to the right while (buffer.x < cols) { - bufferRow.setCellFromCodePoint(buffer.x++, 0, 1, curAttr.fg, curAttr.bg); + bufferRow.setCellFromCodePoint(buffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } buffer.x = 0; buffer.y++; @@ -593,12 +593,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 @@ -606,7 +606,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); } } } @@ -627,7 +627,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); @@ -2105,6 +2105,21 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** + * Helper to write color information packed with color mode. + */ + private _updateAttrColor(color: number, mode: number, c1: number, c2: number, c3: number): number { + if (mode === 2) { + color |= Attributes.CM_RGB; + color &= ~Attributes.RGB_MASK; + color |= AttributeData.fromColorRGB([c1, c2, c3]); + } else if (mode === 5) { + color &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + color |= Attributes.CM_P256 | (c1 & 0xff); + } + return color; + } + /** * Helper to extract and apply color params/subparams. * Returns advance for params index. @@ -2154,35 +2169,57 @@ export class InputHandler extends Disposable implements IInputHandler { } // apply colors - if (accu[0] === 38) { - if (accu[1] === 2) { - attr.fg |= Attributes.CM_RGB; - attr.fg &= ~Attributes.RGB_MASK; - attr.fg |= AttributeData.fromColorRGB([accu[3], accu[4], accu[5]]); - } else if (accu[1] === 5) { - attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); - attr.fg |= Attributes.CM_P256 | (accu[3] & 0xff); - } - } else if (accu[0] === 48) { - if (accu[1] === 2) { - attr.bg |= Attributes.CM_RGB; - attr.bg &= ~Attributes.RGB_MASK; - attr.bg |= AttributeData.fromColorRGB([accu[3], accu[4], accu[5]]); - } else if (accu[1] === 5) { - attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); - attr.bg |= Attributes.CM_P256 | (accu[3] & 0xff); - } + switch (accu[0]) { + case 38: + attr.fg = this._updateAttrColor(attr.fg, accu[1], accu[3], accu[4], accu[5]); + break; + case 48: + attr.bg = this._updateAttrColor(attr.bg, accu[1], accu[3], accu[4], accu[5]); + break; + case 58: + attr.extended = attr.extended.clone(); + attr.extended.underlineColor = this._updateAttrColor(attr.extended.underlineColor, accu[1], accu[3], accu[4], accu[5]); } 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(style: number, 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(); + + // default to 1 == single underline + if (!~style || style > 5) { + style = 1; + } + attr.extended.underlineStyle = style; + attr.fg |= FgFlags.UNDERLINE; + + // 0 deactivates underline + if (style === 0) { + attr.fg &= ~FgFlags.UNDERLINE; + } + + // update HAS_EXTENDED in BG + attr.updateExtended(); + } + /** * CSI Pm m Character Attributes (SGR). * * @vt: #P[See below for supported attributes.] CSI SGR "Select Graphic Rendition" "CSI Pm m" "Set/Reset various text attributes." * SGR selects one or more character attributes at the same time. Multiple params (up to 32) - * are applied from in order from left to right. The changed attributes are applied to all new + * are applied in order from left to right. The changed attributes are applied to all new * characters received. If you move characters in the viewport by scrolling or any other means, * then the attributes move with the characters. * @@ -2194,13 +2231,13 @@ export class InputHandler extends Disposable implements IInputHandler { * | 1 | Bold. (also see `options.drawBoldTextInBrightColors`) | #Y | * | 2 | Faint, decreased intensity. | #Y | * | 3 | Italic. | #Y | - * | 4 | Underlined. (no support for newer underline styles) | #Y | + * | 4 | Underlined (see below for style support). | #Y | * | 5 | Slowly blinking. | #N | * | 6 | Rapidly blinking. | #N | * | 7 | Inverse. Flips foreground and background color. | #Y | * | 8 | Invisible (hidden). | #Y | * | 9 | Crossed-out characters. | #N | - * | 21 | Doubly underlined. | #N | + * | 21 | Doubly underlined. | #P[Currently outputs a single underline.] | * | 22 | Normal (neither bold nor faint). | #Y | * | 23 | No italic. | #Y | * | 24 | Not underlined. | #Y | @@ -2231,6 +2268,18 @@ export class InputHandler extends Disposable implements IInputHandler { * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y | * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y | * + * Underline supports subparams to denote the style in the form `4 : x`: + * + * | x | Meaning | Support | + * | ------ | ------------------------------------------------------------- | ------- | + * | 0 | No underline. Same as `SGR 24 m`. | #Y | + * | 1 | Single underline. Same as `SGR 4 m`. | #Y | + * | 2 | Double underline. | #P[Currently outputs a single underline.] | + * | 3 | Curly underline. | #P[Currently outputs a single underline.] | + * | 4 | Dotted underline. | #P[Currently outputs a single underline.] | + * | 5 | Dashed underline. | #P[Currently outputs a single underline.] | + * | other | Single underline. Same as `SGR 4 m`. | #Y | + * * Extended colors are supported for foreground (Ps=38) and background (Ps=48) as follows: * * | Ps + 1 | Meaning | Support | @@ -2289,6 +2338,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 4) { // underlined text attr.fg |= FgFlags.UNDERLINE; + this._processUnderline(params.hasSubParams(i) ? params.getSubParams(i)![0] : UnderlineStyle.SINGLE, attr); } else if (p === 5) { // blink attr.fg |= FgFlags.BLINK; @@ -2302,6 +2352,9 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 2) { // dimmed text attr.bg |= BgFlags.DIM; + } else if (p === 21) { + // double underline + this._processUnderline(UnderlineStyle.DOUBLE, attr); } else if (p === 22) { // not bold nor faint attr.fg &= ~FgFlags.BOLD; @@ -2329,9 +2382,13 @@ export class InputHandler extends Disposable implements IInputHandler { // reset bg attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); - } else if (p === 38 || p === 48) { + } else if (p === 38 || p === 48 || p === 58) { // fg color 256 and RGB i += this._extractColor(params, i, attr); + } else if (p === 59) { + attr.extended = attr.extended.clone(); + attr.extended.underlineColor = -1; + attr.updateExtended(); } else if (p === 100) { // FIXME: dead branch, p=100 already handled above! // reset fg/bg attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index a82cfc4eb5..6ce69483d9 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -84,10 +84,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; @@ -114,6 +122,16 @@ export interface IAttributeData { // colors getFgColor(): number; getBgColor(): number; + + // extended attrs + hasExtendedAttrs(): number; + updateExtended(): void; + getUnderlineColor(): number; + getUnderlineColorMode(): number; + isUnderlineColorRGB(): boolean; + isUnderlineColorPalette(): boolean; + isUnderlineColorDefault(): boolean; + getUnderlineStyle(): number; } /** Cell data */ @@ -138,7 +156,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 52bd0b15b6..c7217a2a0c 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 { public static toColorRGB(value: number): IColorRGB { @@ -23,12 +23,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; } @@ -67,4 +69,79 @@ 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 updateExtended(): void { + if (this.extended.isEmpty()) { + this.bg &= ~BgFlags.HAS_EXTENDED; + } else { + this.bg |= BgFlags.HAS_EXTENDED; + } + } + public getUnderlineColor(): number { + if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) { + switch (this.extended.underlineColor & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.extended.underlineColor & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.extended.underlineColor & Attributes.RGB_MASK; + default: return this.getFgColor(); + } + } + return this.getFgColor(); + } + public getUnderlineColorMode(): number { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? this.extended.underlineColor & Attributes.CM_MASK + : this.getFgColorMode(); + } + public isUnderlineColorRGB(): boolean { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_RGB + : this.isFgRGB(); + } + public isUnderlineColorPalette(): boolean { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P16 + || (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P256 + : this.isFgPalette(); + } + public isUnderlineColorDefault(): boolean { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? (this.extended.underlineColor & Attributes.CM_MASK) === 0 + : this.isFgDefault(); + } + 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, + * that needs to be persistant in the buffer. + */ + public isEmpty(): boolean { + return this.underlineStyle === UnderlineStyle.NONE; + } } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 1de39dbd2f..a418651be8 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 686371f250..4c5fc3a42f 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,147 @@ 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 - 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 = -1; + 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 = -1; + 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); + }); + }); +}); + 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 +172,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 +186,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 +202,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 +218,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 +234,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 +247,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 +340,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 +356,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 +371,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 +389,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 +411,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 +435,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 +448,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 +461,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 +477,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 d54b59aaf5..f0bf4fcb67 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 21ad2ee5ca..a87b5795d3 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 276a5c5430..ee86a804d6 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 }