Merge branch 'master' into 1737_inverse_dynamic_cache

This commit is contained in:
Daniel Imms
2018-11-23 08:52:32 -08:00
committed by GitHub
42 changed files with 949 additions and 366 deletions
+66 -26
View File
@@ -133,31 +133,71 @@ describe('BufferLine', function(): void {
const line3 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
});
it('resize enlarge', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('resize shrink(true)', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('resize shrink(false)', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('resize shrink(false) + shrink(false)', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('resize shrink(false) + enlarge', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(15).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
describe('resize', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) - should not apply new size', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + shrink(false) - should not apply new size', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + enlarge(false) to smaller than before', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + enlarge(false) to bigger than before', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(25, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(25).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) + resize shrink=true should enforce shrinking', function(): void {
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge from 0 length', function(): void {
const line = new TestBufferLine(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(false) to 0 and enlarge to different sizes', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], false);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], true);
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
});
});
+14 -15
View File
@@ -77,15 +77,15 @@ export class BufferLine implements IBufferLine {
/** resize line to cols filling new cells with fill */
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
while (this._data.length < cols) {
this._data.push(fillCharData);
}
if (shrink) {
while (this._data.length > cols) {
this._data.pop();
}
}
while (this._data.length < cols) {
this._data.push(fillCharData);
}
this.length = cols;
this.length = this._data.length;
}
public fill(fillCharData: CharData): void {
@@ -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;
}
@@ -141,11 +138,13 @@ export class BufferLineTypedArray implements IBufferLine {
if (!fillCharData) {
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
this._data = new Uint32Array(cols * CELL_SIZE);
for (let i = 0; i < cols; ++i) {
this.set(i, fillCharData);
if (cols) {
this._data = new Uint32Array(cols * CELL_SIZE);
for (let i = 0; i < cols; ++i) {
this.set(i, fillCharData);
}
}
this.length = cols || 0;
this.length = cols;
}
public get(index: number): CharData {
@@ -212,12 +211,12 @@ export class BufferLineTypedArray implements IBufferLine {
}
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
if (cols === this.length) {
if (cols === this.length || (!shrink && cols < this.length)) {
return;
}
if (cols > this.length) {
const data = new Uint32Array(cols * CELL_SIZE);
if (this._data) {
if (this.length) {
if (cols * CELL_SIZE < this._data.length) {
data.set(this._data.subarray(0, cols * CELL_SIZE));
} else {
+176 -1
View File
@@ -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}`);
}
});
+144 -166
View File
@@ -3,171 +3,139 @@
* @license MIT
*/
import { fill } from './core/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);
}
+10 -2
View File
@@ -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();
+4 -1
View File
@@ -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 {
+30
View File
@@ -438,6 +438,36 @@ describe('InputHandler', () => {
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([2]);
expect(termContent(termNew)).eql(termContent(termOld));
// reset and add a wrapped line
termNew.buffer.y = 0;
termNew.buffer.x = 0;
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(termNew.buffer.lines.get(2).isWrapped).true;
termNew.buffer.y = 2;
termNew.buffer.x = 40;
inputHandlerNew.eraseInDisplay([1]);
expect(termNew.buffer.lines.get(2).isWrapped).false;
// reset and add a wrapped line
termNew.buffer.y = 0;
termNew.buffer.x = 0;
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(termNew.buffer.lines.get(2).isWrapped).true;
termNew.buffer.y = 1;
termNew.buffer.x = 90; // Cursor is beyond last column
inputHandlerNew.eraseInDisplay([1]);
expect(termNew.buffer.lines.get(2).isWrapped).false;
});
});
it('convertEol setting', function(): void {
+47 -25
View File
@@ -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
@@ -722,12 +726,25 @@ export class InputHandler extends Disposable implements IInputHandler {
* @param start first cell index to be erased
* @param end end - 1 is last erased cell
*/
private _eraseInBufferLine(y: number, start: number, end: number): void {
this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y).replaceCells(
private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
line.replaceCells(
start,
end,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
if (clearWrap) {
line.isWrapped = false;
}
}
/**
* Helper method to reset cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false.
* @param y row index
*/
private _resetBufferLine(y: number): void {
this._eraseInBufferLine(y, 0, this._terminal.cols, true);
}
/**
@@ -748,18 +765,23 @@ export class InputHandler extends Disposable implements IInputHandler {
case 0:
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols);
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols, this._terminal.buffer.x === 0);
for (; j < this._terminal.rows; j++) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._resetBufferLine(j);
}
this._terminal.updateRange(j);
break;
case 1:
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1);
// Deleted front part of line and everything before. This line will no longer be wrapped.
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1, true);
if (this._terminal.buffer.x + 1 >= this._terminal.cols) {
// Deleted entire previous line. This next line can no longer be wrapped.
this._terminal.buffer.lines.get(j + 1).isWrapped = false;
}
while (j--) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._resetBufferLine(j);
}
this._terminal.updateRange(0);
break;
@@ -767,7 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler {
j = this._terminal.rows;
this._terminal.updateRange(j - 1);
while (j--) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._resetBufferLine(j);
}
this._terminal.updateRange(0);
break;
+13
View File
@@ -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]));
});
});
});
+7 -2
View File
@@ -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]);
}
/**
+22 -16
View File
@@ -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 = (<any>window).AudioContext || (<any>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 = (<any>window).AudioContext || (<any>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 {
+35 -10
View File
@@ -21,11 +21,11 @@
* 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';
@@ -208,6 +208,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;
@@ -342,7 +345,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
public get isFocused(): boolean {
return document.activeElement === this.textarea;
return document.activeElement === this.textarea && document.hasFocus();
}
/**
@@ -473,6 +476,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this._theme) {
this.renderer.setTheme(this._theme);
}
this.mouseHelper.setRenderer(this.renderer);
break;
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
@@ -496,6 +500,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
@@ -984,7 +989,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
: 65;
break;
case 'wheel':
button = (<WheelEvent>ev).wheelDeltaY > 0
button = (<WheelEvent>ev).deltaY < 0
? 64
: 65;
break;
@@ -1173,20 +1178,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(DEFAULT_ATTR, 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
@@ -1208,7 +1233,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
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as attach from './attach';
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as fit from './fit';
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as fullscreen from './fullscreen';
+6 -5
View File
@@ -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 {
+13 -8
View File
@@ -4,7 +4,7 @@
*/
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?';
const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
/**
* A class that knows how to search the terminal and how to display the results.
@@ -19,7 +19,7 @@ export class SearchHelper implements ISearchHelper {
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
*/
@@ -33,7 +33,9 @@ export class SearchHelper implements ISearchHelper {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionEnd[1];
if (this._terminal.getSelection().length !== 0) {
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
}
// Search from ydisp + 1 to end
@@ -61,7 +63,7 @@ export class SearchHelper implements ISearchHelper {
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
*/
@@ -75,7 +77,9 @@ export class SearchHelper implements ISearchHelper {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionStart[1];
if (this._terminal.getSelection().length !== 0) {
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
}
// Search from ydisp + 1 to end
@@ -108,7 +112,7 @@ export class SearchHelper implements ISearchHelper {
*/
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
}
/**
@@ -116,7 +120,7 @@ export class SearchHelper implements ISearchHelper {
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* @param term Tne search term.
* @param term The search term.
* @param y The line to search.
* @param searchOptions Search options.
* @return The search result if it was found.
@@ -191,7 +195,7 @@ export class SearchHelper implements ISearchHelper {
do {
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight);
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
@@ -205,6 +209,7 @@ export class SearchHelper implements ISearchHelper {
*/
private _selectResult(result: ISearchResult): boolean {
if (!result) {
this._terminal.clearSelection();
return false;
}
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
+4
View File
@@ -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';
@@ -81,6 +82,9 @@ describe('search addon', () => {
expect(hello3).eql(undefined);
expect(llo).eql(undefined);
expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'});
term.core.resize(9, 5);
const hello0Resize = term.searchHelper.findInLine('Hello', 0);
expect(hello0Resize).eql({col: 8, row: 0, term: 'Hello'});
});
it('should respect search regex', () => {
search.apply(<any>MockTerminal);
+2 -2
View File
@@ -10,7 +10,7 @@ import { ISearchAddonTerminal, ISearchOptions } from './Interfaces';
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options
* @return Whether a result was found.
*/
@@ -25,7 +25,7 @@ export function findNext(terminal: Terminal, term: string, searchOptions: ISearc
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param term The search term.
* @param searchOptions Search options
* @return Whether a result was found.
*/
-3
View File
@@ -18,8 +18,5 @@
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as terminado from './terminado';
+25 -1
View File
@@ -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(<any>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(<any>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');
});
});
+1 -1
View File
@@ -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 + ')?';
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as winptyCompat from './winptyCompat';
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import * as zmodem from './zmodem';
+2 -2
View File
@@ -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<number>) => ws.send(new Uint8Array(octets));
let zsentry;
let zsentry: any;
function shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
+27 -9
View File
@@ -90,16 +90,34 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
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<T> extends EventEmitter implements ICircularList<T> {
}
// 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<T> extends EventEmitter implements ICircularList<T> {
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<T> extends EventEmitter implements ICircularList<T> {
* @returns The cyclic index.
*/
private _getCyclicIndex(index: number): number {
return (this._startIndex + index) % this.maxLength;
return (this._startIndex + index) % this._maxLength;
}
}
+2
View File
@@ -24,10 +24,12 @@ export interface IKeyboardEvent {
export interface ICircularList<T> 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;
+158
View File
@@ -0,0 +1,158 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { fill } from './TypedArrayUtils';
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
| Int8Array | Int16Array | Int32Array
| Float32Array | Float64Array;
// we explicitly test against the looping version in the test cases
function loopFill(array: TypedArray, value: number, start: number = 0, end?: number | undefined): TypedArray {
if (start >= array.length) {
return array;
}
start = (array.length + start) % array.length;
if (end === undefined) {
end = 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;
}
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(fill(u81, 2), u82.fill(2));
deepEquals(fill(u81, -1), u82.fill(-1));
const u161 = new Uint16Array(5);
const u162 = new Uint16Array(5);
deepEquals(fill(u161, 2), u162.fill(2));
deepEquals(fill(u161, 65535), u162.fill(65535));
deepEquals(fill(u161, -1), u162.fill(-1));
const u321 = new Uint32Array(5);
const u322 = new Uint32Array(5);
deepEquals(fill(u321, 2), u322.fill(2));
deepEquals(fill(u321, 65537), u322.fill(65537));
deepEquals(fill(u321, -1), u322.fill(-1));
const i81 = new Int8Array(5);
const i82 = new Int8Array(5);
deepEquals(fill(i81, 2), i82.fill(2));
deepEquals(fill(i81, -1), i82.fill(-1));
const i161 = new Int16Array(5);
const i162 = new Int16Array(5);
deepEquals(fill(i161, 2), i162.fill(2));
deepEquals(fill(i161, 65535), i162.fill(65535));
deepEquals(fill(i161, -1), i162.fill(-1));
const i321 = new Int32Array(5);
const i322 = new Int32Array(5);
deepEquals(fill(i321, 2), i322.fill(2));
deepEquals(fill(i321, 65537), i322.fill(65537));
deepEquals(fill(i321, -1), i322.fill(-1));
const f321 = new Float32Array(5);
const f322 = new Float32Array(5);
deepEquals(fill(f321, 1.2345), f322.fill(1.2345));
const f641 = new Float64Array(5);
const f642 = new Float64Array(5);
deepEquals(fill(f641, 1.2345), f642.fill(1.2345));
const u8Clamped1 = new Uint8ClampedArray(5);
const u8Clamped2 = new Uint8ClampedArray(5);
deepEquals(fill(u8Clamped1, 2), u8Clamped2.fill(2));
deepEquals(fill(u8Clamped1, 257), u8Clamped2.fill(257));
});
it('should work with all typed array types - explicit looping', function(): void {
const u81 = new Uint8Array(5);
const u82 = new Uint8Array(5);
deepEquals(loopFill(u81, 2), u82.fill(2));
deepEquals(loopFill(u81, -1), u82.fill(-1));
const u161 = new Uint16Array(5);
const u162 = new Uint16Array(5);
deepEquals(loopFill(u161, 2), u162.fill(2));
deepEquals(loopFill(u161, 65535), u162.fill(65535));
deepEquals(loopFill(u161, -1), u162.fill(-1));
const u321 = new Uint32Array(5);
const u322 = new Uint32Array(5);
deepEquals(loopFill(u321, 2), u322.fill(2));
deepEquals(loopFill(u321, 65537), u322.fill(65537));
deepEquals(loopFill(u321, -1), u322.fill(-1));
const i81 = new Int8Array(5);
const i82 = new Int8Array(5);
deepEquals(loopFill(i81, 2), i82.fill(2));
deepEquals(loopFill(i81, -1), i82.fill(-1));
const i161 = new Int16Array(5);
const i162 = new Int16Array(5);
deepEquals(loopFill(i161, 2), i162.fill(2));
deepEquals(loopFill(i161, 65535), i162.fill(65535));
deepEquals(loopFill(i161, -1), i162.fill(-1));
const i321 = new Int32Array(5);
const i322 = new Int32Array(5);
deepEquals(loopFill(i321, 2), i322.fill(2));
deepEquals(loopFill(i321, 65537), i322.fill(65537));
deepEquals(loopFill(i321, -1), i322.fill(-1));
const f321 = new Float32Array(5);
const f322 = new Float32Array(5);
deepEquals(loopFill(f321, 1.2345), f322.fill(1.2345));
const f641 = new Float64Array(5);
const f642 = new Float64Array(5);
deepEquals(loopFill(f641, 1.2345), f642.fill(1.2345));
const u8Clamped1 = new Uint8ClampedArray(5);
const u8Clamped2 = new Uint8ClampedArray(5);
deepEquals(loopFill(u8Clamped1, 2), u8Clamped2.fill(2));
deepEquals(loopFill(u8Clamped1, 257), u8Clamped2.fill(257));
});
it('start offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u82 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fill(u81, 2, i), u83.fill(2, i));
deepEquals(fill(u81, -1, i), u83.fill(-1, i));
deepEquals(loopFill(u82, 2, i), u83.fill(2, i));
deepEquals(loopFill(u82, -1, i), u83.fill(-1, i));
}
});
it('end offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u82 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fill(u81, 2, 0, i), u83.fill(2, 0, i));
deepEquals(fill(u81, -1, 0, i), u83.fill(-1, 0, i));
deepEquals(loopFill(u82, 2, 0, i), u83.fill(2, 0, i));
deepEquals(loopFill(u82, -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 u82 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fill(u81, 2, i, j), u83.fill(2, i, j));
deepEquals(fill(u81, -1, i, j), u83.fill(-1, i, j));
deepEquals(loopFill(u82, 2, i, j), u83.fill(2, i, j));
deepEquals(loopFill(u82, -1, i, j), u83.fill(-1, i, j));
}
}
});
});
});
+40
View File
@@ -0,0 +1,40 @@
/**
* 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: TypedArray, value: number, start: number = 0, end?: number | undefined): TypedArray {
// all modern engines that support .fill
if (array.fill) {
return array.fill(value, start, end);
}
// 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 === undefined) {
end = 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;
}
+6 -1
View File
@@ -129,12 +129,17 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
return true;
}
// Exit early for uncachable glyphs
if (!this._canCache(glyph)) {
return false;
}
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue !== null && cacheValue !== undefined) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
} else if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
let index;
if (this._cacheMap.size < this._cacheMap.capacity) {
index = this._cacheMap.size;
+25 -18
View File
@@ -95,22 +95,25 @@ export class DomRenderer extends EventEmitter implements IRenderer {
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth;
this.dimensions.scaledCellHeight = this.dimensions.scaledCharHeight;
this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols;
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows;
this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols;
this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows;
this.dimensions.actualCellWidth = this._terminal.charMeasure.width;
this.dimensions.actualCellHeight = this._terminal.charMeasure.height;
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this._rowElements.forEach(element => {
element.style.width = `${this.dimensions.canvasWidth}px`;
element.style.height = `${this._terminal.charMeasure.height}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,14 +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 = (<any>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.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public setTheme(theme: ITheme | undefined): IColorSet {
@@ -294,10 +297,10 @@ export class DomRenderer extends EventEmitter implements IRenderer {
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = document.createElement('div');
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
element.style.top = `${row * this._terminal.charMeasure.height}px`;
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
element.style.top = `${row * this.dimensions.actualCellHeight}px`;
element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
return element;
}
@@ -333,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});
@@ -356,7 +359,11 @@ export class DomRenderer extends EventEmitter implements IRenderer {
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
while (x !== x2 || y !== y2) {
const span = <HTMLElement>this._rowElements[y].children[x];
const row = this._rowElements[y];
if (!row) {
return;
}
const span = <HTMLElement>row.children[x];
span.style.textDecoration = enabled ? 'underline' : 'none';
x = (x + 1) % cols;
if (x === 0) {
+11 -21
View File
@@ -24,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),
'<span> </span>' +
'<span> </span>'
''
);
});
@@ -46,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),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>` +
'<span> </span>'
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
);
}
});
@@ -66,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),
'<span class="xterm-bold">a</span>' +
'<span> </span>'
'<span class="xterm-bold">a</span>'
);
});
@@ -75,8 +72,7 @@ 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),
'<span class="xterm-italic">a</span>' +
'<span> </span>'
'<span class="xterm-italic">a</span>'
);
});
@@ -86,8 +82,7 @@ describe('DomRendererRowFactory', () => {
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),
`<span class="xterm-fg-${i}">a</span>` +
'<span> </span>'
`<span class="xterm-fg-${i}">a</span>`
);
}
});
@@ -98,8 +93,7 @@ describe('DomRendererRowFactory', () => {
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),
`<span class="xterm-bg-${i}">a</span>` +
'<span> </span>'
`<span class="xterm-bg-${i}">a</span>`
);
}
});
@@ -108,8 +102,7 @@ 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),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
'<span> </span>'
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
);
});
@@ -117,8 +110,7 @@ describe('DomRendererRowFactory', () => {
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),
'<span class="xterm-fg-1 xterm-bg-257">a</span>' +
'<span> </span>'
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
);
});
@@ -126,8 +118,7 @@ describe('DomRendererRowFactory', () => {
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),
'<span class="xterm-fg-257 xterm-bg-1">a</span>' +
'<span> </span>'
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
);
});
@@ -136,8 +127,7 @@ describe('DomRendererRowFactory', () => {
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),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
'<span> </span>'
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
}
});
+20 -12
View File
@@ -3,7 +3,7 @@
* @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';
@@ -23,18 +23,27 @@ 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 char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
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 = charData[CHAR_DATA_CHAR_INDEX];
const attr = charData[CHAR_DATA_ATTR_INDEX];
const width = charData[CHAR_DATA_WIDTH_INDEX];
// The character to the left is a wide character, drawing is owned by the char at x-1
if (width === 0) {
@@ -100,7 +109,6 @@ export class DomRendererRowFactory {
charElement.classList.add(`xterm-bg-${bg}`);
}
fragment.appendChild(charElement);
colCount += width;
}
return fragment;
}
+4
View File
@@ -9,6 +9,10 @@ import { IRenderer } from '../renderer/Types';
export class MouseHelper {
constructor(private _renderer: IRenderer) {}
public setRenderer(renderer: IRenderer): void {
this._renderer = renderer;
}
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX === null || event.pageX === undefined) {
+1 -1
View File
@@ -121,7 +121,7 @@ export class MockTerminal implements ITerminal {
handler(data: string): void {
throw new Error('Method not implemented.');
}
on(event: string, callback: () => void): void {
on(event: string, callback: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: XtermListener): void {