diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4b3a9f2..e7924027 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,8 @@ opening an issue, read these pointers. ## Contributing code -- Make sure you have a [GitHub account](https://github.com/join) +You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: + - Fork [xterm.js](https://github.com/sourcelair/xterm.js/) ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) - Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running diff --git a/package.json b/package.json index ad763f0b..c84a0164 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.0", + "typescript": "3.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", diff --git a/src/Buffer.ts b/src/Buffer.ts index 63b0f8d2..f9eed90f 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -8,8 +8,9 @@ import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIt import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine, BufferLineTypedArray } from './BufferLine'; +import { DEFAULT_COLOR } from './renderer/atlas/Types'; -export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); +export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; @@ -42,6 +43,7 @@ export class Buffer implements IBuffer { public tabs: any; public savedY: number; public savedX: number; + public savedCurAttr: number; public markers: Marker[] = []; private _bufferLineConstructor: IBufferLineConstructor; @@ -117,11 +119,14 @@ export class Buffer implements IBuffer { /** * Fills the buffer's viewport with blank lines. */ - public fillViewportRows(): void { + public fillViewportRows(fillAttr?: number): void { if (this.lines.length === 0) { + if (fillAttr === undefined) { + fillAttr = DEFAULT_ATTR; + } let i = this._terminal.rows; while (i--) { - this.lines.push(this.getBlankLine(DEFAULT_ATTR)); + this.lines.push(this.getBlankLine(fillAttr)); } } } diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 12a0bf5d..53a194c5 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -94,11 +94,8 @@ export class BufferLine implements IBufferLine { } } - public copyFrom(line: IBufferLine): void { - this._data = []; - for (let i = 0; i < line.length; ++i) { - this._push(line.get(i)); - } + public copyFrom(line: BufferLine): void { + this._data = line._data.slice(0); this.length = line.length; this.isWrapped = line.isWrapped; } diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 009ebf2e..38f2ddab 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -48,4 +48,30 @@ describe('BufferSet', () => { assert.equal(bufferSet.active, bufferSet.alt); }); }); + + describe('cursor handling when swapping buffers', () => { + beforeEach(() => { + bufferSet.normal.x = 0; + bufferSet.normal.y = 0; + bufferSet.alt.x = 0; + bufferSet.alt.y = 0; + }); + + it('should keep the cursor stationary when activating alt buffer', () => { + bufferSet.activateNormalBuffer(); + bufferSet.active.x = 30; + bufferSet.active.y = 10; + bufferSet.activateAltBuffer(); + assert.equal(bufferSet.active.x, 30); + assert.equal(bufferSet.active.y, 10); + }); + it('should keep the cursor stationary when activating normal buffer', () => { + bufferSet.activateAltBuffer(); + bufferSet.active.x = 30; + bufferSet.active.y = 10; + bufferSet.activateNormalBuffer(); + assert.equal(bufferSet.active.x, 30); + assert.equal(bufferSet.active.y, 10); + }); + }); }); diff --git a/src/BufferSet.ts b/src/BufferSet.ts index c91ab751..f84757d1 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -61,6 +61,8 @@ export class BufferSet extends EventEmitter implements IBufferSet { if (this._activeBuffer === this._normal) { return; } + this._normal.x = this._alt.x; + this._normal.y = this._alt.y; // The alt buffer should always be cleared when we switch to the normal // buffer. This frees up memory since the alt buffer should always be new // when activated. @@ -75,13 +77,15 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Sets the alt Buffer of the BufferSet as its currently active Buffer */ - public activateAltBuffer(): void { + public activateAltBuffer(fillAttr?: number): void { if (this._activeBuffer === this._alt) { return; } // Since the alt buffer is always cleared when the normal buffer is // activated, we want to fill it when switching to it. - this._alt.fillViewportRows(); + this._alt.fillViewportRows(fillAttr); + this._alt.x = this._normal.x; + this._alt.y = this._normal.y; this._activeBuffer = this._alt; this.emit('activate', { activeBuffer: this._alt, diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index 3242dd77..d4ddd24c 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -5,7 +5,7 @@ import { TestTerminal } from './utils/TestUtils.test'; import { assert } from 'chai'; -import { getStringCellWidth } from './CharWidth'; +import { getStringCellWidth, wcwidth } from './CharWidth'; import { IBuffer } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; @@ -77,3 +77,178 @@ describe('getStringCellWidth', function(): void { }); // TODO: multiline tests once #1685 is resolved }); + +it('wcwidth should match all values from the old implementation', function(): void { + // old implementation + const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number { + // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c + // combining characters + const COMBINING_BMP = [ + [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], + [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], + [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], + [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], + [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], + [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], + [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], + [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], + [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], + [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], + [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], + [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], + [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], + [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], + [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], + [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], + [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], + [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], + [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], + [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], + [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], + [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], + [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], + [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], + [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], + [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], + [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], + [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], + [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], + [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], + [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], + [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], + [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], + [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], + [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], + [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], + [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], + [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], + [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], + [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], + [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], + [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] + ]; + const COMBINING_HIGH = [ + [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], + [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], + [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], + [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], + [0xE0100, 0xE01EF] + ]; + // binary search + function bisearch(ucs: number, data: number[][]): boolean { + let min = 0; + let max = data.length - 1; + let mid; + if (ucs < data[0][0] || ucs > data[max][1]) { + return false; + } + while (max >= min) { + mid = (min + max) >> 1; + if (ucs > data[mid][1]) { + min = mid + 1; + } else if (ucs < data[mid][0]) { + max = mid - 1; + } else { + return true; + } + } + return false; + } + function wcwidthBMP(ucs: number): number { + // test for 8-bit control characters + if (ucs === 0) { + return opts.nul; + } + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { + return opts.control; + } + // binary search in table of non-spacing characters + if (bisearch(ucs, COMBINING_BMP)) { + return 0; + } + // if we arrive here, ucs is not a combining or C0/C1 control character + if (isWideBMP(ucs)) { + return 2; + } + return 1; + } + function isWideBMP(ucs: number): boolean { + return ( + ucs >= 0x1100 && ( + ucs <= 0x115f || // Hangul Jamo init. consonants + ucs === 0x2329 || + ucs === 0x232a || + (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi + (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables + (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs + (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms + (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms + (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms + (ucs >= 0xffe0 && ucs <= 0xffe6))); + } + function wcwidthHigh(ucs: number): 0 | 1 | 2 { + if (bisearch(ucs, COMBINING_HIGH)) { + return 0; + } + if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { + return 2; + } + return 1; + } + const control = opts.control | 0; + let table: number[] | Uint32Array = null; + function initTable(): number[] | Uint32Array { + // lookup table for BMP + const CODEPOINTS = 65536; // BMP holds 65536 codepoints + const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 + const ITEMSIZE = 32; // using uint32_t + const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE; + const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH; + table = (typeof Uint32Array === 'undefined') + ? new Array(CONTAINERSIZE) + : new Uint32Array(CONTAINERSIZE); + for (let i = 0; i < CONTAINERSIZE; ++i) { + let num = 0; + let pos = CODEPOINTS_PER_ITEM; + while (pos--) { + num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); + } + table[i] = num; + } + return table; + } + // get width from lookup table + // position in container : num / CODEPOINTS_PER_ITEM + // ==> n = table[Math.floor(num / 16)] + // ==> n = table[num >> 4] + // 16 codepoints per number: FFEEDDCCBBAA99887766554433221100 + // position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH + // ==> m = (n % 16) * 2 + // ==> m = (num & 15) << 1 + // right shift to position m + // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 + // we are only interested in 2 LSBs, cut off higher bits + // ==> n = n & 3 e.g. 000000000000000000000000000000XX + return function (num: number): number { + num = num | 0; // get asm.js like optimization under V8 + if (num < 32) { + return control | 0; + } + if (num < 127) { + return 1; + } + const t = table || initTable(); + if (num < 65536) { + return t[num >> 4] >> ((num & 15) << 1) & 3; + } + // do a full search for high codepoints + return wcwidthHigh(num); + }; + })({nul: 0, control: 0}); // configurable options + + // test full BMP range old vs new implmenetation + for (let i = 0; i < 65536; ++i) { + assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`); + } +}); diff --git a/src/CharWidth.ts b/src/CharWidth.ts index d54e9392..43cd948e 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -3,171 +3,139 @@ * @license MIT */ +import { fill } from './common/TypedArrayUtils'; + export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { - // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c - // combining characters - const COMBINING_BMP = [ - [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], - [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], - [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], - [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], - [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], - [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], - [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], - [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], - [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], - [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], - [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], - [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], - [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], - [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], - [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], - [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], - [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], - [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], - [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], - [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], - [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], - [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], - [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], - [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], - [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], - [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], - [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], - [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], - [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], - [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], - [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], - [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], - [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], - [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], - [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], - [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], - [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], - [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], - [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], - [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], - [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], - [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], - [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] - ]; - const COMBINING_HIGH = [ - [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], - [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], - [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], - [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], - [0xE0100, 0xE01EF] - ]; - // binary search - function bisearch(ucs: number, data: number[][]): boolean { - let min = 0; - let max = data.length - 1; - let mid; - if (ucs < data[0][0] || ucs > data[max][1]) { - return false; - } - while (max >= min) { - mid = (min + max) >> 1; - if (ucs > data[mid][1]) { - min = mid + 1; - } else if (ucs < data[mid][0]) { - max = mid - 1; - } else { - return true; - } - } + // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c + // combining characters + const COMBINING_BMP = [ + [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], + [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], + [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], + [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], + [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], + [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], + [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], + [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], + [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], + [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], + [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], + [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], + [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], + [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], + [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], + [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], + [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], + [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], + [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], + [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], + [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], + [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], + [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], + [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], + [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], + [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], + [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], + [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], + [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], + [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], + [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], + [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], + [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], + [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], + [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], + [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], + [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], + [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], + [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], + [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], + [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], + [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] + ]; + const COMBINING_HIGH = [ + [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], + [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], + [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], + [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], + [0xE0100, 0xE01EF] + ]; + // binary search + function bisearch(ucs: number, data: number[][]): boolean { + let min = 0; + let max = data.length - 1; + let mid; + if (ucs < data[0][0] || ucs > data[max][1]) { return false; } - function wcwidthBMP(ucs: number): number { - // test for 8-bit control characters - if (ucs === 0) { - return opts.nul; - } - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { - return opts.control; - } - // binary search in table of non-spacing characters - if (bisearch(ucs, COMBINING_BMP)) { - return 0; - } - // if we arrive here, ucs is not a combining or C0/C1 control character - if (isWideBMP(ucs)) { - return 2; + while (max >= min) { + mid = (min + max) >> 1; + if (ucs > data[mid][1]) { + min = mid + 1; + } else if (ucs < data[mid][0]) { + max = mid - 1; + } else { + return true; } + } + return false; + } + function wcwidthHigh(ucs: number): 0 | 1 | 2 { + if (bisearch(ucs, COMBINING_HIGH)) { + return 0; + } + if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { + return 2; + } + return 1; + } + const control = opts.control | 0; + + // create lookup table for BMP plane + const table = new Uint8Array(65536); + fill(table, 1); + table[0] = opts.nul; + // control chars + fill(table, opts.control, 1, 32); + fill(table, opts.control, 0x7f, 0xa0); + + // apply wide char rules first + // wide chars + fill(table, 2, 0x1100, 0x1160); + table[0x2329] = 2; + table[0x232a] = 2; + fill(table, 2, 0x2e80, 0xa4d0); + table[0x303f] = 1; // wrongly in last line + + fill(table, 2, 0xac00, 0xd7a4); + fill(table, 2, 0xf900, 0xfb00); + fill(table, 2, 0xfe10, 0xfe1a); + fill(table, 2, 0xfe30, 0xfe70); + fill(table, 2, 0xff00, 0xff61); + fill(table, 2, 0xffe0, 0xffe7); + + // apply combining last to ensure we overwrite + // wrongly wide set chars: + // the original algo evals combining first and falls + // through to wide check so we simply do here the opposite + // combining 0 + for (let r = 0; r < COMBINING_BMP.length; ++r) { + fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1); + } + + return function (num: number): number { + if (num < 32) { + return control | 0; + } + if (num < 127) { return 1; } - function isWideBMP(ucs: number): boolean { - return ( - ucs >= 0x1100 && ( - ucs <= 0x115f || // Hangul Jamo init. consonants - ucs === 0x2329 || - ucs === 0x232a || - (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi - (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables - (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs - (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms - (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms - (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms - (ucs >= 0xffe0 && ucs <= 0xffe6))); + if (num < 65536) { + return table[num]; } - function wcwidthHigh(ucs: number): 0 | 1 | 2 { - if (bisearch(ucs, COMBINING_HIGH)) { - return 0; - } - if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { - return 2; - } - return 1; - } - const control = opts.control | 0; - let table: number[] | Uint32Array = null; - function initTable(): number[] | Uint32Array { - // lookup table for BMP - const CODEPOINTS = 65536; // BMP holds 65536 codepoints - const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 - const ITEMSIZE = 32; // using uint32_t - const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE; - const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH; - table = (typeof Uint32Array === 'undefined') - ? new Array(CONTAINERSIZE) - : new Uint32Array(CONTAINERSIZE); - for (let i = 0; i < CONTAINERSIZE; ++i) { - let num = 0; - let pos = CODEPOINTS_PER_ITEM; - while (pos--) { - num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); - } - table[i] = num; - } - return table; - } - // get width from lookup table - // position in container : num / CODEPOINTS_PER_ITEM - // ==> n = table[Math.floor(num / 16)] - // ==> n = table[num >> 4] - // 16 codepoints per number: FFEEDDCCBBAA99887766554433221100 - // position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH - // ==> m = (n % 16) * 2 - // ==> m = (num & 15) << 1 - // right shift to position m - // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 - // we are only interested in 2 LSBs, cut off higher bits - // ==> n = n & 3 e.g. 000000000000000000000000000000XX - return function (num: number): number { - num = num | 0; // get asm.js like optimization under V8 - if (num < 32) { - return control | 0; - } - if (num < 127) { - return 1; - } - const t = table || initTable(); - if (num < 65536) { - return t[num >> 4] >> ((num & 15) << 1) & 3; - } - // do a full search for high codepoints - return wcwidthHigh(num); - }; + // do a full search for high codepoints + return wcwidthHigh(num); + }; })({nul: 0, control: 0}); // configurable options /** @@ -175,17 +143,27 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu */ export function getStringCellWidth(s: string): number { let result = 0; - for (let i = 0; i < s.length; ++i) { + const length = s.length; + for (let i = 0; i < length; ++i) { let code = s.charCodeAt(i); + // surrogate pair first if (0xD800 <= code && code <= 0xDBFF) { - const low = s.charCodeAt(i + 1); - if (isNaN(low)) { - return result; + if (++i >= length) { + // this should not happen with strings retrieved from + // Buffer.translateToString as it converts from UTF-32 + // and therefore always should contain the second part + // for any other string we still have to handle it somehow: + // simply treat the lonely surrogate first as a single char (UCS-2 behavior) + return result + wcwidth(code); + } + const second = s.charCodeAt(i); + // convert surrogate pair to high codepoint only for valid second part (UTF-16) + // otherwise treat them independently (UCS-2 behavior) + if (0xDC00 <= second && second <= 0xDFFF) { + code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + result += wcwidth(second); } - code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - } - if (0xDC00 <= code && code <= 0xDFFF) { - continue; } result += wcwidth(code); } diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 5db6d7ff..e92f4125 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -369,7 +369,8 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; parser.parse(collect[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['esc', '', collect[i]]]); + // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled + testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); parser.reset(); testTerminal.clear(); } @@ -1051,6 +1052,13 @@ describe('EscapeSequenceParser', function (): void { ['csi', '<', [0, 0], 'c'] ], null); }); + it('7bit ST should be swallowed', function(): void { + test('abc\x9d123tzf\x1b\\defg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); }); }); @@ -1089,7 +1097,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.GROUND; - parser.parse('\x1e'); + parser.parse('\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index bf744e05..b38c50f5 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -66,7 +66,7 @@ export class TransitionTable { const PRINTABLES = r(0x20, 0x7f); const EXECUTABLES = r(0x00, 0x18); EXECUTABLES.push(0x19); -EXECUTABLES.concat(r(0x1c, 0x20)); +EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND; /** @@ -261,6 +261,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsHandlers = Object.create(null); this._activeDcsHandler = null; this._errorHandler = this._errorHandlerFb; + + // swallow 7bit ST (ESC+\) + this.setEscHandler('\\', () => {}); } public dispose(): void { diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index eacc54db..bc2b10c2 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -6,6 +6,7 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal } from './utils/TestUtils.test'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; @@ -339,4 +340,65 @@ describe('InputHandler', () => { inputHandler.print(String.fromCharCode(0x200B), 0, 1); }); }); + + describe('alt screen', () => { + let term: Terminal; + let handler: InputHandler; + + function lineContent(line: IBufferLine): string { + let content = ''; + for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX]; + return content; + } + + beforeEach(() => { + term = new Terminal(); + handler = new InputHandler(term); + }); + it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { + handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' ')); + // Text color of 'TEST' should be red + expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { + handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' ')); + // Text color of 'TEST' should be red + expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { + handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal('JUNK' + Array(term.cols - 3).join(' ')); + // Text color of 'TEST' should be default + expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + // Text color of 'JUNK' should be red + expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { + handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal(Array(term.cols + 1).join(' ')); + // Text color of 'TEST' should be default + expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + }); + it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { + handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + // Text color of 'TEST' should be default + expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + handler.parse('\x1b[?1049h\x1b[uTEST'); + expect(lineContent(term.buffer.lines.get(1))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + // Text color of 'TEST' should be red + expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { + handler.parse('\x1b[42m\x1b[?1049h'); + // Buffer should be filled with green background + expect(term.buffer.lines.get(20).get(10)[CHAR_DATA_ATTR_INDEX] & 0x1ff).to.equal(2); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a34590ef..7604b01f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -113,7 +113,7 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _surrogateHigh: string; + private _surrogateFirst: string; constructor( protected _terminal: IInputHandlingTerminal, @@ -123,7 +123,7 @@ export class InputHandler extends Disposable implements IInputHandler { this.register(this._parser); - this._surrogateHigh = ''; + this._surrogateFirst = ''; /** * custom fallback handlers @@ -311,9 +311,9 @@ export class InputHandler extends Disposable implements IInputHandler { } // apply leftover surrogate high from last write - if (this._surrogateHigh) { - data = this._surrogateHigh + data; - this._surrogateHigh = ''; + if (this._surrogateFirst) { + data = this._surrogateFirst + data; + this._surrogateFirst = ''; } this._parser.parse(data); @@ -327,7 +327,6 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: string, start: number, end: number): void { let char: string; let code: number; - let low: number; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -345,20 +344,25 @@ export class InputHandler extends Disposable implements IInputHandler { // surrogate pair handling if (0xD800 <= code && code <= 0xDBFF) { - // we got a surrogate high - // get surrogate low (next 2 bytes) - low = data.charCodeAt(stringPosition + 1); - if (isNaN(low)) { - // end of data stream, save surrogate high - this._surrogateHigh = char; + if (++stringPosition >= end) { + // end of input: + // handle pairs as true UTF-16 and wait for the second part + // since we expect the input comming from a stream there is + // a small chance that the surrogate pair got split + // therefore we dont process the first char here, instead + // it gets added as first char to the next processed chunk + this._surrogateFirst = char; continue; } - code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - char += data.charAt(stringPosition + 1); - } - // surrogate low - already handled above - if (0xDC00 <= code && code <= 0xDFFF) { - continue; + const second = data.charCodeAt(stringPosition); + // if the second part is in surrogate pair range create the high codepoint + // otherwise fall back to UCS-2 behavior (handle codepoints independently) + if (0xDC00 <= second && second <= 0xDFFF) { + code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + char += data.charAt(stringPosition); + } else { + stringPosition--; + } } // calculate print space @@ -1280,7 +1284,9 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._terminal.log('Serial port requested application keypad.'); this._terminal.applicationKeypad = true; - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. @@ -1329,14 +1335,19 @@ export class InputHandler extends Disposable implements IInputHandler { case 25: // show cursor this._terminal.cursorHidden = false; break; + case 1048: // alt screen cursor + this.saveCursor(params); + break; case 1049: // alt screen buffer cursor - // TODO: Not sure if we need to save/restore after switching the buffer - // this.saveCursor(params); + this.saveCursor(params); // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer - this._terminal.buffers.activateAltBuffer(); - this._terminal.viewport.syncScrollArea(); + this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr()); + this._terminal.refresh(0, this._terminal.rows - 1); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1469,7 +1480,9 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._terminal.log('Switching back to normal keypad.'); this._terminal.applicationKeypad = false; - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } break; case 9: // X10 Mouse case 1000: // vt200 mouse @@ -1497,18 +1510,22 @@ export class InputHandler extends Disposable implements IInputHandler { case 25: // hide cursor this._terminal.cursorHidden = true; break; + case 1048: // alt screen cursor + this.restoreCursor(params); + break; case 1049: // alt screen buffer cursor // FALL-THROUGH case 47: // normal screen buffer case 1047: // normal screen buffer - clearing it first // Ensure the selection manager has the correct buffer this._terminal.buffers.activateNormalBuffer(); - // TODO: Not sure if we need to save/restore after switching the buffer - // if (params[0] === 1049) { - // this.restoreCursor(params); - // } + if (params[0] === 1049) { + this.restoreCursor(params); + } this._terminal.refresh(0, this._terminal.rows - 1); - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1787,7 +1804,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.originMode = false; this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false this._terminal.applicationKeypad = false; // ? - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1; @@ -1854,7 +1873,7 @@ export class InputHandler extends Disposable implements IInputHandler { public saveCursor(params: number[]): void { this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedY = this._terminal.buffer.y; - this._terminal.savedCurAttr = this._terminal.curAttr; + this._terminal.buffer.savedCurAttr = this._terminal.curAttr; } @@ -1866,7 +1885,7 @@ export class InputHandler extends Disposable implements IInputHandler { public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; - this._terminal.curAttr = this._terminal.savedCurAttr || DEFAULT_ATTR; + this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c42735d5..8735e894 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -30,6 +30,7 @@ class TestSelectionManager extends SelectionManager { public selectLineAt(line: number): void { this._selectLineAt(line); } public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } + public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); } // Disable DOM interaction public enable(): void {} @@ -478,5 +479,17 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'a\n😁\nc'); }); }); + + describe('_areCoordsInSelection', () => { + it('should return whether coords are in the selection', () => { + assert.isFalse(selectionManager.areCoordsInSelection([0, 0], [2, 0], [2, 1])); + assert.isFalse(selectionManager.areCoordsInSelection([1, 0], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([2, 0], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([10, 0], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([0, 1], [2, 0], [2, 1])); + assert.isTrue(selectionManager.areCoordsInSelection([1, 1], [2, 0], [2, 1])); + assert.isFalse(selectionManager.areCoordsInSelection([2, 1], [2, 0], [2, 1])); + }); + }); }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index bfb57177..49e21e2c 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -289,9 +289,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager return false; } + return this._areCoordsInSelection(coords, start, end); + } + + protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return (coords[1] > start[1] && coords[1] < end[1]) || - (start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) || - (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]); + (start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]); } /** @@ -342,7 +347,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); + const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } diff --git a/src/SoundManager.ts b/src/SoundManager.ts index 4139c207..6084edcb 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -12,7 +12,19 @@ import { ITerminal, ISoundManager } from './Types'; export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; export class SoundManager implements ISoundManager { - private _audioContext: AudioContext; + private static _audioContext: AudioContext; + + static get audioContext(): AudioContext | null { + if (!SoundManager._audioContext) { + const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; + if (!audioContextCtor) { + console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); + return null; + } + SoundManager._audioContext = new audioContextCtor(); + } + return SoundManager._audioContext; + } constructor( private _terminal: ITerminal @@ -20,22 +32,16 @@ export class SoundManager implements ISoundManager { } public playBellSound(): void { - const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; - if (!this._audioContext && audioContextCtor) { - this._audioContext = new audioContextCtor(); - } - - if (this._audioContext) { - const bellAudioSource = this._audioContext.createBufferSource(); - const context = this._audioContext; - this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { - bellAudioSource.buffer = buffer; - bellAudioSource.connect(context.destination); - bellAudioSource.start(0); - }); - } else { - console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version'); + const ctx = SoundManager.audioContext; + if (!ctx) { + return; } + const bellAudioSource = ctx.createBufferSource(); + ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { + bellAudioSource.buffer = buffer; + bellAudioSource.connect(ctx.destination); + bellAudioSource.start(0); + }); } private _base64ToArrayBuffer(base64: string): ArrayBuffer { diff --git a/src/Terminal.ts b/src/Terminal.ts index e8871462..c79bc45e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,15 +21,15 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; -import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; +import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './ui/Clipboard'; import { C0 } from './common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; @@ -170,7 +170,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public savedCols: number; public curAttr: number; - public savedCurAttr: number; public params: (string | number)[]; public currentParam: string | number; @@ -208,6 +207,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; + // bufferline to clone/copy from for new blank lines + private _blankLine: IBufferLine = null; + public cols: number; public rows: number; @@ -497,6 +499,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'experimentalBufferLineImpl': this.buffers.normal.setBufferLineFactory(value); this.buffers.alt.setBufferLineFactory(value); + this._blankLine = null; break; } // Inform renderer of changes @@ -804,7 +807,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II button = getButton(ev); // get mouse coordinates - pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -830,7 +833,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -985,7 +988,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II : 65; break; case 'wheel': - button = (ev).wheelDeltaY > 0 + button = (ev).deltaY < 0 ? 64 : 65; break; @@ -1154,9 +1157,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public updateCursorStyle(ev: KeyboardEvent): void { if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) { - this.element.classList.add('xterm-cursor-crosshair'); + this.element.classList.add('column-select'); } else { - this.element.classList.remove('xterm-cursor-crosshair'); + this.element.classList.remove('column-select'); } } @@ -1174,20 +1177,40 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Scroll the terminal down 1 row, creating a blank line. * @param isWrapped Whether the new line is wrapped from the previous line. */ - public scroll(isWrapped?: boolean): void { - const newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + public scroll(isWrapped: boolean = false): void { + let newLine: IBufferLine; + const useRecycling = this.options.experimentalBufferLineImpl === 'TypedArray'; + if (useRecycling) { + newLine = this._blankLine; + if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { + newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + this._blankLine = newLine; + } + newLine.isWrapped = isWrapped; + } else { + newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + } + const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; if (this.buffer.scrollTop === 0) { // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength; + const willBufferBeTrimmed = this.buffer.lines.isFull; // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { - this.buffer.lines.push(newLine); + if (useRecycling) { + if (willBufferBeTrimmed) { + this.buffer.lines.recycle().copyFrom(newLine); + } else { + this.buffer.lines.push(newLine.clone()); + } + } else { + this.buffer.lines.push(newLine); + } } else { - this.buffer.lines.splice(bottomRow + 1, 0, newLine); + this.buffer.lines.splice(bottomRow + 1, 0, (useRecycling) ? newLine.clone() : newLine); } // Only adjust ybase and ydisp when the buffer is not trimmed @@ -1209,7 +1232,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, newLine); + this.buffer.lines.set(bottomRow, (useRecycling) ? newLine.clone() : newLine); } // Move the viewport to the bottom of the buffer unless the user is diff --git a/src/Types.ts b/src/Types.ts index b0ccf88d..bfc82431 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -49,7 +49,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { wraparoundMode: boolean; bracketedPasteMode: boolean; curAttr: number; - savedCurAttr: number; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -246,8 +245,8 @@ export interface ILinkifierAccessor { } export interface IMouseHelper { - getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; - getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; + getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number }; } export interface ICharMeasure { @@ -290,6 +289,7 @@ export interface IBuffer { hasScrollback: boolean; savedY: number; savedX: number; + savedCurAttr: number; isCursorInViewport: boolean; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; getWrappedRangeForLine(y: number): { first: number, last: number }; @@ -306,7 +306,7 @@ export interface IBufferSet extends IEventEmitter { active: IBuffer; activateNormalBuffer(): void; - activateAltBuffer(): void; + activateAltBuffer(fillAttr?: number): void; } export interface ISelectionManager { diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts index 018cfb31..e280b656 100644 --- a/src/addons/attach/attach.test.ts +++ b/src/addons/attach/attach.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as attach from './attach'; diff --git a/src/addons/fit/fit.test.ts b/src/addons/fit/fit.test.ts index 9a6d89fd..781b5010 100644 --- a/src/addons/fit/fit.test.ts +++ b/src/addons/fit/fit.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as fit from './fit'; diff --git a/src/addons/fullscreen/fullscreen.test.ts b/src/addons/fullscreen/fullscreen.test.ts index bb98bd30..6d41bdfd 100644 --- a/src/addons/fullscreen/fullscreen.test.ts +++ b/src/addons/fullscreen/fullscreen.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as fullscreen from './fullscreen'; diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 297a7f5b..4d05e904 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -11,17 +11,18 @@ import { Terminal } from 'xterm'; * @param fullscreen Toggle fullscreen on (true) or off (false) */ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { - let fn: string; + let fn: (...tokens: string[]) => void; if (typeof fullscreen === 'undefined') { - fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add'; + fn = (term.element.classList.contains('fullscreen')) ? + term.element.classList.remove : term.element.classList.add; } else if (!fullscreen) { - fn = 'remove'; + fn = term.element.classList.remove; } else { - fn = 'add'; + fn = term.element.classList.add; } - term.element.classList[fn]('fullscreen'); + fn('fullscreen'); } export function apply(terminalConstructor: typeof Terminal): void { diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 243e555c..3e0b8154 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -2,6 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ +declare var require: any; import { assert, expect } from 'chai'; import * as search from './search'; diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 9998dc1b..c34a0bc5 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -18,8 +18,5 @@ }, "include": [ "**/*.ts" - ], - "exclude": [ - "**/*.test.ts" ] } diff --git a/src/addons/terminado/terminado.test.ts b/src/addons/terminado/terminado.test.ts index 2e4a53c5..e46eafdf 100644 --- a/src/addons/terminado/terminado.test.ts +++ b/src/addons/terminado/terminado.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as terminado from './terminado'; diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index c84ee1a5..8ada2510 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as webLinks from './webLinks'; @@ -39,4 +39,28 @@ describe('webLinks addon', () => { assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); }); + + it('should allow : character in URI path', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/colon:test '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/colon:test'); + }); + + it('should not allow : character at the end of a URI path', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/colon:test: '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/colon:test'); + }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index a007bbd6..75d79104 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; +const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; diff --git a/src/addons/winptyCompat/winptyCompat.test.ts b/src/addons/winptyCompat/winptyCompat.test.ts index 0c9269ed..c3a7e479 100644 --- a/src/addons/winptyCompat/winptyCompat.test.ts +++ b/src/addons/winptyCompat/winptyCompat.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as winptyCompat from './winptyCompat'; diff --git a/src/addons/zmodem/zmodem.test.ts b/src/addons/zmodem/zmodem.test.ts index 682e62c8..d0c7c5fb 100644 --- a/src/addons/zmodem/zmodem.test.ts +++ b/src/addons/zmodem/zmodem.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as zmodem from './zmodem'; diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts index 23204f0f..70fc6e98 100644 --- a/src/addons/zmodem/zmodem.ts +++ b/src/addons/zmodem/zmodem.ts @@ -34,7 +34,7 @@ import { Terminal } from 'xterm'; * via `detach()` and a re-`attach()`.) */ -let zmodem; +let zmodem: any; export interface IZmodemOptions { noTerminalWriteOutsideSession?: boolean; @@ -44,7 +44,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { const term = this; const senderFunc = (octets: ArrayLike) => ws.send(new Uint8Array(octets)); - let zsentry; + let zsentry: any; function shouldWrite(): boolean { return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 542dbf12..9faf534a 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -90,16 +90,34 @@ export class CircularList extends EventEmitter implements ICircularList { public push(value: T): void { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { - this._startIndex++; - if (this._startIndex === this._maxLength) { - this._startIndex = 0; - } + this._startIndex = ++this._startIndex % this._maxLength; this.emit('trim', 1); } else { this._length++; } } + /** + * Advance ringbuffer index and return current element for recycling. + * Note: The buffer must be full for this method to work. + * @throws When the buffer is not full. + */ + public recycle(): T { + if (this._length !== this._maxLength) { + throw new Error('Can only recycle when the buffer is full'); + } + this._startIndex = ++this._startIndex % this._maxLength; + this.emit('trim', 1); + return this._array[this._getCyclicIndex(this._length - 1)]!; + } + + /** + * Ringbuffer is at max length. + */ + public get isFull(): boolean { + return this._length === this._maxLength; + } + /** * Removes and returns the last value on the list. * @return The popped value. @@ -136,10 +154,10 @@ export class CircularList extends EventEmitter implements ICircularList { } // Adjust length as needed - if (this._length + items.length > this.maxLength) { - const countToTrim = (this._length + items.length) - this.maxLength; + if (this._length + items.length > this._maxLength) { + const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; - this._length = this.maxLength; + this._length = this._maxLength; this.emit('trim', countToTrim); } else { this._length += items.length; @@ -178,7 +196,7 @@ export class CircularList extends EventEmitter implements ICircularList { const expandListBy = (start + count + offset) - this._length; if (expandListBy > 0) { this._length += expandListBy; - while (this._length > this.maxLength) { + while (this._length > this._maxLength) { this._length--; this._startIndex++; this.emit('trim', 1); @@ -198,6 +216,6 @@ export class CircularList extends EventEmitter implements ICircularList { * @returns The cyclic index. */ private _getCyclicIndex(index: number): number { - return (this._startIndex + index) % this.maxLength; + return (this._startIndex + index) % this._maxLength; } } diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts new file mode 100644 index 00000000..69a62abc --- /dev/null +++ b/src/common/TypedArrayUtils.test.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { fillFallback } from './TypedArrayUtils'; + +type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + +describe('polyfill conformance tests', function(): void { + + function deepEquals(a: TypedArray, b: TypedArray): void { + assert.equal(a.length, b.length); + for (let i = 0; i < a.length; ++i) { + assert.equal(a[i], b[i]); + } + } + + describe('TypedArray.fill', function(): void { + it('should work with all typed array types', function(): void { + const u81 = new Uint8Array(5); + const u82 = new Uint8Array(5); + deepEquals(fillFallback(u81, 2), u82.fill(2)); + deepEquals(fillFallback(u81, -1), u82.fill(-1)); + const u161 = new Uint16Array(5); + const u162 = new Uint16Array(5); + deepEquals(fillFallback(u161, 2), u162.fill(2)); + deepEquals(fillFallback(u161, 65535), u162.fill(65535)); + deepEquals(fillFallback(u161, -1), u162.fill(-1)); + const u321 = new Uint32Array(5); + const u322 = new Uint32Array(5); + deepEquals(fillFallback(u321, 2), u322.fill(2)); + deepEquals(fillFallback(u321, 65537), u322.fill(65537)); + deepEquals(fillFallback(u321, -1), u322.fill(-1)); + const i81 = new Int8Array(5); + const i82 = new Int8Array(5); + deepEquals(fillFallback(i81, 2), i82.fill(2)); + deepEquals(fillFallback(i81, -1), i82.fill(-1)); + const i161 = new Int16Array(5); + const i162 = new Int16Array(5); + deepEquals(fillFallback(i161, 2), i162.fill(2)); + deepEquals(fillFallback(i161, 65535), i162.fill(65535)); + deepEquals(fillFallback(i161, -1), i162.fill(-1)); + const i321 = new Int32Array(5); + const i322 = new Int32Array(5); + deepEquals(fillFallback(i321, 2), i322.fill(2)); + deepEquals(fillFallback(i321, 65537), i322.fill(65537)); + deepEquals(fillFallback(i321, -1), i322.fill(-1)); + const f321 = new Float32Array(5); + const f322 = new Float32Array(5); + deepEquals(fillFallback(f321, 1.2345), f322.fill(1.2345)); + const f641 = new Float64Array(5); + const f642 = new Float64Array(5); + deepEquals(fillFallback(f641, 1.2345), f642.fill(1.2345)); + const u8Clamped1 = new Uint8ClampedArray(5); + const u8Clamped2 = new Uint8ClampedArray(5); + deepEquals(fillFallback(u8Clamped1, 2), u8Clamped2.fill(2)); + deepEquals(fillFallback(u8Clamped1, 257), u8Clamped2.fill(257)); + }); + it('start offset', function(): void { + for (let i = -2; i < 10; ++i) { + const u81 = new Uint8Array(5); + const u83 = new Uint8Array(5); + deepEquals(fillFallback(u81, 2, i), u83.fill(2, i)); + deepEquals(fillFallback(u81, -1, i), u83.fill(-1, i)); + } + }); + it('end offset', function(): void { + for (let i = -2; i < 10; ++i) { + const u81 = new Uint8Array(5); + const u83 = new Uint8Array(5); + deepEquals(fillFallback(u81, 2, 0, i), u83.fill(2, 0, i)); + deepEquals(fillFallback(u81, -1, 0, i), u83.fill(-1, 0, i)); + } + }); + it('start/end offset', function(): void { + for (let i = -2; i < 10; ++i) { + for (let j = -2; j < 10; ++j) { + const u81 = new Uint8Array(5); + const u83 = new Uint8Array(5); + deepEquals(fillFallback(u81, 2, i, j), u83.fill(2, i, j)); + deepEquals(fillFallback(u81, -1, i, j), u83.fill(-1, i, j)); + } + } + }); + }); +}); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts new file mode 100644 index 00000000..6e1a3630 --- /dev/null +++ b/src/common/TypedArrayUtils.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * polyfill for TypedArray.fill + * This is needed to support .fill in all safari versions and IE 11. + */ + +type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + +export function fill(array: T, value: number, start?: number, end?: number): T { + // all modern engines that support .fill + if (array.fill) { + return array.fill(value, start, end) as T; + } + return fillFallback(array, value, start, end); +} + +export function fillFallback(array: T, value: number, start: number = 0, end: number = array.length): T { + // safari and IE 11 + // since IE 11 does not support Array.prototype.fill either + // we cannot use the suggested polyfill from MDN + // instead we simply fall back to looping + if (start >= array.length) { + return array; + } + start = (array.length + start) % array.length; + if (end >= array.length) { + end = array.length; + } else { + end = (array.length + end) % array.length; + } + for (let i = start; i < end; ++i) { + array[i] = value; + } + return array; +} diff --git a/src/common/Types.ts b/src/common/Types.ts index aabe721e..8a416bf1 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -24,10 +24,12 @@ export interface IKeyboardEvent { export interface ICircularList extends IEventEmitter { length: number; maxLength: number; + isFull: boolean; get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; + recycle(): T | undefined; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 8226fd96..9286421e 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -33,7 +33,6 @@ export class AltClickHandler { this._mouseEvent, this._terminal.element, this._terminal.charMeasure, - this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, false diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 84e290e7..2afdebb5 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -9,6 +9,7 @@ import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/T import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; +import { is256Color } from './atlas/CharAtlasUtils'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -298,7 +299,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; - } else if (fg < 256) { + } else if (is256Color(fg)) { // 256 color support this._ctx.fillStyle = this._colors.ansi[fg].css; } else { diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 8679939a..855830e4 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -7,6 +7,7 @@ import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } f import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { is256Color } from './atlas/CharAtlasUtils'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkHoverEvent = null; @@ -42,7 +43,7 @@ export class LinkRenderLayer extends BaseRenderLayer { private _onLinkHover(e: ILinkHoverEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; - } else if (e.fg < 256) { + } else if (is256Color(e.fg)) { // 256 color support this._ctx.fillStyle = this._colors.ansi[e.fg].css; } else { diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 24c948eb..7ab2d238 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -6,9 +6,10 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { CharData, ITerminal } from '../Types'; -import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; +import { is256Color } from './atlas/CharAtlasUtils'; /** * This CharData looks like a null character, which will forc a clear and render @@ -143,10 +144,10 @@ export class TextRenderLayer extends BaseRenderLayer { const temp = bg; bg = fg; fg = temp; - if (fg === 256) { + if (fg === DEFAULT_COLOR) { fg = INVERTED_DEFAULT_COLOR; } - if (bg === 257) { + if (bg === DEFAULT_COLOR) { bg = INVERTED_DEFAULT_COLOR; } } @@ -186,7 +187,7 @@ export class TextRenderLayer extends BaseRenderLayer { let nextFillStyle = null; // null represents default background color if (bg === INVERTED_DEFAULT_COLOR) { nextFillStyle = this._colors.foreground.css; - } else if (bg < 256) { + } else if (is256Color(bg)) { nextFillStyle = this._colors.ansi[bg].css; } @@ -230,7 +231,7 @@ export class TextRenderLayer extends BaseRenderLayer { this._ctx.save(); if (fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; - } else if (fg < 256) { + } else if (is256Color(fg)) { // 256 color support this._ctx.fillStyle = this._colors.ansi[fg].css; } else { diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 59ac07df..c504f77e 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -6,6 +6,7 @@ import { ITerminal } from '../../Types'; import { IColorSet } from '../Types'; import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import { DEFAULT_COLOR } from './Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter @@ -51,3 +52,7 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } + +export function is256Color(colorCode: number): boolean { + return colorCode < DEFAULT_COLOR; +} diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 63336900..b6b323f0 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -42,7 +42,7 @@ interface IGlyphCacheValue { inBitmap: boolean; } -function getGlyphCacheKey(glyph: IGlyphIdentifier): number { +export function getGlyphCacheKey(glyph: IGlyphIdentifier): number { // Note that this only returns a valid key when code < 256 // Layout: // 0b00000000000000000000000000000001: italic (1) diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts index c0d8a814..8dc8be74 100644 --- a/src/renderer/atlas/StaticCharAtlas.ts +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { DIM_OPACITY, IGlyphIdentifier } from './Types'; +import { DIM_OPACITY, IGlyphIdentifier, DEFAULT_COLOR } from './Types'; import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from '../../shared/atlas/Types'; import { generateStaticCharAtlasTexture } from '../../shared/atlas/CharAtlasGenerator'; import BaseCharAtlas from './BaseCharAtlas'; +import { is256Color } from './CharAtlasUtils'; export default class StaticCharAtlas extends BaseCharAtlas { private _texture: HTMLCanvasElement | ImageBitmap; @@ -41,8 +42,8 @@ export default class StaticCharAtlas extends BaseCharAtlas { const isAscii = glyph.code < 256; // A color is basic if it is one of the 4 bit ANSI colors. const isBasicColor = glyph.fg < 16; - const isDefaultColor = glyph.fg >= 256; - const isDefaultBackground = glyph.bg >= 256; + const isDefaultColor = glyph.fg === DEFAULT_COLOR; + const isDefaultBackground = glyph.bg === DEFAULT_COLOR; return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !glyph.italic; } @@ -58,9 +59,9 @@ export default class StaticCharAtlas extends BaseCharAtlas { } let colorIndex = 0; - if (glyph.fg < 256) { + if (is256Color(glyph.fg)) { colorIndex = 2 + glyph.fg + (glyph.bold ? 16 : 0); - } else { + } else if (glyph.fg === DEFAULT_COLOR) { // If default color and bold if (glyph.bold) { colorIndex = 1; diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 6fb3c5d1..76cfd07d 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -3,7 +3,8 @@ * @license MIT */ -export const INVERTED_DEFAULT_COLOR = -1; +export const DEFAULT_COLOR = 256; +export const INVERTED_DEFAULT_COLOR = 257; export const DIM_OPACITY = 0.5; export interface IGlyphIdentifier { diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 22ecd0cd..a0cefd67 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -10,6 +10,7 @@ import { EventEmitter } from '../../common/EventEmitter'; import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -111,6 +112,8 @@ export class DomRenderer extends EventEmitter implements IRenderer { element.style.width = `${this.dimensions.canvasWidth}px`; element.style.height = `${this.dimensions.actualCellHeight}px`; element.style.lineHeight = `${this.dimensions.actualCellHeight}px`; + // Make sure rows don't overflow onto following row + element.style.overflow = 'hidden'; }); if (!this._dimensionsStyleElement) { @@ -123,16 +126,14 @@ export class DomRenderer extends EventEmitter implements IRenderer { ` display: inline-block;` + ` height: 100%;` + ` vertical-align: top;` + - ` width: ${this._terminal.charMeasure.width}px` + + ` width: ${this.dimensions.actualCellWidth}px` + `}`; this._dimensionsStyleElement.innerHTML = styles; this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height; - this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`; - this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`; - this._terminal.screenElement.style.width = ''; - this._terminal.screenElement.style.height = ''; + this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; + this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public setTheme(theme: ITheme | undefined): IColorSet { @@ -199,6 +200,9 @@ export class DomRenderer extends EventEmitter implements IRenderer { `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); + styles += + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this.colorManager.colors.background.css}; }` + + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this.colorManager.colors.foreground.css}; }`; this._themeStyleElement.innerHTML = styles; return this.colorManager.colors; @@ -332,7 +336,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const row = y + terminal.buffer.ydisp; const lineData = terminal.buffer.lines.get(row); const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, terminal.charMeasure.width, terminal.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols)); } this._terminal.emit('refresh', {start, end}); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 2c46d8cc..67342da0 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -10,6 +10,7 @@ import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '. import { FLAGS } from '../Types'; import { BufferLine } from '../../BufferLine'; import { IBufferLine } from '../../Types'; +import { DEFAULT_COLOR } from '../atlas/Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -23,11 +24,10 @@ describe('DomRendererRowFactory', () => { }); describe('createRow', () => { - it('should create an element for every character in the row', () => { + it('should not create anything for an empty row', () => { const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - ' ' + - ' ' + '' ); }); @@ -45,8 +45,7 @@ describe('DomRendererRowFactory', () => { for (const style of ['block', 'bar', 'underline']) { const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - ` ` + - ' ' + ` ` ); } }); @@ -65,8 +64,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); @@ -74,31 +72,28 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); it('should add classes for 256 foreground colors', () => { - const defaultAttrNoFgColor = (0 << 9) | (256 << 0); + const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0); for (let i = 0; i < 256; i++) { lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - `a` + - ' ' + `a` ); } }); it('should add classes for 256 background colors', () => { - const defaultAttrNoBgColor = (257 << 9) | (0 << 0); + const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0); for (let i = 0; i < 256; i++) { lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - `a` + - ' ' + `a` ); } }); @@ -107,36 +102,32 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); it('should correctly invert default fg color', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.set(0, [(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); it('should correctly invert default bg color', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + - ' ' + 'a' ); }); it('should turn bold fg text bright', () => { for (let i = 0; i < 8; i++) { - lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), - `a` + - ' ' + `a` ); } }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index ce94ce33..4a3715c5 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,9 +3,10 @@ * @license MIT */ -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE } from '../../Buffer'; import { FLAGS } from '../Types'; import { IBufferLine } from '../../Types'; +import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; @@ -22,14 +23,23 @@ export class DomRendererRowFactory { public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); - let colCount = 0; - for (let x = 0; x < lineData.length; x++) { - // Don't allow any buffer to the right to be displayed - if (colCount >= cols) { - continue; + // Find the line length first, this prevents the need to output a bunch of + // empty cells at the end. This cannot easily be integrated into the main + // loop below because of the colCount feature (which can be removed after we + // properly support reflow and disallow data to go beyond the right-side of + // the viewport). + let lineLength = 0; + for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { + const charData = lineData.get(x); + const code = charData[CHAR_DATA_CODE_INDEX]; + if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + lineLength = x + 1; + break; } + } + for (let x = 0; x < lineLength; x++) { const charData = lineData.get(x); const char: string = charData[CHAR_DATA_CHAR_INDEX] || ' '; const attr: number = charData[CHAR_DATA_ATTR_INDEX]; @@ -70,16 +80,17 @@ export class DomRendererRowFactory { const temp = bg; bg = fg; fg = temp; - if (fg === 256) { - fg = 0; + if (fg === DEFAULT_COLOR) { + fg = INVERTED_DEFAULT_COLOR; } - if (bg === 257) { - bg = 15; + if (bg === DEFAULT_COLOR) { + bg = INVERTED_DEFAULT_COLOR; } } if (flags & FLAGS.BOLD) { - // Convert the FG color to the bold variant + // Convert the FG color to the bold variant. This should not happen when + // the fg is the inverse default color as there is no bold variant. if (fg < 8) { fg += 8; } @@ -91,14 +102,13 @@ export class DomRendererRowFactory { } charElement.textContent = char; - if (fg !== 257) { + if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); } - if (bg !== 256) { + if (bg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-bg-${bg}`); } fragment.appendChild(charElement); - colCount += width; } return fragment; } diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index f78a6d41..60138d79 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -8,22 +8,13 @@ import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types'; import { IColor } from '../Types'; import { isFirefox, isSafari } from '../utils/Browser'; -declare const Promise: any; - -export interface IOffscreenCanvas { - width: number; - height: number; - getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D; - transferToImageBitmap(): ImageBitmap; -} - /** * Generates a char atlas. * @param context The window or worker context. * @param canvasFactory A function to generate a canvas with a width or height. * @param config The config for the new char atlas. */ -export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise { +export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement, config: ICharAtlasConfig): HTMLCanvasElement | Promise { const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const cellHeight = config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; const canvas = canvasFactory( @@ -101,12 +92,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( // performance (tested on v55). if (!('createImageBitmap' in context) || isFirefox || isSafari) { // Don't attempt to clear background colors if createImageBitmap is not supported - if (canvas instanceof HTMLCanvasElement) { - // Just return the HTMLCanvas if it's a HTMLCanvasElement - return canvas; - } - // Transfer to an ImageBitmap is this is an OffscreenCanvas - return new Promise((r: (bitmap: ImageBitmap) => void) => r(canvas.transferToImageBitmap())); + return canvas; } const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); diff --git a/src/handlers/Clipboard.test.ts b/src/ui/Clipboard.test.ts similarity index 100% rename from src/handlers/Clipboard.test.ts rename to src/ui/Clipboard.test.ts diff --git a/src/handlers/Clipboard.ts b/src/ui/Clipboard.ts similarity index 100% rename from src/handlers/Clipboard.ts rename to src/ui/Clipboard.ts diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index 491e2a05..a232f5b9 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -180,7 +180,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); + const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.cols, this._terminal.rows); if (!coords) { return null; } diff --git a/src/ui/ScreenDprMonitor.ts b/src/ui/ScreenDprMonitor.ts index 9247a032..d66eeb64 100644 --- a/src/ui/ScreenDprMonitor.ts +++ b/src/ui/ScreenDprMonitor.ts @@ -19,7 +19,7 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat */ export class ScreenDprMonitor extends Disposable { private _currentDevicePixelRatio: number; - private _outerListener: MediaQueryListListener; + private _outerListener: (this: MediaQueryList, ev: MediaQueryListEvent) => any; private _listener: ScreenDprListener; private _resolutionMediaMatchList: MediaQueryList; diff --git a/src/utils/MouseHelper.test.ts b/src/utils/MouseHelper.test.ts index ac3137f4..94d63b2b 100644 --- a/src/utils/MouseHelper.test.ts +++ b/src/utils/MouseHelper.test.ts @@ -37,34 +37,34 @@ describe('MouseHelper.getCoords', () => { describe('when charMeasure is not initialized', () => { it('should return null', () => { charMeasure = new MockCharMeasure(); - assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null); + assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 10, 10), null); }); }); describe('when pageX/pageY are not supported', () => { it('should return null', () => { - assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 1, 10, 10), null); + assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 10, 10), null); }); }); it('should return the cell that was clicked', () => { let coords: [number, number]; - coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10); assert.deepEqual(coords, [1, 1]); - coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10); assert.deepEqual(coords, [1, 1]); - coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10); assert.deepEqual(coords, [1, 2]); - coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10); assert.deepEqual(coords, [2, 1]); }); it('should ensure the coordinates are returned within the terminal bounds', () => { let coords: [number, number]; - coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10); + coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 10, 10); assert.deepEqual(coords, [1, 1]); // Event are double the cols/rows - coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10); assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); }); }); diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts index ca1bb27e..e4b3f211 100644 --- a/src/utils/MouseHelper.ts +++ b/src/utils/MouseHelper.ts @@ -52,7 +52,7 @@ export class MouseHelper { * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ - public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { + public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { // Coordinates cannot be measured if charMeasure has not been initialized if (!charMeasure.width || !charMeasure.height) { return null; @@ -85,8 +85,8 @@ export class MouseHelper { * @param colCount The number of columns in the terminal. * @param rowCount The number of rows in the terminal. */ - public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { - const coords = this.getCoords(event, element, charMeasure, lineHeight, colCount, rowCount); + public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number } { + const coords = this.getCoords(event, element, charMeasure, colCount, rowCount); let x = coords[0]; let y = coords[1]; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 353e615f..a5ef4b9f 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -182,7 +182,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { wraparoundMode: boolean; bracketedPasteMode: boolean; curAttr: number; - savedCurAttr: number; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -304,6 +303,7 @@ export class MockBuffer implements IBuffer { scrollTop: number; savedY: number; savedX: number; + savedCurAttr: number; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { return Buffer.prototype.translateBufferLineToString.apply(this, arguments); } diff --git a/src/xterm.css b/src/xterm.css index 8e129f50..24cd475f 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -139,7 +139,7 @@ cursor: pointer; } -.xterm.xterm-cursor-crosshair { +.xterm.column-select.focus { /* Column selection mode */ cursor: crosshair; } diff --git a/yarn.lock b/yarn.lock index 267c3bfc..db31bfe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6716,10 +6716,10 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" - integrity sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg== +typescript@3.1: + version "3.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" + integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== uglify-es@^3.3.4: version "3.3.9"