From 06e89549a88f89fd95978346345f554398d05a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 23 Nov 2018 23:30:37 +0100 Subject: [PATCH 001/140] typed array support for parser --- src/EscapeSequenceParser.test.ts | 294 +++++++++++++++++-------------- src/EscapeSequenceParser.ts | 24 +-- src/InputHandler.test.ts | 4 +- src/InputHandler.ts | 92 ++++++---- src/Types.ts | 8 +- 5 files changed, 236 insertions(+), 186 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index e92f4125..3e310b08 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -50,8 +50,12 @@ const testTerminal: any = { compare: function (value: any): void { chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here }, - print: function (data: string, start: number, end: number): void { - this.calls.push(['print', data.substring(start, end)]); + print: function (data: Uint16Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += String.fromCharCode(data[i]); + } + this.calls.push(['print', s]); }, actionOSC: function (s: string): void { this.calls.push(['osc', s]); @@ -68,8 +72,12 @@ const testTerminal: any = { actionDCSHook: function (collect: string, params: number[], flag: string): void { this.calls.push(['dcs hook', collect, params, flag]); }, - actionDCSPrint: function (data: string, start: number, end: number): void { - this.calls.push(['dcs put', data.substring(start, end)]); + actionDCSPrint: function (data: Uint16Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += String.fromCharCode(data[i]); + } + this.calls.push(['dcs put', s]); }, actionDCSUnhook: function (): void { this.calls.push(['dcs unhook']); @@ -81,7 +89,7 @@ class DcsTest implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); } - put(data: string, start: number, end: number): void { + put(data: Uint16Array, start: number, end: number): void { testTerminal.actionDCSPrint(data, start, end); } unhook(): void { @@ -155,6 +163,14 @@ interface IRun { parser: TestEscapeSequenceParser; } +// translate string based parse calls into typed array based +function parse(parser: TestEscapeSequenceParser, data: string): void { + const container = new Uint16Array(data.length); + for (let i = 0; i < data.length; ++i) { + container[i] = data.charCodeAt(i); + } + parser.parse(container, data.length); +} describe('EscapeSequenceParser', function (): void { let parser: TestEscapeSequenceParser | null = null; @@ -205,12 +221,12 @@ describe('EscapeSequenceParser', function (): void { it('state GROUND execute action', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.GROUND; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -223,7 +239,7 @@ describe('EscapeSequenceParser', function (): void { const printables = r(0x20, 0x7f); // NOTE: DEL excluded for (let i = 0; i < printables.length; ++i) { parser.currentState = ParserState.GROUND; - parser.parse(printables[i]); + parse(parser, printables[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['print', printables[i]]]); parser.reset(); @@ -245,13 +261,13 @@ describe('EscapeSequenceParser', function (): void { for (state in states) { for (let i = 0; i < exes.length; ++i) { parser.currentState = state; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } - parser.parse('\x9c'); + parse(parser, '\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -265,7 +281,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [23]; parser.collect = '#'; - parser.parse('\x1b'); + parse(parser, '\x1b'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -276,12 +292,12 @@ describe('EscapeSequenceParser', function (): void { it('state ESCAPE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -292,7 +308,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); testTerminal.compare([]); parser.reset(); @@ -301,13 +317,13 @@ describe('EscapeSequenceParser', function (): void { it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { parser.reset(); testTerminal.clear(); - const dispatches = r(0x30, 0x50); - dispatches.concat(r(0x51, 0x58)); - dispatches.concat(['\x59', '\x5a', '\x5c']); - dispatches.concat(r(0x60, 0x7f)); + let dispatches = r(0x30, 0x50); + dispatches = dispatches.concat(r(0x51, 0x58)); + dispatches = dispatches.concat(['\x59', '\x5a']); // excluded \x5c + dispatches = dispatches.concat(r(0x60, 0x7f)); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['esc', '', dispatches[i]]]); parser.reset(); @@ -319,7 +335,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -328,12 +344,12 @@ describe('EscapeSequenceParser', function (): void { it('state ESCAPE_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -344,7 +360,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([]); parser.reset(); @@ -355,7 +371,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -367,7 +383,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x30, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); @@ -382,7 +398,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [123]; parser.collect = '#'; - parser.parse('['); + parse(parser, '['); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -394,7 +410,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [123]; parser.collect = '#'; - parser.parse('\x9b'); + parse(parser, '\x9b'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -405,12 +421,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_ENTRY execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -421,7 +437,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); testTerminal.compare([]); parser.reset(); @@ -432,7 +448,7 @@ describe('EscapeSequenceParser', function (): void { const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0], dispatches[i]]]); parser.reset(); @@ -445,19 +461,19 @@ describe('EscapeSequenceParser', function (): void { const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -466,12 +482,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_PARAM execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -483,13 +499,13 @@ describe('EscapeSequenceParser', function (): void { const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -498,7 +514,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); testTerminal.compare([]); parser.reset(); @@ -510,7 +526,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); @@ -522,7 +538,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -533,7 +549,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -542,12 +558,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -559,7 +575,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -569,7 +585,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); testTerminal.compare([]); parser.reset(); @@ -581,7 +597,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); @@ -591,7 +607,7 @@ describe('EscapeSequenceParser', function (): void { it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { parser.reset(); parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x3a'); + parse(parser, '\x3a'); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); parser.reset(); }); @@ -600,7 +616,7 @@ describe('EscapeSequenceParser', function (): void { const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x3b' + chars[i]); + parse(parser, '\x3b' + chars[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -611,7 +627,7 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(chars[i]); + parse(parser, chars[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); chai.expect(parser.params).eql([0]); parser.reset(); @@ -620,12 +636,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_IGNORE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -635,11 +651,11 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_IGNORE ignore', function (): void { parser.reset(); testTerminal.clear(); - const ignored = r(0x20, 0x40); - ignored.concat(['\x7f']); + let ignored = r(0x20, 0x40); + ignored = ignored.concat(['\x7f']); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); @@ -652,7 +668,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -664,7 +680,7 @@ describe('EscapeSequenceParser', function (): void { // C0 let initializers = ['\x58', '\x5e', '\x5f']; for (let i = 0; i < initializers.length; ++i) { - parser.parse('\x1b' + initializers[i]); + parse(parser, '\x1b' + initializers[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -673,7 +689,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = state; initializers = ['\x98', '\x9e', '\x9f']; for (let i = 0; i < initializers.length; ++i) { - parser.parse(initializers[i]); + parse(parser, initializers[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -681,13 +697,13 @@ describe('EscapeSequenceParser', function (): void { }); it('state SOS_PM_APC_STRING ignore rules', function (): void { parser.reset(); - const ignored = r(0x00, 0x18); - ignored.concat(['\x19']); - ignored.concat(r(0x1c, 0x20)); - ignored.concat(r(0x20, 0x80)); + let ignored = r(0x00, 0x18); + ignored = ignored.concat(['\x19']); + ignored = ignored.concat(r(0x1c, 0x20)); + ignored = ignored.concat(r(0x20, 0x80)); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.SOS_PM_APC_STRING; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -695,13 +711,13 @@ describe('EscapeSequenceParser', function (): void { it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { parser.reset(); // C0 - parser.parse('\x1b]'); + parse(parser, '\x1b]'); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.parse('\x9d'); + parse(parser, '\x9d'); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); parser.reset(); } @@ -714,7 +730,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.OSC_STRING; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); chai.expect(parser.osc).equal(''); parser.reset(); @@ -725,7 +741,7 @@ describe('EscapeSequenceParser', function (): void { const puts = r(0x20, 0x80); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.OSC_STRING; - parser.parse(puts[i]); + parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); chai.expect(parser.osc).equal(puts[i]); parser.reset(); @@ -734,13 +750,13 @@ describe('EscapeSequenceParser', function (): void { it('state DCS_ENTRY', function (): void { parser.reset(); // C0 - parser.parse('\x1bP'); + parse(parser, '\x1bP'); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.parse('\x90'); + parse(parser, '\x90'); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); } @@ -753,7 +769,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); } @@ -764,19 +780,19 @@ describe('EscapeSequenceParser', function (): void { const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.DCS_ENTRY; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -790,7 +806,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); parser.reset(); } @@ -800,13 +816,13 @@ describe('EscapeSequenceParser', function (): void { const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(params[i]); + parse(parser, params[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.DCS_PARAM; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -814,7 +830,7 @@ describe('EscapeSequenceParser', function (): void { it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { parser.reset(); parser.currentState = ParserState.DCS_ENTRY; - parser.parse('\x3a'); + parse(parser, '\x3a'); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); }); @@ -823,7 +839,7 @@ describe('EscapeSequenceParser', function (): void { const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse('\x3b' + chars[i]); + parse(parser, '\x3b' + chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -834,21 +850,21 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(chars[i]); + parse(parser, chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); } }); it('state DCS_IGNORE ignore rules', function (): void { parser.reset(); - const ignored = [ + let ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - ignored.concat(r(0x20, 0x80)); + ignored = ignored.concat(r(0x20, 0x80)); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_IGNORE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); } @@ -858,7 +874,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -869,7 +885,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -883,7 +899,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); parser.reset(); } @@ -893,7 +909,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -904,7 +920,7 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse('\x20' + chars[i]); + parse(parser, '\x20' + chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); chai.expect(parser.collect).equal('\x20'); parser.reset(); @@ -916,7 +932,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -929,7 +945,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -942,7 +958,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -952,14 +968,14 @@ describe('EscapeSequenceParser', function (): void { it('state DCS_PASSTHROUGH put action', function (): void { parser.reset(); testTerminal.clear(); - const puts = r(0x00, 0x18); - puts.concat(['\x19']); - puts.concat(r(0x1c, 0x20)); - puts.concat(r(0x20, 0x7f)); + let puts = r(0x00, 0x18); + puts = puts.concat(['\x19']); + puts = puts.concat(r(0x1c, 0x20)); + puts = puts.concat(r(0x20, 0x7f)); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.DCS_PASSTHROUGH; parser.mockActiveDcsHandler(); - parser.parse(puts[i]); + parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); parser.reset(); @@ -970,7 +986,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([]); parser.reset(); @@ -989,7 +1005,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); } - parser.parse(s); + parse(parser, s); testTerminal.compare(value); }; }); @@ -1067,7 +1083,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_IGNORE; - parser.parse('€öäü'); + parse(parser, '€öäü'); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); @@ -1077,7 +1093,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_IGNORE; - parser.parse('€öäü'); + parse(parser, '€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); testTerminal.compare([]); parser.reset(); @@ -1087,7 +1103,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.parse('\x901;2;3+$a€öäü'); + parse(parser, '\x901;2;3+$a€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); parser.reset(); @@ -1097,7 +1113,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.GROUND; - parser.parse('\x9c'); + parse(parser, '\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -1127,15 +1143,17 @@ describe('EscapeSequenceParser', function (): void { clearAccu(); }); it('print handler', function (): void { - parser2.setPrintHandler(function (data: string, start: number, end: number): void { - print += data.substring(start, end); + parser2.setPrintHandler(function (data: Uint16Array, start: number, end: number): void { + for (let i = start; i < end; ++i) { + print += String.fromCharCode(data[i]); + } }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(print).equal('hello world!$>'); parser2.clearPrintHandler(); parser2.clearPrintHandler(); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(print).equal(''); }); it('ESC handler', function (): void { @@ -1145,28 +1163,28 @@ describe('EscapeSequenceParser', function (): void { parser2.setEscHandler('E', function (): void { esc.push('E'); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql(['%G', 'E']); parser2.clearEscHandler('%G'); parser2.clearEscHandler('%G'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql(['E']); parser2.clearEscHandler('E'); clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql([]); }); it('CSI handler', function (): void { parser2.setCsiHandler('m', function (params: number[], collect: string): void { csi.push(['m', params, collect]); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); parser2.clearCsiHandler('m'); parser2.clearCsiHandler('m'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([]); }); it('EXECUTE handler', function (): void { @@ -1176,24 +1194,24 @@ describe('EscapeSequenceParser', function (): void { parser2.setExecuteHandler('\r', function (): void { exe.push('\r'); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(exe).eql(['\r', '\n']); parser2.clearExecuteHandler('\r'); parser2.clearExecuteHandler('\r'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { parser2.setOscHandler(1, function (data: string): void { osc.push([1, data]); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); parser2.clearOscHandler(1); parser2.clearOscHandler(1); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([]); }); it('DCS handler', function (): void { @@ -1201,15 +1219,19 @@ describe('EscapeSequenceParser', function (): void { hook: function (collect: string, params: number[], flag: number): void { dcs.push(['hook', collect, params, flag]); }, - put: function (data: string, start: number, end: number): void { - dcs.push(['put', data.substring(start, end)]); + put: function (data: Uint16Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += String.fromCharCode(data[i]); + } + dcs.push(['put', s]); }, unhook: function (): void { dcs.push(['unhook']); } }); - parser2.parse('\x1bP1;2;3+pabc'); - parser2.parse(';de\x9c'); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); chai.expect(dcs).eql([ ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], ['put', 'abc'], ['put', ';de'], @@ -1218,8 +1240,8 @@ describe('EscapeSequenceParser', function (): void { parser2.clearDcsHandler('+p'); parser2.clearDcsHandler('+p'); // should not throw clearAccu(); - parser2.parse('\x1bP1;2;3+pabc'); - parser2.parse(';de\x9c'); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); chai.expect(dcs).eql([]); }); it('ERROR handler', function (): void { @@ -1228,7 +1250,7 @@ describe('EscapeSequenceParser', function (): void { errorState = state; return state; }); - parser2.parse('\x1b[1;2;€;3m'); // faulty escape sequence + parse(parser2, '\x1b[1;2;€;3m'); // faulty escape sequence chai.expect(errorState).eql({ position: 6, code: '€'.charCodeAt(0), @@ -1243,7 +1265,7 @@ describe('EscapeSequenceParser', function (): void { parser2.clearErrorHandler(); parser2.clearErrorHandler(); // should not throw errorState = null; - parser2.parse('\x1b[1;2;a;3m'); + parse(parser2, '\x1b[1;2;a;3m'); chai.expect(errorState).eql(null); }); }); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index b38c50f5..9d0da3ef 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -125,6 +125,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // csi entries table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); @@ -192,7 +193,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { */ class DcsDummy implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { } - put(data: string, start: number, end: number): void { } + put(data: Uint16Array, start: number, end: number): void { } unhook(): void { } } @@ -218,7 +219,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: string; // handler lookup containers - protected _printHandler: (data: string, start: number, end: number) => void; + protected _printHandler: (data: Uint16Array, start: number, end: number) => void; protected _executeHandlers: any; protected _csiHandlers: any; protected _escHandlers: any; @@ -228,7 +229,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: (data: string, start: number, end: number) => void; + protected _printHandlerFb: (data: Uint16Array, start: number, end: number) => void; protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; @@ -284,7 +285,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = null; } - setPrintHandler(callback: (data: string, start: number, end: number) => void): void { + setPrintHandler(callback: (data: Uint16Array, start: number, end: number) => void): void { this._printHandler = callback; } clearPrintHandler(): void { @@ -356,7 +357,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._activeDcsHandler = null; } - parse(data: string): void { + parse(data: Uint16Array, length: number): void { let code = 0; let transition = 0; let error = false; @@ -371,15 +372,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let callback: Function | null = null; // process input string - const l = data.length; - for (let i = 0; i < l; ++i) { - code = data.charCodeAt(i); + for (let i = 0; i < length; ++i) { + code = data[i]; // shortcut for most chars (print action) if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) { print = (~print) ? print : i; do i++; - while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80); + while (i < length && data[i] > 0x1f && data[i] < 0x80); i--; continue; } @@ -517,7 +517,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; break; case ParserAction.OSC_PUT: - osc += data.charAt(i); + osc += String.fromCharCode(code); break; case ParserAction.OSC_END: if (osc && code !== 0x18 && code !== 0x1a) { @@ -549,9 +549,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // push leftover pushable buffers to terminal if (currentState === ParserState.GROUND && ~print) { - this._printHandler(data, print, data.length); + this._printHandler(data, print, length); } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { - dcsHandler.put(data, dcs, data.length); + dcsHandler.put(data, dcs, length); } // save non pushable buffers diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index b2fea06a..c92d317d 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -503,7 +503,9 @@ describe('InputHandler', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); const inputHandler = new InputHandler(term); - inputHandler.print(String.fromCharCode(0x200B), 0, 1); + const container = new Uint16Array(10); + container[0] = 0x200B; + inputHandler.print(container, 0, 1); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f..9f548606 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -29,18 +29,26 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * Request Terminfo String * not supported */ -class RequestTerminfo implements IDcsHandler { - private _data: string; + class RequestTerminfo implements IDcsHandler { + private _data: Uint16Array = new Uint16Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { - this._data = ''; } - put(data: string, start: number, end: number): void { - this._data += data.substring(start, end); + put(data: Uint16Array, start: number, end: number): void { + const tmp = new Uint16Array(this._data.length + end - start); + tmp.set(this._data); + tmp.set(data.subarray(start, end), this._data.length); + this._data = data; } unhook(): void { + let data = ''; + for (let i = 0; i < this._data.length; ++i) { + data += String.fromCharCode(this._data[i]); + } + this._data = new Uint16Array(0); // dont hold memory longer than needed // invalid: DCS 0 + r Pt ST - this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`); + this._terminal.handler(`${C0.ESC}P0+r${data}${C0.ESC}\\`); + this._data = new Uint16Array(0); } } @@ -51,21 +59,27 @@ class RequestTerminfo implements IDcsHandler { * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) */ class DECRQSS implements IDcsHandler { - private _data: string; + private _data: Uint16Array = new Uint16Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { - // reset data - this._data = ''; } - put(data: string, start: number, end: number): void { - this._data += data.substring(start, end); + put(data: Uint16Array, start: number, end: number): void { + const tmp = new Uint16Array(this._data.length + end - start); + tmp.set(this._data); + tmp.set(data.subarray(start, end), this._data.length); + this._data = data; } unhook(): void { - switch (this._data) { + let data = ''; + for (let i = 0; i < this._data.length; ++i) { + data += String.fromCharCode(this._data[i]); + } + this._data = new Uint16Array(0); // dont hold memory longer than needed + switch (data) { // valid: DCS 1 $ r Pt ST (xterm) case '"q': // DECSCA return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`); @@ -85,8 +99,8 @@ class DECRQSS implements IDcsHandler { return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); default: // invalid: DCS 0 $ r Pt ST (xterm) - this._terminal.error('Unknown DCS $q %s', this._data); - this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`); + this._terminal.error('Unknown DCS $q %s', data); + this._terminal.handler(`${C0.ESC}P0$r${data}${C0.ESC}\\`); } } } @@ -97,11 +111,11 @@ class DECRQSS implements IDcsHandler { * not supported */ - /** - * DCS + p Pt ST (xterm) - * Set Terminfo Data - * not supported - */ +/** + * DCS + p Pt ST (xterm) + * Set Terminfo Data + * not supported + */ @@ -114,6 +128,7 @@ class DECRQSS implements IDcsHandler { */ export class InputHandler extends Disposable implements IInputHandler { private _surrogateFirst: string; + private _parseBuffer: Uint16Array = new Uint16Array(4096); constructor( protected _terminal: IInputHandlingTerminal, @@ -316,7 +331,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._surrogateFirst = ''; } - this._parser.parse(data); + if (this._parseBuffer.length < data.length) { + this._parseBuffer = new Uint16Array(data.length); + } + for (let i = 0; i < data.length; ++i) { + this._parseBuffer[i] = data.charCodeAt(i); + } + this._parser.parse(this._parseBuffer, data.length); buffer = this._terminal.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { @@ -324,9 +345,9 @@ export class InputHandler extends Disposable implements IInputHandler { } } - public print(data: string, start: number, end: number): void { - let char: string; + public print(data: Uint16Array, start: number, end: number): void { let code: number; + let char: string; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -338,30 +359,30 @@ export class InputHandler extends Disposable implements IInputHandler { let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._terminal.updateRange(buffer.y); - for (let stringPosition = start; stringPosition < end; ++stringPosition) { - char = data.charAt(stringPosition); - code = data.charCodeAt(stringPosition); + for (let pos = start; pos < end; ++pos) { + code = data[pos]; + char = String.fromCharCode(code); // surrogate pair handling if (0xD800 <= code && code <= 0xDBFF) { - if (++stringPosition >= end) { + if (++pos >= 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; + this._surrogateFirst = String.fromCharCode(code); continue; } - const second = data.charCodeAt(stringPosition); + const second = data[pos]; // 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); + char += String.fromCharCode(second); } else { - stringPosition--; + pos--; } } @@ -370,9 +391,14 @@ export class InputHandler extends Disposable implements IInputHandler { chWidth = wcwidth(code); // get charset replacement character - if (charset) { - char = charset[char] || char; - code = char.charCodeAt(0); + // charset are only defined for ASCII, therefore we only + // search for an replacement char if code < 127 + if (code < 127 && charset) { + const ch = charset[char]; + if (ch) { + code = ch.charCodeAt(0); + char = ch; + } } if (screenReaderMode) { diff --git a/src/Types.ts b/src/Types.ts index 0f60e6f7..b1c125dc 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -111,7 +111,7 @@ export interface ICompositionHelper { */ export interface IInputHandler { parse(data: string): void; - print(data: string, start: number, end: number): void; + print(data: Uint16Array, start: number, end: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -463,7 +463,7 @@ export interface IParsingState { */ export interface IDcsHandler { hook(collect: string, params: number[], flag: number): void; - put(data: string, start: number, end: number): void; + put(data: Uint16Array, start: number, end: number): void; unhook(): void; } @@ -480,9 +480,9 @@ export interface IEscapeSequenceParser extends IDisposable { * Parse string `data`. * @param data The data to parse. */ - parse(data: string): void; + parse(data: Uint16Array, length: number): void; - setPrintHandler(callback: (data: string, start: number, end: number) => void): void; + setPrintHandler(callback: (data: Uint16Array, start: number, end: number) => void): void; clearPrintHandler(): void; setExecuteHandler(flag: string, callback: () => void): void; From 591dfd506d0e18120bdaa2b895adb507ce11ee63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 29 Nov 2018 16:33:56 +0100 Subject: [PATCH 002/140] fix DCS handler --- src/InputHandler.ts | 27 +++++++----------------- src/common/TypedArrayUtils.test.ts | 32 +++++++++++++++++++++------- src/common/TypedArrayUtils.ts | 34 +++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 9f548606..3190dc60 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -13,6 +13,7 @@ import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { Disposable } from './common/Lifecycle'; +import { concat, utf16ToString } from './common/TypedArrayUtils'; /** * Map collect to glevel. Used in `selectCharset`. @@ -33,22 +34,16 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, private _data: Uint16Array = new Uint16Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { + this._data = new Uint16Array(0); } put(data: Uint16Array, start: number, end: number): void { - const tmp = new Uint16Array(this._data.length + end - start); - tmp.set(this._data); - tmp.set(data.subarray(start, end), this._data.length); - this._data = data; + this._data = concat(this._data, data.subarray(start, end)); } unhook(): void { - let data = ''; - for (let i = 0; i < this._data.length; ++i) { - data += String.fromCharCode(this._data[i]); - } - this._data = new Uint16Array(0); // dont hold memory longer than needed + const data = utf16ToString(this._data); + this._data = new Uint16Array(0); // invalid: DCS 0 + r Pt ST this._terminal.handler(`${C0.ESC}P0+r${data}${C0.ESC}\\`); - this._data = new Uint16Array(0); } } @@ -67,18 +62,12 @@ class DECRQSS implements IDcsHandler { } put(data: Uint16Array, start: number, end: number): void { - const tmp = new Uint16Array(this._data.length + end - start); - tmp.set(this._data); - tmp.set(data.subarray(start, end), this._data.length); - this._data = data; + this._data = concat(this._data, data.subarray(start, end)); } unhook(): void { - let data = ''; - for (let i = 0; i < this._data.length; ++i) { - data += String.fromCharCode(this._data[i]); - } - this._data = new Uint16Array(0); // dont hold memory longer than needed + const data = utf16ToString(this._data); + this._data = new Uint16Array(0); switch (data) { // valid: DCS 1 $ r Pt ST (xterm) case '"q': // DECSCA diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts index 69a62abc..b25b9b9a 100644 --- a/src/common/TypedArrayUtils.test.ts +++ b/src/common/TypedArrayUtils.test.ts @@ -3,21 +3,20 @@ * @license MIT */ import { assert } from 'chai'; -import { fillFallback } from './TypedArrayUtils'; +import { fillFallback, concat, utf16ToString } 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]); - } +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('polyfill conformance tests', function(): void { describe('TypedArray.fill', function(): void { it('should work with all typed array types', function(): void { const u81 = new Uint8Array(5); @@ -87,3 +86,20 @@ describe('polyfill conformance tests', function(): void { }); }); }); + +describe('typed array convenience functions', () => { + it('concat', () => { + const a = new Uint8Array([1, 2, 3, 4, 5]); + const b = new Uint8Array([6, 7, 8, 9, 0]); + const merged = concat(a, b); + deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); + }); + it('utf16ToString', () => { + const s = 'abcdefg'; + const data = new Uint16Array(s.length); + for (let i = 0; i < s.length; ++i) { + data[i] = s.charCodeAt(i); + } + assert.equal(utf16ToString(data), s); + }); +}); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 6e1a3630..8b540548 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -3,15 +3,14 @@ * @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; +/** + * polyfill for TypedArray.fill + * This is needed to support .fill in all safari versions and IE 11. + */ export function fill(array: T, value: number, start?: number, end?: number): T { // all modern engines that support .fill if (array.fill) { @@ -39,3 +38,28 @@ export function fillFallback(array: T, value: number, star } return array; } + +/** + * Concat two typed arrays `a` and `b`. + * Returns a new typed array. + */ +export function concat(a: T, b: T): T { + const result = new (a.constructor as any)(a.length + b.length); + result.set(a); + result.set(b, a.length); + return result; +} + +/** + * Convert UTF16 char codes into JS string. + * Note the typed array is not limited to Uint16Array, make sure to align + * the values to 0-65535 integer for other typed array types, otherwise + * the conversion will fail. + */ +export function utf16ToString(data: T): string { + let s = ''; + for (let i = 0; i < data.length; ++i) { + s += String.fromCharCode(data[i]); + } + return s; +} From a6ef64dcb6c7a28e360f36104d40e54a5aa2f36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 29 Nov 2018 16:39:17 +0100 Subject: [PATCH 003/140] always clear DCS buffer on HOOK --- src/InputHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3190dc60..7438cf1e 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -59,6 +59,7 @@ class DECRQSS implements IDcsHandler { constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { + this._data = new Uint16Array(0); } put(data: Uint16Array, start: number, end: number): void { From 2483a98f63eac5ced2bd42d4e93aaf01b669da1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 29 Nov 2018 21:13:18 +0100 Subject: [PATCH 004/140] use faster string conversion --- src/common/TypedArrayUtils.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 8b540548..5d3bcb8a 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -57,9 +57,5 @@ export function concat(a: T, b: T): T { * the conversion will fail. */ export function utf16ToString(data: T): string { - let s = ''; - for (let i = 0; i < data.length; ++i) { - s += String.fromCharCode(data[i]); - } - return s; + return String.fromCharCode.apply(null, data); } From fd34caee2322af515fc1e94785d3a41dbc7e85da Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Fri, 7 Dec 2018 22:50:59 -0500 Subject: [PATCH 005/140] Use a time-based limit to Terminal._innerWrite The idea is that it should run for a bit and then let the renderer draw a frame so that the terminal look responsive. The existing approach limits the work done using a fixed number elements from the write buffer so the duration of a frame can vary widely. This approach looks at the clock to determine when to stop, we basically allocate an amount of time each frame to write, while the rest can be used for rendering. From my tests this change makes the terminal feel a lot smoother. --- src/Terminal.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 2cfc1ca8..a641a86b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -64,10 +64,12 @@ const document = (typeof window !== 'undefined') ? window.document : null; const WRITE_BUFFER_PAUSE_THRESHOLD = 5; /** - * The number of writes to perform in a single batch before allowing the - * renderer to catch up with a 0ms setTimeout. + * The max number of ms to spend on writes before allowing the renderer to + * catch up with a 0ms setTimeout. A value of < 33 to keep us close to + * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS + * depends on the time it takes for the renderer to draw the frame. */ -const WRITE_BATCH_SIZE = 300; +const WRITE_TIMEOUT_MS = 12; /** * The set of options that only have an effect when set in the Terminal constructor. @@ -1358,13 +1360,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.writeBuffer = []; } - const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - const data = writeBatch.shift(); + const time = Date.now(); + while (this.writeBuffer.length > 0) { + const data = this.writeBuffer.shift(); // If XOFF was sent in order to catch up with the pty process, resume it if // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + if (this._xoffSentToCatchUp && this.writeBuffer.length === 0 && this.writeBuffer.length === 0) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1382,6 +1384,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); + + if (Date.now() - time >= WRITE_TIMEOUT_MS) { + break; + } } if (this.writeBuffer.length > 0) { // Allow renderer to catch up before processing the next batch From 3d2ae2b01edd34e9475c7d58884a501c2426237f Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Sun, 9 Dec 2018 17:58:44 -0500 Subject: [PATCH 006/140] Removing redundant condition. Clearer variable name --- src/Terminal.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index a641a86b..2c7f648b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1360,13 +1360,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.writeBuffer = []; } - const time = Date.now(); + const startTime = Date.now(); while (this.writeBuffer.length > 0) { const data = this.writeBuffer.shift(); // If XOFF was sent in order to catch up with the pty process, resume it if // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && this.writeBuffer.length === 0 && this.writeBuffer.length === 0) { + if (this._xoffSentToCatchUp && this.writeBuffer.length === 0) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1385,7 +1385,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); - if (Date.now() - time >= WRITE_TIMEOUT_MS) { + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { break; } } From 8a50729a98b3c9ea4daf249e35d96ab709e6b859 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 21:58:54 -0800 Subject: [PATCH 007/140] Reflow wider --- src/Buffer.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++++ src/BufferLine.ts | 10 ++++++ 2 files changed, 87 insertions(+) diff --git a/src/Buffer.ts b/src/Buffer.ts index 74750a8b..6b247afe 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -233,6 +233,83 @@ export class Buffer implements IBuffer { } this.scrollBottom = newRows - 1; + + if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { + this._reflow(newCols, newRows); + } + } + + private _reflow(newCols: number, newRows: number): void { + if (this._terminal.cols === newCols) { + return; + } + + // Iterate through rows, ignore the last one as it cannot be wrapped + for (let y = 0; y < this.lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = this.lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; + while (nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++i) as BufferLine; + } + + if (newCols > this._terminal.cols) { + let destLineIndex = 0; + let destCol = this._terminal.cols; + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcRemainingCells = this._terminal.cols - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy); + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === this._terminal.cols) { + srcLineIndex++; + srcCol = 0; + } + } + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + // Remove rows and adjust cursor + if (countToRemove > 0) { + this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); + while (countToRemove-- > 0) { + if (this.ybase === 0) { + this.y--; + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } + } else { + + } + } } /** diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 3f93af62..aafc9051 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -304,6 +304,16 @@ export class BufferLine implements IBufferLine { return 0; } + public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number): void { + console.log(' copyCellsFrom', srcCol, destCol, length); + const srcData = src._data; + for (let cell = 0; cell < length; cell++) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + } + } + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { endCol = Math.min(endCol, this.getTrimmedLength()); From 314f98f2a63cd100f1084df46aab0bdbe5c82624 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 23:35:52 -0800 Subject: [PATCH 008/140] Mostly working for reflowing to smaller --- src/Buffer.ts | 231 +++++++++++++++++++++++++++++++++------------- src/BufferLine.ts | 17 +++- 2 files changed, 179 insertions(+), 69 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 6b247afe..fc8e008e 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -235,83 +235,187 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { - this._reflow(newCols, newRows); + this._reflow(newCols); } } - private _reflow(newCols: number, newRows: number): void { + private _reflow(newCols: number): void { if (this._terminal.cols === newCols) { return; } // Iterate through rows, ignore the last one as it cannot be wrapped for (let y = 0; y < this.lines.length - 1; y++) { - // Check if this row is wrapped - let i = y; - let nextLine = this.lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - continue; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++i) as BufferLine; - } - if (newCols > this._terminal.cols) { - let destLineIndex = 0; - let destCol = this._terminal.cols; - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._terminal.cols - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy); - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; - } - srcCol += cellsToCopy; - if (srcCol === this._terminal.cols) { - srcLineIndex++; - srcCol = 0; - } - } - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - // Remove rows and adjust cursor - if (countToRemove > 0) { - this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - while (countToRemove-- > 0) { - if (this.ybase === 0) { - this.y--; - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; - } - } - } + y += this._reflowLarger(y, newCols); } else { - + y += this._reflowSmaller(y, newCols); } } } + private _reflowLarger(y: number, newCols: number): number { + // Check if this row is wrapped + let i = y; + let nextLine = this.lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + return 0; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; + while (nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++i) as BufferLine; + } + + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = this._terminal.cols; + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcRemainingCells = this._terminal.cols - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === this._terminal.cols) { + srcLineIndex++; + srcCol = 0; + } + } + + // Clear out remaining cells or fragments could remain + // TODO: @jerch can this be a const? + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + // Remove rows and adjust cursor + if (countToRemove > 0) { + this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); + let removing = countToRemove; + while (removing-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } + // TODO: Handle list trimming + + return wrappedLines.length - countToRemove - 1; + } + + private _reflowSmaller(y: number, newCols: number): number { + // Check whether this line is a problem + const line = this.lines.get(y) as BufferLine; + if (line.getTrimmedLength() <= newCols) { + return 0; + } + + // TODO: How is the cursor x handled if it's wrapped? Do something special when the cursor is this line? + + + // Gather wrapped lines if it's wrapped + let lineIndex = y; + let nextLine = this.lines.get(++lineIndex) as BufferLine; + const wrappedLines: BufferLine[] = [line]; + while (nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++lineIndex) as BufferLine; + } + + // Determine how many lines need to be inserted at the end, based on the trimmed length of + // the last wrapped line + if (wrappedLines[wrappedLines.length - 1].getTrimmedLength() === undefined) { + debugger; + } + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; + const linesNeeded = Math.ceil(cellsNeeded / newCols); + const linesToAdd = linesNeeded - wrappedLines.length; + + // Add the new lines + const newLines: BufferLine[] = []; + for (let i = 0; i < linesToAdd; i++) { + // TODO: Remove any! + const newLine = this.getBlankLine((this._terminal as any).eraseAttr(), true) as BufferLine; + newLines.push(newLine); + } + this.lines.splice(y + wrappedLines.length, 0, ...newLines); + wrappedLines.push(...newLines); + + // Copy buffer data to new locations, this needs to happen backwards to do in-place + let destLineIndex = Math.floor(cellsNeeded / newCols); + let destCol = cellsNeeded % newCols; + if (destCol === 0) { + destLineIndex--; + destCol = newCols; + } + let srcLineIndex = wrappedLines.length - linesToAdd - 1; + let srcCol = lastLineLength; + while (srcLineIndex >= 0) { // Don't need to copy any from the first line + const cellsToCopy = Math.min(srcCol, destCol); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); + destCol -= cellsToCopy; + if (destCol === 0) { + destLineIndex--; + destCol = newCols; + } + srcCol -= cellsToCopy; + if (srcCol === 0) { + srcLineIndex--; + srcCol = this._terminal.cols; + } + } + + // Adjust viewport as needed + let viewportAdjustments = linesToAdd; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + if (this.y < this._terminal.rows) { + this.y++; + this.lines.pop(); + } else { + this.ybase++; + this.ydisp++; + } + } else { + if (this.ybase === this.ydisp) { + this.ybase++; + this.ydisp++; + } + } + } + + // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. + // TODO: Handle list trimming + + return wrappedLines.length - 1; + } + /** * Translates a string index back to a BufferIndex. * To get the correct buffer position the string must start at `startCol` 0 @@ -339,10 +443,9 @@ export class Buffer implements IBuffer { } lineIndex++; } - return [lineIndex, 0]; } - /** + /** // TODO: Handle list trimming * Translates a buffer line to a string, with optional start and end columns. * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection diff --git a/src/BufferLine.ts b/src/BufferLine.ts index aafc9051..ea44de87 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -304,12 +304,19 @@ export class BufferLine implements IBufferLine { return 0; } - public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number): void { - console.log(' copyCellsFrom', srcCol, destCol, length); + public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { const srcData = src._data; - for (let cell = 0; cell < length; cell++) { - for (let i = 0; i < CELL_SIZE; i++) { - this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + if (applyInReverse) { + for (let cell = length - 1; cell >= 0; cell--) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + } + } else { + for (let cell = 0; cell < length; cell++) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } } } } From fa47036982cb8aecfb10d9c86427198b5fc91982 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 23:49:59 -0800 Subject: [PATCH 009/140] Fix row removal in reflowLarger --- src/Buffer.ts | 13 ++----------- src/BufferLine.ts | 6 +++--- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index fc8e008e..961032d9 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -292,21 +292,12 @@ export class Buffer implements IBuffer { } // Clear out remaining cells or fragments could remain - // TODO: @jerch can this be a const? + // TODO: @jerch can fillCharData be a const? const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - // Remove rows and adjust cursor + const countToRemove = wrappedLines.length - destLineIndex - 1; if (countToRemove > 0) { this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); let removing = countToRemove; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index ea44de87..fd28af84 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -228,8 +228,8 @@ export class BufferLine implements IBufferLine { } } - public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { - if (cols === this.length || (!shrink && cols < this.length)) { + public resize(cols: number, fillCharData: CharData): void { + if (cols === this.length) { return; } if (cols > this.length) { @@ -245,7 +245,7 @@ export class BufferLine implements IBufferLine { for (let i = this.length; i < cols; ++i) { this.set(i, fillCharData); } - } else if (shrink) { + } else { if (cols) { const data = new Uint32Array(cols * CELL_SIZE); data.set(this._data.subarray(0, cols * CELL_SIZE)); From 8bc04c2fa73206f2e4ba3404297ebe187578d96c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 23:58:57 -0800 Subject: [PATCH 010/140] Tidy up --- src/Buffer.ts | 14 +++----------- src/Types.ts | 1 + 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 961032d9..5c1d8e45 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -300,8 +300,8 @@ export class Buffer implements IBuffer { const countToRemove = wrappedLines.length - destLineIndex - 1; if (countToRemove > 0) { this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - let removing = countToRemove; - while (removing-- > 0) { + let viewportAdjustments = countToRemove; + while (viewportAdjustments-- > 0) { if (this.ybase === 0) { this.y--; // Add an extra row at the bottom of the viewport @@ -314,7 +314,6 @@ export class Buffer implements IBuffer { } } } - // TODO: Handle list trimming return wrappedLines.length - countToRemove - 1; } @@ -326,9 +325,6 @@ export class Buffer implements IBuffer { return 0; } - // TODO: How is the cursor x handled if it's wrapped? Do something special when the cursor is this line? - - // Gather wrapped lines if it's wrapped let lineIndex = y; let nextLine = this.lines.get(++lineIndex) as BufferLine; @@ -340,9 +336,6 @@ export class Buffer implements IBuffer { // Determine how many lines need to be inserted at the end, based on the trimmed length of // the last wrapped line - if (wrappedLines[wrappedLines.length - 1].getTrimmedLength() === undefined) { - debugger; - } const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); @@ -351,8 +344,7 @@ export class Buffer implements IBuffer { // Add the new lines const newLines: BufferLine[] = []; for (let i = 0; i < linesToAdd; i++) { - // TODO: Remove any! - const newLine = this.getBlankLine((this._terminal as any).eraseAttr(), true) as BufferLine; + const newLine = this.getBlankLine(this._terminal.eraseAttr(), true) as BufferLine; newLines.push(newLine); } this.lines.splice(y + wrappedLines.length, 0, ...newLines); diff --git a/src/Types.ts b/src/Types.ts index 8ebb28d3..b1fea903 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -230,6 +230,7 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; showCursor(): void; + eraseAttr(): number; } export interface IBufferAccessor { From 3311ed509aaca3372062b1b59457d487b6b59bd4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 00:40:30 -0800 Subject: [PATCH 011/140] Do shrink in reverse, fix up row remove count again --- src/Buffer.ts | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 5c1d8e45..dcb05049 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -245,11 +245,14 @@ export class Buffer implements IBuffer { } // Iterate through rows, ignore the last one as it cannot be wrapped - for (let y = 0; y < this.lines.length - 1; y++) { - if (newCols > this._terminal.cols) { + if (newCols > this._terminal.cols) { + for (let y = 0; y < this.lines.length - 1; y++) { y += this._reflowLarger(y, newCols); - } else { - y += this._reflowSmaller(y, newCols); + } + } else { + // Go backwards as many lines may be trimmed and this will avoid considering them + for (let y = this.lines.length - 1; y >= 0; y--) { + y -= this._reflowSmaller(y, newCols); } } } @@ -296,8 +299,16 @@ export class Buffer implements IBuffer { const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); - // Remove rows and adjust cursor - const countToRemove = wrappedLines.length - destLineIndex - 1; + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + if (countToRemove > 0) { this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); let viewportAdjustments = countToRemove; @@ -320,18 +331,22 @@ export class Buffer implements IBuffer { private _reflowSmaller(y: number, newCols: number): number { // Check whether this line is a problem - const line = this.lines.get(y) as BufferLine; - if (line.getTrimmedLength() <= newCols) { + let nextLine = this.lines.get(y) as BufferLine; + if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { return 0; } - // Gather wrapped lines if it's wrapped - let lineIndex = y; - let nextLine = this.lines.get(++lineIndex) as BufferLine; - const wrappedLines: BufferLine[] = [line]; - while (nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++lineIndex) as BufferLine; + // Gather wrapped lines and adjust y to be the starting line + const wrappedLines: BufferLine[] = [nextLine]; + if (nextLine.isWrapped) { + while (true) { + nextLine = this.lines.get(--y) as BufferLine; + // TODO: unshift is expensive + wrappedLines.unshift(nextLine); + if (!nextLine.isWrapped || y === 0) { + break; + } + } } // Determine how many lines need to be inserted at the end, based on the trimmed length of @@ -396,7 +411,7 @@ export class Buffer implements IBuffer { // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. // TODO: Handle list trimming - return wrappedLines.length - 1; + return wrappedLines.length - 1 - linesToAdd; } /** From ae29dbb133dc18348a70ab7af81a3f099d886782 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 01:26:49 -0800 Subject: [PATCH 012/140] Fix scrollbar when wrapping beyond single viewport of data --- src/Buffer.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index dcb05049..ef91055d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -355,6 +355,9 @@ export class Buffer implements IBuffer { const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; + const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + console.log('linesToAdd', linesToAdd); + console.log('trimmedLines', trimmedLines); // Add the new lines const newLines: BufferLine[] = []; @@ -374,7 +377,7 @@ export class Buffer implements IBuffer { } let srcLineIndex = wrappedLines.length - linesToAdd - 1; let srcCol = lastLineLength; - while (srcLineIndex >= 0) { // Don't need to copy any from the first line + while (srcLineIndex >= 0) { const cellsToCopy = Math.min(srcCol, destCol); wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); destCol -= cellsToCopy; @@ -393,11 +396,12 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._terminal.rows) { + if (this.y < this._terminal.rows - 1) { this.y++; this.lines.pop(); } else { this.ybase++; + // TODO: Use this? if (this._terminal._userScrolling) { this.ydisp++; } } else { @@ -411,7 +415,7 @@ export class Buffer implements IBuffer { // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. // TODO: Handle list trimming - return wrappedLines.length - 1 - linesToAdd; + return wrappedLines.length - 1 - linesToAdd + trimmedLines; } /** From 7684f93773fbde3bd9c16e9d56230c17ab312fc0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 01:32:19 -0800 Subject: [PATCH 013/140] Fix ydisp/ybase after trimming buffer --- src/Buffer.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ef91055d..acba0c32 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -356,8 +356,6 @@ export class Buffer implements IBuffer { const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); - console.log('linesToAdd', linesToAdd); - console.log('trimmedLines', trimmedLines); // Add the new lines const newLines: BufferLine[] = []; @@ -393,7 +391,7 @@ export class Buffer implements IBuffer { } // Adjust viewport as needed - let viewportAdjustments = linesToAdd; + let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { if (this.y < this._terminal.rows - 1) { @@ -412,9 +410,6 @@ export class Buffer implements IBuffer { } } - // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. - // TODO: Handle list trimming - return wrappedLines.length - 1 - linesToAdd + trimmedLines; } From 358898daf9ca63e3da8ff649bad00c2b6a9129fa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 11:02:08 -0800 Subject: [PATCH 014/140] Fix some tests --- src/Buffer.ts | 3 ++- src/BufferLine.test.ts | 51 +++------------------------------------- src/ui/TestUtils.test.ts | 3 +++ 3 files changed, 8 insertions(+), 49 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index acba0c32..143e2b2b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -440,9 +440,10 @@ export class Buffer implements IBuffer { } lineIndex++; } + return [lineIndex, 0]; } - /** // TODO: Handle list trimming + /** * Translates a buffer line to a string, with optional start and end columns. * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index fbf8b051..c652ff4e 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -141,64 +141,19 @@ describe('BufferLine', function(): void { }); 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); + line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); 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); + line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); 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); + line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); 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)])); - }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 10033a33..3b59ee5d 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -19,6 +19,9 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { + eraseAttr(): number { + throw new Error('Method not implemented.'); + } markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); From a33e8a6af79b9f2aa9838f300b5e8366ddd010ba Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 12:57:10 -0800 Subject: [PATCH 015/140] Properly shrink rows to cols every time --- src/Buffer.ts | 8 +++++++- src/Types.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 143e2b2b..41a76ccc 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -162,7 +162,7 @@ export class Buffer implements IBuffer { // The following adjustments should only happen if the buffer has been // initialized/filled. if (this.lines.length > 0) { - // Deal with columns increasing (we don't do anything when columns reduce) + // Deal with columns increasing (reducing needs to happen after reflow) if (this._terminal.cols < newCols) { const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { @@ -236,6 +236,12 @@ export class Buffer implements IBuffer { if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { this._reflow(newCols); + + if (this._terminal.cols > newCols) { + for (let i = 0; i < this.lines.length; i++) { + this.lines.get(i).resize(newCols, null); + } + } } } diff --git a/src/Types.ts b/src/Types.ts index b1fea903..d7715f81 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -521,7 +521,7 @@ export interface IBufferLine { insertCells(pos: number, n: number, ch: CharData): void; deleteCells(pos: number, n: number, fill: CharData): void; replaceCells(start: number, end: number, fill: CharData): void; - resize(cols: number, fill: CharData, shrink?: boolean): void; + resize(cols: number, fill: CharData): void; fill(fillCharData: CharData): void; copyFrom(line: IBufferLine): void; clone(): IBufferLine; From 2a0da173f200b006f292b0592d2d47334e78bbc9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 14:55:15 -0800 Subject: [PATCH 016/140] Add a bunch of reflow tests --- src/Buffer.test.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 18 +++++-- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..189b3ad6 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -233,6 +233,121 @@ describe('Buffer', () => { } }); }); + + describe('reflow', () => { + beforeEach(() => { + terminal.eraseAttr = () => DEFAULT_ATTR; + // Needed until the setting is removed + terminal.options.experimentalBufferLineImpl = 'TypedArray'; + }); + it('should not wrap empty lines', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + buffer.resize(INIT_COLS - 5, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + }); + it('should shrink row length', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).length, 5); + assert.equal(buffer.lines.get(1).length, 5); + assert.equal(buffer.lines.get(2).length, 5); + assert.equal(buffer.lines.get(3).length, 5); + assert.equal(buffer.lines.get(4).length, 5); + assert.equal(buffer.lines.get(5).length, 5); + assert.equal(buffer.lines.get(6).length, 5); + assert.equal(buffer.lines.get(7).length, 5); + assert.equal(buffer.lines.get(8).length, 5); + assert.equal(buffer.lines.get(9).length, 5); + }); + it('should wrap and unwrap lines', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + terminal.cols = 5; + const firstLine = buffer.lines.get(0); + for (let i = 0; i < 5; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [null, char, 1, code]); + } + assert.equal(buffer.lines.get(0).length, 5); + assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); + buffer.resize(1, 10); + terminal.cols = 1; + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'a'); + assert.equal(buffer.lines.get(1).translateToString(), 'b'); + assert.equal(buffer.lines.get(2).translateToString(), 'c'); + assert.equal(buffer.lines.get(3).translateToString(), 'd'); + assert.equal(buffer.lines.get(4).translateToString(), 'e'); + assert.equal(buffer.lines.get(5).translateToString(), ' '); + assert.equal(buffer.lines.get(6).translateToString(), ' '); + assert.equal(buffer.lines.get(7).translateToString(), ' '); + assert.equal(buffer.lines.get(8).translateToString(), ' '); + assert.equal(buffer.lines.get(9).translateToString(), ' '); + buffer.resize(5, 10); + terminal.cols = 5; + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); + assert.equal(buffer.lines.get(1).translateToString(), ' '); + assert.equal(buffer.lines.get(2).translateToString(), ' '); + assert.equal(buffer.lines.get(3).translateToString(), ' '); + assert.equal(buffer.lines.get(4).translateToString(), ' '); + assert.equal(buffer.lines.get(5).translateToString(), ' '); + assert.equal(buffer.lines.get(6).translateToString(), ' '); + assert.equal(buffer.lines.get(7).translateToString(), ' '); + assert.equal(buffer.lines.get(8).translateToString(), ' '); + assert.equal(buffer.lines.get(9).translateToString(), ' '); + }); + it('should discard parts of wrapped lines that go out of the scrollback', () => { + buffer.fillViewportRows(); + terminal.options.scrollback = 1; + buffer.resize(10, 5); + terminal.cols = 10; + terminal.rows = 5; + const lastLine = buffer.lines.get(4); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + lastLine.set(i, [null, char, 1, code]); + } + assert.equal(buffer.lines.length, 5); + buffer.y = 4; + buffer.resize(2, 5); + terminal.cols = 2; + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0).translateToString(), ' '); + assert.equal(buffer.lines.get(1).translateToString(), 'ab'); + assert.equal(buffer.lines.get(2).translateToString(), 'cd'); + assert.equal(buffer.lines.get(3).translateToString(), 'ef'); + assert.equal(buffer.lines.get(4).translateToString(), 'gh'); + assert.equal(buffer.lines.get(5).translateToString(), 'ij'); + buffer.resize(1, 5); + terminal.cols = 1; + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0).translateToString(), 'e'); + assert.equal(buffer.lines.get(1).translateToString(), 'f'); + assert.equal(buffer.lines.get(2).translateToString(), 'g'); + assert.equal(buffer.lines.get(3).translateToString(), 'h'); + assert.equal(buffer.lines.get(4).translateToString(), 'i'); + assert.equal(buffer.lines.get(5).translateToString(), 'j'); + buffer.resize(10, 5); + terminal.cols = 10; + assert.equal(buffer.y, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 5); + assert.equal(buffer.lines.get(0).translateToString(), 'efghij '); + assert.equal(buffer.lines.get(1).translateToString(), ' '); + assert.equal(buffer.lines.get(2).translateToString(), ' '); + assert.equal(buffer.lines.get(3).translateToString(), ' '); + assert.equal(buffer.lines.get(4).translateToString(), ' '); + }); + }); }); describe('buffer marked to have no scrollback', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 41a76ccc..5f6791d8 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -164,9 +164,9 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) if (this._terminal.cols < newCols) { - const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr? + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, ch); + this.lines.get(i).resize(newCols, fillCharData); } } @@ -237,9 +237,11 @@ export class Buffer implements IBuffer { if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { this._reflow(newCols); + // Trim the end of the line off if cols shrunk if (this._terminal.cols > newCols) { + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, null); + this.lines.get(i).resize(newCols, fillCharData); } } } @@ -273,7 +275,7 @@ export class Buffer implements IBuffer { // Check how many lines it's wrapped for const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped) { + while (nextLine.isWrapped && i < this.lines.length) { wrappedLines.push(nextLine); nextLine = this.lines.get(++i) as BufferLine; } @@ -361,7 +363,13 @@ export class Buffer implements IBuffer { const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; - const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + let trimmedLines: number; + if (this.ybase === 0 && this.y !== this.lines.length - 1) { + // If the top section of the buffer is not yet filled + trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); + } else { + trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + } // Add the new lines const newLines: BufferLine[] = []; From 72369e060e602152709956dc6deae6aa2295d606 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 14:55:58 -0800 Subject: [PATCH 017/140] Only enable reflow on the normal buffer --- src/Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 5f6791d8..0b660321 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -234,7 +234,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { + if (this.hasScrollback && this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { this._reflow(newCols); // Trim the end of the line off if cols shrunk From 4a9f10d062c2f92b08dbbc862e7a5279ed5f2a3e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 19:22:05 -0800 Subject: [PATCH 018/140] Remove some of Buffer's dependency on Terminal --- src/Buffer.test.ts | 9 ++------- src/Buffer.ts | 4 ++-- src/Types.ts | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 189b3ad6..cee0fbad 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -108,12 +108,12 @@ describe('Buffer', () => { describe('resize', () => { describe('column size is reduced', () => { - it('should not trim the data in the buffer', () => { + it('should trim the data in the buffer', () => { buffer.fillViewportRows(); buffer.resize(INIT_COLS / 2, INIT_ROWS); assert.equal(buffer.lines.length, INIT_ROWS); for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS); + assert.equal(buffer.lines.get(i).length, INIT_COLS / 2); } }); }); @@ -235,11 +235,6 @@ describe('Buffer', () => { }); describe('reflow', () => { - beforeEach(() => { - terminal.eraseAttr = () => DEFAULT_ATTR; - // Needed until the setting is removed - terminal.options.experimentalBufferLineImpl = 'TypedArray'; - }); it('should not wrap empty lines', () => { buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); diff --git a/src/Buffer.ts b/src/Buffer.ts index 0b660321..feb3a1ee 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -234,7 +234,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this.hasScrollback && this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { + if (this.hasScrollback && this._bufferLineConstructor === BufferLine) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -374,7 +374,7 @@ export class Buffer implements IBuffer { // Add the new lines const newLines: BufferLine[] = []; for (let i = 0; i < linesToAdd; i++) { - const newLine = this.getBlankLine(this._terminal.eraseAttr(), true) as BufferLine; + const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; newLines.push(newLine); } this.lines.splice(y + wrappedLines.length, 0, ...newLines); diff --git a/src/Types.ts b/src/Types.ts index d7715f81..d48362b8 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -230,7 +230,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; showCursor(): void; - eraseAttr(): number; } export interface IBufferAccessor { From dde96187fa7b6502b357c1a614d0b2cacd6d79e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 19:27:38 -0800 Subject: [PATCH 019/140] Keep track of cols/rows inside Buffer --- src/Buffer.test.ts | 8 ------- src/Buffer.ts | 56 +++++++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index cee0fbad..bece8c22 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -259,7 +259,6 @@ describe('Buffer', () => { it('should wrap and unwrap lines', () => { buffer.fillViewportRows(); buffer.resize(5, 10); - terminal.cols = 5; const firstLine = buffer.lines.get(0); for (let i = 0; i < 5; i++) { const code = 'a'.charCodeAt(0) + i; @@ -269,7 +268,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(0).length, 5); assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); buffer.resize(1, 10); - terminal.cols = 1; assert.equal(buffer.lines.length, 10); assert.equal(buffer.lines.get(0).translateToString(), 'a'); assert.equal(buffer.lines.get(1).translateToString(), 'b'); @@ -282,7 +280,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(8).translateToString(), ' '); assert.equal(buffer.lines.get(9).translateToString(), ' '); buffer.resize(5, 10); - terminal.cols = 5; assert.equal(buffer.lines.length, 10); assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); assert.equal(buffer.lines.get(1).translateToString(), ' '); @@ -299,8 +296,6 @@ describe('Buffer', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; buffer.resize(10, 5); - terminal.cols = 10; - terminal.rows = 5; const lastLine = buffer.lines.get(4); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; @@ -310,7 +305,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.length, 5); buffer.y = 4; buffer.resize(2, 5); - terminal.cols = 2; assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); @@ -321,7 +315,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(4).translateToString(), 'gh'); assert.equal(buffer.lines.get(5).translateToString(), 'ij'); buffer.resize(1, 5); - terminal.cols = 1; assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); @@ -332,7 +325,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(4).translateToString(), 'i'); assert.equal(buffer.lines.get(5).translateToString(), 'j'); buffer.resize(10, 5); - terminal.cols = 10; assert.equal(buffer.y, 0); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 5); diff --git a/src/Buffer.ts b/src/Buffer.ts index feb3a1ee..ba335309 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -46,6 +46,8 @@ export class Buffer implements IBuffer { public savedCurAttr: number; public markers: Marker[] = []; private _bufferLineConstructor: IBufferLineConstructor; + private _cols: number; + private _rows: number; /** * Create a new Buffer. @@ -57,6 +59,8 @@ export class Buffer implements IBuffer { private _terminal: ITerminal, private _hasScrollback: boolean ) { + this._cols = this._terminal.cols; + this._rows = this._terminal.rows; this.clear(); } @@ -88,17 +92,17 @@ export class Buffer implements IBuffer { public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new this._bufferLineConstructor(this._terminal.cols, fillCharData, isWrapped); + return new this._bufferLineConstructor(this._cols, fillCharData, isWrapped); } public get hasScrollback(): boolean { - return this._hasScrollback && this.lines.maxLength > this._terminal.rows; + return this._hasScrollback && this.lines.maxLength > this._rows; } public get isCursorInViewport(): boolean { const absoluteY = this.ybase + this.y; const relativeY = absoluteY - this.ydisp; - return (relativeY >= 0 && relativeY < this._terminal.rows); + return (relativeY >= 0 && relativeY < this._rows); } /** @@ -124,7 +128,7 @@ export class Buffer implements IBuffer { if (fillAttr === undefined) { fillAttr = DEFAULT_ATTR; } - let i = this._terminal.rows; + let i = this._rows; while (i--) { this.lines.push(this.getBlankLine(fillAttr)); } @@ -140,9 +144,9 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = new CircularList(this._getCorrectBufferLength(this._rows)); this.scrollTop = 0; - this.scrollBottom = this._terminal.rows - 1; + this.scrollBottom = this._rows - 1; this.setupTabStops(); } @@ -163,7 +167,7 @@ export class Buffer implements IBuffer { // initialized/filled. if (this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) - if (this._terminal.cols < newCols) { + if (this._cols < newCols) { const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { this.lines.get(i).resize(newCols, fillCharData); @@ -172,8 +176,8 @@ export class Buffer implements IBuffer { // Resize rows in both directions as needed let addToY = 0; - if (this._terminal.rows < newRows) { - for (let y = this._terminal.rows; y < newRows; y++) { + if (this._rows < newRows) { + for (let y = this._rows; y < newRows; y++) { if (this.lines.length < newRows + this.ybase) { if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { // There is room above the buffer and there are no empty elements below the line, @@ -192,8 +196,8 @@ export class Buffer implements IBuffer { } } } - } else { // (this._terminal.rows >= newRows) - for (let y = this._terminal.rows; y > newRows; y--) { + } else { // (this._rows >= newRows) + for (let y = this._rows; y > newRows; y--) { if (this.lines.length > newRows + this.ybase) { if (this.lines.length > this.ybase + this.y + 1) { // The line is a blank line below the cursor, remove it @@ -238,22 +242,25 @@ export class Buffer implements IBuffer { this._reflow(newCols); // Trim the end of the line off if cols shrunk - if (this._terminal.cols > newCols) { + if (this._cols > newCols) { const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { this.lines.get(i).resize(newCols, fillCharData); } } } + + this._cols = newCols; + this._rows = newRows; } private _reflow(newCols: number): void { - if (this._terminal.cols === newCols) { + if (this._cols === newCols) { return; } // Iterate through rows, ignore the last one as it cannot be wrapped - if (newCols > this._terminal.cols) { + if (newCols > this._cols) { for (let y = 0; y < this.lines.length - 1; y++) { y += this._reflowLarger(y, newCols); } @@ -282,11 +289,11 @@ export class Buffer implements IBuffer { // Copy buffer data to new locations let destLineIndex = 0; - let destCol = this._terminal.cols; + let destCol = this._cols; let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._terminal.cols - srcCol; + const srcRemainingCells = this._cols - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); @@ -296,7 +303,7 @@ export class Buffer implements IBuffer { destCol = 0; } srcCol += cellsToCopy; - if (srcCol === this._terminal.cols) { + if (srcCol === this._cols) { srcLineIndex++; srcCol = 0; } @@ -360,7 +367,7 @@ export class Buffer implements IBuffer { // Determine how many lines need to be inserted at the end, based on the trimmed length of // the last wrapped line const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; + const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; let trimmedLines: number; @@ -400,7 +407,7 @@ export class Buffer implements IBuffer { srcCol -= cellsToCopy; if (srcCol === 0) { srcLineIndex--; - srcCol = this._terminal.cols; + srcCol = this._cols; } } @@ -408,12 +415,11 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._terminal.rows - 1) { + if (this.y < this._rows - 1) { this.y++; this.lines.pop(); } else { this.ybase++; - // TODO: Use this? if (this._terminal._userScrolling) { this.ydisp++; } } else { @@ -503,7 +509,7 @@ export class Buffer implements IBuffer { i = 0; } - for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) { + for (; i < this._cols; i += this._terminal.options.tabStopWidth) { this.tabs[i] = true; } } @@ -517,7 +523,7 @@ export class Buffer implements IBuffer { x = this.x; } while (!this.tabs[--x] && x > 0); - return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; + return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } /** @@ -528,8 +534,8 @@ export class Buffer implements IBuffer { if (x === null || x === undefined) { x = this.x; } - while (!this.tabs[++x] && x < this._terminal.cols); - return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; + while (!this.tabs[++x] && x < this._cols); + return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } public addMarker(y: number): Marker { From 478742a22e7c3c8aacfa0ea81757a7f6d9121504 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 30 Dec 2018 12:14:48 -0800 Subject: [PATCH 020/140] Make reflow small crazy fast This messy but this drops 100000 scrollback reflow from 87 cols to 40 cols go from ~18 seconds to < 1 second, 10000 takes around 70ms --- src/Buffer.ts | 221 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 149 insertions(+), 72 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ba335309..acb462ab 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -265,10 +265,7 @@ export class Buffer implements IBuffer { y += this._reflowLarger(y, newCols); } } else { - // Go backwards as many lines may be trimmed and this will avoid considering them - for (let y = this.lines.length - 1; y >= 0; y--) { - y -= this._reflowSmaller(y, newCols); - } + this._reflowSmaller(newCols); } } @@ -344,93 +341,173 @@ export class Buffer implements IBuffer { return wrappedLines.length - countToRemove - 1; } - private _reflowSmaller(y: number, newCols: number): number { - // Check whether this line is a problem - let nextLine = this.lines.get(y) as BufferLine; - if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { - return 0; - } + private _reflowSmaller(newCols: number): void { + // Gather all BufferLines that need to be inserted into the Buffer here so that they can be + // batched up and only committed once + const toInsert = []; + let countToInsert = 0; + // Go backwards as many lines may be trimmed and this will avoid considering them + for (let y = this.lines.length - 1; y >= 0; y--) { + // Check whether this line is a problem + let nextLine = this.lines.get(y) as BufferLine; + if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { + continue; + } - // Gather wrapped lines and adjust y to be the starting line - const wrappedLines: BufferLine[] = [nextLine]; - if (nextLine.isWrapped) { - while (true) { + // Gather wrapped lines and adjust y to be the starting line + const wrappedLines: BufferLine[] = [nextLine]; + while (nextLine.isWrapped && y > 0) { nextLine = this.lines.get(--y) as BufferLine; // TODO: unshift is expensive wrappedLines.unshift(nextLine); - if (!nextLine.isWrapped || y === 0) { - break; - } } - } - // Determine how many lines need to be inserted at the end, based on the trimmed length of - // the last wrapped line - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - const linesNeeded = Math.ceil(cellsNeeded / newCols); - const linesToAdd = linesNeeded - wrappedLines.length; - let trimmedLines: number; - if (this.ybase === 0 && this.y !== this.lines.length - 1) { - // If the top section of the buffer is not yet filled - trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); - } else { - trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); - } + // Determine how many lines need to be inserted at the end, based on the trimmed length of + // the last wrapped line + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; + const linesNeeded = Math.ceil(cellsNeeded / newCols); + const linesToAdd = linesNeeded - wrappedLines.length; + let trimmedLines: number; + if (this.ybase === 0 && this.y !== this.lines.length - 1) { + // If the top section of the buffer is not yet filled + trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); + } else { + trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + } - // Add the new lines - const newLines: BufferLine[] = []; - for (let i = 0; i < linesToAdd; i++) { - const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; - newLines.push(newLine); - } - this.lines.splice(y + wrappedLines.length, 0, ...newLines); - wrappedLines.push(...newLines); + // Add the new lines + const newLines: BufferLine[] = []; + for (let i = 0; i < linesToAdd; i++) { + const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; + newLines.push(newLine); + } + if (newLines.length > 0) { + toInsert.push({ + // countToInsert here gets the actual index, taking into account other inserted items. + // using this we can iterate through the list forwards + start: y + wrappedLines.length + countToInsert, + newLines + }); + countToInsert += newLines.length; + } + // this.lines.splice(y + wrappedLines.length, 0, ...newLines); + wrappedLines.push(...newLines); - // Copy buffer data to new locations, this needs to happen backwards to do in-place - let destLineIndex = Math.floor(cellsNeeded / newCols); - let destCol = cellsNeeded % newCols; - if (destCol === 0) { - destLineIndex--; - destCol = newCols; - } - let srcLineIndex = wrappedLines.length - linesToAdd - 1; - let srcCol = lastLineLength; - while (srcLineIndex >= 0) { - const cellsToCopy = Math.min(srcCol, destCol); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); - destCol -= cellsToCopy; + // Copy buffer data to new locations, this needs to happen backwards to do in-place + let destLineIndex = Math.floor(cellsNeeded / newCols); + let destCol = cellsNeeded % newCols; if (destCol === 0) { destLineIndex--; destCol = newCols; } - srcCol -= cellsToCopy; - if (srcCol === 0) { - srcLineIndex--; - srcCol = this._cols; + let srcLineIndex = wrappedLines.length - linesToAdd - 1; + let srcCol = lastLineLength; + while (srcLineIndex >= 0) { + const cellsToCopy = Math.min(srcCol, destCol); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); + destCol -= cellsToCopy; + if (destCol === 0) { + destLineIndex--; + destCol = newCols; + } + srcCol -= cellsToCopy; + if (srcCol === 0) { + srcLineIndex--; + srcCol = this._cols; + } } - } - // Adjust viewport as needed - let viewportAdjustments = linesToAdd - trimmedLines; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - if (this.y < this._rows - 1) { - this.y++; - this.lines.pop(); + // Adjust viewport as needed + let viewportAdjustments = linesToAdd - trimmedLines; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + if (this.y < this._rows - 1) { + this.y++; + this.lines.pop(); + } else { + this.ybase++; + this.ydisp++; + } } else { - this.ybase++; - this.ydisp++; - } - } else { - if (this.ybase === this.ydisp) { - this.ybase++; - this.ydisp++; + if (this.ybase === this.ydisp) { + this.ybase++; + this.ydisp++; + } } } + + // y -= wrappedLines.length - 1 /*- linesToAdd*/ /*+ trimmedLines */; } - return wrappedLines.length - 1 - linesToAdd + trimmedLines; + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + // if (toInsert.length) { + // let insertIndex = toInsert.length - 1; + // let nextToInsert = toInsert[insertIndex]; + // let originalLineIndex = 0; + // for (let i = 0; i < Math.min(this.lines.maxLength - 1, this.lines.length + countToInsert); i++) { + // if (nextToInsert && nextToInsert.start === i) { + // this.lines.set(i, nextToInsert.newLines.shift()); + // if (nextToInsert.newLines.length === 0) { + // nextToInsert = toInsert[--insertIndex]; + // } + // } else { + // this.lines.set(i, originalLines[originalLineIndex++]); + // } + // } + // } + + if (toInsert.length > 0) { + let nextToInsertIndex = 0; + let nextToInsert = toInsert[nextToInsertIndex]; + let originalLineIndex = originalLines.length - 1; + const originalLinesLength = this.lines.length; + this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); + // let countToBeInserted = countToInsert; + let countInsertedSoFar = 0; + for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { + if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) { + for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { + this.lines.set(i--, nextToInsert.newLines[nextI]); + } + i++; // Don't skip for the first row + // this.lines.set(i, nextToInsert.newLines.pop()); + countInsertedSoFar += nextToInsert.newLines.length; + // countToBeInserted--; + // if (nextToInsert.newLines.length === 0) { + nextToInsert = toInsert[++nextToInsertIndex]; + // } + + + + // this.lines.set(i, nextToInsert.newLines.pop()); + // countInsertedSoFar++; + // // countToBeInserted--; + // if (nextToInsert.newLines.length === 0) { + // nextToInsert = toInsert[++nextToInsertIndex]; + // } + } else { + this.lines.set(i, originalLines[originalLineIndex--]); + } + } + // TODO: Throw trim event + } + + // let offset = 0; + // const listener = (countToTrim: number) => { + // offset -= countToTrim; + // }; + // this.lines.on('trim', listener); + // toInsert.forEach(value => { + // console.log('Insert at ', value.start + offset, value.newLines); + // this.lines.splice(value.start + offset, 0, ...value.newLines); + // // offset -= value.start; + // }); + // this.lines.off('trim', listener); } /** From c9f4a650c6c24c350e526d8bb8ed063fb0d8f8d3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 07:26:08 -0800 Subject: [PATCH 021/140] Clean up comments and todos --- src/Buffer.ts | 72 ++++++++++----------------------------------------- 1 file changed, 14 insertions(+), 58 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index acb462ab..1dfc3ef6 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -25,6 +25,8 @@ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; +const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): @@ -168,9 +170,8 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, fillCharData); + this.lines.get(i).resize(newCols, FILL_CHAR_DATA); } } @@ -191,8 +192,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); } } } @@ -243,9 +243,8 @@ export class Buffer implements IBuffer { // Trim the end of the line off if cols shrunk if (this._cols > newCols) { - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, fillCharData); + this.lines.get(i).resize(newCols, FILL_CHAR_DATA); } } } @@ -306,10 +305,8 @@ export class Buffer implements IBuffer { } } - // Clear out remaining cells or fragments could remain - // TODO: @jerch can fillCharData be a const? - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); // Work backwards and remove any rows at the end that only contain null cells let countToRemove = 0; @@ -328,7 +325,7 @@ export class Buffer implements IBuffer { if (this.ybase === 0) { this.y--; // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); } else { if (this.ydisp === this.ybase) { this.ydisp--; @@ -358,7 +355,6 @@ export class Buffer implements IBuffer { const wrappedLines: BufferLine[] = [nextLine]; while (nextLine.isWrapped && y > 0) { nextLine = this.lines.get(--y) as BufferLine; - // TODO: unshift is expensive wrappedLines.unshift(nextLine); } @@ -391,7 +387,6 @@ export class Buffer implements IBuffer { }); countToInsert += newLines.length; } - // this.lines.splice(y + wrappedLines.length, 0, ...newLines); wrappedLines.push(...newLines); // Copy buffer data to new locations, this needs to happen backwards to do in-place @@ -436,8 +431,6 @@ export class Buffer implements IBuffer { } } } - - // y -= wrappedLines.length - 1 /*- linesToAdd*/ /*+ trimmedLines */; } // Record original lines so they don't get overridden when we rearrange the list @@ -445,69 +438,32 @@ export class Buffer implements IBuffer { for (let i = 0; i < this.lines.length; i++) { originalLines.push(this.lines.get(i) as BufferLine); } - // if (toInsert.length) { - // let insertIndex = toInsert.length - 1; - // let nextToInsert = toInsert[insertIndex]; - // let originalLineIndex = 0; - // for (let i = 0; i < Math.min(this.lines.maxLength - 1, this.lines.length + countToInsert); i++) { - // if (nextToInsert && nextToInsert.start === i) { - // this.lines.set(i, nextToInsert.newLines.shift()); - // if (nextToInsert.newLines.length === 0) { - // nextToInsert = toInsert[--insertIndex]; - // } - // } else { - // this.lines.set(i, originalLines[originalLineIndex++]); - // } - // } - // } + // Rearrange lines in the buffer if there are any insertions, this is done at the end rather + // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many + // costly calls to CircularList.splice. if (toInsert.length > 0) { let nextToInsertIndex = 0; let nextToInsert = toInsert[nextToInsertIndex]; let originalLineIndex = originalLines.length - 1; const originalLinesLength = this.lines.length; this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); - // let countToBeInserted = countToInsert; let countInsertedSoFar = 0; for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) { + // Insert extra lines here, adjusting i as needed for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { this.lines.set(i--, nextToInsert.newLines[nextI]); } i++; // Don't skip for the first row - // this.lines.set(i, nextToInsert.newLines.pop()); countInsertedSoFar += nextToInsert.newLines.length; - // countToBeInserted--; - // if (nextToInsert.newLines.length === 0) { - nextToInsert = toInsert[++nextToInsertIndex]; - // } - - - - // this.lines.set(i, nextToInsert.newLines.pop()); - // countInsertedSoFar++; - // // countToBeInserted--; - // if (nextToInsert.newLines.length === 0) { - // nextToInsert = toInsert[++nextToInsertIndex]; - // } + nextToInsert = toInsert[++nextToInsertIndex]; } else { this.lines.set(i, originalLines[originalLineIndex--]); } } // TODO: Throw trim event - } - - // let offset = 0; - // const listener = (countToTrim: number) => { - // offset -= countToTrim; - // }; - // this.lines.on('trim', listener); - // toInsert.forEach(value => { - // console.log('Insert at ', value.start + offset, value.newLines); - // this.lines.splice(value.start + offset, 0, ...value.newLines); - // // offset -= value.start; - // }); - // this.lines.off('trim', listener); + } } /** From b7081abfceb41b82dda4921e2f96b59cde26f0f4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 07:54:16 -0800 Subject: [PATCH 022/140] Move loop into reflowLarger (adjust indent) --- src/Buffer.ts | 130 +++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 1dfc3ef6..65e42de0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -260,82 +260,82 @@ export class Buffer implements IBuffer { // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { - for (let y = 0; y < this.lines.length - 1; y++) { - y += this._reflowLarger(y, newCols); - } + this._reflowLarger(newCols); } else { this._reflowSmaller(newCols); } } - private _reflowLarger(y: number, newCols: number): number { - // Check if this row is wrapped - let i = y; - let nextLine = this.lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - return 0; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped && i < this.lines.length) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++i) as BufferLine; - } - - // Copy buffer data to new locations - let destLineIndex = 0; - let destCol = this._cols; - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._cols - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; + private _reflowLarger(newCols: number): void { + for (let y = 0; y < this.lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = this.lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; } - srcCol += cellsToCopy; - if (srcCol === this._cols) { - srcLineIndex++; - srcCol = 0; + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; + while (nextLine.isWrapped && i < this.lines.length) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++i) as BufferLine; } - } - // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - if (countToRemove > 0) { - this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - let viewportAdjustments = countToRemove; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = this._cols; + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcRemainingCells = this._cols - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === this._cols) { + srcLineIndex++; + srcCol = 0; } } - } - return wrappedLines.length - countToRemove - 1; + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + if (countToRemove > 0) { + this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); + let viewportAdjustments = countToRemove; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } + + y += wrappedLines.length - countToRemove - 1; + } } private _reflowSmaller(newCols: number): void { From 135e31f2ca653932d5b85c78780326b5a4776179 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 08:58:04 -0800 Subject: [PATCH 023/140] Speed up reflow larger by batching removals --- src/Buffer.ts | 82 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 65e42de0..fc74f679 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -267,6 +267,9 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number): void { + // Gather all BufferLines that need to be removed from the Buffer here so that they can be + // batched up and only committed once + const toRemove: number[] = []; for (let y = 0; y < this.lines.length - 1; y++) { // Check if this row is wrapped let i = y; @@ -318,24 +321,59 @@ export class Buffer implements IBuffer { } if (countToRemove > 0) { - this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - let viewportAdjustments = countToRemove; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; - } - } + toRemove.push(y + wrappedLines.length - countToRemove); // index + toRemove.push(countToRemove); } y += wrappedLines.length - countToRemove - 1; } + + if (toRemove.length > 0) { + // First iterate through the list and get the actual indexes to use for rows + const newLayout: number[] = []; + + let nextToRemoveIndex = 0; + let nextToRemoveStart = toRemove[nextToRemoveIndex]; + let countRemovedSoFar = 0; + for (let i = 0; i < this.lines.length; i++) { + if (nextToRemoveStart === i) { + const countToRemove = toRemove[++nextToRemoveIndex]; + i += countToRemove - 1; + countRemovedSoFar += countToRemove; + nextToRemoveStart = toRemove[++nextToRemoveIndex]; + } else { + newLayout.push(i); + } + } + + // TODO: THis and the next loop could be improved, only gather the new layout lines, not the original lines + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + + // Rearrange the list + for (let i = 0; i < newLayout.length; i++) { + this.lines.set(i, originalLines[newLayout[i]]); + } + this.lines.length = newLayout.length; + + // Adjust viewport based on number of items removed + let viewportAdjustments = countRemovedSoFar; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } } private _reflowSmaller(newCols: number): void { @@ -433,20 +471,20 @@ export class Buffer implements IBuffer { } } - // Record original lines so they don't get overridden when we rearrange the list - const originalLines: BufferLine[] = []; - for (let i = 0; i < this.lines.length; i++) { - originalLines.push(this.lines.get(i) as BufferLine); - } - // Rearrange lines in the buffer if there are any insertions, this is done at the end rather // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many // costly calls to CircularList.splice. if (toInsert.length > 0) { + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + const originalLinesLength = this.lines.length; + + let originalLineIndex = originalLinesLength - 1; let nextToInsertIndex = 0; let nextToInsert = toInsert[nextToInsertIndex]; - let originalLineIndex = originalLines.length - 1; - const originalLinesLength = this.lines.length; this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); let countInsertedSoFar = 0; for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { From 1612cec3e093861f6f46da4101876d32c635affa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 09:54:19 -0800 Subject: [PATCH 024/140] Fix reflow larger bug, add regression test --- src/Buffer.test.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 15 +++++++-------- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index bece8c22..faed1808 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -334,6 +334,49 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(3).translateToString(), ' '); assert.equal(buffer.lines.get(4).translateToString(), ' '); }); + it('should remove the correct amount of rows when reflowing larger', () => { + // This is a regression test to ensure that successive wrapped lines that are getting + // 3+ lines removed on a reflow actually remove the right lines + buffer.fillViewportRows(); + buffer.resize(10, 10); + const firstLine = buffer.lines.get(0); + const secondLine = buffer.lines.get(1); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + secondLine.set(i, [null, char, 1, code]); + } + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + buffer.resize(2, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), '01'); + assert.equal(buffer.lines.get(6).translateToString(), '23'); + assert.equal(buffer.lines.get(7).translateToString(), '45'); + assert.equal(buffer.lines.get(8).translateToString(), '67'); + assert.equal(buffer.lines.get(9).translateToString(), '89'); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index fc74f679..27662708 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -280,7 +280,7 @@ export class Buffer implements IBuffer { // Check how many lines it's wrapped for const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped && i < this.lines.length) { + while (i < this.lines.length && nextLine.isWrapped) { wrappedLines.push(nextLine); nextLine = this.lines.get(++i) as BufferLine; } @@ -325,7 +325,7 @@ export class Buffer implements IBuffer { toRemove.push(countToRemove); } - y += wrappedLines.length - countToRemove - 1; + y += wrappedLines.length - 1; } if (toRemove.length > 0) { @@ -346,16 +346,15 @@ export class Buffer implements IBuffer { } } - // TODO: THis and the next loop could be improved, only gather the new layout lines, not the original lines // Record original lines so they don't get overridden when we rearrange the list - const originalLines: BufferLine[] = []; - for (let i = 0; i < this.lines.length; i++) { - originalLines.push(this.lines.get(i) as BufferLine); + const newLayoutLines: BufferLine[] = []; + for (let i = 0; i < newLayout.length; i++) { + newLayoutLines.push(this.lines.get(newLayout[i]) as BufferLine); } // Rearrange the list - for (let i = 0; i < newLayout.length; i++) { - this.lines.set(i, originalLines[newLayout[i]]); + for (let i = 0; i < newLayoutLines.length; i++) { + this.lines.set(i, newLayoutLines[i]); } this.lines.length = newLayout.length; From db488ebcc9699ef97c21bd6695f0983006947dfd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 10:18:31 -0800 Subject: [PATCH 025/140] Reflow combined chars --- src/Buffer.test.ts | 15 +++++++++++++++ src/BufferLine.ts | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index faed1808..4c348ec6 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -377,6 +377,21 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(i).translateToString(), ' '); } }); + it('should transfer combined char data over to reflowed lines', () => { + buffer.fillViewportRows(); + buffer.resize(4, 2); + const firstLine = buffer.lines.get(0); + firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]); + firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]); + firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]); + firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]); + assert.equal(buffer.lines.length, 2); + assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); + assert.equal(buffer.lines.get(1).translateToString(), ' '); + buffer.resize(2, 2); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); + }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index fd28af84..619fa9ca 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -319,6 +319,15 @@ export class BufferLine implements IBufferLine { } } } + + // Move any combined data over as needed + const srcCombinedKeys = Object.keys(src._combined); + for (let i = 0; i < srcCombinedKeys.length; i++) { + const key = parseInt(srcCombinedKeys[i], 10); + if (key >= srcCol) { + this._combined[key - srcCol + destCol] = src._combined[key]; + } + } } public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { From 40e8618cf5810e61b8d42e6d0a1d37aab31516f3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 10:26:10 -0800 Subject: [PATCH 026/140] Discard cut off combined data when resizing BufferLines --- src/BufferLine.test.ts | 13 +++++++++++++ src/BufferLine.ts | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index c652ff4e..f5e95fc8 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -9,6 +9,10 @@ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from '. class TestBufferLine extends BufferLine { + public get combined(): {[index: number]: string} { + return this._combined; + } + public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { @@ -154,6 +158,15 @@ describe('BufferLine', function(): void { line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); + it('should remove combining data', () => { + const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); + line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]); + chai.expect(line.translateToString()).eql('aaaaaaaaa😁'); + chai.expect(Object.keys(line.combined).length).eql(1); + line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); + chai.expect(line.translateToString()).eql('aaaaa'); + chai.expect(Object.keys(line.combined).length).eql(0); + }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 619fa9ca..1c391471 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -250,8 +250,17 @@ export class BufferLine implements IBufferLine { const data = new Uint32Array(cols * CELL_SIZE); data.set(this._data.subarray(0, cols * CELL_SIZE)); this._data = data; + // Remove any cut off combined data + const keys = Object.keys(this._combined); + for (let i = 0; i < keys.length; i++) { + const key = parseInt(keys[i], 10); + if (key >= cols) { + delete this._combined[key]; + } + } } else { this._data = null; + this._combined = {}; } } this.length = cols; From 840970eac7e1d157f68dd3e1d98a073921b31a07 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 13:15:23 -0800 Subject: [PATCH 027/140] Update markers after a reflow --- src/Buffer.test.ts | 118 +++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 54 +++++++++++++++-- src/common/CircularList.ts | 20 +++++-- src/common/EventEmitter.ts | 13 ++++ 4 files changed, 195 insertions(+), 10 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 4c348ec6..7abf6f72 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -392,6 +392,124 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); }); + it('should adjust markers when reflowing', () => { + buffer.fillViewportRows(); + buffer.resize(10, 15); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2).set(i, [null, char, 1, code]); + } + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 15); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), '01'); + assert.equal(buffer.lines.get(6).translateToString(), '23'); + assert.equal(buffer.lines.get(7).translateToString(), '45'); + assert.equal(buffer.lines.get(8).translateToString(), '67'); + assert.equal(buffer.lines.get(9).translateToString(), '89'); + assert.equal(buffer.lines.get(10).translateToString(), 'kl'); + assert.equal(buffer.lines.get(11).translateToString(), 'mn'); + assert.equal(buffer.lines.get(12).translateToString(), 'op'); + assert.equal(buffer.lines.get(13).translateToString(), 'qr'); + assert.equal(buffer.lines.get(14).translateToString(), 'st'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); + assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); + buffer.resize(10, 15); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); + assert.equal(firstMarker.isDisposed, false); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + }); + it('should dispose markers whose rows are trimmed during a reflow', () => { + buffer.fillViewportRows(); + terminal.options.scrollback = 1; + buffer.resize(10, 10); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2).set(i, [null, char, 1, code]); + } + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + buffer.y = 2; + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ij'); + assert.equal(buffer.lines.get(1).translateToString(), '01'); + assert.equal(buffer.lines.get(2).translateToString(), '23'); + assert.equal(buffer.lines.get(3).translateToString(), '45'); + assert.equal(buffer.lines.get(4).translateToString(), '67'); + assert.equal(buffer.lines.get(5).translateToString(), '89'); + assert.equal(buffer.lines.get(6).translateToString(), 'kl'); + assert.equal(buffer.lines.get(7).translateToString(), 'mn'); + assert.equal(buffer.lines.get(8).translateToString(), 'op'); + assert.equal(buffer.lines.get(9).translateToString(), 'qr'); + assert.equal(buffer.lines.get(10).translateToString(), 'st'); + assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4'); + assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped'); + assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ij '); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(secondMarker.line, 1, 'second marker should be restored'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored'); + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 27662708..5b9af742 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CircularList } from './common/CircularList'; +import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; @@ -238,7 +238,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this.hasScrollback && this._bufferLineConstructor === BufferLine) { + if (this._hasScrollback && this._bufferLineConstructor === BufferLine) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -338,6 +338,13 @@ export class Buffer implements IBuffer { for (let i = 0; i < this.lines.length; i++) { if (nextToRemoveStart === i) { const countToRemove = toRemove[++nextToRemoveIndex]; + + // Tell markers that there was a deletion + this.lines.emit('delete', { + index: i - countRemovedSoFar, + amount: countToRemove + } as IDeleteEvent); + i += countToRemove - 1; countRemovedSoFar += countToRemove; nextToRemoveStart = toRemove[++nextToRemoveIndex]; @@ -474,6 +481,10 @@ export class Buffer implements IBuffer { // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many // costly calls to CircularList.splice. if (toInsert.length > 0) { + // Record buffer insert events and then play them back backwards so that the indexes are + // correct + const insertEvents: IInsertEvent[] = []; + // Record original lines so they don't get overridden when we rearrange the list const originalLines: BufferLine[] = []; for (let i = 0; i < this.lines.length; i++) { @@ -492,14 +503,32 @@ export class Buffer implements IBuffer { for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { this.lines.set(i--, nextToInsert.newLines[nextI]); } - i++; // Don't skip for the first row + i++; + + // Create insert events for later + insertEvents.push({ + index: originalLineIndex + 1, + amount: nextToInsert.newLines.length + } as IInsertEvent); + countInsertedSoFar += nextToInsert.newLines.length; nextToInsert = toInsert[++nextToInsertIndex]; } else { this.lines.set(i, originalLines[originalLineIndex--]); } } - // TODO: Throw trim event + + // Update markers + let insertCountEmitted = 0; + for (let i = insertEvents.length - 1; i >= 0; i--) { + insertEvents[i].index += insertCountEmitted; + this.lines.emit('insert', insertEvents[i]); + insertCountEmitted += insertEvents[i].amount; + } + const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); + if (amountToTrim > 0) { + this.lines.emitMayRemoveListeners('trim', amountToTrim); + } } } @@ -618,12 +647,27 @@ export class Buffer implements IBuffer { marker.dispose(); } })); + marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => { + if (marker.line >= event.index) { + marker.line += event.amount; + } + })); + marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => { + // Delete the marker if it's within the range + if (marker.line >= event.index && marker.line < event.index + event.amount) { + marker.dispose(); + } + + // Shift the marker if it's after the deleted range + if (marker.line > event.index) { + marker.line -= event.amount; + } + })); marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker))); return marker; } private _removeMarker(marker: Marker): void { - // TODO: This could probably be optimized by relying on sort order and trimming the array using .length this.markers.splice(this.markers.indexOf(marker), 1); } diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 9faf534a..af4d8af5 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -6,6 +6,16 @@ import { EventEmitter } from './EventEmitter'; import { ICircularList } from './Types'; +export interface IInsertEvent { + index: number; + amount: number; +} + +export interface IDeleteEvent { + index: number; + amount: number; +} + /** * Represents a circular list; a list with a maximum size that wraps around when push is called, * overriding values at the start of the list. @@ -91,7 +101,7 @@ export class CircularList extends EventEmitter implements ICircularList { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); } else { this._length++; } @@ -107,7 +117,7 @@ export class CircularList extends EventEmitter implements ICircularList { throw new Error('Can only recycle when the buffer is full'); } this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); return this._array[this._getCyclicIndex(this._length - 1)]!; } @@ -158,7 +168,7 @@ export class CircularList extends EventEmitter implements ICircularList { const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; this._length = this._maxLength; - this.emit('trim', countToTrim); + this.emitMayRemoveListeners('trim', countToTrim); } else { this._length += items.length; } @@ -175,7 +185,7 @@ export class CircularList extends EventEmitter implements ICircularList { } this._startIndex += count; this._length -= count; - this.emit('trim', count); + this.emitMayRemoveListeners('trim', count); } public shiftElements(start: number, count: number, offset: number): void { @@ -199,7 +209,7 @@ export class CircularList extends EventEmitter implements ICircularList { while (this._length > this._maxLength) { this._length--; this._startIndex++; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); } } } else { diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index f9c0c001..c6b09901 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -75,6 +75,19 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab } } + public emitMayRemoveListeners(type: string, ...args: any[]): void { + if (!this._events[type]) { + return; + } + const obj = this._events[type]; + let length = obj.length; + for (let i = 0; i < obj.length; i++) { + obj[i].apply(this, args); + i -= length - obj.length; + length = obj.length; + } + } + public listeners(type: string): XtermListener[] { return this._events[type] || []; } From 6559931f34239fb0dd1c9e26b4b1cedc1c6c5294 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 14:28:52 -0800 Subject: [PATCH 028/140] Add lots of tests --- src/Buffer.test.ts | 393 +++++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 2 +- 2 files changed, 394 insertions(+), 1 deletion(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 7abf6f72..81d4484b 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -510,6 +510,399 @@ describe('Buffer', () => { assert.equal(secondMarker.line, 1, 'second marker should be restored'); assert.equal(thirdMarker.line, 2, 'third marker should be restored'); }); + + describe('reflowLarger cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'ab' + // 'cd' (wrapped) + // 'ef' + // 'gh' (wrapped) + // 'ij' + // 'kl' (wrapped) + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(2, 10); + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).isWrapped = true; + buffer.lines.get(2).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(2).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(3).set(0, [null, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(3).set(1, [null, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(3).isWrapped = true; + buffer.lines.get(4).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(4).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(5).set(0, [null, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(5).set(1, [null, 'l', 1, 'l'.charCodeAt(0)]); + buffer.lines.get(5).isWrapped = true; + }); + describe('viewport not yet filled', () => { + it('should move the cursor up and add empty lines', () => { + buffer.y = 6; + buffer.resize(4, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should move the cursor up and add empty lines', () => { + buffer.resize(4, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + terminal.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.y = 9; + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); + describe('reflowSmaller cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'abcd' + // 'efgh' (wrapped) + // 'ijkl' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(0).set(2, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(0).set(3, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(1).set(2, [null, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(1).set(3, [null, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(2).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(2).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(2).set(2, [null, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(2).set(3, [null, 'l', 1, 'l'.charCodeAt(0)]); + }); + describe('viewport not yet filled', () => { + it('should move the cursor down', () => { + buffer.y = 3; + buffer.resize(2, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), 'kl'); + for (let i = 6; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should trim the top', () => { + buffer.resize(2, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 3); + assert.equal(buffer.ybase, 3); + assert.equal(buffer.lines.length, 13); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), 'kl'); + for (let i = 6; i < 13; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 13); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'ab'); + assert.equal(buffer.lines.get(11).translateToString(), 'cd'); + assert.equal(buffer.lines.get(12).translateToString(), 'ef'); + assert.equal(buffer.lines.get(13).translateToString(), 'gh'); + assert.equal(buffer.lines.get(14).translateToString(), 'ij'); + assert.equal(buffer.lines.get(15).translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'ab'); + assert.equal(buffer.lines.get(11).translateToString(), 'cd'); + assert.equal(buffer.lines.get(12).translateToString(), 'ef'); + assert.equal(buffer.lines.get(13).translateToString(), 'gh'); + assert.equal(buffer.lines.get(14).translateToString(), 'ij'); + assert.equal(buffer.lines.get(15).translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + terminal.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 10); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(7).translateToString(), 'ab'); + assert.equal(buffer.lines.get(8).translateToString(), 'cd'); + assert.equal(buffer.lines.get(9).translateToString(), 'ef'); + assert.equal(buffer.lines.get(10).translateToString(), 'gh'); + assert.equal(buffer.lines.get(11).translateToString(), 'ij'); + assert.equal(buffer.lines.get(12).translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(7).translateToString(), 'ab'); + assert.equal(buffer.lines.get(8).translateToString(), 'cd'); + assert.equal(buffer.lines.get(9).translateToString(), 'ef'); + assert.equal(buffer.lines.get(10).translateToString(), 'gh'); + assert.equal(buffer.lines.get(11).translateToString(), 'ij'); + assert.equal(buffer.lines.get(12).translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 5b9af742..861471b5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -470,9 +470,9 @@ export class Buffer implements IBuffer { } } else { if (this.ybase === this.ydisp) { - this.ybase++; this.ydisp++; } + this.ybase++; } } } From 2ce67b89bcb62ccf95d34ceb285d02e2f1ed4a6c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 14:43:50 -0800 Subject: [PATCH 029/140] Remove unneeded MockTerminal member --- src/ui/TestUtils.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 3b59ee5d..10033a33 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -19,9 +19,6 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { - eraseAttr(): number { - throw new Error('Method not implemented.'); - } markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); From 338558d86c6a5a018347664bf7bdead3b89c3e63 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 20:00:16 -0800 Subject: [PATCH 030/140] Make Terminal API properties readonly Fixes #1867 --- src/Buffer.test.ts | 4 ++-- src/BufferSet.test.ts | 4 ++-- src/Linkifier.test.ts | 14 +++++++------- src/SelectionManager.test.ts | 4 ++-- src/SelectionModel.test.ts | 4 ++-- src/Types.ts | 2 +- typings/xterm.d.ts | 18 +++++++++++------- 7 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..9d1d50c8 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -19,8 +19,8 @@ describe('Buffer', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = INIT_COLS; - terminal.rows = INIT_ROWS; + (terminal as any).cols = INIT_COLS; + (terminal as any).rows = INIT_ROWS; terminal.options.scrollback = 1000; buffer = new Buffer(terminal, true); }); diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 26f9cd42..576a8ca4 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -15,8 +15,8 @@ describe('BufferSet', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = 80; - terminal.rows = 24; + (terminal as any).cols = 80; + (terminal as any).rows = 24; terminal.options.scrollback = 1000; bufferSet = new BufferSet(terminal); }); diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0ba1294a..07dbf1b3 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -41,8 +41,8 @@ describe('Linkifier', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = 100; - terminal.rows = 10; + (terminal as any).cols = 100; + (terminal as any).rows = 10; terminal.buffer = new MockBuffer(); (terminal.buffer).setLines(new CircularList(20)); terminal.buffer.ydisp = 0; @@ -65,7 +65,7 @@ describe('Linkifier', () => { function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { addRow(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - terminal.rows = terminal.buffer.lines.length - 1; + (terminal as any).rows = terminal.buffer.lines.length - 1; linkifier.linkifyRows(); // Allow linkify to happen setTimeout(() => { @@ -142,19 +142,19 @@ describe('Linkifier', () => { }); describe('multi-line links', () => { it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); }); it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); }); it('should match links that start and end on line 2/2 of a wrapped line', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); }); it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); }); }); diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 2f74ccda..21bc6754 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -45,8 +45,8 @@ describe('SelectionManager', () => { beforeEach(() => { terminal = new TestMockTerminal(); - terminal.cols = 80; - terminal.rows = 2; + (terminal as any).cols = 80; + (terminal as any).rows = 2; terminal.options.scrollback = 100; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 8d4b30bb..d49f41d0 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -23,8 +23,8 @@ describe('SelectionManager', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = 80; - terminal.rows = 2; + (terminal as any).cols = 80; + (terminal as any).rows = 2; terminal.options.scrollback = 10; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/Types.ts b/src/Types.ts index 8ebb28d3..e6f36ac4 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -237,7 +237,7 @@ export interface IBufferAccessor { } export interface IElementAccessor { - element: HTMLElement; + readonly element: HTMLElement; } export interface ILinkifierAccessor { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7528bb55..b0924296 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -325,28 +325,32 @@ declare module 'xterm' { /** * The element containing the terminal. */ - element: HTMLElement; + readonly element: HTMLElement; /** * The textarea that accepts input for the terminal. */ - textarea: HTMLTextAreaElement; + readonly textarea: HTMLTextAreaElement; /** - * The number of rows in the terminal's viewport. + * The number of rows in the terminal's viewport. Use + * `ITerminalOptions.rows` to set this in the constructor and + * `Terminal.resize` for when the terminal exists. */ - rows: number; + readonly rows: number; /** - * The number of columns in the terminal's viewport. + * The number of columns in the terminal's viewport. Use + * `ITerminalOptions.cols` to set this in the constructor and + * `Terminal.resize` for when the terminal exists. */ - cols: number; + readonly cols: number; /** * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt * buffer is active this will always return []. */ - markers: IMarker[]; + readonly markers: IMarker[]; /** * Natural language strings that can be localized. From 5fd764a89d5bfeed0661608f38d3a3f7a301963f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 2 Jan 2019 18:10:51 +0100 Subject: [PATCH 031/140] dont pullin everything from .vscode folder --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b50ab2d9..2ee021f8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ coverage/ # Keep bundled code out of Git dist/ demo/dist/ + +# dont pullin other files from .vscode than launch.json +.vscode/ +!.vscode/launch.json From dfc853aef4506150689f98cf38b2992d99d8a9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 2 Jan 2019 22:50:15 +0100 Subject: [PATCH 032/140] add TextDecoder for string to UTF32 --- src/EscapeSequenceParser.test.ts | 30 +++++------ src/core/input/TextDecoder.test.ts | 67 +++++++++++++++++++++++ src/core/input/TextDecoder.ts | 87 ++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 src/core/input/TextDecoder.test.ts create mode 100644 src/core/input/TextDecoder.ts diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 011ff2ae..64e61ba3 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1192,7 +1192,7 @@ describe('EscapeSequenceParser', function (): void { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); @@ -1200,7 +1200,7 @@ describe('EscapeSequenceParser', function (): void { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); @@ -1210,7 +1210,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1221,7 +1221,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([], 'Should not fallback once'); chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1231,15 +1231,15 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', () => order.push(1)); parser2.addCsiHandler('m', () => { order.push(2); return false; }); parser2.addCsiHandler('m', () => { order.push(3); return false; }); - parser2.parse('\x1b[0m'); - chai.expect(order).eql([3, 2, 1]); + parse(parser2, INPUT); + chai.expect(order).eql([3, 2, 1, 3, 2, 1]); }); it('Dispose should work', () => { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1249,7 +1249,7 @@ describe('EscapeSequenceParser', function (): void { const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); customHandler.dispose(); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1286,7 +1286,7 @@ describe('EscapeSequenceParser', function (): void { const oscCustom: [number, string][] = []; parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); @@ -1294,7 +1294,7 @@ describe('EscapeSequenceParser', function (): void { const oscCustom: [number, string][] = []; parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); @@ -1304,7 +1304,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); chai.expect(oscCustom2).eql([[1, 'foo=bar']]); @@ -1315,7 +1315,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, data => osc.push([1, data])); parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([], 'Should not fallback once'); chai.expect(oscCustom2).eql([[1, 'foo=bar']]); @@ -1325,7 +1325,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, () => order.push(1)); parser2.addOscHandler(1, () => { order.push(2); return false; }); parser2.addOscHandler(1, () => { order.push(3); return false; }); - parser2.parse('\x1b]1;foo=bar\x1b\\'); + parse(parser2, '\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { @@ -1333,7 +1333,7 @@ describe('EscapeSequenceParser', function (): void { parser2.setOscHandler(1, data => osc.push([1, data])); const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1343,7 +1343,7 @@ describe('EscapeSequenceParser', function (): void { const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); customHandler.dispose(); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts new file mode 100644 index 00000000..f69fbded --- /dev/null +++ b/src/core/input/TextDecoder.test.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { StringToUtf32, stringFromCodePoint } from './TextDecoder'; + + +// convert UTF32 codepoints to string +function toString(data: Uint32Array, length: number): string { + if ((String as any).fromCodePoint) { + return (String as any).fromCodePoint.apply(null, data.subarray(0, length)); + } + let result = ''; + for (let i = 0; i < length; ++i) { + result += stringFromCodePoint(data[i]); + } + return result; +} + +describe('StringToUtf32 Decoder', () => { + describe('full codepoint test', () => { + it('0..65535', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 0; i < 65536; ++i) { + // skip surrogate pairs + if (i >= 0xD800 && i <= 0xDFFF) { + continue; + } + const length = decoder.decode(String.fromCharCode(i), target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(toString(target, length), String.fromCharCode(i)); + decoder.clear(); + } + }); + it('65536..0x10FFFF (surrogates)', function(): void { + this.timeout(20000); + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 65536; i < 0x10FFFF; ++i) { + const codePoint = i - 0x10000; + const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + const length = decoder.decode(s, target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(toString(target, length), s); + decoder.clear(); + } + }); + }); + describe('stream handling', () => { + it('surrogates mixed advance by 1', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; + let decoded = ''; + for (let i = 0; i < input.length; ++i) { + const written = decoder.decode(input[i], target); + decoded += toString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); + }); +}); diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts new file mode 100644 index 00000000..77e6971c --- /dev/null +++ b/src/core/input/TextDecoder.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. + * To keep the decoder in line with JS strings it handles single surrogates as UCS2. + */ +export class StringToUtf32 { + private _interim: number = 0; + + /** + * Clears interim and resets decoder to clean state. + */ + public clear(): void { + this._interim = 0; + } + + /** + * Decode JS string to UTF32 codepoints. + * The methods assumes stream input and will store partly transmitted + * surrogate pairs and decode them with the next data chunk. + * Note: The method does no bound checks for target, therefore make sure + * the provided input data does not exceed the size of `target`. + * Returns the number of written codepoints in `target`. + */ + decode(input: string, target: Uint32Array): number { + const length = input.length; + + if (!length) { + return 0; + } + + let size = 0; + let startPos = 0; + + // handle leftover surrogate high + if (this._interim) { + const second = input.charCodeAt(startPos++); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = this._interim; + target[size++] = second; + } + this._interim = 0; + } + + for (let i = startPos; i < length; ++i) { + const code = input.charCodeAt(i); + // surrogate pair first + if (0xD800 <= code && code <= 0xDBFF) { + if (++i >= length) { + this._interim = code; + return size; + } + const second = input.charCodeAt(i); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = code; + target[size++] = second; + } + continue; + } + target[size++] = code; + } + return size; + } +} + +/** + * Polyfill - Convert UTF32 codepoint into JS string. + */ +export function stringFromCodePoint(codePoint: number): string { + if ((String as any).fromCodePoint) { + return (String as any).fromCodePoint(codePoint); + } + if (codePoint > 0xFFFF) { + codePoint -= 0x10000; + return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + } + return String.fromCharCode(codePoint); +} From 2918a6763f5023b3032069c3785d2bc6abf0df78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 2 Jan 2019 22:55:50 +0100 Subject: [PATCH 033/140] fix wrongly changed test case --- src/EscapeSequenceParser.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 64e61ba3..3c9db585 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1231,8 +1231,8 @@ describe('EscapeSequenceParser', function (): void { parser2.setCsiHandler('m', () => order.push(1)); parser2.addCsiHandler('m', () => { order.push(2); return false; }); parser2.addCsiHandler('m', () => { order.push(3); return false; }); - parse(parser2, INPUT); - chai.expect(order).eql([3, 2, 1, 3, 2, 1]); + parse(parser2, '\x1b[0m'); + chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const csiCustom: [string, number[], string][] = []; From 8fc66a39e32c6465ad5e49f492f0f5530b72983c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Jan 2019 00:48:00 +0100 Subject: [PATCH 034/140] switch parse buffer to UTF32 --- src/EscapeSequenceParser.test.ts | 27 ++++++------ src/EscapeSequenceParser.ts | 14 +++---- src/InputHandler.test.ts | 2 +- src/InputHandler.ts | 66 ++++++++---------------------- src/Types.ts | 8 ++-- src/common/TypedArrayUtils.test.ts | 4 +- src/common/TypedArrayUtils.ts | 15 +++++-- 7 files changed, 57 insertions(+), 79 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 3c9db585..135cc393 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -6,6 +6,7 @@ import { ParserState, IDcsHandler, IParsingState } from './Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from './EscapeSequenceParser'; import * as chai from 'chai'; +import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; function r(a: number, b: number): string[] { let c = b - a; @@ -50,10 +51,10 @@ const testTerminal: any = { compare: function (value: any): void { chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here }, - print: function (data: Uint16Array, start: number, end: number): void { + print: function (data: Uint32Array, start: number, end: number): void { let s = ''; for (let i = start; i < end; ++i) { - s += String.fromCharCode(data[i]); + s += stringFromCodePoint(data[i]); } this.calls.push(['print', s]); }, @@ -72,10 +73,10 @@ const testTerminal: any = { actionDCSHook: function (collect: string, params: number[], flag: string): void { this.calls.push(['dcs hook', collect, params, flag]); }, - actionDCSPrint: function (data: Uint16Array, start: number, end: number): void { + actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { let s = ''; for (let i = start; i < end; ++i) { - s += String.fromCharCode(data[i]); + s += stringFromCodePoint(data[i]); } this.calls.push(['dcs put', s]); }, @@ -89,7 +90,7 @@ class DcsTest implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); } - put(data: Uint16Array, start: number, end: number): void { + put(data: Uint32Array, start: number, end: number): void { testTerminal.actionDCSPrint(data, start, end); } unhook(): void { @@ -165,11 +166,9 @@ interface IRun { // translate string based parse calls into typed array based function parse(parser: TestEscapeSequenceParser, data: string): void { - const container = new Uint16Array(data.length); - for (let i = 0; i < data.length; ++i) { - container[i] = data.charCodeAt(i); - } - parser.parse(container, data.length); + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + parser.parse(container, decoder.decode(data, container)); } describe('EscapeSequenceParser', function (): void { @@ -1143,9 +1142,9 @@ describe('EscapeSequenceParser', function (): void { clearAccu(); }); it('print handler', function (): void { - parser2.setPrintHandler(function (data: Uint16Array, start: number, end: number): void { + parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { for (let i = start; i < end; ++i) { - print += String.fromCharCode(data[i]); + print += stringFromCodePoint(data[i]); } }); parse(parser2, INPUT); @@ -1353,10 +1352,10 @@ describe('EscapeSequenceParser', function (): void { hook: function (collect: string, params: number[], flag: number): void { dcs.push(['hook', collect, params, flag]); }, - put: function (data: Uint16Array, start: number, end: number): void { + put: function (data: Uint32Array, start: number, end: number): void { let s = ''; for (let i = start; i < end; ++i) { - s += String.fromCharCode(data[i]); + s += stringFromCodePoint(data[i]); } dcs.push(['put', s]); }, diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index cee9957b..7bd425d4 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -6,7 +6,7 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -import { utf16ToString } from './common/TypedArrayUtils'; +import { utf32ToString } from './common/TypedArrayUtils'; interface IHandlerCollection { [key: string]: T[]; @@ -204,7 +204,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { */ class DcsDummy implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { } - put(data: Uint16Array, start: number, end: number): void { } + put(data: Uint32Array, start: number, end: number): void { } unhook(): void { } } @@ -230,7 +230,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: string; // handler lookup containers - protected _printHandler: (data: Uint16Array, start: number, end: number) => void; + protected _printHandler: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlers: any; protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; @@ -240,7 +240,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: (data: Uint16Array, start: number, end: number) => void; + protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; @@ -296,7 +296,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = null; } - setPrintHandler(callback: (data: Uint16Array, start: number, end: number) => void): void { + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { this._printHandler = callback; } clearPrintHandler(): void { @@ -399,7 +399,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._activeDcsHandler = null; } - parse(data: Uint16Array, length: number): void { + parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; let error = false; @@ -567,7 +567,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc += utf16ToString(data.subarray(i, j)); + osc += utf32ToString(data.subarray(i, j)); i = j - 1; break; } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index b92d009b..a7963a2c 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -337,7 +337,7 @@ describe('InputHandler', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); const inputHandler = new InputHandler(term); - const container = new Uint16Array(10); + const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3a2dab11..e270a5f3 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,7 +14,8 @@ import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -import { concat, utf16ToString } from './common/TypedArrayUtils'; +import { concat, utf32ToString } from './common/TypedArrayUtils'; +import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; /** * Map collect to glevel. Used in `selectCharset`. @@ -32,17 +33,17 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * not supported */ class RequestTerminfo implements IDcsHandler { - private _data: Uint16Array = new Uint16Array(0); + private _data: Uint32Array = new Uint32Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { - this._data = new Uint16Array(0); + this._data = new Uint32Array(0); } - put(data: Uint16Array, start: number, end: number): void { + put(data: Uint32Array, start: number, end: number): void { this._data = concat(this._data, data.subarray(start, end)); } unhook(): void { - const data = utf16ToString(this._data); - this._data = new Uint16Array(0); + const data = utf32ToString(this._data); + this._data = new Uint32Array(0); // invalid: DCS 0 + r Pt ST this._terminal.handler(`${C0.ESC}P0+r${data}${C0.ESC}\\`); } @@ -55,21 +56,21 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) */ class DECRQSS implements IDcsHandler { - private _data: Uint16Array = new Uint16Array(0); + private _data: Uint32Array = new Uint32Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { - this._data = new Uint16Array(0); + this._data = new Uint32Array(0); } - put(data: Uint16Array, start: number, end: number): void { + put(data: Uint32Array, start: number, end: number): void { this._data = concat(this._data, data.subarray(start, end)); } unhook(): void { - const data = utf16ToString(this._data); - this._data = new Uint16Array(0); + const data = utf32ToString(this._data); + this._data = new Uint32Array(0); switch (data) { // valid: DCS 1 $ r Pt ST (xterm) case '"q': // DECSCA @@ -118,8 +119,8 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _surrogateFirst: string; - private _parseBuffer: Uint16Array = new Uint16Array(4096); + private _parseBuffer: Uint32Array = new Uint32Array(4096); + private _stringDecoder: StringToUtf32 = new StringToUtf32(); constructor( protected _terminal: IInputHandlingTerminal, @@ -129,8 +130,6 @@ export class InputHandler extends Disposable implements IInputHandler { this.register(this._parser); - this._surrogateFirst = ''; - /** * custom fallback handlers */ @@ -316,19 +315,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.log('data: ' + data); } - // apply leftover surrogate high from last write - if (this._surrogateFirst) { - data = this._surrogateFirst + data; - this._surrogateFirst = ''; - } - if (this._parseBuffer.length < data.length) { - this._parseBuffer = new Uint16Array(data.length); + this._parseBuffer = new Uint32Array(data.length); } for (let i = 0; i < data.length; ++i) { this._parseBuffer[i] = data.charCodeAt(i); } - this._parser.parse(this._parseBuffer, data.length); + this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer)); buffer = this._terminal.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { @@ -336,7 +329,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } - public print(data: Uint16Array, start: number, end: number): void { + public print(data: Uint32Array, start: number, end: number): void { let code: number; let char: string; let chWidth: number; @@ -352,30 +345,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); for (let pos = start; pos < end; ++pos) { code = data[pos]; - char = String.fromCharCode(code); - - // surrogate pair handling - if (0xD800 <= code && code <= 0xDBFF) { - if (++pos >= 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 = String.fromCharCode(code); - continue; - } - const second = data[pos]; - // 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 += String.fromCharCode(second); - } else { - pos--; - } - } + char = stringFromCodePoint(code); // calculate print space // expensive call, therefore we save width in line buffer diff --git a/src/Types.ts b/src/Types.ts index ca50afd5..60b86de1 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -111,7 +111,7 @@ export interface ICompositionHelper { */ export interface IInputHandler { parse(data: string): void; - print(data: Uint16Array, start: number, end: number): void; + print(data: Uint32Array, start: number, end: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -463,7 +463,7 @@ export interface IParsingState { */ export interface IDcsHandler { hook(collect: string, params: number[], flag: number): void; - put(data: Uint16Array, start: number, end: number): void; + put(data: Uint32Array, start: number, end: number): void; unhook(): void; } @@ -480,9 +480,9 @@ export interface IEscapeSequenceParser extends IDisposable { * Parse string `data`. * @param data The data to parse. */ - parse(data: Uint16Array, length: number): void; + parse(data: Uint32Array, length: number): void; - setPrintHandler(callback: (data: Uint16Array, start: number, end: number) => void): void; + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; clearPrintHandler(): void; setExecuteHandler(flag: string, callback: () => void): void; diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts index b25b9b9a..79546ca9 100644 --- a/src/common/TypedArrayUtils.test.ts +++ b/src/common/TypedArrayUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { fillFallback, concat, utf16ToString } from './TypedArrayUtils'; +import { fillFallback, concat, utf32ToString } from './TypedArrayUtils'; type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array @@ -100,6 +100,6 @@ describe('typed array convenience functions', () => { for (let i = 0; i < s.length; ++i) { data[i] = s.charCodeAt(i); } - assert.equal(utf16ToString(data), s); + assert.equal(utf32ToString(data), s); }); }); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 5d3bcb8a..5ec1fd48 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -1,3 +1,5 @@ +import { stringFromCodePoint } from '../core/input/TextDecoder'; + /** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT @@ -51,11 +53,18 @@ export function concat(a: T, b: T): T { } /** - * Convert UTF16 char codes into JS string. + * Convert UTF32 char codes into JS string. * Note the typed array is not limited to Uint16Array, make sure to align * the values to 0-65535 integer for other typed array types, otherwise * the conversion will fail. */ -export function utf16ToString(data: T): string { - return String.fromCharCode.apply(null, data); +export function utf32ToString(data: T): string { + if ((String as any).fromCodePoint) { + return (String as any).fromCodePoint.apply(null, data); + } + let result = ''; + for (let i = 0; i < data.length; ++i) { + result += stringFromCodePoint(data[i]); + } + return result; } From 27b91f6b00c3a8d8d25bb0850518b8ccb75d8485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Jan 2019 02:55:11 +0100 Subject: [PATCH 035/140] speedup utf32ToString conversion --- src/EscapeSequenceParser.ts | 2 +- src/common/TypedArrayUtils.ts | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 7bd425d4..ec7a9da7 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -567,7 +567,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc += utf32ToString(data.subarray(i, j)); + osc += utf32ToString(data, i, j); i = j - 1; break; } diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 5ec1fd48..5f5be782 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -1,5 +1,3 @@ -import { stringFromCodePoint } from '../core/input/TextDecoder'; - /** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT @@ -54,17 +52,17 @@ export function concat(a: T, b: T): T { /** * Convert UTF32 char codes into JS string. - * Note the typed array is not limited to Uint16Array, make sure to align - * the values to 0-65535 integer for other typed array types, otherwise - * the conversion will fail. */ -export function utf32ToString(data: T): string { - if ((String as any).fromCodePoint) { - return (String as any).fromCodePoint.apply(null, data); - } +export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { let result = ''; - for (let i = 0; i < data.length; ++i) { - result += stringFromCodePoint(data[i]); + let cp; + for (let i = start; i < end; ++i) { + if ((cp = data[i]) > 0xFFFF) { + cp -= 0x10000; + result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); + } else { + result += String.fromCharCode(cp); + } } return result; } From ba1582cf3c5e0c6d57523b8f703bc9fa8eff2526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Jan 2019 19:01:43 +0100 Subject: [PATCH 036/140] apply utf32 buffer layout --- src/BufferLine.ts | 109 ++++++++++++++++++++++++++-------- src/core/input/TextDecoder.ts | 3 - 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 3f93af62..c16c3808 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -3,7 +3,8 @@ * @license MIT */ import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; +import { stringFromCodePoint } from './core/input/TextDecoder'; /** * Class representing a terminal line. @@ -131,18 +132,75 @@ export class BufferLineJSArray implements IBufferLine { } } + +/** + * buffer memory layout: + * + * | uint32_t | uint32_t | uint32_t | + * | `content` | `FG` | `BG` | + * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | + */ + + /** typed array slots taken by one cell */ const CELL_SIZE = 3; -/** cell member indices */ +/** + * Cell member indices. + * + * Direct access: + * `content = data[column * CELL_SIZE + Cell.CONTENT];` + * `fg = data[column * CELL_SIZE + Cell.FG];` + * `bg = data[column * CELL_SIZE + Cell.BG];` + */ const enum Cell { - FLAGS = 0, - STRING = 1, - WIDTH = 2 + CONTENT = 0, + FG = 1, // currently simply holds all known attrs + BG = 2 // currently unused } -/** single vs. combined char distinction */ -const IS_COMBINED_BIT_MASK = 0x80000000; +/** + * Bitmasks and helper for accessing data in `content`. + */ +const enum Content { + /** + * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) + * read: `codepoint = content & Content.codepointMask;` + * write: `content |= codepoint & Content.codepointMask;` + * shortcut if precondition `codepoint <= 0x10FFFF` is met: + * `content |= codepoint;` + */ + CODEPOINT_MASK = 0x1FFFFF, + + /** + * bit 22 flag indication whether a cell contains combined content + * read: `isCombined = content & Content.isCombined;` + * set: `content |= Content.isCombined;` + * clear: `content &= ~Content.isCombined;` + */ + IS_COMBINED = 0x200000, // 1 << 21 + + /** + * bit 1..22 mask to check whether a cell contains any string data + * we need to check for codepoint and isCombined bits to see + * whether a cell contains anything + * read: `isEmtpy = !(content & Content.hasContent)` + */ + HAS_CONTENT = 0x2FFFFF, + + /** + * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) + * read: `width = (content & Content.widthMask) >> Content.widthShift;` + * `hasWidth = content & Content.widthMask;` + * as long as wcwidth is highest value in `content`: + * `width = content >> Content.widthShift;` + * write: `content |= (width << Content.widthShift) & Content.widthMask;` + * shortcut if precondition `0 <= width <= 3` is met: + * `content |= width << Content.widthShift;` + */ + WIDTH_MASK = 0xC00000, // 3 << 22 + WIDTH_SHIFT = 22 +} /** * Typed array based bufferline implementation. @@ -166,28 +224,28 @@ export class BufferLine implements IBufferLine { } public get(index: number): CharData { - const stringData = this._data[index * CELL_SIZE + Cell.STRING]; + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + const cp = content & Content.CODEPOINT_MASK; return [ - this._data[index * CELL_SIZE + Cell.FLAGS], - (stringData & IS_COMBINED_BIT_MASK) + this._data[index * CELL_SIZE + Cell.FG], + (content & Content.IS_COMBINED) ? this._combined[index] - : (stringData) ? String.fromCharCode(stringData) : '', - this._data[index * CELL_SIZE + Cell.WIDTH], - (stringData & IS_COMBINED_BIT_MASK) + : (cp) ? String.fromCharCode(cp) : '', + content >> Content.WIDTH_SHIFT, + (content & Content.IS_COMBINED) ? this._combined[index].charCodeAt(this._combined[index].length - 1) - : stringData + : cp ]; } public set(index: number, value: CharData): void { - this._data[index * CELL_SIZE + Cell.FLAGS] = value[0]; - if (value[1].length > 1) { + this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; + if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK; + this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } else { - this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0); + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } - this._data[index * CELL_SIZE + Cell.WIDTH] = value[2]; } public insertCells(pos: number, n: number, fillCharData: CharData): void { @@ -284,8 +342,6 @@ export class BufferLine implements IBufferLine { /** create a new clone */ public clone(): IBufferLine { const newLine = new BufferLine(0); - // creation of new typed array from another is actually pretty slow :( - // still faster than copying values one by one newLine._data = new Uint32Array(this._data); newLine.length = this.length; for (const el in this._combined) { @@ -297,8 +353,8 @@ export class BufferLine implements IBufferLine { public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { - if (this._data[i * CELL_SIZE + Cell.STRING] !== 0) { // 0 ==> ''.charCodeAt(0) ==> NaN ==> 0 - return i + this._data[i * CELL_SIZE + Cell.WIDTH]; + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); } } return 0; @@ -310,9 +366,10 @@ export class BufferLine implements IBufferLine { } let result = ''; while (startCol < endCol) { - const stringData = this._data[startCol * CELL_SIZE + Cell.STRING]; - result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR; - startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1; + const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; + const cp = content & Content.CODEPOINT_MASK; + result += (content & Content.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 } return result; } diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index 77e6971c..04407a09 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -76,9 +76,6 @@ export class StringToUtf32 { * Polyfill - Convert UTF32 codepoint into JS string. */ export function stringFromCodePoint(codePoint: number): string { - if ((String as any).fromCodePoint) { - return (String as any).fromCodePoint(codePoint); - } if (codePoint > 0xFFFF) { codePoint -= 0x10000; return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); From a2a8c3d47f2e448a9358d9195651db3146565d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Jan 2019 21:08:18 +0100 Subject: [PATCH 037/140] extend buffer line with more direct access methods --- src/BufferLine.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++ src/InputHandler.ts | 13 +++++------ src/Types.ts | 3 +++ 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index c16c3808..7c70c93b 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -248,6 +248,59 @@ export class BufferLine implements IBufferLine { } } + /** + * Set cell data from input handler. + * Since the input handler see the incoming chars as UTF32 codepoints, + * it gets an optimized access method. + */ + public setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.FG] = fg; + this._data[index * CELL_SIZE + Cell.BG] = bg; + } + + /** + * Add a char to a cell from input handler. + * During input stage combining chars with a width of 0 follow and stack + * onto a leading char. Since we already set the attrs + * by the previous `setDataFromCodePoint` call, we can omit it here. + */ + public addCharToCell(index: number, codePoint: number): void { + let content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED) { + // we already have a combined string, simply add + this._combined[index] += stringFromCodePoint(codePoint); + } else { + if (content & Content.CODEPOINT_MASK) { + // normal case for combining chars: + // - move current leading char + new one into combined string + // - set codepoint in cell buffer to index + // - set combined flag + this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); + content &= ~Content.CODEPOINT_MASK; + content |= index | Content.IS_COMBINED; + } else { + // should not happen - we actually have no data in the cell yet + // simply set the data in the cell buffer with a width of 1 + content = codePoint | (1 << Content.WIDTH_SHIFT); + } + this._data[index * CELL_SIZE + Cell.CONTENT] = content; + } + } + + /** + * Set data from another buffer cell. + * Useful for basic in buffer copy action. + */ + public setDataFromCellData(index: number, content: number, fg: number, bg: number, combined?: string): void { + this._data[index * CELL_SIZE + Cell.CONTENT] = content; + this._data[index * CELL_SIZE + Cell.FG] = fg; + this._data[index * CELL_SIZE + Cell.BG] = bg; + if (content & Content.IS_COMBINED && combined) { + this._combined[index] = combined; + } + } + public insertCells(pos: number, n: number, fillCharData: CharData): void { pos %= this.length; if (n < this.length - pos) { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e270a5f3..bf8149b2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -331,7 +331,6 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { let code: number; - let char: string; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -345,7 +344,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); for (let pos = start; pos < end; ++pos) { code = data[pos]; - char = stringFromCodePoint(code); // calculate print space // expensive call, therefore we save width in line buffer @@ -355,15 +353,14 @@ export class InputHandler extends Disposable implements IInputHandler { // charset are only defined for ASCII, therefore we only // search for an replacement char if code < 127 if (code < 127 && charset) { - const ch = charset[char]; + const ch = charset[String.fromCharCode(code)]; if (ch) { code = ch.charCodeAt(0); - char = ch; } } if (screenReaderMode) { - this._terminal.emit('a11y.char', char); + this._terminal.emit('a11y.char', stringFromCodePoint(code)); } // insert combining char at last cursor position @@ -380,12 +377,12 @@ export class InputHandler extends Disposable implements IInputHandler { // since an empty cell is only set by fullwidth chars const chMinusTwo = bufferRow.get(buffer.x - 2); if (chMinusTwo) { - chMinusTwo[CHAR_DATA_CHAR_INDEX] += char; + chMinusTwo[CHAR_DATA_CHAR_INDEX] += stringFromCodePoint(code); chMinusTwo[CHAR_DATA_CODE_INDEX] = code; bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now } } else { - chMinusOne[CHAR_DATA_CHAR_INDEX] += char; + chMinusOne[CHAR_DATA_CHAR_INDEX] += stringFromCodePoint(code); chMinusOne[CHAR_DATA_CODE_INDEX] = code; bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now } @@ -438,7 +435,7 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]); + bufferRow.set(buffer.x++, [curAttr, stringFromCodePoint(code), chWidth, code]); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero diff --git a/src/Types.ts b/src/Types.ts index 60b86de1..5c4b4880 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -519,6 +519,9 @@ export interface IBufferLine { isWrapped: boolean; get(index: number): CharData; set(index: number, value: CharData): void; + setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; + addCharToCell(index: number, codePoint: number): void; + setDataFromCellData(index: number, content: number, fg: number, bg: number, combined?: string): void; insertCells(pos: number, n: number, ch: CharData): void; deleteCells(pos: number, n: number, fill: CharData): void; replaceCells(start: number, end: number, fill: CharData): void; From b0eecea45aa3c13053bed58d3c744067913424fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Jan 2019 21:40:34 +0100 Subject: [PATCH 038/140] partially apply fast data access in InputHandler.print --- src/BufferLine.ts | 2 +- src/InputHandler.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 7963934e..af838144 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -105,7 +105,7 @@ export class BufferLine implements IBufferLine { this._data[index * CELL_SIZE + Cell.FG], (content & Content.IS_COMBINED) ? this._combined[index] - : (cp) ? String.fromCharCode(cp) : '', + : (cp) ? stringFromCodePoint(cp) : '', content >> Content.WIDTH_SHIFT, (content & Content.IS_COMBINED) ? this._combined[index].charCodeAt(this._combined[index].length - 1) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index bf8149b2..d19be5a1 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -435,14 +435,14 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow.set(buffer.x++, [curAttr, stringFromCodePoint(code), chWidth, code]); + bufferRow.setDataFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero // we already made sure above, that buffer.x + chWidth will not overflow right if (chWidth > 0) { while (--chWidth) { - bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]); + bufferRow.setDataFromCodePoint(buffer.x++, 0, 0, curAttr, 0); } } } From 75fbda1fe6297f5aeb4fbb0f8493d16e137f3e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 01:32:20 +0100 Subject: [PATCH 039/140] further opt for InputHandler.print --- src/InputHandler.ts | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index d19be5a1..2c6198d3 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -350,7 +350,7 @@ export class InputHandler extends Disposable implements IInputHandler { chWidth = wcwidth(code); // get charset replacement character - // charset are only defined for ASCII, therefore we only + // charset is only defined for ASCII, therefore we only // search for an replacement char if code < 127 if (code < 127 && charset) { const ch = charset[String.fromCharCode(code)]; @@ -370,22 +370,13 @@ export class InputHandler extends Disposable implements IInputHandler { // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { const chMinusOne = bufferRow.get(buffer.x - 1); - if (chMinusOne) { - if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) { - // found empty cell after fullwidth, need to go 2 cells back - // it is save to step 2 cells back here - // since an empty cell is only set by fullwidth chars - const chMinusTwo = bufferRow.get(buffer.x - 2); - if (chMinusTwo) { - chMinusTwo[CHAR_DATA_CHAR_INDEX] += stringFromCodePoint(code); - chMinusTwo[CHAR_DATA_CODE_INDEX] = code; - bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now - } - } else { - chMinusOne[CHAR_DATA_CHAR_INDEX] += stringFromCodePoint(code); - chMinusOne[CHAR_DATA_CODE_INDEX] = code; - bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now - } + if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) { + // found empty cell after fullwidth, need to go 2 cells back + // it is save to step 2 cells back here + // since an empty cell is only set by fullwidth chars + bufferRow.addCharToCell(buffer.x - 2, code); + } else { + bufferRow.addCharToCell(buffer.x - 1, code); } continue; } @@ -430,7 +421,7 @@ export class InputHandler extends Disposable implements IInputHandler { // and will be set to eraseChar const lastCell = bufferRow.get(cols - 1); if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) { - bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } From 2f66efa49a107cfd4585b3d272b8248dabdec8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 01:32:56 +0100 Subject: [PATCH 040/140] first try to optimize read access --- src/BufferLine.ts | 44 +++++++++++++++++++++++++++++++-- src/renderer/TextRenderLayer.ts | 22 ++++++++--------- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index af838144..1700279c 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -7,7 +7,6 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, import { stringFromCodePoint } from './core/input/TextDecoder'; - /** * buffer memory layout: * @@ -37,7 +36,7 @@ const enum Cell { /** * Bitmasks and helper for accessing data in `content`. */ -const enum Content { +export const enum Content { /** * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) * read: `codepoint = content & Content.codepointMask;` @@ -77,6 +76,25 @@ const enum Content { WIDTH_SHIFT = 22 } +export class CellData { + public content: number = 0; + public fg: number = 0; + public bg: number = 0; + public combinedData: string = ''; + public get combined(): number { + return this.content & Content.IS_COMBINED; + } + public get width(): number { + return this.content >> Content.WIDTH_SHIFT; + } + public get chars(): string { + return (this.content & Content.IS_COMBINED) ? this.combinedData : stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + } + public get code(): number { + return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); + } +} + /** * Typed array based bufferline implementation. */ @@ -123,6 +141,28 @@ export class BufferLine implements IBufferLine { } } + public loadCell(index: number, cell: CellData): CellData { + cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; + cell.fg = this._data[index * CELL_SIZE + Cell.FG]; + cell.bg = this._data[index * CELL_SIZE + Cell.BG]; + if (cell.content & Content.IS_COMBINED) { + cell.combinedData = this._combined[index]; + } + return cell; + } + + public setCell(index: number, cell: CellData): void { + if (cell.content & Content.IS_COMBINED) { + this._combined[index] = cell.combinedData; + // we also need to clear and set codepoint to index + cell.content &= ~Content.CODEPOINT_MASK; + cell.content |= index; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; + this._data[index * CELL_SIZE + Cell.FG] = cell.fg; + this._data[index * CELL_SIZE + Cell.BG] = cell.bg; + } + /** * Set cell data from input handler. * Since the input handler see the incoming chars as UTF32 codepoints, diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index ade2dd4c..931c4651 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { CharData, ITerminal } from '../Types'; import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; import { is256Color } from './atlas/CharAtlasUtils'; +import { CellData } from '../BufferLine'; /** * This CharData looks like a null character, which will forc a clear and render @@ -24,6 +25,7 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterFont: string; private _characterOverlapCache: { [key: string]: boolean } = {}; private _characterJoinerRegistry: ICharacterJoinerRegistry; + private _cell = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) { super(container, 'text', zIndex, alpha, colors); @@ -72,14 +74,14 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - const charData = line.get(x); - let code: number = charData[CHAR_DATA_CODE_INDEX] || WHITESPACE_CELL_CODE; + (line as any).loadCell(x, this._cell); + let code: number = this._cell.code || WHITESPACE_CELL_CODE; // Can either represent character(s) for a single cell or multiple cells // if indicated by a character joiner. - let chars: string = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; - const attr: number = charData[CHAR_DATA_ATTR_INDEX]; - let width: number = charData[CHAR_DATA_WIDTH_INDEX]; + let chars = this._cell.chars || WHITESPACE_CELL_CHAR; + const attr = this._cell.fg; + let width = this._cell.width; // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -117,7 +119,7 @@ export class TextRenderLayer extends BaseRenderLayer { // right is a space, take ownership of the cell to the right. We skip // this check for joined characters because their rendering likely won't // yield the same result as rendering the last character individually. - if (!isJoined && this._isOverlapping(charData)) { + if (!isJoined && this._isOverlapping(chars, width, code)) { // If the character is overlapping, we want to force a re-render on every // frame. This is specifically to work around the case where two // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a @@ -271,21 +273,19 @@ export class TextRenderLayer extends BaseRenderLayer { /** * Whether a character is overlapping to the next cell. */ - private _isOverlapping(charData: CharData): boolean { + private _isOverlapping(char: string, width: number, code: number): boolean { // Only single cell characters can be overlapping, rendering issues can // occur without this check - if (charData[CHAR_DATA_WIDTH_INDEX] !== 1) { + if (width !== 1) { return false; } // We assume that any ascii character will not overlap - const code = charData[CHAR_DATA_CODE_INDEX]; if (code < 256) { return false; } // Deliver from cache if available - const char = charData[CHAR_DATA_CHAR_INDEX]; if (this._characterOverlapCache.hasOwnProperty(char)) { return this._characterOverlapCache[char]; } From f3619ab61c4aedc2bb74e824be9c6e7184f64ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 01:33:49 +0100 Subject: [PATCH 041/140] fix linter error --- src/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2c6198d3..7cc2b8b6 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; +import { CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; From 802543f7cf2a593be079a5d9e782bb07ac6c526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 02:58:15 +0100 Subject: [PATCH 042/140] replace set(CharData) with setCell --- src/BufferLine.ts | 78 ++++++++++++++++++++++++++++----------------- src/InputHandler.ts | 10 +++--- src/Types.ts | 16 +++++++++- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 1700279c..827e7c93 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -2,7 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine } from './Types'; +import { CharData, IBufferLine, ICellData } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { stringFromCodePoint } from './core/input/TextDecoder'; @@ -76,7 +76,7 @@ export const enum Content { WIDTH_SHIFT = 22 } -export class CellData { +export class CellData implements ICellData { public content: number = 0; public fg: number = 0; public bg: number = 0; @@ -93,6 +93,31 @@ export class CellData { public get code(): number { return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); } + public setFromCharData(value: CharData): void { + this.fg = value[CHAR_DATA_ATTR_INDEX]; + this.bg = 0; + let combined = false; + if (value[CHAR_DATA_CHAR_INDEX].length > 2) { + combined = true; + } else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { + const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); + if (0xD800 <= code && code <= 0xDBFF) { + const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); + if (0xDC00 <= second && second <= 0xDFFF) { + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } else { + combined = true; + } + } + combined = true; + } else { + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + if (combined) { + this.combinedData = value[CHAR_DATA_CHAR_INDEX]; + this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + } } /** @@ -101,16 +126,15 @@ export class CellData { export class BufferLine implements IBufferLine { protected _data: Uint32Array | null = null; protected _combined: {[index: number]: string} = {}; + protected _cell: CellData = new CellData(); public length: number; constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) { - if (!fillCharData) { - fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } if (cols) { this._data = new Uint32Array(cols * CELL_SIZE); + this._cell.setFromCharData(fillCharData || [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let i = 0; i < cols; ++i) { - this.set(i, fillCharData); + this.setCell(i, this._cell); } } this.length = cols; @@ -141,7 +165,7 @@ export class BufferLine implements IBufferLine { } } - public loadCell(index: number, cell: CellData): CellData { + public loadCell(index: number, cell: ICellData): ICellData { cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; cell.fg = this._data[index * CELL_SIZE + Cell.FG]; cell.bg = this._data[index * CELL_SIZE + Cell.BG]; @@ -151,7 +175,7 @@ export class BufferLine implements IBufferLine { return cell; } - public setCell(index: number, cell: CellData): void { + public setCell(index: number, cell: ICellData): void { if (cell.content & Content.IS_COMBINED) { this._combined[index] = cell.combinedData; // we also need to clear and set codepoint to index @@ -203,31 +227,20 @@ export class BufferLine implements IBufferLine { } } - /** - * Set data from another buffer cell. - * Useful for basic in buffer copy action. - */ - public setDataFromCellData(index: number, content: number, fg: number, bg: number, combined?: string): void { - this._data[index * CELL_SIZE + Cell.CONTENT] = content; - this._data[index * CELL_SIZE + Cell.FG] = fg; - this._data[index * CELL_SIZE + Cell.BG] = bg; - if (content & Content.IS_COMBINED && combined) { - this._combined[index] = combined; - } - } - public insertCells(pos: number, n: number, fillCharData: CharData): void { pos %= this.length; if (n < this.length - pos) { for (let i = this.length - pos - n - 1; i >= 0; --i) { - this.set(pos + n + i, this.get(pos + i)); + this.setCell(pos + n + i, this.loadCell(pos + i, this._cell)); } + this._cell.setFromCharData(fillCharData); for (let i = 0; i < n; ++i) { - this.set(pos + i, fillCharData); + this.setCell(pos + i, this._cell); } } else { + this._cell.setFromCharData(fillCharData); for (let i = pos; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, this._cell); } } } @@ -236,21 +249,24 @@ export class BufferLine implements IBufferLine { pos %= this.length; if (n < this.length - pos) { for (let i = 0; i < this.length - pos - n; ++i) { - this.set(pos + i, this.get(pos + n + i)); + this.setCell(pos + i, this.loadCell(pos + n + i, this._cell)); } + this._cell.setFromCharData(fillCharData); for (let i = this.length - n; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, this._cell); } } else { + this._cell.setFromCharData(fillCharData); for (let i = pos; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, this._cell); } } } public replaceCells(start: number, end: number, fillCharData: CharData): void { + this._cell.setFromCharData(fillCharData); while (start < end && start < this.length) { - this.set(start++, fillCharData); + this.setCell(start++, this._cell); } } @@ -268,8 +284,9 @@ export class BufferLine implements IBufferLine { } } this._data = data; + this._cell.setFromCharData(fillCharData); for (let i = this.length; i < cols; ++i) { - this.set(i, fillCharData); + this.setCell(i, this._cell); } } else if (shrink) { if (cols) { @@ -286,8 +303,9 @@ export class BufferLine implements IBufferLine { /** fill a line with fillCharData */ public fill(fillCharData: CharData): void { this._combined = {}; + this._cell.setFromCharData(fillCharData); for (let i = 0; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, this._cell); } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7cc2b8b6..f837f295 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; +import { DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; @@ -16,6 +16,7 @@ import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat, utf32ToString } from './common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; +import { CellData } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -121,6 +122,7 @@ class DECRQSS implements IDcsHandler { export class InputHandler extends Disposable implements IInputHandler { private _parseBuffer: Uint32Array = new Uint32Array(4096); private _stringDecoder: StringToUtf32 = new StringToUtf32(); + private _cell: CellData = new CellData(); constructor( protected _terminal: IInputHandlingTerminal, @@ -369,8 +371,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - const chMinusOne = bufferRow.get(buffer.x - 1); - if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) { + if (!bufferRow.loadCell(buffer.x - 1, this._cell).width) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars @@ -419,8 +420,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to eraseChar - const lastCell = bufferRow.get(cols - 1); - if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) { + if (bufferRow.loadCell(cols - 1, this._cell).width === 2) { bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } diff --git a/src/Types.ts b/src/Types.ts index b89d5422..180fe000 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -511,6 +511,19 @@ export interface IEscapeSequenceParser extends IDisposable { clearErrorHandler(): void; } +/** Cell data */ +export interface ICellData { + content: number; + fg: number; + bg: number; + combinedData: string; + combined: number; + width: number; + chars: string; + code: number; + setFromCharData(value: CharData): void; +} + /** * Interface for a line in the terminal buffer. */ @@ -519,9 +532,10 @@ export interface IBufferLine { isWrapped: boolean; get(index: number): CharData; set(index: number, value: CharData): void; + loadCell(index: number, cell: ICellData): ICellData; + setCell(index: number, cell: ICellData): void; setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; addCharToCell(index: number, codePoint: number): void; - setDataFromCellData(index: number, content: number, fg: number, bg: number, combined?: string): void; insertCells(pos: number, n: number, ch: CharData): void; deleteCells(pos: number, n: number, fill: CharData): void; replaceCells(start: number, end: number, fill: CharData): void; From 7a416aa18ffb395c716d622dcde275bacc157c59 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 3 Jan 2019 22:49:03 -0800 Subject: [PATCH 043/140] Don't print keys that print more than a single char Fixes #1880 --- src/core/input/Keyboard.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 9d86b349..4fea8adb 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,9 +349,8 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && - ev.keyCode >= 48 && ev.keyCode !== 144 && ev.keyCode !== 145) { - // Include only keys that that result in a character; don't include num lock and scroll lock + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) { + // Include only keys that that result in a _single_ character; don't include num lock, volume up, etc. result.key = ev.key; } break; From d7e5977d9ab825c5c9941f7a81371b6df897c69c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 16:19:01 +0100 Subject: [PATCH 044/140] remove leftover --- src/InputHandler.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f837f295..b53e9115 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -320,9 +320,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); } - for (let i = 0; i < data.length; ++i) { - this._parseBuffer[i] = data.charCodeAt(i); - } this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer)); buffer = this._terminal.buffer; From 13ffed392d5067dc2c0f9aed51b064d2c367cf26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 18:36:07 +0100 Subject: [PATCH 045/140] remove get calls from renderer --- src/BufferLine.ts | 13 ++++++++++++- src/renderer/TextRenderLayer.ts | 2 +- src/renderer/dom/DomRendererRowFactory.ts | 17 ++++++++--------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 827e7c93..2792d710 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -77,6 +77,11 @@ export const enum Content { } export class CellData implements ICellData { + public static fromCharData(value: CharData): CellData { + const obj = new CellData(); + obj.setFromCharData(value); + return obj; + } public content: number = 0; public fg: number = 0; public bg: number = 0; @@ -88,7 +93,13 @@ export class CellData implements ICellData { return this.content >> Content.WIDTH_SHIFT; } public get chars(): string { - return (this.content & Content.IS_COMBINED) ? this.combinedData : stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + if (this.content & Content.IS_COMBINED) { + return this.combinedData; + } + if (this.content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + } + return ''; } public get code(): number { return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 931c4651..815eef17 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -127,7 +127,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).code === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8bcde39a..83a4651e 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; import { IBufferLine } from '../../Types'; import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { CellData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; @@ -16,6 +17,7 @@ export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { + private _cell: CellData = new CellData(); constructor( private _document: Document ) { @@ -31,19 +33,16 @@ export class DomRendererRowFactory { // 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)) { + if (lineData.loadCell(x, this._cell).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] || WHITESPACE_CELL_CHAR; - const attr = charData[CHAR_DATA_ATTR_INDEX]; - const width = charData[CHAR_DATA_WIDTH_INDEX]; + lineData.loadCell(x, this._cell); + const attr = this._cell.fg; + const width = this._cell.width; // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -101,7 +100,7 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } - charElement.textContent = char; + charElement.textContent = this._cell.chars || WHITESPACE_CELL_CHAR; if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); } From 6552a023c168fd94f4b0be70d597a8ed4534b354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 18:46:45 +0100 Subject: [PATCH 046/140] remove get from linkifier --- src/Linkifier.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 53247c95..3499c87d 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -7,8 +7,8 @@ import { IMouseZoneManager } from './ui/Types'; import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './common/EventEmitter'; -import { CHAR_DATA_ATTR_INDEX } from './Buffer'; import { getStringCellWidth } from './CharWidth'; +import { CellData } from './BufferLine'; /** * The Linkifier applies links to rows shortly after they have been refreshed. @@ -34,6 +34,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _rowsTimeoutId: number; private _nextLinkMatcherId = 0; private _rowsToLinkify: { start: number, end: number }; + private _cell: CellData = new CellData(); constructor( protected _terminal: ITerminal @@ -232,11 +233,10 @@ export class Linkifier extends EventEmitter implements ILinkifier { } const line = this._terminal.buffer.lines.get(bufferIndex[0]); - const char = line.get(bufferIndex[1]); + line.loadCell(bufferIndex[1], this._cell); let fg: number | undefined; - if (char) { - const attr: number = char[CHAR_DATA_ATTR_INDEX]; - fg = (attr >> 9) & 0x1ff; + if (this._cell.fg) { + fg = (this._cell.fg >> 9) & 0x1ff; } if (matcher.validationCallback) { From b93b774971a9be93093d6eb553bd2b6c152610da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Jan 2019 19:14:56 +0100 Subject: [PATCH 047/140] direct cell attrs getter --- src/BufferLine.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/Types.ts | 10 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 2792d710..b3a94672 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -176,6 +176,47 @@ export class BufferLine implements IBufferLine { } } + /** + * primitive getters + * use these when only one value is needed, otherwise use `loadCell` + */ + public getWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; + } + public hasWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; + } + public getFG(index: number): number { + return this._data[index * CELL_SIZE + Cell.FG]; + } + public getBG(index: number): number { + return this._data[index * CELL_SIZE + Cell.BG]; + } + public hasContent(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; + } + public getCodePoint(index: number): number { + // returns either the single codepoint or the last charCode in combined + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED) { + return this._combined[index].charCodeAt(this._combined[index].length - 1); + } + return content & Content.CODEPOINT_MASK; + } + public isCombined(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; + } + public getString(index: number): string { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED) { + return this._combined[index]; + } + if (content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(content & Content.CODEPOINT_MASK); + } + return ''; // return empty string for empty cells + } + public loadCell(index: number, cell: ICellData): ICellData { cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; cell.fg = this._data[index * CELL_SIZE + Cell.FG]; diff --git a/src/Types.ts b/src/Types.ts index 180fe000..c1a7788a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -545,4 +545,14 @@ export interface IBufferLine { clone(): IBufferLine; getTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; + + /* direct access to cell attrs */ + getWidth(index: number): number; + hasWidth(index: number): number; + getFG(index: number): number; + getBG(index: number): number; + hasContent(index: number): number; + getCodePoint(index: number): number; + isCombined(index: number): number; + getString(index: number): string; } From 62dfa84d5d9ce248f955421007243220973aae81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 5 Jan 2019 01:01:10 +0100 Subject: [PATCH 048/140] remove get CharData calls from codebase (beside tests) --- src/Buffer.ts | 2 +- src/Linkifier.ts | 8 ++- src/SelectionManager.ts | 70 +++++++++++++------------ src/Terminal.ts | 4 +- src/renderer/BaseRenderLayer.ts | 10 ++-- src/renderer/CharacterJoinerRegistry.ts | 18 +++---- src/renderer/CursorRenderLayer.ts | 35 +++++++------ src/renderer/TextRenderLayer.ts | 2 +- 8 files changed, 75 insertions(+), 74 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 625a2497..32548eeb 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -227,7 +227,7 @@ export class Buffer implements IBuffer { return [-1, -1]; } for (let i = 0; i < line.length; ++i) { - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; + stringIndex -= line.getString(i).length; if (stringIndex < 0) { return [lineIndex, i]; } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 3499c87d..a2b9045b 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -8,7 +8,6 @@ import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './common/EventEmitter'; import { getStringCellWidth } from './CharWidth'; -import { CellData } from './BufferLine'; /** * The Linkifier applies links to rows shortly after they have been refreshed. @@ -34,7 +33,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _rowsTimeoutId: number; private _nextLinkMatcherId = 0; private _rowsToLinkify: { start: number, end: number }; - private _cell: CellData = new CellData(); constructor( protected _terminal: ITerminal @@ -233,10 +231,10 @@ export class Linkifier extends EventEmitter implements ILinkifier { } const line = this._terminal.buffer.lines.get(bufferIndex[0]); - line.loadCell(bufferIndex[1], this._cell); + const attr = line.getFG(bufferIndex[1]); let fg: number | undefined; - if (this._cell.fg) { - fg = (this._cell.fg >> 9) & 0x1ff; + if (attr) { + fg = (attr >> 9) & 0x1ff; } if (matcher.validationCallback) { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1aea1cb5..d615dda0 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,15 +3,15 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types'; +import { ITerminal, ISelectionManager, IBuffer, IBufferLine } from './Types'; import { XtermListener } from './common/Types'; import { MouseHelper } from './ui/MouseHelper'; import * as Browser from './core/Platform'; import { CharMeasure } from './ui/CharMeasure'; import { EventEmitter } from './common/EventEmitter'; import { SelectionModel } from './SelectionModel'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { AltClickHandler } from './handlers/AltClickHandler'; +import { CellData } from './BufferLine'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -103,6 +103,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; private _trimListener: XtermListener; + private _cell: CellData = new CellData(); private _mouseDownTimeStamp: number; @@ -506,8 +507,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the mouse is over the second half of a wide character, adjust the // selection to cover the whole character - const char = line.get(this._model.selectionStart[0]); - if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + if (line.hasWidth(this._model.selectionStart[0]) === 0) { this._model.selectionStart[0]++; } } @@ -596,8 +596,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // selection. Note that selections at the very end of the line will never // have a character. if (this._model.selectionEnd[1] < this._buffer.lines.length) { - const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]); - if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) { + if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) { this._model.selectionEnd[0]++; } } @@ -670,16 +669,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const char = bufferLine.get(i); - if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + const length = bufferLine.loadCell(i, this._cell).chars.length; + if (this._cell.width === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. charIndex--; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) { + } else if (length > 1 && coords[0] !== i) { // Emojis take up multiple characters, so adjust accordingly. For these // we don't want ot include the character at the column as we're // returning the start index in the string, not the end index. - charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + charIndex += length - 1; } } return charIndex; @@ -739,48 +738,51 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Consider the initial position, skip it and increment the wide char // variable - if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) { + if (bufferLine.getWidth(startCol) === 0) { leftWideCharCount++; startCol--; } - if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) { + if (bufferLine.getWidth(endCol) === 2) { rightWideCharCount++; endCol++; } // Adjust the end index for characters whose length are > 1 (emojis) - if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) { - rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; - endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; + const length = bufferLine.getString(endCol).length; + if (length > 1) { + rightLongCharOffset += length - 1; + endIndex += length - 1; } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) { - const char = bufferLine.get(startCol - 1); - if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._cell))) { + bufferLine.loadCell(startCol - 1, this._cell); + const length = this._cell.chars.length; + if (this._cell.width === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + } else if (length > 1) { // If the next character's string is longer than 1 char (eg. emoji), // adjust the index - leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1; - startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1; + leftLongCharOffset += length - 1; + startIndex -= length - 1; } startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) { - const char = bufferLine.get(endCol + 1); - if (char[CHAR_DATA_WIDTH_INDEX] === 2) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._cell))) { + bufferLine.loadCell(endCol + 1, this._cell); + const length = this._cell.chars.length; + if (this._cell.width === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + } else if (length > 1) { // If the next character's string is longer than 1 char (eg. emoji), // adjust the index - rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1; - endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + rightLongCharOffset += length - 1; + endIndex += length - 1; } endIndex++; endCol++; @@ -814,9 +816,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Recurse upwards if the line is wrapped and the word wraps to the above line if (followWrappedLinesAbove) { - if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) { const previousBufferLine = this._buffer.lines.get(coords[1] - 1); - if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) { const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false); if (previousLineWordPosition) { const offset = this._terminal.cols - previousLineWordPosition.start; @@ -829,9 +831,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Recurse downwards if the line is wrapped and the word wraps to the next line if (followWrappedLinesBelow) { - if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) { const nextBufferLine = this._buffer.lines.get(coords[1] + 1); - if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) { const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true); if (nextLineWordPosition) { length += nextLineWordPosition.length; @@ -894,13 +896,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * word logic. * @param char The character to check. */ - private _isCharWordSeparator(charData: CharData): boolean { + private _isCharWordSeparator(cell: CellData): boolean { // Zero width characters are never separators as they are always to the // right of wide characters - if (charData[CHAR_DATA_WIDTH_INDEX] === 0) { + if (cell.width === 0) { return false; } - return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0; + return WORD_SEPARATORS.indexOf(cell.chars) >= 0; } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index bc97de29..a467d6dd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,7 +25,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions 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, CHAR_DATA_ATTR_INDEX } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; @@ -1175,7 +1175,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { + if (!newLine || newLine.length !== this.cols || newLine.getFG(0) !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); this._blankLine = newLine; } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3e0b8643..f84fbe6b 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -4,12 +4,12 @@ */ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; -import { CharData, ITerminal } from '../Types'; +import { ITerminal } from '../Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { is256Color } from './atlas/CharAtlasUtils'; +import { CellData } from '../BufferLine'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -229,17 +229,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { * ensure that it fits with the cell, including the cell to the right if it's * a wide character. This uses the existing fillStyle on the context. * @param terminal The terminal. - * @param charData The char data for the character to draw. + * @param cell The cell data for the character to draw. * @param x The column to draw at. * @param y The row to draw at. * @param color The color of the character. */ - protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { + protected fillCharTrueColor(terminal: ITerminal, cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( - charData[CHAR_DATA_CHAR_INDEX], + cell.chars, x * this._scaledCellWidth + this._scaledCharLeft, (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); } diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index dc9e95dd..4cad7c72 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,11 +1,12 @@ -import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { ITerminal, IBufferLine } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; +import { CellData } from '../BufferLine'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { private _characterJoiners: ICharacterJoiner[] = []; private _nextCharacterJoinerId: number = 0; + private _cell: CellData = new CellData(); constructor(private _terminal: ITerminal) { } @@ -51,13 +52,13 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9; + let rangeAttr = line.getFG(0) >> 9; for (let x = 0; x < this._terminal.cols; x++) { - const charData = line.get(x); - const chars = charData[CHAR_DATA_CHAR_INDEX]; - const width = charData[CHAR_DATA_WIDTH_INDEX]; - const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9; + line.loadCell(x, this._cell); + const chars = this._cell.chars; + const width = this._cell.width; + const attr = this._cell.fg >> 9; if (width === 0) { // If this character is of width 0, skip it. @@ -152,9 +153,8 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { } for (let x = startCol; x < this._terminal.cols; x++) { - const charData = line.get(x); - const width = charData[CHAR_DATA_WIDTH_INDEX]; - const length = charData[CHAR_DATA_CHAR_INDEX].length; + const width = line.getWidth(x); + const length = line.getString(x).length; // We skip zero-width characters when creating the string to join the text // so we do the same here diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 08a14739..18ba7ada 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CharData, ITerminal } from '../Types'; +import { ITerminal, ICellData } from '../Types'; +import { CellData } from '../BufferLine'; interface ICursorState { x: number; @@ -23,8 +23,9 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; - private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; + private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, cell: ICellData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; + private _cell: ICellData = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'cursor', zIndex, true, colors); @@ -127,8 +128,8 @@ export class CursorRenderLayer extends BaseRenderLayer { return; } - const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x); - if (!charData) { + terminal.buffer.lines.get(cursorY).loadCell(terminal.buffer.x, this._cell); + if (this._cell.content === undefined) { return; } @@ -136,13 +137,13 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); + this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, this._cell); this._ctx.restore(); this._state.x = terminal.buffer.x; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; this._state.style = terminal.options.cursorStyle; - this._state.width = charData[CHAR_DATA_WIDTH_INDEX]; + this._state.width = this._cell.width; return; } @@ -158,21 +159,21 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state.y === viewportRelativeCursorY && this._state.isFocused === terminal.isFocused && this._state.style === terminal.options.cursorStyle && - this._state.width === charData[CHAR_DATA_WIDTH_INDEX]) { + this._state.width === this._cell.width) { return; } this._clearCursor(); } this._ctx.save(); - this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); + this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, this._cell); this._ctx.restore(); this._state.x = terminal.buffer.x; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; this._state.style = terminal.options.cursorStyle; - this._state.width = charData[CHAR_DATA_WIDTH_INDEX]; + this._state.width = this._cell.width; } private _clearCursor(): void { @@ -188,33 +189,33 @@ export class CursorRenderLayer extends BaseRenderLayer { } } - private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderBarCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; this.fillLeftLineAtCell(x, y); this._ctx.restore(); } - private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderBlockCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); + this.fillCells(x, y, cell.width, 1); this._ctx.fillStyle = this._colors.cursorAccent.css; - this.fillCharTrueColor(terminal, charData, x, y); + this.fillCharTrueColor(terminal, cell, x, y); this._ctx.restore(); } - private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; this.fillBottomLineAtCells(x, y); this._ctx.restore(); } - private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderBlurCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.strokeStyle = this._colors.cursor.css; - this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); + this.strokeRectAtCell(x, y, cell.width, 1); this._ctx.restore(); } } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 815eef17..bf7c5616 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { CharData, ITerminal } from '../Types'; import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; From 6581eb7d88015ac5b499d576436133d37ca51ae3 Mon Sep 17 00:00:00 2001 From: jerch Date: Sat, 5 Jan 2019 01:47:44 +0100 Subject: [PATCH 049/140] fix leftover BufferLineConstructor --- src/Buffer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 74cec107..987e0324 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -210,7 +210,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback && this._bufferLineConstructor === BufferLine) { + if (this._hasScrollback) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -343,7 +343,7 @@ export class Buffer implements IBuffer { if (this.ybase === 0) { this.y--; // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); } else { if (this.ydisp === this.ybase) { this.ydisp--; From f090fa822d145bb1b4414819a39b50d9233433dd Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 6 Jan 2019 15:07:36 -0800 Subject: [PATCH 050/140] doc: fix typos --- src/Terminal.ts | 2 +- src/common/EventEmitter.ts | 2 +- src/ui/Lifecycle.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index bc97de29..4c0cd0f8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1391,7 +1391,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * processed by the terminal and what keys should not. * @param customKeyEventHandler The custom KeyboardEvent handler to attach. * This is a function that takes a KeyboardEvent, allowing consumers to stop - * propogation and/or prevent the default action. The function returns whether + * propagation and/or prevent the default action. The function returns whether * the event should be processed by xterm.js. */ public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index f9c0c001..fb95ae92 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -23,7 +23,7 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab } /** - * Adds a disposabe listener to the EventEmitter, returning the disposable. + * Adds a disposable listener to the EventEmitter, returning the disposable. * @param type The event type. * @param handler The handler for the listener. */ diff --git a/src/ui/Lifecycle.ts b/src/ui/Lifecycle.ts index d8367113..9f058106 100644 --- a/src/ui/Lifecycle.ts +++ b/src/ui/Lifecycle.ts @@ -6,7 +6,7 @@ import { IDisposable } from 'xterm'; /** - * Adds a disposabe listener to a node in the DOM, returning the disposable. + * Adds a disposable listener to a node in the DOM, returning the disposable. * @param type The event type. * @param handler The handler for the listener. */ From f04f2efc52b8dad12ca9fab46d47981b8ee9a9f7 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 6 Jan 2019 15:09:57 -0800 Subject: [PATCH 051/140] doc: fix more typos --- src/CompositionHelper.ts | 12 ++++++------ src/SelectionManager.ts | 2 +- src/core/input/Keyboard.ts | 2 +- typings/xterm.d.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index b1745d41..31ad866b 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -111,17 +111,17 @@ export class CompositionHelper { /** * Finalizes the composition, resuming regular input actions. This is called when a composition * is ending. - * @param waitForPropogation Whether to wait for events to propogate before sending + * @param waitForPropagation Whether to wait for events to propagate before sending * the input. This should be false if a non-composition keystroke is entered before the - * compositionend event is triggered, such as enter, so that the composition is send before + * compositionend event is triggered, such as enter, so that the composition is sent before * the command is executed. */ - private _finalizeComposition(waitForPropogation: boolean): void { + private _finalizeComposition(waitForPropagation: boolean): void { this._compositionView.classList.remove('active'); this._isComposing = false; this._clearTextareaPosition(); - if (!waitForPropogation) { + if (!waitForPropagation) { // Cancel any delayed composition send requests and send the input immediately. this._isSendingComposition = false; const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end); @@ -136,8 +136,8 @@ export class CompositionHelper { // Since composition* events happen before the changes take place in the textarea on most // browsers, use a setTimeout with 0ms time to allow the native compositionend event to - // complete. This ensures the correct character is retrieved, this solution was used - // because: + // complete. This ensures the correct character is retrieved. + // This solution was used because: // - The compositionend event's data property is unreliable, at least on Chromium // - The last compositionupdate event's data property does not always accurately describe // the character, a counter example being Korean where an ending consonsant can move to diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1aea1cb5..f93328fe 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -552,7 +552,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager */ private _onMouseMove(event: MouseEvent): void { // If the mousemove listener is active it means that a selection is - // currently being made, we should stop propogation to prevent mouse events + // currently being made, we should stop propagation to prevent mouse events // to be sent to the pty. event.stopImmediatePropagation(); diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 9d86b349..b5df46c4 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -44,7 +44,7 @@ export function evaluateKeyboardEvent( ): IKeyboardResult { const result: IKeyboardResult = { type: KeyboardResultType.SEND_KEY, - // Whether to cancel event propogation (NOTE: this may not be needed since the event is + // Whether to cancel event propagation (NOTE: this may not be needed since the event is // canceled at the end of keyDown cancel: false, // The new key even to emit diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c5d2b020..11fab909 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -465,7 +465,7 @@ declare module 'xterm' { * should be processed by the terminal and what keys should not. * @param customKeyEventHandler The custom KeyboardEvent handler to attach. * This is a function that takes a KeyboardEvent, allowing consumers to stop - * propogation and/or prevent the default action. The function returns + * propagation and/or prevent the default action. The function returns * whether the event should be processed by xterm.js. */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; From 249f8800af98bf9a715b77bedd086bd350f975c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 7 Jan 2019 11:30:26 +0100 Subject: [PATCH 052/140] account empty cells in stringIndexToBufferIndex --- src/Buffer.test.ts | 6 +++--- src/Buffer.ts | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..663c7000 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -511,7 +511,7 @@ describe('Buffer', () => { const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 10; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); const j = (i - 0) << 1; assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); } @@ -523,7 +523,7 @@ describe('Buffer', () => { const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); @@ -535,7 +535,7 @@ describe('Buffer', () => { const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); assert.equal( (!(i % 3)) ? input[i] diff --git a/src/Buffer.ts b/src/Buffer.ts index 625a2497..7b3bcee5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -220,14 +220,16 @@ export class Buffer implements IBuffer { * @param stringIndex index within the string * @param startCol column offset the string was retrieved from */ - public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex { + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { return [-1, -1]; } - for (let i = 0; i < line.length; ++i) { - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; + const length = (trimRight) ? line.getTrimmedLength() : line.length; + for (let i = 0; i < length; ++i) { + if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; if (stringIndex < 0) { return [lineIndex, i]; } From 883ad01bd508e6df1f69127449b31710c8d9b6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 7 Jan 2019 11:39:16 +0100 Subject: [PATCH 053/140] make linter happy --- src/Buffer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7b3bcee5..0ad86bf1 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -228,8 +228,9 @@ export class Buffer implements IBuffer { } const length = (trimRight) ? line.getTrimmedLength() : line.length; for (let i = 0; i < length; ++i) { - if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; + if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) { + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; // WHITESPACE_CELL_CHAR.length + } if (stringIndex < 0) { return [lineIndex, i]; } From 7fe3f0a4e3e094ed82fec0b17219cc0187a3ed69 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 11 Jan 2019 12:58:41 -0800 Subject: [PATCH 054/140] Improve BufferLine test Ensure combined is cleared when shrinking and enlarging --- src/BufferLine.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index f5e95fc8..0ef29505 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -158,14 +158,17 @@ describe('BufferLine', function(): void { line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); - it('should remove combining data', () => { + it('should remove combining data on replaced cells after shrinking then enlarging', () => { const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); + line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]); line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - chai.expect(line.translateToString()).eql('aaaaaaaaa😁'); - chai.expect(Object.keys(line.combined).length).eql(1); + chai.expect(line.translateToString()).eql('aa😁aaaaaa😁'); + chai.expect(Object.keys(line.combined).length).eql(2); line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); - chai.expect(line.translateToString()).eql('aaaaa'); - chai.expect(Object.keys(line.combined).length).eql(0); + chai.expect(line.translateToString()).eql('aa😁aa'); + line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); + chai.expect(line.translateToString()).eql('aa😁aaaaaaa'); + chai.expect(Object.keys(line.combined).length).eql(1); }); }); describe('getTrimLength', function(): void { From 329d40ab14676dd02e5478dbd2c207c7a2bcdfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 13:52:26 +0100 Subject: [PATCH 055/140] cleanup DCS handler --- src/InputHandler.ts | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e270a5f3..dbb01608 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -27,28 +27,6 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * DCS subparser implementations */ - /** - * DCS + q Pt ST (xterm) - * Request Terminfo String - * not supported - */ - class RequestTerminfo implements IDcsHandler { - private _data: Uint32Array = new Uint32Array(0); - constructor(private _terminal: any) { } - hook(collect: string, params: number[], flag: number): void { - this._data = new Uint32Array(0); - } - put(data: Uint32Array, start: number, end: number): void { - this._data = concat(this._data, data.subarray(start, end)); - } - unhook(): void { - const data = utf32ToString(this._data); - this._data = new Uint32Array(0); - // invalid: DCS 0 + r Pt ST - this._terminal.handler(`${C0.ESC}P0+r${data}${C0.ESC}\\`); - } -} - /** * DCS $ q Pt ST * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) @@ -92,7 +70,7 @@ class DECRQSS implements IDcsHandler { default: // invalid: DCS 0 $ r Pt ST (xterm) this._terminal.error('Unknown DCS $q %s', data); - this._terminal.handler(`${C0.ESC}P0$r${data}${C0.ESC}\\`); + this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`); } } } @@ -103,6 +81,12 @@ class DECRQSS implements IDcsHandler { * not supported */ +/** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not implemented + */ + /** * DCS + p Pt ST (xterm) * Set Terminfo Data @@ -292,7 +276,6 @@ export class InputHandler extends Disposable implements IInputHandler { * DCS handler */ this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); - this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); } public dispose(): void { From 88a037b3092f4d05862848ba0ee9ba3fbaed9502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 14:34:29 +0100 Subject: [PATCH 056/140] change insertCells to new interface --- src/BufferLine.test.ts | 4 ++-- src/BufferLine.ts | 8 +++----- src/InputHandler.ts | 10 ++++++++-- src/Types.ts | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index fbf8b051..4ccfe96b 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -41,7 +41,7 @@ describe('BufferLine', function(): void { line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], diff --git a/src/BufferLine.ts b/src/BufferLine.ts index b3a94672..64cb1e19 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -279,20 +279,18 @@ export class BufferLine implements IBufferLine { } } - public insertCells(pos: number, n: number, fillCharData: CharData): void { + public insertCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; if (n < this.length - pos) { for (let i = this.length - pos - n - 1; i >= 0; --i) { this.setCell(pos + n + i, this.loadCell(pos + i, this._cell)); } - this._cell.setFromCharData(fillCharData); for (let i = 0; i < n; ++i) { - this.setCell(pos + i, this._cell); + this.setCell(pos + i, fillCellData); } } else { - this._cell.setFromCharData(fillCharData); for (let i = pos; i < this.length; ++i) { - this.setCell(i, this._cell); + this.setCell(i, fillCellData); } } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ad3713e9..72a86e42 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -396,7 +396,10 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + this._cell.fg = curAttr; + this._cell.bg = 0; + this._cell.content = 0; + bufferRow.insertCells(buffer.x, chWidth, this._cell); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to eraseChar @@ -516,10 +519,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ public insertChars(params: number[]): void { + this._cell.content = 0; + this._cell.fg = this._terminal.eraseAttr(); + this._cell.bg = 0; this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._cell ); this._terminal.updateRange(this._terminal.buffer.y); } diff --git a/src/Types.ts b/src/Types.ts index c1a7788a..216139b7 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -536,7 +536,7 @@ export interface IBufferLine { setCell(index: number, cell: ICellData): void; setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; addCharToCell(index: number, codePoint: number): void; - insertCells(pos: number, n: number, ch: CharData): void; + insertCells(pos: number, n: number, ch: ICellData): void; deleteCells(pos: number, n: number, fill: CharData): void; replaceCells(start: number, end: number, fill: CharData): void; resize(cols: number, fill: CharData, shrink?: boolean): void; From 523ff6e5fc2e0f981e661ede0551a499983f628c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 14:54:54 +0100 Subject: [PATCH 057/140] add null and whitespace placeholder cells to buffer --- src/Buffer.ts | 18 ++++++++++++++++-- src/InputHandler.ts | 5 +---- src/Types.ts | 2 ++ src/ui/TestUtils.test.ts | 8 +++++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 32548eeb..463b2451 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,10 +4,10 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); @@ -45,6 +45,8 @@ export class Buffer implements IBuffer { public savedX: number; public savedCurAttr: number; public markers: Marker[] = []; + private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); /** * Create a new Buffer. @@ -59,6 +61,18 @@ export class Buffer implements IBuffer { this.clear(); } + public getNullCell(fg: number = 0, bg: number = 0): ICellData { + this._nullCell.fg = fg; + this._nullCell.bg = bg; + return this._nullCell; + } + + public getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { + this._whitespaceCell.fg = fg; + this._whitespaceCell.bg = bg; + return this._whitespaceCell; + } + public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; return new BufferLine(this._terminal.cols, fillCharData, isWrapped); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 72a86e42..c8f9e7f2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -519,13 +519,10 @@ export class InputHandler extends Disposable implements IInputHandler { * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ public insertChars(params: number[]): void { - this._cell.content = 0; - this._cell.fg = this._terminal.eraseAttr(); - this._cell.bg = 0; this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, - this._cell + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); this._terminal.updateRange(this._terminal.buffer.y); } diff --git a/src/Types.ts b/src/Types.ts index 216139b7..9a4401b6 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -298,6 +298,8 @@ export interface IBuffer { getBlankLine(attr: number, isWrapped?: boolean): IBufferLine; stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; + getNullCell(fg?: number, bg?: number): ICellData; + getWhitespaceCell(fg?: number, bg?: number): ICellData; } export interface IBufferSet extends IEventEmitter { diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index e6e4aaa3..9d525fbf 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -4,7 +4,7 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData } from '../Types'; import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; import * as Browser from '../core/Platform'; @@ -334,6 +334,12 @@ export class MockBuffer implements IBuffer { iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { return Buffer.prototype.iterator.apply(this, arguments); } + getNullCell(fg: number = 0, bg: number = 0): ICellData { + throw new Error('Method not implemented.'); + } + getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { + throw new Error('Method not implemented.'); + } } export class MockRenderer implements IRenderer { From 4bf7f3eb894d4fe98e674370e783ce17f5c07267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 14:58:30 +0100 Subject: [PATCH 058/140] change deleteCells to new interface --- src/BufferLine.test.ts | 2 +- src/BufferLine.ts | 8 +++----- src/InputHandler.ts | 2 +- src/Types.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 4ccfe96b..68384f35 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -55,7 +55,7 @@ describe('BufferLine', function(): void { line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]); + line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 64cb1e19..fb1d40a9 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -295,20 +295,18 @@ export class BufferLine implements IBufferLine { } } - public deleteCells(pos: number, n: number, fillCharData: CharData): void { + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; if (n < this.length - pos) { for (let i = 0; i < this.length - pos - n; ++i) { this.setCell(pos + i, this.loadCell(pos + n + i, this._cell)); } - this._cell.setFromCharData(fillCharData); for (let i = this.length - n; i < this.length; ++i) { - this.setCell(i, this._cell); + this.setCell(i, fillCellData); } } else { - this._cell.setFromCharData(fillCharData); for (let i = pos; i < this.length; ++i) { - this.setCell(i, this._cell); + this.setCell(i, fillCellData); } } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index c8f9e7f2..2776051d 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -865,7 +865,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, params[0] || 1, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); this._terminal.updateRange(this._terminal.buffer.y); } diff --git a/src/Types.ts b/src/Types.ts index 9a4401b6..e991ac42 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -539,7 +539,7 @@ export interface IBufferLine { setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; addCharToCell(index: number, codePoint: number): void; insertCells(pos: number, n: number, ch: ICellData): void; - deleteCells(pos: number, n: number, fill: CharData): void; + deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: CharData): void; resize(cols: number, fill: CharData, shrink?: boolean): void; fill(fillCharData: CharData): void; From fe6919b52a2d9cd90a6ab59eead49d63d6af640d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 15:05:52 +0100 Subject: [PATCH 059/140] change replaceCells to new interface --- src/BufferLine.test.ts | 2 +- src/BufferLine.ts | 5 ++--- src/InputHandler.ts | 9 +++++---- src/Types.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 68384f35..c2329113 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -71,7 +71,7 @@ describe('BufferLine', function(): void { line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]); + line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [2, 'b', 0, 'b'.charCodeAt(0)], diff --git a/src/BufferLine.ts b/src/BufferLine.ts index fb1d40a9..d9ddae70 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -311,10 +311,9 @@ export class BufferLine implements IBufferLine { } } - public replaceCells(start: number, end: number, fillCharData: CharData): void { - this._cell.setFromCharData(fillCharData); + public replaceCells(start: number, end: number, fillCellData: ICellData): void { while (start < end && start < this.length) { - this.setCell(start++, this._cell); + this.setCell(start++, fillCellData); } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2776051d..37583602 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; +import { DEFAULT_ATTR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; @@ -696,7 +696,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); if (clearWrap) { line.isWrapped = false; @@ -916,7 +916,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, this._terminal.buffer.x + (params[0] || 1), - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); } @@ -972,9 +972,10 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); + line.loadCell(buffer.x - 1, this._cell); line.replaceCells(buffer.x, buffer.x + (params[0] || 1), - line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + (this._cell.content !== undefined) ? this._cell : buffer.getNullCell(DEFAULT_ATTR) ); // FIXME: no updateRange here? } diff --git a/src/Types.ts b/src/Types.ts index e991ac42..83f4e492 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -540,7 +540,7 @@ export interface IBufferLine { addCharToCell(index: number, codePoint: number): void; insertCells(pos: number, n: number, ch: ICellData): void; deleteCells(pos: number, n: number, fill: ICellData): void; - replaceCells(start: number, end: number, fill: CharData): void; + replaceCells(start: number, end: number, fill: ICellData): void; resize(cols: number, fill: CharData, shrink?: boolean): void; fill(fillCharData: CharData): void; copyFrom(line: IBufferLine): void; From 56dec2849fe56af428436207f238819e0c47240f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 15:21:55 +0100 Subject: [PATCH 060/140] change resize, fill to new interface, remove _cell on buffer line --- src/Buffer.ts | 4 +-- src/BufferLine.test.ts | 38 ++++++++++---------- src/BufferLine.ts | 21 ++++++----- src/Types.ts | 4 +-- src/renderer/CharacterJoinerRegistry.test.ts | 16 ++++----- 5 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 463b2451..edba479c 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -150,9 +150,9 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { - const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr? + const cell = this.getNullCell(DEFAULT_ATTR); // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, ch); + this.lines.get(i).resize(newCols, cell); } } diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index c2329113..402b1a02 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -87,7 +87,7 @@ describe('BufferLine', function(): void { line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - line.fill([123, 'z', 0, 'z'.charCodeAt(0)]); + line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], @@ -136,67 +136,67 @@ describe('BufferLine', function(): void { 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)]); + line.resize(10, CellData.fromCharData([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); + line.resize(10, CellData.fromCharData([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); + line.resize(5, CellData.fromCharData([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); + line.resize(5, CellData.fromCharData([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); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(5, CellData.fromCharData([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)]); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(15, CellData.fromCharData([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)]); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(25, CellData.fromCharData([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); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(10, CellData.fromCharData([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); + line.resize(10, CellData.fromCharData([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); + line.resize(0, CellData.fromCharData([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); + line.resize(0, CellData.fromCharData([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); + line.resize(5, CellData.fromCharData([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); + line.resize(7, CellData.fromCharData([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); + line.resize(7, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index d9ddae70..086fe15b 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -137,15 +137,14 @@ export class CellData implements ICellData { export class BufferLine implements IBufferLine { protected _data: Uint32Array | null = null; protected _combined: {[index: number]: string} = {}; - protected _cell: CellData = new CellData(); public length: number; constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) { if (cols) { this._data = new Uint32Array(cols * CELL_SIZE); - this._cell.setFromCharData(fillCharData || [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + const cell = CellData.fromCharData(fillCharData || [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let i = 0; i < cols; ++i) { - this.setCell(i, this._cell); + this.setCell(i, cell); } } this.length = cols; @@ -282,8 +281,9 @@ export class BufferLine implements IBufferLine { public insertCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; if (n < this.length - pos) { + const cell = new CellData(); for (let i = this.length - pos - n - 1; i >= 0; --i) { - this.setCell(pos + n + i, this.loadCell(pos + i, this._cell)); + this.setCell(pos + n + i, this.loadCell(pos + i, cell)); } for (let i = 0; i < n; ++i) { this.setCell(pos + i, fillCellData); @@ -298,8 +298,9 @@ export class BufferLine implements IBufferLine { public deleteCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; if (n < this.length - pos) { + const cell = new CellData(); for (let i = 0; i < this.length - pos - n; ++i) { - this.setCell(pos + i, this.loadCell(pos + n + i, this._cell)); + this.setCell(pos + i, this.loadCell(pos + n + i, cell)); } for (let i = this.length - n; i < this.length; ++i) { this.setCell(i, fillCellData); @@ -317,7 +318,7 @@ export class BufferLine implements IBufferLine { } } - public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { + public resize(cols: number, fillCellData: ICellData, shrink: boolean = false): void { if (cols === this.length || (!shrink && cols < this.length)) { return; } @@ -331,9 +332,8 @@ export class BufferLine implements IBufferLine { } } this._data = data; - this._cell.setFromCharData(fillCharData); for (let i = this.length; i < cols; ++i) { - this.setCell(i, this._cell); + this.setCell(i, fillCellData); } } else if (shrink) { if (cols) { @@ -348,11 +348,10 @@ export class BufferLine implements IBufferLine { } /** fill a line with fillCharData */ - public fill(fillCharData: CharData): void { + public fill(fillCellData: ICellData): void { this._combined = {}; - this._cell.setFromCharData(fillCharData); for (let i = 0; i < this.length; ++i) { - this.setCell(i, this._cell); + this.setCell(i, fillCellData); } } diff --git a/src/Types.ts b/src/Types.ts index 83f4e492..644a9b49 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -541,8 +541,8 @@ export interface IBufferLine { insertCells(pos: number, n: number, ch: ICellData): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData): void; - resize(cols: number, fill: CharData, shrink?: boolean): void; - fill(fillCharData: CharData): void; + resize(cols: number, fill: ICellData, shrink?: boolean): void; + fill(fillCellData: ICellData): void; copyFrom(line: IBufferLine): void; clone(): IBufferLine; getTrimmedLength(): number; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 0c29566a..2f9f45be 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -5,7 +5,7 @@ import { CircularList } from '../common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { BufferLine } from '../BufferLine'; +import { BufferLine, CellData } from '../BufferLine'; import { IBufferLine } from '../Types'; describe('CharacterJoinerRegistry', () => { @@ -24,17 +24,17 @@ describe('CharacterJoinerRegistry', () => { lines.set(4, new BufferLine(0)); lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]])); const line6 = lineData([['wi']]); - line6.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]); - line6.resize(line6.length + 1, [0, '', 0, null]); + line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)])); + line6.resize(line6.length + 1, CellData.fromCharData([0, '', 0, null])); let sub = lineData([['deemo']]); let oldSize = line6.length; - line6.resize(oldSize + sub.length, [0, '', 0, 0]); + line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); - line6.resize(line6.length + 1, [0, '\xf0\x9f\x98\x81', 1, 128513]); - line6.resize(line6.length + 1, [0, ' ', 1, ' '.charCodeAt(0)]); + line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513])); + line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)])); sub = lineData([['jiabc']]); oldSize = line6.length; - line6.resize(oldSize + sub.length, [0, '', 0, 0]); + line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); lines.set(6, line6); @@ -273,7 +273,7 @@ function lineData(data: IPartialLineData[]): IBufferLine { const line = data[i][0]; const attr = (data[i][1] || 0); const offset = tline.length; - tline.resize(tline.length + line.split('').length, [0, '', 0, 0]); + tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0])); line.split('').map((char, idx) => tline.set(idx + offset, [attr, char, 1, char.charCodeAt(0)])); } return tline; From 60a591e8c8a445ded42746e709bbfb5543ee5e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 15:34:22 +0100 Subject: [PATCH 061/140] use getNullChar in InputHandler.print --- src/InputHandler.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 37583602..c41b4165 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -396,13 +396,10 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - this._cell.fg = curAttr; - this._cell.bg = 0; - this._cell.content = 0; - bufferRow.insertCells(buffer.x, chWidth, this._cell); + bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr)); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost - // and will be set to eraseChar + // and will be set to empty cell if (bufferRow.loadCell(cols - 1, this._cell).width === 2) { bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } @@ -416,6 +413,7 @@ export class InputHandler extends Disposable implements IInputHandler { // we already made sure above, that buffer.x + chWidth will not overflow right if (chWidth > 0) { while (--chWidth) { + // other than a regular empty cell a cell following a wide char has no width bufferRow.setDataFromCodePoint(buffer.x++, 0, 0, curAttr, 0); } } From 3e456971b14e8cc27708ddd8ef31b67db2427812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 15:47:50 +0100 Subject: [PATCH 062/140] change buffer line ctor to new interface --- src/Buffer.ts | 8 +++--- src/BufferLine.test.ts | 56 +++++++++++++++++++++--------------------- src/BufferLine.ts | 4 +-- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index edba479c..6295d175 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,7 +4,7 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; +import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine, CellData } from './BufferLine'; @@ -74,8 +74,7 @@ export class Buffer implements IBuffer { } public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { - const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new BufferLine(this._terminal.cols, fillCharData, isWrapped); + return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped); } public get hasScrollback(): boolean { @@ -173,8 +172,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - this.lines.push(new BufferLine(newCols, fillCharData)); + this.lines.push(new BufferLine(newCols, this.getNullCell(DEFAULT_ATTR))); } } } diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 402b1a02..210ba851 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -31,7 +31,7 @@ describe('BufferLine', function(): void { chai.expect(line.length).equals(10); chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(true); - line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true); + line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); chai.expect(line.length).equals(10); chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); @@ -115,7 +115,7 @@ describe('BufferLine', function(): void { line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true); + const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); line2.copyFrom(line); chai.expect(line2.toArray()).eql(line.toArray()); chai.expect(line2.length).equals(line.length); @@ -125,9 +125,9 @@ describe('BufferLine', function(): void { // CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print // --> set code to the last charCodeAt value of the string // Note: needs to be fixed once the string pointer is in place - const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]); + const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)])); chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); - const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true); + const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true); line2.copyFrom(line); chai.expect(line2.toArray()).eql(line.toArray()); const line3 = line.clone(); @@ -135,61 +135,61 @@ describe('BufferLine', function(): void { }); describe('resize', function(): void { it('enlarge(false)', function(): void { - const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); + const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { - const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); + const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), 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); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), 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); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), 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); + const line = new TestBufferLine(20, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([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); + const line = new TestBufferLine(20, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(15, CellData.fromCharData([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); + const line = new TestBufferLine(20, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(25, CellData.fromCharData([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); + const line = new TestBufferLine(20, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([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); + const line = new TestBufferLine(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([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); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), 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); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); @@ -202,29 +202,29 @@ describe('BufferLine', function(): void { }); describe('getTrimLength', function(): void { it('empty line', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); chai.expect(line.getTrimmedLength()).equal(0); }); it('ASCII', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); chai.expect(line.getTrimmedLength()).equal(3); }); it('surrogate', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); chai.expect(line.getTrimmedLength()).equal(3); }); it('combining', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.expect(line.getTrimmedLength()).equal(3); }); it('fullwidth', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, '1', 2, '1'.charCodeAt(0)]); line.set(3, [0, '', 0, undefined]); @@ -233,12 +233,12 @@ describe('BufferLine', function(): void { }); describe('translateToString with and w\'o trimming', function(): void { it('empty line', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(true)).equal(''); }); it('ASCII', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]); @@ -254,7 +254,7 @@ describe('BufferLine', function(): void { }); it('surrogate', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); @@ -269,7 +269,7 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞'); }); it('combining', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); @@ -284,7 +284,7 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301'); }); it('fullwidth', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, '1', 2, '1'.charCodeAt(0)]); line.set(3, [0, '', 0, undefined]); @@ -308,7 +308,7 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 2)).equal('a '); }); it('space at end', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]); @@ -321,12 +321,12 @@ describe('BufferLine', function(): void { // sanity check - broken line with invalid out of bound null width cells // this can atm happen with deleting/inserting chars in inputhandler by "breaking" // fullwidth pairs --> needs to be fixed after settling BufferLine impl - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(true)).equal(''); }); it('should work with endCol=0', () => { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); chai.expect(line.translateToString(true, 0, 0)).equal(''); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 086fe15b..ccea8fe1 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -139,10 +139,10 @@ export class BufferLine implements IBufferLine { protected _combined: {[index: number]: string} = {}; public length: number; - constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) { + constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { if (cols) { this._data = new Uint32Array(cols * CELL_SIZE); - const cell = CellData.fromCharData(fillCharData || [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let i = 0; i < cols; ++i) { this.setCell(i, cell); } From ece7192db74859aa146d42bc68912138e946159f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 16:15:24 +0100 Subject: [PATCH 063/140] remove set with CharData from codebase and deprecate method --- src/Buffer.test.ts | 40 +++--- src/BufferLine.test.ts | 124 +++++++++--------- src/BufferLine.ts | 4 + src/Linkifier.test.ts | 4 +- src/SelectionManager.test.ts | 8 +- src/Terminal.test.ts | 65 ++++----- src/renderer/CharacterJoinerRegistry.test.ts | 6 +- .../dom/DomRendererRowFactory.test.ts | 28 ++-- 8 files changed, 142 insertions(+), 137 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..d1a1d563 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -8,7 +8,7 @@ import { ITerminal } from './Types'; import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal, TestTerminal } from './ui/TestUtils.test'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -157,10 +157,10 @@ describe('Buffer', () => { buffer.fillViewportRows(); let chData = buffer.lines.get(5).get(0); chData[1] = 'a'; - buffer.lines.get(5).set(0, chData); + buffer.lines.get(5).setCell(0, CellData.fromCharData(chData)); chData = buffer.lines.get(INIT_ROWS - 1).get(0); chData[1] = 'b'; - buffer.lines.get(INIT_ROWS - 1).set(0, chData); + buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData)); buffer.resize(INIT_COLS, INIT_ROWS - 5); assert.equal(buffer.lines.get(0).get(0)[1], 'a'); assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b'); @@ -278,10 +278,10 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { const line = new BufferLine(4); - line.set(0, [ null, 'a', 1, 'a'.charCodeAt(0)]); - line.set(1, [ null, 'b', 1, 'b'.charCodeAt(0)]); - line.set(2, [ null, 'c', 1, 'c'.charCodeAt(0)]); - line.set(3, [ null, 'd', 1, 'd'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([ null, 'b', 1, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([ null, 'c', 1, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([ null, 'd', 1, 'd'.charCodeAt(0)])); buffer.lines.set(0, line); const str = buffer.translateBufferLineToString(0, true, 0, 2); @@ -290,9 +290,9 @@ describe('Buffer', () => { it('should handle a cut-off double width character by including it', () => { const line = new BufferLine(3); - line.set(0, [ null, '語', 2, 35486 ]); - line.set(1, [ null, '', 0, null]); - line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, '語', 2, 35486 ])); + line.setCell(1, CellData.fromCharData([ null, '', 0, null])); + line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -301,9 +301,9 @@ describe('Buffer', () => { it('should handle a zero width character in the middle of the string by not including it', () => { const line = new BufferLine(3); - line.set(0, [ null, '語', 2, '語'.charCodeAt(0) ]); - line.set(1, [ null, '', 0, null]); - line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, '語', 2, '語'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ null, '', 0, null])); + line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); const str0 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -318,8 +318,8 @@ describe('Buffer', () => { it('should handle single width emojis', () => { const line = new BufferLine(2); - line.set(0, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - line.set(1, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, '😁', 1, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -331,8 +331,8 @@ describe('Buffer', () => { it('should handle double width emojis', () => { const line = new BufferLine(2); - line.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]); - line.set(1, [ null, '', 0, null]); + line.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ null, '', 0, null])); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -342,9 +342,9 @@ describe('Buffer', () => { assert.equal(str2, '😁'); const line2 = new BufferLine(3); - line2.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]); - line2.set(1, [ null, '', 0, null]); - line2.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line2.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); + line2.setCell(1, CellData.fromCharData([ null, '', 0, null])); + line2.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line2); const str3 = buffer.translateBufferLineToString(0, true, 0, 3); diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 210ba851..95ec77c3 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -38,9 +38,9 @@ describe('BufferLine', function(): void { }); it('insertCells', function(): void { const line = new TestBufferLine(3); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], @@ -50,11 +50,11 @@ describe('BufferLine', function(): void { }); it('deleteCells', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], @@ -66,11 +66,11 @@ describe('BufferLine', function(): void { }); it('replaceCells', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], @@ -82,11 +82,11 @@ describe('BufferLine', function(): void { }); it('fill', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [123, 'z', 0, 'z'.charCodeAt(0)], @@ -98,11 +98,11 @@ describe('BufferLine', function(): void { }); it('clone', function(): void { const line = new TestBufferLine(5, null, true); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = line.clone(); chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray()); chai.expect(line2.length).equals(line.length); @@ -110,11 +110,11 @@ describe('BufferLine', function(): void { }); it('copyFrom', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); line2.copyFrom(line); chai.expect(line2.toArray()).eql(line.toArray()); @@ -207,27 +207,27 @@ describe('BufferLine', function(): void { }); it('ASCII', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); chai.expect(line.getTrimmedLength()).equal(3); }); it('surrogate', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); chai.expect(line.getTrimmedLength()).equal(3); }); it('combining', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); chai.expect(line.getTrimmedLength()).equal(3); }); it('fullwidth', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(3, [0, '', 0, undefined]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([0, '', 0, undefined])); chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth }); }); @@ -239,10 +239,10 @@ describe('BufferLine', function(): void { }); it('ASCII', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a a aa '); chai.expect(line.translateToString(true)).equal('a a aa'); chai.expect(line.translateToString(false, 0, 5)).equal('a a a'); @@ -255,10 +255,10 @@ describe('BufferLine', function(): void { }); it('surrogate', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); - line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); - line.set(5, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 '); chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞'); chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞'); @@ -270,10 +270,10 @@ describe('BufferLine', function(): void { }); it('combining', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - line.set(5, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 '); chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301'); chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301'); @@ -285,13 +285,13 @@ describe('BufferLine', function(): void { }); it('fullwidth', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(3, [0, '', 0, undefined]); - line.set(5, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(6, [0, '', 0, undefined]); - line.set(7, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(8, [0, '', 0, undefined]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(6, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(8, CellData.fromCharData([0, '', 0, undefined])); chai.expect(line.translateToString(false)).equal('a 1 11 '); chai.expect(line.translateToString(true)).equal('a 1 11'); chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1'); @@ -309,11 +309,11 @@ describe('BufferLine', function(): void { }); it('space at end', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(6, [1, ' ', 1, ' '.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a a aa '); chai.expect(line.translateToString(true)).equal('a a aa '); }); @@ -327,7 +327,7 @@ describe('BufferLine', function(): void { }); it('should work with endCol=0', () => { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); chai.expect(line.translateToString(true, 0, 0)).equal(''); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index ccea8fe1..4a074441 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -165,6 +165,10 @@ export class BufferLine implements IBufferLine { ]; } + /** + * Set cell data from CharData. + * @deprecated + */ public set(index: number, value: CharData): void { this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0ba1294a..d40e5069 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -9,7 +9,7 @@ import { ILinkMatcher, ITerminal, IBufferLine } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test'; import { CircularList } from './common/CircularList'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { @@ -53,7 +53,7 @@ describe('Linkifier', () => { function stringToRow(text: string): IBufferLine { const result = new BufferLine(text.length); for (let i = 0; i < text.length; i++) { - result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); + result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); } return result; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 2f74ccda..42591168 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,7 +10,7 @@ import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { ITerminal, IBuffer, IBufferLine } from './Types'; import { MockTerminal } from './ui/TestUtils.test'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -57,14 +57,14 @@ describe('SelectionManager', () => { function stringToRow(text: string): IBufferLine { const result = new BufferLine(text.length); for (let i = 0; i < text.length; i++) { - result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); + result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); } return result; } function stringArrayToRow(chars: string[]): IBufferLine { const line = new BufferLine(chars.length); - chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)])); + chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)]))); return line; } @@ -119,7 +119,7 @@ describe('SelectionManager', () => { [null, 'o', 1, 'o'.charCodeAt(0)] ]; const line = new BufferLine(data.length); - for (let i = 0; i < data.length; ++i) line.set(i, data[i]); + for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i])); buffer.lines.set(0, line); // Ensure wide characters take up 2 columns selectionManager.selectWordAt([0, 0]); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index fdc9678b..2f9ae6a5 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -7,6 +7,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; +import { CellData } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -227,7 +228,7 @@ describe('term.js addons', () => { }); describe('setOption', () => { - it('should set the option correctly', () => { + it('should set option correctly', () => { term.setOption('cursorBlink', true); assert.equal(term.options.cursorBlink, true); term.setOption('cursorBlink', false); @@ -455,8 +456,8 @@ describe('term.js addons', () => { describe('scroll() function', () => { describe('when scrollback > 0', () => { it('should create a new line and scroll', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); @@ -466,9 +467,9 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); @@ -478,11 +479,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(); @@ -496,11 +497,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; @@ -521,9 +522,9 @@ describe('term.js addons', () => { }); it('should create a new line and shift everything up', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); term.scroll(); @@ -536,9 +537,9 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); @@ -548,11 +549,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(); @@ -565,11 +566,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 2f9f45be..0bbb982e 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -29,13 +29,13 @@ describe('CharacterJoinerRegistry', () => { let sub = lineData([['deemo']]); let oldSize = line6.length; line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); - for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); + for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, CellData.fromCharData(sub.get(i))); line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513])); line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)])); sub = lineData([['jiabc']]); oldSize = line6.length; line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); - for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); + for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, CellData.fromCharData(sub.get(i))); lines.set(6, line6); (terminal.buffer).setLines(lines); @@ -274,7 +274,7 @@ function lineData(data: IPartialLineData[]): IBufferLine { const attr = (data[i][1] || 0); const offset = tline.length; tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0])); - line.split('').map((char, idx) => tline.set(idx + offset, [attr, char, 1, char.charCodeAt(0)])); + line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)]))); } return tline; } diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 67342da0..e06990d7 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -8,7 +8,7 @@ import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { BufferLine } from '../../BufferLine'; +import { BufferLine, CellData } from '../../BufferLine'; import { IBufferLine } from '../../Types'; import { DEFAULT_COLOR } from '../atlas/Types'; @@ -32,9 +32,9 @@ describe('DomRendererRowFactory', () => { }); it('should set correct attributes for double width characters', () => { - lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)])); // There should be no element for the following "empty" cell - lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]); + lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, undefined])); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), '' @@ -51,8 +51,8 @@ describe('DomRendererRowFactory', () => { }); it('should not render cells that go beyond the terminal\'s columns', () => { - lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]); - lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); + lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' @@ -61,7 +61,7 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { - lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([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' @@ -69,7 +69,7 @@ describe('DomRendererRowFactory', () => { }); it('should add class for italic', () => { - lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([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' @@ -79,7 +79,7 @@ describe('DomRendererRowFactory', () => { it('should add classes for 256 foreground colors', () => { 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)]); + lineData.setCell(0, CellData.fromCharData([defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)])); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` @@ -90,7 +90,7 @@ describe('DomRendererRowFactory', () => { it('should add classes for 256 background colors', () => { 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)]); + lineData.setCell(0, CellData.fromCharData([defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)])); const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` @@ -99,7 +99,7 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert colors', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([(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' @@ -107,7 +107,7 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert default fg color', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([(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' @@ -115,7 +115,7 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert default bg color', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([(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' @@ -124,7 +124,7 @@ describe('DomRendererRowFactory', () => { it('should turn bold fg text bright', () => { for (let i = 0; i < 8; i++) { - lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([(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` @@ -143,7 +143,7 @@ describe('DomRendererRowFactory', () => { function createEmptyLineData(cols: number): IBufferLine { const lineData = new BufferLine(cols); for (let i = 0; i < cols; i++) { - lineData.set(i, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE])); } return lineData; } From 711ae6594780dc19e1d0c1879db1958dce56cc4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 17:50:19 +0100 Subject: [PATCH 064/140] remove get CharData from codebase and deprecate method --- src/Buffer.test.ts | 20 +- src/BufferLine.test.ts | 8 +- src/BufferLine.ts | 7 + src/CharWidth.test.ts | 3 +- src/InputHandler.test.ts | 19 +- src/Terminal.integration.ts | 5 +- src/Terminal.test.ts | 353 ++++++++++--------- src/Types.ts | 1 + src/renderer/CharacterJoinerRegistry.test.ts | 4 +- 9 files changed, 221 insertions(+), 199 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index d1a1d563..27e0e836 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,7 +5,7 @@ import { assert, expect } from 'chai'; import { ITerminal } from './Types'; -import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { Buffer, DEFAULT_ATTR } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal, TestTerminal } from './ui/TestUtils.test'; import { BufferLine, CellData } from './BufferLine'; @@ -37,13 +37,13 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).get(0); + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).loadCell(0, new CellData()).asCharData; buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { assert.equal(buffer.lines.get(y).length, INIT_COLS); for (let x = 0; x < INIT_COLS; x++) { - assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar); + assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).asCharData, blankLineChar); } } }); @@ -155,15 +155,15 @@ describe('Buffer', () => { assert.equal(buffer.lines.maxLength, INIT_ROWS); buffer.y = INIT_ROWS - 1; buffer.fillViewportRows(); - let chData = buffer.lines.get(5).get(0); + let chData = buffer.lines.get(5).loadCell(0, new CellData()).asCharData; chData[1] = 'a'; buffer.lines.get(5).setCell(0, CellData.fromCharData(chData)); - chData = buffer.lines.get(INIT_ROWS - 1).get(0); + chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).asCharData; chData[1] = 'b'; buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData)); buffer.resize(INIT_COLS, INIT_ROWS - 5); - assert.equal(buffer.lines.get(0).get(0)[1], 'a'); - assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b'); + assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).asCharData[1], 'a'); + assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).asCharData[1], 'b'); }); }); }); @@ -497,7 +497,7 @@ describe('Buffer', () => { assert.equal(input, s); const stringIndex = s.match(/😃/).index; const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃'); + assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).chars, '😃'); }); it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { @@ -524,7 +524,7 @@ describe('Buffer', () => { assert.equal(input, s); for (let i = 0; i < input.length; ++i) { const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).chars); } }); @@ -542,7 +542,7 @@ describe('Buffer', () => { : (i % 3 === 1) ? input.substr(i, 2) : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).chars); } }); }); diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 95ec77c3..874eb14f 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -12,7 +12,7 @@ class TestBufferLine extends BufferLine { public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { - result.push(this.get(i)); + result.push(this.loadCell(i, new CellData()).asCharData); } return result; } @@ -25,15 +25,15 @@ describe('BufferLine', function(): void { chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.loadCell(0, new CellData()).asCharData).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.loadCell(0, new CellData()).asCharData).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(true); line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); chai.expect(line.length).equals(10); - chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]); + chai.expect(line.loadCell(0, new CellData()).asCharData).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); }); it('insertCells', function(): void { diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 4a074441..8b62141e 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -129,6 +129,9 @@ export class CellData implements ICellData { this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } + public get asCharData(): CharData { + return [this.fg, this.chars, this.width, this.code]; + } } /** @@ -150,6 +153,10 @@ export class BufferLine implements IBufferLine { this.length = cols; } + /** + * Get cell data CharData. + * @deprecated + */ public get(index: number): CharData { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; const cp = content & Content.CODEPOINT_MASK; diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index 0747fdf1..7cab3882 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -8,6 +8,7 @@ import { assert } from 'chai'; import { getStringCellWidth, wcwidth } from './CharWidth'; import { IBuffer } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { CellData } from './BufferLine'; describe('getStringCellWidth', function(): void { @@ -22,7 +23,7 @@ describe('getStringCellWidth', function(): void { for (let i = start; i < end; ++i) { const line = buffer.lines.get(i); for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline - const ch = line.get(j); + const ch = line.loadCell(j, new CellData()).asCharData; result += ch[CHAR_DATA_WIDTH_INDEX]; // return on sentinel if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) { diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index a7963a2c..24eb0884 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -6,9 +6,10 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal } from './ui/TestUtils.test'; -import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer'; +import { DEFAULT_ATTR } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; +import { CellData } from './BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -356,45 +357,45 @@ describe('InputHandler', () => { expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 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(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 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(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).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); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 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(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(''); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).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(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); handler.parse('\x1b[?1049h\x1b[uTEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 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); + expect(term.buffer.lines.get(20).loadCell(10, new CellData()).fg & 0x1ff).to.equal(2); }); }); }); diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index d2a5cd7c..b5165490 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -13,8 +13,9 @@ import * as path from 'path'; import * as pty from 'node-pty'; import { assert } from 'chai'; import { Terminal } from './Terminal'; -import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; +import { WHITESPACE_CELL_CHAR } from './Buffer'; import { IViewport } from './Types'; +import { CellData } from './BufferLine'; class TestTerminal extends Terminal { innerWrite(): void { this._innerWrite(); } @@ -67,7 +68,7 @@ function terminalToString(term: Terminal): string { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { lineText = ''; for (let cell = 0; cell < term.cols; ++cell) { - lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; + lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).chars || WHITESPACE_CELL_CHAR; } // rtrim empty cells as xterm does lineText = lineText.replace(/\s+$/, ''); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 2f9ae6a5..e06ceb78 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; +import { DEFAULT_ATTR } from './Buffer'; import { CellData } from './BufferLine'; const INIT_COLS = 80; @@ -461,9 +461,9 @@ describe('term.js addons', () => { term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ''); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).chars, 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).chars, ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { @@ -474,8 +474,8 @@ describe('term.js addons', () => { term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { @@ -488,12 +488,12 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'b'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'c'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, 'd'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).chars, 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { @@ -507,11 +507,11 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, 'e'); }); }); @@ -530,10 +530,10 @@ describe('term.js addons', () => { term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ''); - assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ''); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'b'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, ''); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).chars, 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).chars, ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { @@ -544,8 +544,8 @@ describe('term.js addons', () => { term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { @@ -558,11 +558,11 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'b'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { @@ -576,11 +576,11 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, 'e'); }); }); }); @@ -771,116 +771,126 @@ describe('term.js addons', () => { it('2 characters per cell', function (): void { this.timeout(10000); // This is needed because istanbul patches code and slows it down const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + const tchar = term.buffer.lines.get(0).loadCell(0, cell); + expect(tchar.chars).eql(high + String.fromCharCode(i)); + expect(tchar.chars.length).eql(2); + expect(tchar.width).eql(1); + expect(term.buffer.lines.get(0).loadCell(1, cell).chars).eql(''); term.reset(); } }); it('2 characters at last cell', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.write(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).chars).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).chars.length).eql(2); + expect(term.buffer.lines.get(1).loadCell(0, cell).chars).eql(''); term.reset(); } }); it('2 characters per cell over line end with autowrap', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = true; term.write('a' + high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).chars).eql('a'); + expect(term.buffer.lines.get(1).loadCell(0, cell).chars).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1).loadCell(0, cell).chars.length).eql(2); + expect(term.buffer.lines.get(1).loadCell(1, cell).chars).eql(''); term.reset(); } }); it('2 characters per cell over line end without autowrap', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = false; term.write('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).chars).eql('a'); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).chars.length).eql(1); + expect(term.buffer.lines.get(1).loadCell(1, cell).chars).eql(''); term.reset(); } }); it('splitted surrogates', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high); term.write(String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + const tchar = term.buffer.lines.get(0).loadCell(0, cell); + expect(tchar.chars).eql(high + String.fromCharCode(i)); + expect(tchar.chars.length).eql(2); + expect(tchar.width).eql(1); + expect(term.buffer.lines.get(0).loadCell(1, cell).chars).eql(''); term.reset(); } }); }); describe('unicode - combining characters', () => { + const cell = new CellData(); it('café', () => { term.write('cafe\u0301'); - expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(3, cell); + expect(cell.chars).eql('e\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(1); }); it('café - end of line', () => { term.buffer.x = term.cols - 1 - 3; term.write('cafe\u0301'); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.chars).eql('e\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(1); + term.buffer.lines.get(0).loadCell(1, cell); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(1); }); it('multiple combined é', () => { term.wraparoundMode = true; term.write(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(i, cell); + expect(cell.chars).eql('e\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(1); } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('e\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(1); }); it('multiple surrogate with combined', () => { term.wraparoundMode = true; term.write(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(i, cell); + expect(cell.chars).eql('\uD800\uDC00\u0301'); + expect(cell.chars.length).eql(3); + expect(cell.width).eql(1); } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('\uD800\uDC00\u0301'); + expect(cell.chars.length).eql(3); + expect(cell.width).eql(1); }); }); describe('unicode - fullwidth characters', () => { + const cell = new CellData(); it('cursor movement even', () => { expect(term.buffer.x).eql(0); term.write('¥'); @@ -896,140 +906,141 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.chars).eql('¥'); + expect(cell.chars.length).eql(1); + expect(cell.width).eql(2); } } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('¥'); + expect(cell.chars.length).eql(1); + expect(cell.width).eql(2); }); it('line of ¥ odd', () => { term.wraparoundMode = true; term.buffer.x = 1; term.write(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.chars).eql('¥'); + expect(cell.chars.length).eql(1); + expect(cell.width).eql(2); } } - let tchar = term.buffer.lines.get(0).get(term.cols - 1); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('¥'); + expect(cell.chars.length).eql(1); + expect(cell.width).eql(2); }); it('line of ¥ with combining odd', () => { term.wraparoundMode = true; term.buffer.x = 1; term.write(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.chars).eql('¥\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(2); } } - let tchar = term.buffer.lines.get(0).get(term.cols - 1); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('¥\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(2); }); it('line of ¥ with combining even', () => { term.wraparoundMode = true; term.write(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.chars).eql('¥\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(2); } } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('¥\u0301'); + expect(cell.chars.length).eql(2); + expect(cell.width).eql(2); }); it('line of surrogate fullwidth with combining odd', () => { term.wraparoundMode = true; term.buffer.x = 1; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.chars).eql('\ud843\ude6d\u0301'); + expect(cell.chars.length).eql(3); + expect(cell.width).eql(2); } } - let tchar = term.buffer.lines.get(0).get(term.cols - 1); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('\ud843\ude6d\u0301'); + expect(cell.chars.length).eql(3); + expect(cell.width).eql(2); }); it('line of surrogate fullwidth with combining even', () => { term.wraparoundMode = true; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.chars).eql(''); + expect(cell.chars.length).eql(0); + expect(cell.width).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.chars).eql('\ud843\ude6d\u0301'); + expect(cell.chars.length).eql(3); + expect(cell.width).eql(2); } } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.chars).eql('\ud843\ude6d\u0301'); + expect(cell.chars.length).eql(3); + expect(cell.width).eql(2); }); }); describe('insert mode', () => { + const cell = new CellData(); it('halfwidth - all', () => { term.write(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; @@ -1037,10 +1048,10 @@ describe('term.js addons', () => { term.insertMode = true; term.write('abcde'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e'); - expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0'); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4'); + expect(term.buffer.lines.get(0).loadCell(10, cell).chars).eql('a'); + expect(term.buffer.lines.get(0).loadCell(14, cell).chars).eql('e'); + expect(term.buffer.lines.get(0).loadCell(15, cell).chars).eql('0'); + expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql('4'); }); it('fullwidth - insert', () => { term.write(Array(9).join('0123456789').slice(-80)); @@ -1049,11 +1060,11 @@ describe('term.js addons', () => { term.insertMode = true; term.write('¥¥¥'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3'); + expect(term.buffer.lines.get(0).loadCell(10, cell).chars).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(11, cell).chars).eql(''); + expect(term.buffer.lines.get(0).loadCell(14, cell).chars).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(15, cell).chars).eql(''); + expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql('3'); }); it('fullwidth - right border', () => { term.write(Array(41).join('¥')); @@ -1062,14 +1073,14 @@ describe('term.js addons', () => { term.insertMode = true; term.write('a'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // fullwidth char got replaced + expect(term.buffer.lines.get(0).loadCell(10, cell).chars).eql('a'); + expect(term.buffer.lines.get(0).loadCell(11, cell).chars).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql(''); // fullwidth char got replaced term.write('b'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b'); - expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth + expect(term.buffer.lines.get(0).loadCell(11, cell).chars).eql('b'); + expect(term.buffer.lines.get(0).loadCell(12, cell).chars).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql(''); // empty cell after fullwidth }); }); }); diff --git a/src/Types.ts b/src/Types.ts index 644a9b49..d176799f 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -524,6 +524,7 @@ export interface ICellData { chars: string; code: number; setFromCharData(value: CharData): void; + asCharData: CharData; } /** diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 0bbb982e..effdbfaa 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -29,13 +29,13 @@ describe('CharacterJoinerRegistry', () => { let sub = lineData([['deemo']]); let oldSize = line6.length; line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); - for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, CellData.fromCharData(sub.get(i))); + for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData())); line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513])); line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)])); sub = lineData([['jiabc']]); oldSize = line6.length; line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); - for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, CellData.fromCharData(sub.get(i))); + for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData())); lines.set(6, line6); (terminal.buffer).setLines(lines); From 35a6aa83268b444686f730ffc879b4f46d9cd06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 18:04:52 +0100 Subject: [PATCH 065/140] some docs --- src/BufferLine.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 8b62141e..b362c007 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -76,22 +76,35 @@ export const enum Content { WIDTH_SHIFT = 22 } +/** + * CellData - represents a single Cell in the terminal buffer. + */ export class CellData implements ICellData { + + /** Helper to create CellData from CharData. */ public static fromCharData(value: CharData): CellData { const obj = new CellData(); obj.setFromCharData(value); return obj; } + + /** Primitives from terminal buffer. */ public content: number = 0; public fg: number = 0; public bg: number = 0; public combinedData: string = ''; + + /** Whether cell contains a combined string. */ public get combined(): number { return this.content & Content.IS_COMBINED; } + + /** Width of the cell. */ public get width(): number { return this.content >> Content.WIDTH_SHIFT; } + + /** JS string of the content. */ public get chars(): string { if (this.content & Content.IS_COMBINED) { return this.combinedData; @@ -101,9 +114,13 @@ export class CellData implements ICellData { } return ''; } + + /** Codepoint of cell (or last charCode of combined string) */ public get code(): number { return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); } + + /** Set data from CharData */ public setFromCharData(value: CharData): void { this.fg = value[CHAR_DATA_ATTR_INDEX]; this.bg = 0; @@ -129,11 +146,14 @@ export class CellData implements ICellData { this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } + + /** Get data as CharData. */ public get asCharData(): CharData { return [this.fg, this.chars, this.width, this.code]; } } + /** * Typed array based bufferline implementation. */ @@ -193,18 +213,23 @@ export class BufferLine implements IBufferLine { public getWidth(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; } + public hasWidth(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; } + public getFG(index: number): number { return this._data[index * CELL_SIZE + Cell.FG]; } + public getBG(index: number): number { return this._data[index * CELL_SIZE + Cell.BG]; } + public hasContent(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; } + public getCodePoint(index: number): number { // returns either the single codepoint or the last charCode in combined const content = this._data[index * CELL_SIZE + Cell.CONTENT]; @@ -213,9 +238,11 @@ export class BufferLine implements IBufferLine { } return content & Content.CODEPOINT_MASK; } + public isCombined(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; } + public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { @@ -227,6 +254,9 @@ export class BufferLine implements IBufferLine { return ''; // return empty string for empty cells } + /** + * Load data at `index` into `cell`. + */ public loadCell(index: number, cell: ICellData): ICellData { cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; cell.fg = this._data[index * CELL_SIZE + Cell.FG]; @@ -237,6 +267,9 @@ export class BufferLine implements IBufferLine { return cell; } + /** + * Set data at `index` to `cell`. + */ public setCell(index: number, cell: ICellData): void { if (cell.content & Content.IS_COMBINED) { this._combined[index] = cell.combinedData; From 40b231edd1178d82fd88a0d7d49be67b97574a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 18:44:10 +0100 Subject: [PATCH 066/140] tests for CellData --- src/BufferLine.test.ts | 28 +++++++++++++++++++++++++++- src/BufferLine.ts | 5 ++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 874eb14f..110b9b4e 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, CellData } from './BufferLine'; +import { BufferLine, CellData, Content } from './BufferLine'; import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -18,6 +18,32 @@ class TestBufferLine extends BufferLine { } } +describe('CellData', () => { + it('CharData <--> CellData equality', () => { + const cell = new CellData(); + // ASCII + cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); + chai.assert.deepEqual(cell.asCharData, [123, 'a', 1, 'a'.charCodeAt(0)]); + chai.assert.equal(cell.combined, 0); + // combining + cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + chai.assert.equal(cell.combined, Content.IS_COMBINED); + // surrogate + cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); + chai.assert.deepEqual(cell.asCharData, [123, '𝄞', 1, 0x1D11E]); + chai.assert.equal(cell.combined, 0); + // surrogate + combining + cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); + chai.assert.deepEqual(cell.asCharData, [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); + chai.assert.equal(cell.combined, Content.IS_COMBINED); + // wide char + cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); + chai.assert.deepEqual(cell.asCharData, [123, '1', 2, '1'.charCodeAt(0)]); + chai.assert.equal(cell.combined, 0); + }); +}); + describe('BufferLine', function(): void { it('ctor', function(): void { let line: IBufferLine = new TestBufferLine(0); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index b362c007..048bcab6 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -78,6 +78,8 @@ export const enum Content { /** * CellData - represents a single Cell in the terminal buffer. + * + * TODO: attr getter */ export class CellData implements ICellData { @@ -136,8 +138,9 @@ export class CellData implements ICellData { } else { combined = true; } + } else { + combined = true; } - combined = true; } else { this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } From 7c6dad5805e26235f25c4ad7b2f51438aaac2e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 12 Jan 2019 19:06:09 +0100 Subject: [PATCH 067/140] test cases --- src/BufferLine.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 110b9b4e..97508ace 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -357,4 +357,42 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 0)).equal(''); }); }); + describe('addCharToCell', () => { + it('should set width to 1 for empty cell', () => { + const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.addCharToCell(0, '\u0301'.charCodeAt(0)); + const cell = line.loadCell(0, new CellData()); + // chars contains single combining char + // width is set to 1 + chai.assert.deepEqual(cell.asCharData, [DEFAULT_ATTR, '\u0301', 1, 0x0301]); + // do not account a single combining char as combined + chai.assert.equal(cell.combined, 0); + }); + it('should add char to combining string in cell', () => { + const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const cell = line .loadCell(0, new CellData()); + cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); + line.setCell(0, cell); + line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.loadCell(0, cell); + // chars contains 3 chars + // width is set to 1 + chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301\u0301', 1, 0x0301]); + // do not account a single combining char as combined + chai.assert.equal(cell.combined, Content.IS_COMBINED); + }); + it('should create combining string on taken cell', () => { + const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const cell = line .loadCell(0, new CellData()); + cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]); + line.setCell(0, cell); + line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.loadCell(0, cell); + // chars contains 2 chars + // width is set to 1 + chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, 0x0301]); + // do not account a single combining char as combined + chai.assert.equal(cell.combined, Content.IS_COMBINED); + }); + }); }); From e02a9ff09e8026399f84a0eb41b427f7a4c13234 Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Tue, 15 Jan 2019 11:00:34 -0500 Subject: [PATCH 068/140] Prevent charsizechanged from firing unnecessarily --- src/ui/CharMeasure.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/CharMeasure.ts b/src/ui/CharMeasure.ts index 2dfc4eb5..0ac755ea 100644 --- a/src/ui/CharMeasure.ts +++ b/src/ui/CharMeasure.ts @@ -46,9 +46,10 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { if (geometry.width === 0 || geometry.height === 0) { return; } - if (this._width !== geometry.width || this._height !== geometry.height) { + const adjustedHeight = Math.ceil(geometry.height); + if (this._width !== geometry.width || this._height !== adjustedHeight) { this._width = geometry.width; - this._height = Math.ceil(geometry.height); + this._height = adjustedHeight; this.emit('charsizechanged'); } } From 07d3407892a35e6afeb6a0e4c3355868ab686546 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 17 Jan 2019 09:48:18 -0800 Subject: [PATCH 069/140] Remove unused var and unneeded defensive check ...items as an arg will always be an array --- demo/start.js | 1 - src/common/CircularList.ts | 32 +++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/demo/start.js b/demo/start.js index 78f1ff1d..278c572f 100644 --- a/demo/start.js +++ b/demo/start.js @@ -5,7 +5,6 @@ * This file is the entry point for browserify. */ -const cp = require('child_process'); const path = require('path'); const webpack = require('webpack'); const startServer = require('./server.js'); diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 9faf534a..d23e693f 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -144,24 +144,22 @@ export class CircularList extends EventEmitter implements ICircularList { this._length -= deleteCount; } - if (items && items.length) { - // Add items - for (let i = this._length - 1; i >= start; i--) { - this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; - } - for (let i = 0; i < items.length; i++) { - this._array[this._getCyclicIndex(start + i)] = items[i]; - } + // Add items + for (let i = this._length - 1; i >= start; i--) { + this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; + } + for (let i = 0; i < items.length; i++) { + this._array[this._getCyclicIndex(start + i)] = items[i]; + } - // Adjust length as needed - if (this._length + items.length > this._maxLength) { - const countToTrim = (this._length + items.length) - this._maxLength; - this._startIndex += countToTrim; - this._length = this._maxLength; - this.emit('trim', countToTrim); - } else { - this._length += items.length; - } + // Adjust length as needed + if (this._length + items.length > this._maxLength) { + const countToTrim = (this._length + items.length) - this._maxLength; + this._startIndex += countToTrim; + this._length = this._maxLength; + this.emit('trim', countToTrim); + } else { + this._length += items.length; } } From 50abb43e9c0c4704898c8f404a954f54b82accb7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 23 Oct 2018 15:45:52 -0700 Subject: [PATCH 070/140] Remove request term info handler We're not supporting it anyway, no point having it. --- src/InputHandler.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index eb1ca105..8846b4be 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -25,26 +25,6 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * DCS subparser implementations */ - /** - * DCS + q Pt ST (xterm) - * Request Terminfo String - * not supported - */ -class RequestTerminfo implements IDcsHandler { - private _data: string; - constructor(private _terminal: any) { } - hook(collect: string, params: number[], flag: number): void { - this._data = ''; - } - put(data: string, start: number, end: number): void { - this._data += data.substring(start, end); - } - unhook(): void { - // invalid: DCS 0 + r Pt ST - this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`); - } -} - /** * DCS $ q Pt ST * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) @@ -87,7 +67,7 @@ class DECRQSS implements IDcsHandler { default: // invalid: DCS 0 $ r Pt ST (xterm) this._terminal.error('Unknown DCS $q %s', this._data); - this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`); + this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`); } } } @@ -288,7 +268,6 @@ export class InputHandler extends Disposable implements IInputHandler { * DCS handler */ this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); - this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); } public dispose(): void { From 8bdb606fd1f16c95116d251ab6bbb0808870a1d2 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 21 Jan 2019 11:09:00 +0100 Subject: [PATCH 071/140] fix mouse event listener change before term attached --- src/InputHandler.ts | 8 ++++++-- src/Terminal.ts | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 8846b4be..1b3907fd 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1291,7 +1291,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.vt200Mouse = params[0] === 1000; this._terminal.normalMouse = params[0] > 1000; this._terminal.mouseEvents = true; - this._terminal.element.classList.add('enable-mouse-events'); + if (this._terminal.element) { + this._terminal.element.classList.add('enable-mouse-events'); + } this._terminal.selectionManager.disable(); this._terminal.log('Binding to mouse events.'); break; @@ -1479,7 +1481,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.vt200Mouse = false; this._terminal.normalMouse = false; this._terminal.mouseEvents = false; - this._terminal.element.classList.remove('enable-mouse-events'); + if (this._terminal.element) { + this._terminal.element.classList.remove('enable-mouse-events'); + } this._terminal.selectionManager.enable(); break; case 1004: // send focusin/focusout events diff --git a/src/Terminal.ts b/src/Terminal.ts index 4c0cd0f8..cf1a3ba5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -733,6 +733,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); this.mouseHelper = new MouseHelper(this.renderer); + // apply mouse event classes set by escape codes before terminal was attached + if (this.mouseEvents) { + this.element.classList.add('enable-mouse-events'); + } else { + this.element.classList.remove('enable-mouse-events'); + } if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to From 4843ca5bb879ed5e6116cba89d27e6c9c813ba40 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 21 Jan 2019 09:08:18 -0800 Subject: [PATCH 072/140] Fix reflow larger with wide chars --- src/Buffer.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 18 +++++++++++++++--- src/BufferLine.ts | 4 ++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 81d4484b..040eb3ca 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -510,6 +510,43 @@ describe('Buffer', () => { assert.equal(secondMarker.line, 1, 'second marker should be restored'); assert.equal(thirdMarker.line, 2, 'third marker should be restored'); }); + it('should wrap wide characters correctly when reflowing larger', () => { + buffer.fillViewportRows(); + buffer.resize(12, 10); + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0).set(i, [null, '', 0, undefined]); + buffer.lines.get(1).set(i, [null, '', 0, undefined]); + } + buffer.lines.get(1).isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); + buffer.resize(13, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语 '); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(false), '汉语汉语汉语 '); + buffer.resize(14, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); + }); + it('should wrap wide characters correctly when reflowing smaller', () => { + // TODO: .. + }); describe('reflowLarger cases', () => { beforeEach(() => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 987e0324..72e43aa0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -259,24 +259,36 @@ export class Buffer implements IBuffer { // Copy buffer data to new locations let destLineIndex = 0; - let destCol = this._cols; + let destCol = wrappedLines[destLineIndex].getTrimmedLength(); let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._cols - srcCol; + const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); + const srcRemainingCells = srcTrimmedTineLength - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + destCol += cellsToCopy; if (destCol === newCols) { destLineIndex++; destCol = 0; } srcCol += cellsToCopy; - if (srcCol === this._cols) { + if (srcCol === srcTrimmedTineLength) { srcLineIndex++; srcCol = 0; } + + // Make sure the last cell isn't wide, if it is copy it to the current dest + if (destCol === 0) { + if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); + // Null out the end of the last row + wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); + } + } } // Clear out remaining cells or fragments could remain; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 2fa4ef13..6bc30586 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -54,6 +54,10 @@ export class BufferLine implements IBufferLine { ]; } + public getWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.WIDTH]; + } + public set(index: number, value: CharData): void { this._data[index * CELL_SIZE + Cell.FLAGS] = value[0]; if (value[1].length > 1) { From ce69dceee7fec72cc80e2f61aafb382abbd5bfaf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 21:46:35 -0800 Subject: [PATCH 073/140] Progress on reflow smaller with wide chars --- src/Buffer.test.ts | 35 ++++++++++++- src/Buffer.ts | 24 ++++++--- src/BufferReflow.test.ts | 80 +++++++++++++++++++++++++++++ src/BufferReflow.ts | 107 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 src/BufferReflow.test.ts create mode 100644 src/BufferReflow.ts diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 040eb3ca..8e1a2f60 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -545,7 +545,40 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); }); it('should wrap wide characters correctly when reflowing smaller', () => { - // TODO: .. + buffer.fillViewportRows(); + buffer.resize(12, 10); + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0).set(i, [null, '', 0, undefined]); + buffer.lines.get(1).set(i, [null, '', 0, undefined]); + } + buffer.lines.get(1).isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); + buffer.resize(11, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉', '1'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语', '2'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); + buffer.resize(9, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); }); describe('reflowLarger cases', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 72e43aa0..30941324 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,6 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; +import { reflowSmallerGetLinesNeeded, reflowSmallerGetNewLineLengths } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -386,11 +387,17 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } - // Determine how many lines need to be inserted at the end, based on the trimmed length of - // the last wrapped line + + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - const linesNeeded = Math.ceil(cellsNeeded / newCols); + // const linesNeeded = reflowSmallerGetLinesNeeded(wrappedLines, this._cols, newCols); + const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); + console.log(destLineLengths); + const linesNeeded = destLineLengths.length; + + + const linesToAdd = linesNeeded - wrappedLines.length; let trimmedLines: number; if (this.ybase === 0 && this.y !== this.lines.length - 1) { @@ -418,8 +425,8 @@ export class Buffer implements IBuffer { wrappedLines.push(...newLines); // Copy buffer data to new locations, this needs to happen backwards to do in-place - let destLineIndex = Math.floor(cellsNeeded / newCols); - let destCol = cellsNeeded % newCols; + let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols); + let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols; if (destCol === 0) { destLineIndex--; destCol = newCols; @@ -432,12 +439,13 @@ export class Buffer implements IBuffer { destCol -= cellsToCopy; if (destCol === 0) { destLineIndex--; - destCol = newCols; + destCol = destLineLengths[destLineIndex]; } srcCol -= cellsToCopy; if (srcCol === 0) { srcLineIndex--; - srcCol = this._cols; + // TODO: srcCol shoudl take trimmed length into account + srcCol = wrappedLines[Math.max(srcLineIndex, 0)].getTrimmedLength(); // this._cols; } } @@ -516,6 +524,8 @@ export class Buffer implements IBuffer { } } + // private _reflowSmallerGetLinesNeeded() + /** * Translates a string index back to a BufferIndex. * To get the correct buffer position the string must start at `startCol` 0 diff --git a/src/BufferReflow.test.ts b/src/BufferReflow.test.ts new file mode 100644 index 00000000..a788fbc8 --- /dev/null +++ b/src/BufferReflow.test.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { BufferLine } from './BufferLine'; +import { reflowSmallerGetNewLineLengths } from './BufferReflow'; + +describe('BufferReflow', () => { + describe('reflowSmallerGetNewLineLengths', () => { + it('should return correct line lengths for a small line with wide characters', () => { + const line = new BufferLine(4); + line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(1, [null, '', 0, undefined]); + line.set(2, [null, '语', 2, '语'.charCodeAt(0)]); + line.set(3, [null, '', 0, undefined]); + assert.equal(line.translateToString(true), '汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语'); + }); + it('should return correct line lengths for a large line with wide characters', () => { + const line = new BufferLine(12); + for (let i = 0; i < 12; i += 4) { + line.set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(i + 2, [null, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + line.set(i, [null, '', 0, undefined]); + line.set(i, [null, '', 0, undefined]); + } + assert.equal(line.translateToString(), '汉语汉语汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 11), [10, 2], 'line: 汉语汉语汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 10), [10, 2], 'line: 汉语汉语汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 9), [8, 4], 'line: 汉语汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 8), [8, 4], 'line: 汉语汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 7), [6, 6], 'line: 汉语汉, 语汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 6), [6, 6], 'line: 汉语汉, 语汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 5), [4, 4, 4], 'line: 汉语, 汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 4), [4, 4, 4], 'line: 汉语, 汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 3), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语'); + }); + it('should return correct line lengths for a string with wide and single characters', () => { + const line = new BufferLine(6); + line.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + line.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(2, [null, '', 0, undefined]); + line.set(3, [null, '语', 2, '语'.charCodeAt(0)]); + line.set(4, [null, '', 0, undefined]); + line.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]); + assert.equal(line.translateToString(), 'a汉语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 5), [5, 1], 'line: a汉语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 4), [3, 3], 'line: a汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 3), [3, 3], 'line: a汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b'); + }); + it('should return correct line lengths for a wrapped line with wide and single characters', () => { + const line1 = new BufferLine(6); + line1.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + line1.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]); + line1.set(2, [null, '', 0, undefined]); + line1.set(3, [null, '语', 2, '语'.charCodeAt(0)]); + line1.set(4, [null, '', 0, undefined]); + line1.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]); + const line2 = new BufferLine(6, undefined, true); + line2.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + line2.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]); + line2.set(2, [null, '', 0, undefined]); + line2.set(3, [null, '语', 2, '语'.charCodeAt(0)]); + line2.set(4, [null, '', 0, undefined]); + line2.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]); + assert.equal(line1.translateToString(), 'a汉语b'); + assert.equal(line2.translateToString(), 'a汉语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 5), [5, 4, 3], 'lines: a汉语, ba汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 4), [3, 4, 4, 1], 'lines: a汉, 语ba, 汉语, b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 3), [3, 3, 3, 3], 'lines: a汉, 语b, a汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b'); + }); + }); +}); diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts new file mode 100644 index 00000000..f56d48c5 --- /dev/null +++ b/src/BufferReflow.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { BufferLine } from './BufferLine'; + +/** + * Determine how many lines need to be inserted at the end. This is done by finding what each + * wrapping point will be and counting the lines needed This would be a lot simpler but in the case + * of a line ending with a wide character, the wide character needs to be put on the following line + * or it would be cut in half. + * @param wrappedLines The original wrapped lines. + * @param newCols The new column count. + */ +export function reflowSmallerGetLinesNeeded(wrappedLines: BufferLine[], oldCols: number, newCols: number): number { + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + // const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; + + // TODO: Make faster + const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); + + // Lines needed needs to take into account what the ending character of each new line is + let linesNeeded = 0; + let cellsAvailable = 0; + // let currentCol = 0; + + // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and + // linesNeeded + let srcCol = -1; + let srcLine = 0; + while (cellsAvailable < cellsNeeded) { + // if (srcLine === wrappedLines.length - 1) { + // cellsAvailable += newCols; + // linesNeeded++; + // break; + // } + + srcCol += newCols; + if (srcCol >= oldCols) { + srcCol -= oldCols; + srcLine++; + } + if (srcLine >= wrappedLines.length) { + linesNeeded++; + break; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; + if (endsWithWide) { + srcCol--; + } + cellsAvailable += endsWithWide ? newCols - 1 : newCols; + linesNeeded++; + } + + return linesNeeded; + // return Math.ceil(cellsNeeded / newCols); +} + +/** + * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- + * compute the wrapping points since wide characters may need to be wrapped onto the following line. + * This function will return an array of numbers of where each line wraps to, the resulting array + * will only contain the values `newCols` (when the line does not end with a wide character) and + * `newCols - 1` (when the line does end with a wide character), except for the last value which + * will contain the remaining items to fill the line. + * + * Calling this with a `newCols` value of `1` will lock up. + * + * @param wrappedLines The wrapped lines to evaluate. + * @param oldCols The columns before resize. + * @param newCols The columns after resize. + */ +export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { + const newLineLengths: number[] = []; + + // TODO: Force cols = 2 to be minimum possible value, this will lock up + + const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); + + // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and + // linesNeeded + let srcCol = -1; + let srcLine = 0; + let cellsAvailable = 0; + while (cellsAvailable < cellsNeeded) { + srcCol += newCols; + if (srcCol >= oldCols) { + srcCol -= oldCols; + srcLine++; + } + if (srcLine >= wrappedLines.length) { + // Add the final line and exit the loop + newLineLengths.push(cellsNeeded - cellsAvailable); + break; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; + if (endsWithWide) { + srcCol--; + } + const lineLength = endsWithWide ? newCols - 1 : newCols; + newLineLengths.push(lineLength); + cellsAvailable += lineLength; + } + + return newLineLengths; +} From df7cd9c319f42aaff3b3e79ef45a19ebda0cf719 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 22:40:40 -0800 Subject: [PATCH 074/140] Get reflow smaller working for wide chars --- src/Buffer.test.ts | 4 ++-- src/Buffer.ts | 9 ++++++++- src/BufferReflow.test.ts | 13 +++++++++++++ src/BufferReflow.ts | 17 +++++++++-------- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 8e1a2f60..82cdff6b 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -568,8 +568,8 @@ describe('Buffer', () => { buffer.resize(11, 10); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉', '1'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语', '2'); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); buffer.resize(10, 10); assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); diff --git a/src/Buffer.ts b/src/Buffer.ts index 30941324..5d3c1a52 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -429,7 +429,7 @@ export class Buffer implements IBuffer { let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols; if (destCol === 0) { destLineIndex--; - destCol = newCols; + destCol = destLineLengths[destLineIndex]; } let srcLineIndex = wrappedLines.length - linesToAdd - 1; let srcCol = lastLineLength; @@ -449,6 +449,13 @@ export class Buffer implements IBuffer { } } + // Null out the end of the line ends if a wide character wrapped to the following line + for (let i = 0; i < wrappedLines.length; i++) { + if (destLineLengths[i] < newCols) { + wrappedLines[i].set(destLineLengths[i], FILL_CHAR_DATA); + } + } + // Adjust viewport as needed let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { diff --git a/src/BufferReflow.test.ts b/src/BufferReflow.test.ts index a788fbc8..9c978dc0 100644 --- a/src/BufferReflow.test.ts +++ b/src/BufferReflow.test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { BufferLine } from './BufferLine'; import { reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; describe('BufferReflow', () => { describe('reflowSmallerGetNewLineLengths', () => { @@ -76,5 +77,17 @@ describe('BufferReflow', () => { assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 3), [3, 3, 3, 3], 'lines: a汉, 语b, a汉, 语b'); assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b'); }); + it('should work on lines ending in null space', () => { + const line = new BufferLine(5); + line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(1, [null, '', 0, undefined]); + line.set(2, [null, '语', 2, '语'.charCodeAt(0)]); + line.set(3, [null, '', 0, undefined]); + line.set(4, [null, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + assert.equal(line.translateToString(true), '汉语'); + assert.equal(line.translateToString(false), '汉语 '); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语'); + }); }); }); diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index f56d48c5..f99acd1c 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -80,21 +80,22 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and // linesNeeded - let srcCol = -1; + let srcCol = 0; let srcLine = 0; let cellsAvailable = 0; while (cellsAvailable < cellsNeeded) { - srcCol += newCols; - if (srcCol >= oldCols) { - srcCol -= oldCols; - srcLine++; - } - if (srcLine >= wrappedLines.length) { + if (cellsNeeded - cellsAvailable < newCols) { // Add the final line and exit the loop newLineLengths.push(cellsNeeded - cellsAvailable); break; } - const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; + srcCol += newCols; + const oldTrimmedLength = wrappedLines[srcLine].getTrimmedLength(); + if (srcCol > oldTrimmedLength) { + srcCol -= oldTrimmedLength; + srcLine++; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2; if (endsWithWide) { srcCol--; } From 178c513407a86c4d1f64c1cf726091a044a7e16b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 22:48:32 -0800 Subject: [PATCH 075/140] Clean up --- src/Buffer.test.ts | 14 ++++++++++++ src/Buffer.ts | 13 ++--------- src/BufferReflow.ts | 54 --------------------------------------------- src/Terminal.ts | 11 +++++---- 4 files changed, 23 insertions(+), 69 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 82cdff6b..1ef2e92c 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -579,6 +579,20 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); + buffer.resize(8, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); + buffer.resize(7, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); + buffer.resize(6, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); }); describe('reflowLarger cases', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 5d3c1a52..f534087d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,7 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetLinesNeeded, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { reflowSmallerGetNewLineLengths } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -387,18 +387,9 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } - - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - // const linesNeeded = reflowSmallerGetLinesNeeded(wrappedLines, this._cols, newCols); const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); - console.log(destLineLengths); - const linesNeeded = destLineLengths.length; - - - - const linesToAdd = linesNeeded - wrappedLines.length; + const linesToAdd = destLineLengths.length - wrappedLines.length; let trimmedLines: number; if (this.ybase === 0 && this.y !== this.lines.length - 1) { // If the top section of the buffer is not yet filled diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index f99acd1c..5e815c7a 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -5,58 +5,6 @@ import { BufferLine } from './BufferLine'; -/** - * Determine how many lines need to be inserted at the end. This is done by finding what each - * wrapping point will be and counting the lines needed This would be a lot simpler but in the case - * of a line ending with a wide character, the wide character needs to be put on the following line - * or it would be cut in half. - * @param wrappedLines The original wrapped lines. - * @param newCols The new column count. - */ -export function reflowSmallerGetLinesNeeded(wrappedLines: BufferLine[], oldCols: number, newCols: number): number { - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - // const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - - // TODO: Make faster - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); - - // Lines needed needs to take into account what the ending character of each new line is - let linesNeeded = 0; - let cellsAvailable = 0; - // let currentCol = 0; - - // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and - // linesNeeded - let srcCol = -1; - let srcLine = 0; - while (cellsAvailable < cellsNeeded) { - // if (srcLine === wrappedLines.length - 1) { - // cellsAvailable += newCols; - // linesNeeded++; - // break; - // } - - srcCol += newCols; - if (srcCol >= oldCols) { - srcCol -= oldCols; - srcLine++; - } - if (srcLine >= wrappedLines.length) { - linesNeeded++; - break; - } - const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; - if (endsWithWide) { - srcCol--; - } - cellsAvailable += endsWithWide ? newCols - 1 : newCols; - linesNeeded++; - } - - return linesNeeded; - // return Math.ceil(cellsNeeded / newCols); -} - /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- * compute the wrapping points since wide characters may need to be wrapped onto the following line. @@ -74,8 +22,6 @@ export function reflowSmallerGetLinesNeeded(wrappedLines: BufferLine[], oldCols: export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - // TODO: Force cols = 2 to be minimum possible value, this will lock up - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and diff --git a/src/Terminal.ts b/src/Terminal.ts index 4c0cd0f8..8a5d96ba 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -69,6 +69,9 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; */ const WRITE_BATCH_SIZE = 300; +const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars +const MINIMUM_ROWS = 1; + /** * The set of options that only have an effect when set in the Terminal constructor. */ @@ -262,8 +265,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: WHy not document.body? this._parent = document ? document.body : null; - this.cols = this.options.cols; - this.rows = this.options.rows; + this.cols = Math.max(this.options.cols, MINIMUM_COLS); + this.rows = Math.max(this.options.rows, MINIMUM_ROWS); if (this.options.handler) { this.on('data', this.options.handler); @@ -1691,8 +1694,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return; } - if (x < 1) x = 1; - if (y < 1) y = 1; + if (x < MINIMUM_COLS) x = MINIMUM_COLS; + if (y < MINIMUM_ROWS) y = MINIMUM_ROWS; this.buffers.resize(x, y); From dfff04cf3db7e29ba1c0e3097bcf16ac8f91af5e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 23:51:28 -0800 Subject: [PATCH 076/140] Pull toRemove step into BufferReflow --- src/Buffer.ts | 77 +++----------------------------------------- src/BufferReflow.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 72 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index f534087d..2f1c2e49 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,7 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -26,7 +26,7 @@ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; -const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; +export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; /** * This class represents a terminal buffer (an internal state of the terminal), where the @@ -240,78 +240,11 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number): void { + // TODO: Can toRemove be pulled out into BufferReflow? + // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once - const toRemove: number[] = []; - for (let y = 0; y < this.lines.length - 1; y++) { - // Check if this row is wrapped - let i = y; - let nextLine = this.lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - continue; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (i < this.lines.length && nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++i) as BufferLine; - } - - // Copy buffer data to new locations - let destLineIndex = 0; - let destCol = wrappedLines[destLineIndex].getTrimmedLength(); - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); - const srcRemainingCells = srcTrimmedTineLength - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); - - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; - } - srcCol += cellsToCopy; - if (srcCol === srcTrimmedTineLength) { - srcLineIndex++; - srcCol = 0; - } - - // Make sure the last cell isn't wide, if it is copy it to the current dest - if (destCol === 0) { - if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); - // Null out the end of the last row - wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); - } - } - } - - // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - if (countToRemove > 0) { - toRemove.push(y + wrappedLines.length - countToRemove); // index - toRemove.push(countToRemove); - } - - y += wrappedLines.length - 1; - } + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); if (toRemove.length > 0) { // First iterate through the list and get the actual indexes to use for rows diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 5e815c7a..310443b7 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -4,6 +4,84 @@ */ import { BufferLine } from './BufferLine'; +import { CircularList } from './common/CircularList'; +import { IBufferLine } from './Types'; +import { FILL_CHAR_DATA } from './Buffer'; + +export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { + const toRemove: number[] = []; + + for (let y = 0; y < lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine]; + while (i < lines.length && nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = lines.get(++i) as BufferLine; + } + + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = wrappedLines[destLineIndex].getTrimmedLength(); + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); + const srcRemainingCells = srcTrimmedTineLength - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === srcTrimmedTineLength) { + srcLineIndex++; + srcCol = 0; + } + + // Make sure the last cell isn't wide, if it is copy it to the current dest + if (destCol === 0) { + if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); + // Null out the end of the last row + wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); + } + } + } + + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + if (countToRemove > 0) { + toRemove.push(y + wrappedLines.length - countToRemove); // index + toRemove.push(countToRemove); + } + + y += wrappedLines.length - 1; + } + return toRemove; +} /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- From d4bd8ae2be96ff976568811c70e6f3907a700149 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 23:58:38 -0800 Subject: [PATCH 077/140] Pull more parts out of reflow larger --- src/Buffer.ts | 70 ++++++++++++--------------------------------- src/BufferReflow.ts | 43 +++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2f1c2e49..286d1311 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,7 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove } from './BufferReflow'; +import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove, reflowLargerCreateNewLayout, reflowLargerApplyNewLayout } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -240,62 +240,28 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number): void { - // TODO: Can toRemove be pulled out into BufferReflow? - - // Gather all BufferLines that need to be removed from the Buffer here so that they can be - // batched up and only committed once const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); - if (toRemove.length > 0) { - // First iterate through the list and get the actual indexes to use for rows const newLayout: number[] = []; + const countRemoved = reflowLargerCreateNewLayout(this.lines, toRemove, newLayout); + reflowLargerApplyNewLayout(this.lines, newLayout); + this._reflowLargerAdjustViewport(newCols, countRemoved); + } + } - let nextToRemoveIndex = 0; - let nextToRemoveStart = toRemove[nextToRemoveIndex]; - let countRemovedSoFar = 0; - for (let i = 0; i < this.lines.length; i++) { - if (nextToRemoveStart === i) { - const countToRemove = toRemove[++nextToRemoveIndex]; - - // Tell markers that there was a deletion - this.lines.emit('delete', { - index: i - countRemovedSoFar, - amount: countToRemove - } as IDeleteEvent); - - i += countToRemove - 1; - countRemovedSoFar += countToRemove; - nextToRemoveStart = toRemove[++nextToRemoveIndex]; - } else { - newLayout.push(i); - } - } - - // Record original lines so they don't get overridden when we rearrange the list - const newLayoutLines: BufferLine[] = []; - for (let i = 0; i < newLayout.length; i++) { - newLayoutLines.push(this.lines.get(newLayout[i]) as BufferLine); - } - - // Rearrange the list - for (let i = 0; i < newLayoutLines.length; i++) { - this.lines.set(i, newLayoutLines[i]); - } - this.lines.length = newLayout.length; - - // Adjust viewport based on number of items removed - let viewportAdjustments = countRemovedSoFar; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; + private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void { + // Adjust viewport based on number of items removed + let viewportAdjustments = countRemoved; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; } + this.ybase--; } } } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 310443b7..51f83c18 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -4,11 +4,13 @@ */ import { BufferLine } from './BufferLine'; -import { CircularList } from './common/CircularList'; +import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; import { FILL_CHAR_DATA } from './Buffer'; export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { + // Gather all BufferLines that need to be removed from the Buffer here so that they can be + // batched up and only committed once const toRemove: number[] = []; for (let y = 0; y < lines.length - 1; y++) { @@ -83,6 +85,45 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n return toRemove; } +export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[], newLayout: number[]): number { + // First iterate through the list and get the actual indexes to use for rows + let nextToRemoveIndex = 0; + let nextToRemoveStart = toRemove[nextToRemoveIndex]; + let countRemovedSoFar = 0; + for (let i = 0; i < lines.length; i++) { + if (nextToRemoveStart === i) { + const countToRemove = toRemove[++nextToRemoveIndex]; + + // Tell markers that there was a deletion + lines.emit('delete', { + index: i - countRemovedSoFar, + amount: countToRemove + } as IDeleteEvent); + + i += countToRemove - 1; + countRemovedSoFar += countToRemove; + nextToRemoveStart = toRemove[++nextToRemoveIndex]; + } else { + newLayout.push(i); + } + } + return countRemovedSoFar; +} + +export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { + // Record original lines so they don't get overridden when we rearrange the list + const newLayoutLines: BufferLine[] = []; + for (let i = 0; i < newLayout.length; i++) { + newLayoutLines.push(lines.get(newLayout[i]) as BufferLine); + } + + // Rearrange the list + for (let i = 0; i < newLayoutLines.length; i++) { + lines.set(i, newLayoutLines[i]); + } + lines.length = newLayout.length; +} + /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- * compute the wrapping points since wide characters may need to be wrapped onto the following line. From 99ac78031c8ebb1bef723b9aa1e45e11e0c4079a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 24 Jan 2019 09:11:16 -0800 Subject: [PATCH 078/140] Remove out param from reflow large method --- src/Buffer.ts | 7 +++---- src/BufferReflow.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 286d1311..52d9572d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -242,10 +242,9 @@ export class Buffer implements IBuffer { private _reflowLarger(newCols: number): void { const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); if (toRemove.length > 0) { - const newLayout: number[] = []; - const countRemoved = reflowLargerCreateNewLayout(this.lines, toRemove, newLayout); - reflowLargerApplyNewLayout(this.lines, newLayout); - this._reflowLargerAdjustViewport(newCols, countRemoved); + const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); + reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); + this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved); } } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 51f83c18..003a3c78 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -8,6 +8,11 @@ import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; import { FILL_CHAR_DATA } from './Buffer'; +export interface INewLayoutResult { + layout: number[]; + countRemoved: number; +} + export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once @@ -85,7 +90,8 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n return toRemove; } -export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[], newLayout: number[]): number { +export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult { + const layout: number[] = []; // First iterate through the list and get the actual indexes to use for rows let nextToRemoveIndex = 0; let nextToRemoveStart = toRemove[nextToRemoveIndex]; @@ -104,10 +110,13 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to countRemovedSoFar += countToRemove; nextToRemoveStart = toRemove[++nextToRemoveIndex]; } else { - newLayout.push(i); + layout.push(i); } } - return countRemovedSoFar; + return { + layout, + countRemoved: countRemovedSoFar + }; } export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { From 109e3a50e8e91a1b328cac7cc81391222c68dec8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 24 Jan 2019 10:20:18 -0800 Subject: [PATCH 079/140] jsdoc --- src/BufferReflow.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 003a3c78..59934e46 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -13,6 +13,12 @@ export interface INewLayoutResult { countRemoved: number; } +/** + * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed + * when a wrapped line unwraps. + * @param lines The buffer lines. + * @param newCols The columns after resize. + */ export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once @@ -90,6 +96,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n return toRemove; } +/** + * Creates and return the new layout for lines given an array of indexes to be removed. + * @param lines The buffer lines. + * @param toRemove The indexes to remove. + */ export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult { const layout: number[] = []; // First iterate through the list and get the actual indexes to use for rows @@ -119,6 +130,12 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to }; } +/** + * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's + * done all at once in a single iteration through the list since splice is very expensive. + * @param lines The buffer lines. + * @param newLayout The new layout to apply. + */ export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { // Record original lines so they don't get overridden when we rearrange the list const newLayoutLines: BufferLine[] = []; From e1363e9fe7746c4e11d7555df6428b04c5131d4e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 24 Jan 2019 11:32:09 -0800 Subject: [PATCH 080/140] Replace if/add/remove with a toggle --- src/Terminal.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index cf1a3ba5..b7d5bf7f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -734,11 +734,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.mouseHelper = new MouseHelper(this.renderer); // apply mouse event classes set by escape codes before terminal was attached - if (this.mouseEvents) { - this.element.classList.add('enable-mouse-events'); - } else { - this.element.classList.remove('enable-mouse-events'); - } + this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to From 4855b60ffeef2cfd1e3bb36eb958e68e5713ea8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 25 Jan 2019 17:35:12 +0100 Subject: [PATCH 081/140] move utf32ToString to TextDecoder.ts --- src/EscapeSequenceParser.ts | 2 +- src/InputHandler.ts | 4 +- src/common/TypedArrayUtils.test.ts | 10 +-- src/common/TypedArrayUtils.ts | 22 +----- src/common/Types.ts | 4 ++ src/core/input/TextDecoder.test.ts | 112 ++++++++++++++--------------- src/core/input/TextDecoder.ts | 25 +++++-- 7 files changed, 86 insertions(+), 93 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index ec7a9da7..7b65624d 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -6,7 +6,7 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -import { utf32ToString } from './common/TypedArrayUtils'; +import { utf32ToString } from './core/input/TextDecoder'; interface IHandlerCollection { [key: string]: T[]; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a702e2a6..2f53cfcb 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,8 +14,8 @@ import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -import { concat, utf32ToString } from './common/TypedArrayUtils'; -import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; +import { concat } from './common/TypedArrayUtils'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder'; /** * Map collect to glevel. Used in `selectCharset`. diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts index 79546ca9..99b0fd82 100644 --- a/src/common/TypedArrayUtils.test.ts +++ b/src/common/TypedArrayUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { fillFallback, concat, utf32ToString } from './TypedArrayUtils'; +import { fillFallback, concat } from './TypedArrayUtils'; type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array @@ -94,12 +94,4 @@ describe('typed array convenience functions', () => { const merged = concat(a, b); deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); }); - it('utf16ToString', () => { - const s = 'abcdefg'; - const data = new Uint16Array(s.length); - for (let i = 0; i < s.length; ++i) { - data[i] = s.charCodeAt(i); - } - assert.equal(utf32ToString(data), s); - }); }); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 5f5be782..380dff35 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -2,10 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ - -type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray - | Int8Array | Int16Array | Int32Array - | Float32Array | Float64Array; +import { TypedArray } from './Types'; /** * polyfill for TypedArray.fill @@ -49,20 +46,3 @@ export function concat(a: T, b: T): T { result.set(b, a.length); return result; } - -/** - * Convert UTF32 char codes into JS string. - */ -export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { - let result = ''; - let cp; - for (let i = start; i < end; ++i) { - if ((cp = data[i]) > 0xFFFF) { - cp -= 0x10000; - result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); - } else { - result += String.fromCharCode(cp); - } - } - return result; -} diff --git a/src/common/Types.ts b/src/common/Types.ts index 8a416bf1..29b5febb 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -35,3 +35,7 @@ export interface ICircularList extends IEventEmitter { trimStart(count: number): void; shiftElements(start: number, count: number, offset: number): void; } + +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts index f69fbded..12f3099a 100644 --- a/src/core/input/TextDecoder.test.ts +++ b/src/core/input/TextDecoder.test.ts @@ -4,64 +4,64 @@ */ import { assert } from 'chai'; -import { StringToUtf32, stringFromCodePoint } from './TextDecoder'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder'; - -// convert UTF32 codepoints to string -function toString(data: Uint32Array, length: number): string { - if ((String as any).fromCodePoint) { - return (String as any).fromCodePoint.apply(null, data.subarray(0, length)); - } - let result = ''; - for (let i = 0; i < length; ++i) { - result += stringFromCodePoint(data[i]); - } - return result; -} - -describe('StringToUtf32 Decoder', () => { - describe('full codepoint test', () => { - it('0..65535', () => { - const decoder = new StringToUtf32(); - const target = new Uint32Array(5); - for (let i = 0; i < 65536; ++i) { - // skip surrogate pairs - if (i >= 0xD800 && i <= 0xDFFF) { - continue; - } - const length = decoder.decode(String.fromCharCode(i), target); - assert.equal(length, 1); - assert.equal(target[0], i); - assert.equal(toString(target, length), String.fromCharCode(i)); - decoder.clear(); - } - }); - it('65536..0x10FFFF (surrogates)', function(): void { - this.timeout(20000); - const decoder = new StringToUtf32(); - const target = new Uint32Array(5); - for (let i = 65536; i < 0x10FFFF; ++i) { - const codePoint = i - 0x10000; - const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); - const length = decoder.decode(s, target); - assert.equal(length, 1); - assert.equal(target[0], i); - assert.equal(toString(target, length), s); - decoder.clear(); - } - }); +describe('text encodings', () => { + it('stringFromCodePoint/utf32ToString', () => { + const s = 'abcdefg'; + const data = new Uint32Array(s.length); + for (let i = 0; i < s.length; ++i) { + data[i] = s.charCodeAt(i); + assert.equal(stringFromCodePoint(data[i]), s[i]); + } + assert.equal(utf32ToString(data), s); }); - describe('stream handling', () => { - it('surrogates mixed advance by 1', () => { - const decoder = new StringToUtf32(); - const target = new Uint32Array(5); - const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; - let decoded = ''; - for (let i = 0; i < input.length; ++i) { - const written = decoder.decode(input[i], target); - decoded += toString(target, written); - } - assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + + describe('StringToUtf32 Decoder', () => { + describe('full codepoint test', () => { + it('0..65535', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 0; i < 65536; ++i) { + // skip surrogate pairs + if (i >= 0xD800 && i <= 0xDFFF) { + continue; + } + const length = decoder.decode(String.fromCharCode(i), target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i)); + decoder.clear(); + } + }); + it('65536..0x10FFFF (surrogates)', function(): void { + this.timeout(20000); + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 65536; i < 0x10FFFF; ++i) { + const codePoint = i - 0x10000; + const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + const length = decoder.decode(s, target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), s); + decoder.clear(); + } + }); + }); + + describe('stream handling', () => { + it('surrogates mixed advance by 1', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; + let decoded = ''; + for (let i = 0; i < input.length; ++i) { + const written = decoder.decode(input[i], target); + decoded += utf32ToString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); }); }); }); diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index 77e6971c..f83959e6 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -2,6 +2,7 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ +import { TypedArray } from '../../common/Types'; /** * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. @@ -73,15 +74,31 @@ export class StringToUtf32 { } /** - * Polyfill - Convert UTF32 codepoint into JS string. + * Convert UTF32 codepoint into JS string. */ export function stringFromCodePoint(codePoint: number): string { - if ((String as any).fromCodePoint) { - return (String as any).fromCodePoint(codePoint); - } if (codePoint > 0xFFFF) { codePoint -= 0x10000; return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); } return String.fromCharCode(codePoint); } + +/** + * Convert UTF32 char codes into JS string. + * Basically the same as `stringFromCodePoint` but for multiple codepoints + * in a loop (which is a lot faster). + */ +export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { + let result = ''; + let cp; + for (let i = start; i < end; ++i) { + if ((cp = data[i]) > 0xFFFF) { + cp -= 0x10000; + result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); + } else { + result += String.fromCharCode(cp); + } + } + return result; +} From 87e97897488c4bbffbed41163484a931b61d7926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 25 Jan 2019 18:16:01 +0100 Subject: [PATCH 082/140] more explicit docs for DCS parsers --- src/Types.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Types.ts b/src/Types.ts index 7caa1335..5bd97b10 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -452,14 +452,22 @@ export interface IParsingState { * DCS handler signature for EscapeSequenceParser. * EscapeSequenceParser handles DCS commands via separate * subparsers that get hook/unhooked and can handle -* arbitrary amount of print data. +* arbitrary amount of data. +* * On entering a DSC sequence `hook` is called by * `EscapeSequenceParser`. Use it to initialize or reset * states needed to handle the current DCS sequence. +* Note: A DCS parser is only instantiated once, therefore +* you cannot rely on the ctor to reinitialize state. +* * EscapeSequenceParser will call `put` several times if the -* parsed string got splitted, therefore you might have to collect -* `data` until `unhook` is called. `unhook` marks the end -* of the current DCS sequence. +* parsed data got split, therefore you might have to collect +* `data` until `unhook` is called. +* Note: `data` is borrowed, if you cannot process the data +* in chunks you have to copy it, doing otherwise will lead to +* data losses or corruption. +* +* `unhook` marks the end of the current DCS sequence. */ export interface IDcsHandler { hook(collect: string, params: number[], flag: number): void; From 2a1f25e7dce1254dde2b24e74bfbd2c97fc8e29c Mon Sep 17 00:00:00 2001 From: Thomas Zilz Date: Sun, 27 Jan 2019 13:15:37 +0100 Subject: [PATCH 083/140] Make textarea positioning work with css transformations on parent elements --- src/Terminal.ts | 6 +++--- src/ui/Clipboard.ts | 44 +++++++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 4ddd0431..c1fc8ec8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -566,12 +566,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Firefox doesn't appear to fire the contextmenu event on right click this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); } })); } else { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); })); } @@ -583,7 +583,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // that the regular click event doesn't fire for the middle mouse button. this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea); + moveTextAreaUnderMouseCursor(event, this); } })); } diff --git a/src/ui/Clipboard.ts b/src/ui/Clipboard.ts index b1acba9d..2570a8b1 100644 --- a/src/ui/Clipboard.ts +++ b/src/ui/Clipboard.ts @@ -85,26 +85,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void { - // Bring textarea at the cursor position - textarea.style.position = 'fixed'; - textarea.style.width = '20px'; - textarea.style.height = '20px'; - textarea.style.left = (ev.clientX - 10) + 'px'; - textarea.style.top = (ev.clientY - 10) + 'px'; - textarea.style.zIndex = '1000'; +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void { - textarea.focus(); + // Calculate textarea position relative to the screen element + const pos = term.screenElement.getBoundingClientRect(); + const left = ev.clientX - pos.left - 10; + const top = ev.clientY - pos.top - 10; + + // Bring textarea at the cursor position + term.textarea.style.position = 'absolute'; + term.textarea.style.width = '20px'; + term.textarea.style.height = '20px'; + term.textarea.style.left = `${left}px`; + term.textarea.style.top = `${top}px`; + term.textarea.style.zIndex = '1000'; + + term.textarea.focus(); // Reset the terminal textarea's styling // Timeout needs to be long enough for click event to be handled. setTimeout(() => { - textarea.style.position = null; - textarea.style.width = null; - textarea.style.height = null; - textarea.style.left = null; - textarea.style.top = null; - textarea.style.zIndex = null; + term.textarea.style.position = null; + term.textarea.style.width = null; + term.textarea.style.height = null; + term.textarea.style.left = null; + term.textarea.style.top = null; + term.textarea.style.zIndex = null; }, 200); } @@ -115,14 +121,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param selectionManager The terminal's selection manager. * @param shouldSelectWord If true and there is no selection the current word will be selected */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { - moveTextAreaUnderMouseCursor(ev, textarea); +export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { + moveTextAreaUnderMouseCursor(ev, term); if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { selectionManager.selectWordAtCursor(ev); } // Get textarea ready to copy from the context menu - textarea.value = selectionManager.selectionText; - textarea.select(); + term.textarea.value = selectionManager.selectionText; + term.textarea.select(); } From 90a41a113a16c5e445b4bf56556a1ec475983c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 13:22:34 +0100 Subject: [PATCH 084/140] review changes --- src/core/input/TextDecoder.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index f83959e6..1ff00940 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -2,7 +2,6 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { TypedArray } from '../../common/Types'; /** * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. @@ -78,6 +77,7 @@ export class StringToUtf32 { */ export function stringFromCodePoint(codePoint: number): string { if (codePoint > 0xFFFF) { + // UTF32 to UTF16 conversion (see comments in utf32ToString) codePoint -= 0x10000; return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); } @@ -89,15 +89,20 @@ export function stringFromCodePoint(codePoint: number): string { * Basically the same as `stringFromCodePoint` but for multiple codepoints * in a loop (which is a lot faster). */ -export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { +export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string { let result = ''; - let cp; for (let i = start; i < end; ++i) { - if ((cp = data[i]) > 0xFFFF) { - cp -= 0x10000; - result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); + let codepoint = data[i]; + if (codepoint > 0xFFFF) { + // JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair + // conversion rules: + // - subtract 0x10000 from code point, leaving a 20 bit number + // - add high 10 bits to 0xD800 --> first surrogate + // - add low 10 bits to 0xDC00 --> second surrogate + codepoint -= 0x10000; + result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00); } else { - result += String.fromCharCode(cp); + result += String.fromCharCode(codepoint); } } return result; From e67bef69f49d967f030c35e8abbd50f186a0eb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 17:28:15 +0100 Subject: [PATCH 085/140] cleanup types --- src/common/TypedArrayUtils.ts | 6 +++++- src/common/Types.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 380dff35..54699835 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -2,7 +2,11 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { TypedArray } from './Types'; + +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + /** * polyfill for TypedArray.fill diff --git a/src/common/Types.ts b/src/common/Types.ts index 29b5febb..8a416bf1 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -35,7 +35,3 @@ export interface ICircularList extends IEventEmitter { trimStart(count: number): void; shiftElements(start: number, count: number, offset: number): void; } - -export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray - | Int8Array | Int16Array | Int32Array - | Float32Array | Float64Array; From 358d70d7218df418c8e10b9a6aa43ccf9786dccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 17:34:58 +0100 Subject: [PATCH 086/140] cleanup gitignore --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2ee021f8..b50ab2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,3 @@ coverage/ # Keep bundled code out of Git dist/ demo/dist/ - -# dont pullin other files from .vscode than launch.json -.vscode/ -!.vscode/launch.json From b20730ee4d3090c653e7f00e175815641f0bb149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 19:40:28 +0100 Subject: [PATCH 087/140] add more docs --- src/Buffer.ts | 10 ++++++++++ src/BufferLine.ts | 35 +++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2b9b2233..f58d0da0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -18,10 +18,20 @@ export const CHAR_DATA_WIDTH_INDEX = 2; export const CHAR_DATA_CODE_INDEX = 3; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 +/** + * Null cell - a real empty cell (containing nothing). + * Note that code should always be 0 for a null cell as + * several test condition of the buffer line rely on this. + */ export const NULL_CELL_CHAR = ''; export const NULL_CELL_WIDTH = 1; export const NULL_CELL_CODE = 0; +/** + * Whilespace cell. + * This is meant as a replacement for empty cells when needed + * during rendering lines to preserve correct aligment. + */ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index ade21435..3f7ff076 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -78,8 +78,6 @@ export const enum Content { /** * CellData - represents a single Cell in the terminal buffer. - * - * TODO: attr getter */ export class CellData implements ICellData { @@ -117,7 +115,12 @@ export class CellData implements ICellData { return ''; } - /** Codepoint of cell (or last charCode of combined string) */ + /** + * Codepoint of cell + * Note this returns the UTF32 codepoint of single chars, + * if content is a combined string it returns the codepoint + * of the last char in string to be in line with code in CharData. + * */ public get code(): number { return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); } @@ -127,10 +130,14 @@ export class CellData implements ICellData { this.fg = value[CHAR_DATA_ATTR_INDEX]; this.bg = 0; let combined = false; + + // surrogates and combined strings need special treatment if (value[CHAR_DATA_CHAR_INDEX].length > 2) { combined = true; } else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); + // if the 2-char string is a surrogate create single codepoint + // everything else is combined if (0xD800 <= code && code <= 0xDBFF) { const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); if (0xDC00 <= second && second <= 0xDFFF) { @@ -217,24 +224,36 @@ export class BufferLine implements IBufferLine { return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; } + /** Test whether content has width. */ public hasWidth(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; } + /** Get FG cell component. */ public getFG(index: number): number { return this._data[index * CELL_SIZE + Cell.FG]; } + /** Get BG cell component. */ public getBG(index: number): number { return this._data[index * CELL_SIZE + Cell.BG]; } + /** + * Test whether contains any chars. + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + * */ public hasContent(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; } + /** + * Get codepoint of the cell. + * To be in line with `code` in CharData this either returns + * a single UTF32 codepoint or the last codepoint of a combined string. + */ public getCodePoint(index: number): number { - // returns either the single codepoint or the last charCode in combined const content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { return this._combined[index].charCodeAt(this._combined[index].length - 1); @@ -242,10 +261,12 @@ export class BufferLine implements IBufferLine { return content & Content.CODEPOINT_MASK; } + /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; } + /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { @@ -254,7 +275,8 @@ export class BufferLine implements IBufferLine { if (content & Content.CODEPOINT_MASK) { return stringFromCodePoint(content & Content.CODEPOINT_MASK); } - return ''; // return empty string for empty cells + // return empty string for empty cells + return ''; } /** @@ -276,9 +298,6 @@ export class BufferLine implements IBufferLine { public setCell(index: number, cell: ICellData): void { if (cell.content & Content.IS_COMBINED) { this._combined[index] = cell.combinedData; - // we also need to clear and set codepoint to index - cell.content &= ~Content.CODEPOINT_MASK; - cell.content |= index; } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; this._data[index * CELL_SIZE + Cell.FG] = cell.fg; From e77e36cbb724b6e88174551046a0ec71246aa00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 19:59:13 +0100 Subject: [PATCH 088/140] fix typo in HAS_CONTENT --- src/BufferLine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 3f7ff076..09f12028 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -60,7 +60,7 @@ export const enum Content { * whether a cell contains anything * read: `isEmtpy = !(content & Content.hasContent)` */ - HAS_CONTENT = 0x2FFFFF, + HAS_CONTENT = 0x3FFFFF, /** * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) From c60f9b14689520a84ddcdc4060e8875c8cd33262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 20:06:58 +0100 Subject: [PATCH 089/140] code formatting --- src/BufferLine.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 09f12028..84a7bfcd 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -122,7 +122,9 @@ export class CellData implements ICellData { * of the last char in string to be in line with code in CharData. * */ public get code(): number { - return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); + return (this.combined) + ? this.combinedData.charCodeAt(this.combinedData.length - 1) + : this.content & Content.CODEPOINT_MASK; } /** Set data from CharData */ From 39fd6c427d8042dc2199116be1d7414441eb179e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 28 Jan 2019 10:42:44 +0100 Subject: [PATCH 090/140] name polishing, docs --- src/BufferLine.test.ts | 22 +++++++++++----------- src/BufferLine.ts | 27 +++++++++++++++++++-------- src/InputHandler.ts | 10 +++++----- src/Types.ts | 6 +++--- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 2a874f9d..894cc204 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -28,23 +28,23 @@ describe('CellData', () => { // ASCII cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.asCharData, [123, '𝄞', 1, 0x1D11E]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.asCharData, [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, '1', 2, '1'.charCodeAt(0)]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); }); }); @@ -331,39 +331,39 @@ describe('BufferLine', function(): void { describe('addCharToCell', () => { it('should set width to 1 for empty cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); const cell = line.loadCell(0, new CellData()); // chars contains single combining char // width is set to 1 chai.assert.deepEqual(cell.asCharData, [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); }); it('should add char to combining string in cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); line.setCell(0, cell); - line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); line.loadCell(0, cell); // chars contains 3 chars // width is set to 1 chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]); line.setCell(0, cell); - line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); line.loadCell(0, cell); // chars contains 2 chars // width is set to 1 chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 84a7bfcd..120a15fc 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -95,7 +95,7 @@ export class CellData implements ICellData { public combinedData: string = ''; /** Whether cell contains a combined string. */ - public get combined(): number { + public get isCombined(): number { return this.content & Content.IS_COMBINED; } @@ -122,7 +122,7 @@ export class CellData implements ICellData { * of the last char in string to be in line with code in CharData. * */ public get code(): number { - return (this.combined) + return (this.isCombined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK; } @@ -168,6 +168,18 @@ export class CellData implements ICellData { /** * Typed array based bufferline implementation. + * + * There are 2 ways to insert data into the cell buffer: + * - `setCellFromCodepoint` + `addCodepointToCell` + * Use these for data that is already UTF32. + * Used during normal input in `InputHandler` for faster buffer access. + * - `setCell` + * This method takes a CellData object and stores the data in the buffer. + * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string). + * + * To retrieve data from the buffer use either one of the primitive methods + * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop + * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. */ export class BufferLine implements IBufferLine { protected _data: Uint32Array | null = null; @@ -311,19 +323,19 @@ export class BufferLine implements IBufferLine { * Since the input handler see the incoming chars as UTF32 codepoints, * it gets an optimized access method. */ - public setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { + public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; } /** - * Add a char to a cell from input handler. + * Add a codepoint to a cell from input handler. * During input stage combining chars with a width of 0 follow and stack * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ - public addCharToCell(index: number, codePoint: number): void { + public addCodepointToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { // we already have a combined string, simply add @@ -332,11 +344,10 @@ export class BufferLine implements IBufferLine { if (content & Content.CODEPOINT_MASK) { // normal case for combining chars: // - move current leading char + new one into combined string - // - set codepoint in cell buffer to index // - set combined flag this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); - content &= ~Content.CODEPOINT_MASK; - content |= index | Content.IS_COMBINED; + content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 + content |= Content.IS_COMBINED; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 86483a29..a9555c76 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -355,9 +355,9 @@ export class InputHandler extends Disposable implements IInputHandler { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars - bufferRow.addCharToCell(buffer.x - 2, code); + bufferRow.addCodepointToCell(buffer.x - 2, code); } else { - bufferRow.addCharToCell(buffer.x - 1, code); + bufferRow.addCodepointToCell(buffer.x - 1, code); } continue; } @@ -401,12 +401,12 @@ export class InputHandler extends Disposable implements IInputHandler { // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell if (bufferRow.loadCell(cols - 1, this._cell).width === 2) { - bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); + bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } // write current char to buffer and advance cursor - bufferRow.setDataFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); + bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero @@ -414,7 +414,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setDataFromCodePoint(buffer.x++, 0, 0, curAttr, 0); + bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr, 0); } } } diff --git a/src/Types.ts b/src/Types.ts index dad86fa2..9588505c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -527,7 +527,7 @@ export interface ICellData { fg: number; bg: number; combinedData: string; - combined: number; + isCombined: number; width: number; chars: string; code: number; @@ -545,8 +545,8 @@ export interface IBufferLine { set(index: number, value: CharData): void; loadCell(index: number, cell: ICellData): ICellData; setCell(index: number, cell: ICellData): void; - setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; - addCharToCell(index: number, codePoint: number): void; + setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; + addCodepointToCell(index: number, codePoint: number): void; insertCells(pos: number, n: number, ch: ICellData): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData): void; From fc1692b3eb10cacb52284dd11e7e343c40ace716 Mon Sep 17 00:00:00 2001 From: Nikita Chuklinov Date: Wed, 30 Jan 2019 20:34:16 +0300 Subject: [PATCH 091/140] clear state if selection doesn't exists --- src/renderer/SelectionRenderLayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 81782ee8..fc16a2fb 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -55,6 +55,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { // Selection does not exist if (!start || !end) { + this._clearState(); return; } From 33e46682b1a8cbb7bc0946749adee2b1b1113ebd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Jan 2019 15:18:28 -0800 Subject: [PATCH 092/140] Fix various problems with reflow - No longer reflow lines where the full unwrapped line contains the cursor - Add guards to prevent y and ybase becoming invalid values Part of Microsoft/vscode#67364 Fixes #1910 --- src/Buffer.ts | 44 +++++++++++++++++++++++++++++--------------- src/BufferReflow.ts | 14 ++++++++++---- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 52d9572d..bc2d5f20 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; -import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList'; +import { EventEmitter } from './common/EventEmitter'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove, reflowLargerCreateNewLayout, reflowLargerApplyNewLayout } from './BufferReflow'; +import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -212,7 +212,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; if (this._hasScrollback) { - this._reflow(newCols); + this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk if (this._cols > newCols) { @@ -226,7 +226,7 @@ export class Buffer implements IBuffer { this._rows = newRows; } - private _reflow(newCols: number): void { + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; } @@ -235,12 +235,12 @@ export class Buffer implements IBuffer { if (newCols > this._cols) { this._reflowLarger(newCols); } else { - this._reflowSmaller(newCols); + this._reflowSmaller(newCols, newRows); } } private _reflowLarger(newCols: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); @@ -253,9 +253,13 @@ export class Buffer implements IBuffer { let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + if (this.y > 0) { + this.y--; + } + if (this.lines.length < this._rows) { + // Add an extra row at the bottom of the viewport + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + } } else { if (this.ydisp === this.ybase) { this.ydisp--; @@ -265,7 +269,7 @@ export class Buffer implements IBuffer { } } - private _reflowSmaller(newCols: number): void { + private _reflowSmaller(newCols: number, newRows: number): void { // Gather all BufferLines that need to be inserted into the Buffer here so that they can be // batched up and only committed once const toInsert = []; @@ -285,6 +289,13 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + const absoluteY = this.ybase + this.y; + if (absoluteY >= y && absoluteY < y + wrappedLines.length) { + continue; + } + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); const linesToAdd = destLineLengths.length - wrappedLines.length; @@ -357,10 +368,13 @@ export class Buffer implements IBuffer { this.ydisp++; } } else { - if (this.ybase === this.ydisp) { - this.ydisp++; + // Ensure ybase does not exceed its maximum value + if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) { + if (this.ybase === this.ydisp) { + this.ydisp++; + } + this.ybase++; } - this.ybase++; } } } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 59934e46..24ab69e6 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -3,10 +3,10 @@ * @license MIT */ +import { FILL_CHAR_DATA } from './Buffer'; import { BufferLine } from './BufferLine'; import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; -import { FILL_CHAR_DATA } from './Buffer'; export interface INewLayoutResult { layout: number[]; @@ -19,7 +19,7 @@ export interface INewLayoutResult { * @param lines The buffer lines. * @param newCols The columns after resize. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { +export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number, bufferAbsoluteY: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; @@ -39,6 +39,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n nextLine = lines.get(++i) as BufferLine; } + // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped + // lines with the cursor + if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { + y += wrappedLines.length - 1; + continue; + } + // Copy buffer data to new locations let destLineIndex = 0; let destCol = wrappedLines[destLineIndex].getTrimmedLength(); @@ -64,7 +71,7 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n } // Make sure the last cell isn't wide, if it is copy it to the current dest - if (destCol === 0) { + if (destCol === 0 && destLineIndex !== 0) { if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); // Null out the end of the last row @@ -166,7 +173,6 @@ export function reflowLargerApplyNewLayout(lines: CircularList, new */ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and From db31a44da3da0100ae26d0fb6a20eeecf61881f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Jan 2019 22:14:47 -0800 Subject: [PATCH 093/140] Fix tests --- src/Buffer.test.ts | 66 +++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index a56fd63a..2a244472 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -265,6 +265,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); firstLine.set(i, [null, char, 1, code]); } + buffer.y = 1; assert.equal(buffer.lines.get(0).length, 5); assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); buffer.resize(1, 10); @@ -296,7 +297,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; buffer.resize(10, 5); - const lastLine = buffer.lines.get(4); + const lastLine = buffer.lines.get(3); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -308,27 +309,27 @@ describe('Buffer', () => { assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), ' '); - assert.equal(buffer.lines.get(1).translateToString(), 'ab'); - assert.equal(buffer.lines.get(2).translateToString(), 'cd'); - assert.equal(buffer.lines.get(3).translateToString(), 'ef'); - assert.equal(buffer.lines.get(4).translateToString(), 'gh'); - assert.equal(buffer.lines.get(5).translateToString(), 'ij'); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), ' '); buffer.resize(1, 5); assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), 'e'); - assert.equal(buffer.lines.get(1).translateToString(), 'f'); - assert.equal(buffer.lines.get(2).translateToString(), 'g'); - assert.equal(buffer.lines.get(3).translateToString(), 'h'); - assert.equal(buffer.lines.get(4).translateToString(), 'i'); - assert.equal(buffer.lines.get(5).translateToString(), 'j'); + assert.equal(buffer.lines.get(0).translateToString(), 'f'); + assert.equal(buffer.lines.get(1).translateToString(), 'g'); + assert.equal(buffer.lines.get(2).translateToString(), 'h'); + assert.equal(buffer.lines.get(3).translateToString(), 'i'); + assert.equal(buffer.lines.get(4).translateToString(), 'j'); + assert.equal(buffer.lines.get(5).translateToString(), ' '); buffer.resize(10, 5); - assert.equal(buffer.y, 0); + assert.equal(buffer.y, 1); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'efghij '); + assert.equal(buffer.lines.get(0).translateToString(), 'fghij '); assert.equal(buffer.lines.get(1).translateToString(), ' '); assert.equal(buffer.lines.get(2).translateToString(), ' '); assert.equal(buffer.lines.get(3).translateToString(), ' '); @@ -339,6 +340,7 @@ describe('Buffer', () => { // 3+ lines removed on a reflow actually remove the right lines buffer.fillViewportRows(); buffer.resize(10, 10); + buffer.y = 2; const firstLine = buffer.lines.get(0); const secondLine = buffer.lines.get(1); for (let i = 0; i < 10; i++) { @@ -358,8 +360,8 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(i).translateToString(), ' '); } buffer.resize(2, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 11); assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'cd'); assert.equal(buffer.lines.get(2).translateToString(), 'ef'); @@ -370,7 +372,10 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(7).translateToString(), '45'); assert.equal(buffer.lines.get(8).translateToString(), '67'); assert.equal(buffer.lines.get(9).translateToString(), '89'); + assert.equal(buffer.lines.get(10).translateToString(), ' '); buffer.resize(10, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); for (let i = 2; i < 10; i++) { @@ -379,22 +384,23 @@ describe('Buffer', () => { }); it('should transfer combined char data over to reflowed lines', () => { buffer.fillViewportRows(); - buffer.resize(4, 2); + buffer.resize(4, 3); + buffer.y = 2; const firstLine = buffer.lines.get(0); firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]); firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]); firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]); firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - assert.equal(buffer.lines.length, 2); + assert.equal(buffer.lines.length, 3); assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); assert.equal(buffer.lines.get(1).translateToString(), ' '); - buffer.resize(2, 2); + buffer.resize(2, 3); assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); }); it('should adjust markers when reflowing', () => { buffer.fillViewportRows(); - buffer.resize(10, 15); + buffer.resize(10, 16); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -410,6 +416,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); buffer.lines.get(2).set(i, [null, char, 1, code]); } + buffer.y = 3; // Buffer: // abcdefghij // 0123456789 @@ -423,7 +430,7 @@ describe('Buffer', () => { assert.equal(firstMarker.line, 0); assert.equal(secondMarker.line, 1); assert.equal(thirdMarker.line, 2); - buffer.resize(2, 15); + buffer.resize(2, 16); assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'cd'); assert.equal(buffer.lines.get(2).translateToString(), 'ef'); @@ -442,7 +449,7 @@ describe('Buffer', () => { assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); - buffer.resize(10, 15); + buffer.resize(10, 16); assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); @@ -456,7 +463,7 @@ describe('Buffer', () => { it('should dispose markers whose rows are trimmed during a reflow', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; - buffer.resize(10, 10); + buffer.resize(10, 11); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -472,6 +479,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); buffer.lines.get(2).set(i, [null, char, 1, code]); } + buffer.y = 10; // Buffer: // abcdefghij // 0123456789 @@ -479,14 +487,14 @@ describe('Buffer', () => { const firstMarker = buffer.addMarker(0); const secondMarker = buffer.addMarker(1); const thirdMarker = buffer.addMarker(2); - buffer.y = 2; + buffer.y = 3; assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); assert.equal(firstMarker.line, 0); assert.equal(secondMarker.line, 1); assert.equal(thirdMarker.line, 2); - buffer.resize(2, 10); + buffer.resize(2, 11); assert.equal(buffer.lines.get(0).translateToString(), 'ij'); assert.equal(buffer.lines.get(1).translateToString(), '01'); assert.equal(buffer.lines.get(2).translateToString(), '23'); @@ -503,7 +511,7 @@ describe('Buffer', () => { assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); assert.equal(secondMarker.isDisposed, false); assert.equal(thirdMarker.isDisposed, false); - buffer.resize(10, 10); + buffer.resize(10, 11); assert.equal(buffer.lines.get(0).translateToString(), 'ij '); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); @@ -513,6 +521,7 @@ describe('Buffer', () => { it('should wrap wide characters correctly when reflowing larger', () => { buffer.fillViewportRows(); buffer.resize(12, 10); + buffer.y = 2; for (let i = 0; i < 12; i += 4) { buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); @@ -547,6 +556,7 @@ describe('Buffer', () => { it('should wrap wide characters correctly when reflowing smaller', () => { buffer.fillViewportRows(); buffer.resize(12, 10); + buffer.y = 2; for (let i = 0; i < 12; i += 4) { buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); @@ -937,6 +947,7 @@ describe('Buffer', () => { describe('&& ydisp === ybase', () => { it('should trim lines and keep ydisp = ybase', () => { buffer.ydisp = 10; + buffer.y = 13; buffer.resize(2, 10); assert.equal(buffer.ydisp, 10); assert.equal(buffer.ybase, 10); @@ -962,6 +973,7 @@ describe('Buffer', () => { describe('&& ydisp !== ybase', () => { it('should trim lines and not change ydisp', () => { buffer.ydisp = 5; + buffer.y = 13; buffer.resize(2, 10); assert.equal(buffer.ydisp, 5); assert.equal(buffer.ybase, 10); From 7b7f85e3cf0976b157a64c727276ad3b9d807e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 31 Jan 2019 22:56:55 +0100 Subject: [PATCH 094/140] add test for correct tab handling, docs --- src/Buffer.test.ts | 7 +++++++ src/Buffer.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 1ef2271f..53de19b7 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -1312,6 +1312,13 @@ describe('Buffer', () => { terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); + + it('should handle \t in lines correctly', () => { + const input = '\thttps://google.de'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(s, Array(terminal.getOption('tabStopWidth') + 1).join(' ') + 'https://google.de'); + }); }); describe('BufferStringIterator', function(): void { it('iterator does not overflow buffer limits', function(): void { diff --git a/src/Buffer.ts b/src/Buffer.ts index 8eddf73f..7f4b6071 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -458,7 +458,9 @@ export class Buffer implements IBuffer { const length = (trimRight) ? line.getTrimmedLength() : line.length; for (let i = 0; i < length; ++i) { if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) { - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; // WHITESPACE_CELL_CHAR.length + // empty cells report a string length of 0, but get replaced + // with a whitespace in translateToString, thus replace with 1 + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; } if (stringIndex < 0) { return [lineIndex, i]; From 828f14f2abd58f42497903e745fbfc0af02c34bd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Jan 2019 14:00:39 -0800 Subject: [PATCH 095/140] Improve docs on Terminal.resize --- typings/xterm.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index dc9ebbae..d813bc5f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -443,7 +443,9 @@ declare module 'xterm' { addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; /** - * Resizes the terminal. + * Resizes the terminal. It's best practice to debounce calls to resize, + * this will help ensure that the pty can respond to the resize event + * before another one occurs. * @param x The number of columns to resize to. * @param y The number of rows to resize to. */ From 079729f1b43a1c85c6afe03cc18e7e20dd6ad60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 1 Feb 2019 01:10:20 +0100 Subject: [PATCH 096/140] change property getter into methods --- src/Buffer.test.ts | 18 +- src/BufferLine.test.ts | 40 +-- src/BufferLine.ts | 14 +- src/CharWidth.test.ts | 2 +- src/InputHandler.ts | 4 +- src/SelectionManager.ts | 16 +- src/Terminal.integration.ts | 2 +- src/Terminal.test.ts | 298 +++++++++++----------- src/Types.ts | 10 +- src/renderer/BaseRenderLayer.ts | 2 +- src/renderer/CharacterJoinerRegistry.ts | 4 +- src/renderer/CursorRenderLayer.ts | 10 +- src/renderer/TextRenderLayer.ts | 8 +- src/renderer/dom/DomRendererRowFactory.ts | 6 +- 14 files changed, 217 insertions(+), 217 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 57de302a..59475adb 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -37,13 +37,13 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).loadCell(0, new CellData()).asCharData; + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).loadCell(0, new CellData()).getAsCharData; buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { assert.equal(buffer.lines.get(y).length, INIT_COLS); for (let x = 0; x < INIT_COLS; x++) { - assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).asCharData, blankLineChar); + assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData, blankLineChar); } } }); @@ -155,15 +155,15 @@ describe('Buffer', () => { assert.equal(buffer.lines.maxLength, INIT_ROWS); buffer.y = INIT_ROWS - 1; buffer.fillViewportRows(); - let chData = buffer.lines.get(5).loadCell(0, new CellData()).asCharData; + let chData = buffer.lines.get(5).loadCell(0, new CellData()).getAsCharData(); chData[1] = 'a'; buffer.lines.get(5).setCell(0, CellData.fromCharData(chData)); - chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).asCharData; + chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getAsCharData(); chData[1] = 'b'; buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData)); buffer.resize(INIT_COLS, INIT_ROWS - 5); - assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).asCharData[1], 'a'); - assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).asCharData[1], 'b'); + assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).getAsCharData()[1], 'a'); + assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).getAsCharData()[1], 'b'); }); }); }); @@ -1264,7 +1264,7 @@ describe('Buffer', () => { assert.equal(input, s); const stringIndex = s.match(/😃/).index; const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).chars, '😃'); + assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); }); it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { @@ -1291,7 +1291,7 @@ describe('Buffer', () => { assert.equal(input, s); for (let i = 0; i < input.length; ++i) { const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).chars); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); } }); @@ -1309,7 +1309,7 @@ describe('Buffer', () => { : (i % 3 === 1) ? input.substr(i, 2) : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).chars); + terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); } }); diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 894cc204..7dfcbd2c 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -16,7 +16,7 @@ class TestBufferLine extends BufferLine { public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { - result.push(this.loadCell(i, new CellData()).asCharData); + result.push(this.loadCell(i, new CellData()).getAsCharData()); } return result; } @@ -27,24 +27,24 @@ describe('CellData', () => { const cell = new CellData(); // ASCII cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.deepEqual(cell.asCharData, [123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined, 0); + chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]); + chai.assert.equal(cell.isCombined(), 0); // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined, Content.IS_COMBINED); + chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); - chai.assert.deepEqual(cell.asCharData, [123, '𝄞', 1, 0x1D11E]); - chai.assert.equal(cell.isCombined, 0); + chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); + chai.assert.equal(cell.isCombined(), 0); // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.deepEqual(cell.asCharData, [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.isCombined, Content.IS_COMBINED); + chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); - chai.assert.deepEqual(cell.asCharData, [123, '1', 2, '1'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined, 0); + chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); + chai.assert.equal(cell.isCombined(), 0); }); }); @@ -55,15 +55,15 @@ describe('BufferLine', function(): void { chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.loadCell(0, new CellData()).asCharData).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.loadCell(0, new CellData()).asCharData).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(true); line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); chai.expect(line.length).equals(10); - chai.expect(line.loadCell(0, new CellData()).asCharData).eql([123, 'a', 456, 'a'.charCodeAt(0)]); + chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); }); it('insertCells', function(): void { @@ -335,9 +335,9 @@ describe('BufferLine', function(): void { const cell = line.loadCell(0, new CellData()); // chars contains single combining char // width is set to 1 - chai.assert.deepEqual(cell.asCharData, [DEFAULT_ATTR, '\u0301', 1, 0x0301]); + chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined, 0); + chai.assert.equal(cell.isCombined(), 0); }); it('should add char to combining string in cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -348,9 +348,9 @@ describe('BufferLine', function(): void { line.loadCell(0, cell); // chars contains 3 chars // width is set to 1 - chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301\u0301', 1, 0x0301]); + chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -361,9 +361,9 @@ describe('BufferLine', function(): void { line.loadCell(0, cell); // chars contains 2 chars // width is set to 1 - chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, 0x0301]); + chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 120a15fc..14518454 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -95,17 +95,17 @@ export class CellData implements ICellData { public combinedData: string = ''; /** Whether cell contains a combined string. */ - public get isCombined(): number { + public isCombined(): number { return this.content & Content.IS_COMBINED; } /** Width of the cell. */ - public get width(): number { + public getWidth(): number { return this.content >> Content.WIDTH_SHIFT; } /** JS string of the content. */ - public get chars(): string { + public getChars(): string { if (this.content & Content.IS_COMBINED) { return this.combinedData; } @@ -121,8 +121,8 @@ export class CellData implements ICellData { * if content is a combined string it returns the codepoint * of the last char in string to be in line with code in CharData. * */ - public get code(): number { - return (this.isCombined) + public getCode(): number { + return (this.isCombined()) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK; } @@ -160,8 +160,8 @@ export class CellData implements ICellData { } /** Get data as CharData. */ - public get asCharData(): CharData { - return [this.fg, this.chars, this.width, this.code]; + public getAsCharData(): CharData { + return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; } } diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index 7cab3882..8608c6fa 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -23,7 +23,7 @@ describe('getStringCellWidth', function(): void { for (let i = start; i < end; ++i) { const line = buffer.lines.get(i); for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline - const ch = line.loadCell(j, new CellData()).asCharData; + const ch = line.loadCell(j, new CellData()).getAsCharData(); result += ch[CHAR_DATA_WIDTH_INDEX]; // return on sentinel if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a9555c76..ccd74a2f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -351,7 +351,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - if (!bufferRow.loadCell(buffer.x - 1, this._cell).width) { + if (!bufferRow.loadCell(buffer.x - 1, this._cell).getWidth()) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars @@ -400,7 +400,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell - if (bufferRow.loadCell(cols - 1, this._cell).width === 2) { + if (bufferRow.loadCell(cols - 1, this._cell).getWidth() === 2) { bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index e2399173..9d49a860 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -669,8 +669,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const length = bufferLine.loadCell(i, this._cell).chars.length; - if (this._cell.width === 0) { + const length = bufferLine.loadCell(i, this._cell).getChars().length; + if (this._cell.getWidth() === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. charIndex--; @@ -757,8 +757,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Expand the string in both directions until a space is hit while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._cell))) { bufferLine.loadCell(startCol - 1, this._cell); - const length = this._cell.chars.length; - if (this._cell.width === 0) { + const length = this._cell.getChars().length; + if (this._cell.getWidth() === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; @@ -773,8 +773,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._cell))) { bufferLine.loadCell(endCol + 1, this._cell); - const length = this._cell.chars.length; - if (this._cell.width === 2) { + const length = this._cell.getChars().length; + if (this._cell.getWidth() === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; @@ -899,10 +899,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _isCharWordSeparator(cell: CellData): boolean { // Zero width characters are never separators as they are always to the // right of wide characters - if (cell.width === 0) { + if (cell.getWidth() === 0) { return false; } - return WORD_SEPARATORS.indexOf(cell.chars) >= 0; + return WORD_SEPARATORS.indexOf(cell.getChars()) >= 0; } /** diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index b5165490..10043006 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -68,7 +68,7 @@ function terminalToString(term: Terminal): string { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { lineText = ''; for (let cell = 0; cell < term.cols; ++cell) { - lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).chars || WHITESPACE_CELL_CHAR; + lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR; } // rtrim empty cells as xterm does lineText = lineText.replace(/\s+$/, ''); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index e06ceb78..08bceb34 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -461,9 +461,9 @@ describe('term.js addons', () => { term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).chars, 'b'); - assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).chars, ''); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { @@ -474,8 +474,8 @@ describe('term.js addons', () => { term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { @@ -488,12 +488,12 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a', '\'a\' should be pushed to the scrollback'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'b'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'c'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, 'd'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).chars, 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).getChars(), 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { @@ -507,11 +507,11 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'd'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); }); }); @@ -530,10 +530,10 @@ describe('term.js addons', () => { term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'b'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, ''); - assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).chars, 'c'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).chars, ''); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), ''); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { @@ -544,8 +544,8 @@ describe('term.js addons', () => { term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { @@ -558,11 +558,11 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'b'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'd'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { @@ -576,11 +576,11 @@ describe('term.js addons', () => { term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).chars, 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).chars, 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).chars, 'd'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).chars, '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).chars, 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); }); }); }); @@ -775,10 +775,10 @@ describe('term.js addons', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high + String.fromCharCode(i)); const tchar = term.buffer.lines.get(0).loadCell(0, cell); - expect(tchar.chars).eql(high + String.fromCharCode(i)); - expect(tchar.chars.length).eql(2); - expect(tchar.width).eql(1); - expect(term.buffer.lines.get(0).loadCell(1, cell).chars).eql(''); + expect(tchar.getChars()).eql(high + String.fromCharCode(i)); + expect(tchar.getChars().length).eql(2); + expect(tchar.getWidth()).eql(1); + expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -788,9 +788,9 @@ describe('term.js addons', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.write(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).chars).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).chars.length).eql(2); - expect(term.buffer.lines.get(1).loadCell(0, cell).chars).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(''); term.reset(); } }); @@ -801,10 +801,10 @@ describe('term.js addons', () => { term.buffer.x = term.cols - 1; term.wraparoundMode = true; term.write('a' + high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).chars).eql('a'); - expect(term.buffer.lines.get(1).loadCell(0, cell).chars).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(1).loadCell(0, cell).chars.length).eql(2); - expect(term.buffer.lines.get(1).loadCell(1, cell).chars).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -816,9 +816,9 @@ describe('term.js addons', () => { term.wraparoundMode = false; term.write('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line - expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).chars).eql('a'); - expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).chars.length).eql(1); - expect(term.buffer.lines.get(1).loadCell(1, cell).chars).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1); + expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -829,10 +829,10 @@ describe('term.js addons', () => { term.write(high); term.write(String.fromCharCode(i)); const tchar = term.buffer.lines.get(0).loadCell(0, cell); - expect(tchar.chars).eql(high + String.fromCharCode(i)); - expect(tchar.chars.length).eql(2); - expect(tchar.width).eql(1); - expect(term.buffer.lines.get(0).loadCell(1, cell).chars).eql(''); + expect(tchar.getChars()).eql(high + String.fromCharCode(i)); + expect(tchar.getChars().length).eql(2); + expect(tchar.getWidth()).eql(1); + expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -843,49 +843,49 @@ describe('term.js addons', () => { it('café', () => { term.write('cafe\u0301'); term.buffer.lines.get(0).loadCell(3, cell); - expect(cell.chars).eql('e\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(1); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); }); it('café - end of line', () => { term.buffer.x = term.cols - 1 - 3; term.write('cafe\u0301'); term.buffer.lines.get(0).loadCell(term.cols - 1, cell); - expect(cell.chars).eql('e\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(1); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); term.buffer.lines.get(0).loadCell(1, cell); - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(1); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); }); it('multiple combined é', () => { term.wraparoundMode = true; term.write(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); - expect(cell.chars).eql('e\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(1); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); } term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('e\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(1); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); }); it('multiple surrogate with combined', () => { term.wraparoundMode = true; term.write(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); - expect(cell.chars).eql('\uD800\uDC00\u0301'); - expect(cell.chars.length).eql(3); - expect(cell.width).eql(1); + expect(cell.getChars()).eql('\uD800\uDC00\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(1); } term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('\uD800\uDC00\u0301'); - expect(cell.chars.length).eql(3); - expect(cell.width).eql(1); + expect(cell.getChars()).eql('\uD800\uDC00\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(1); }); }); @@ -908,19 +908,19 @@ describe('term.js addons', () => { for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(cell.chars).eql('¥'); - expect(cell.chars.length).eql(1); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); } } term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('¥'); - expect(cell.chars.length).eql(1); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); }); it('line of ¥ odd', () => { term.wraparoundMode = true; @@ -929,23 +929,23 @@ describe('term.js addons', () => { for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(cell.chars).eql('¥'); - expect(cell.chars.length).eql(1); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); } } term.buffer.lines.get(0).loadCell(term.cols - 1, cell); - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(1); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('¥'); - expect(cell.chars.length).eql(1); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining odd', () => { term.wraparoundMode = true; @@ -954,23 +954,23 @@ describe('term.js addons', () => { for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(cell.chars).eql('¥\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); } } term.buffer.lines.get(0).loadCell(term.cols - 1, cell); - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(1); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('¥\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining even', () => { term.wraparoundMode = true; @@ -978,19 +978,19 @@ describe('term.js addons', () => { for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(cell.chars).eql('¥\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); } } term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('¥\u0301'); - expect(cell.chars.length).eql(2); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining odd', () => { term.wraparoundMode = true; @@ -999,23 +999,23 @@ describe('term.js addons', () => { for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(cell.chars).eql('\ud843\ude6d\u0301'); - expect(cell.chars.length).eql(3); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); } } term.buffer.lines.get(0).loadCell(term.cols - 1, cell); - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(1); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('\ud843\ude6d\u0301'); - expect(cell.chars.length).eql(3); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining even', () => { term.wraparoundMode = true; @@ -1023,19 +1023,19 @@ describe('term.js addons', () => { for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(cell.chars).eql(''); - expect(cell.chars.length).eql(0); - expect(cell.width).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(cell.chars).eql('\ud843\ude6d\u0301'); - expect(cell.chars.length).eql(3); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); } } term.buffer.lines.get(1).loadCell(0, cell); - expect(cell.chars).eql('\ud843\ude6d\u0301'); - expect(cell.chars.length).eql(3); - expect(cell.width).eql(2); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); }); }); @@ -1048,10 +1048,10 @@ describe('term.js addons', () => { term.insertMode = true; term.write('abcde'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(10, cell).chars).eql('a'); - expect(term.buffer.lines.get(0).loadCell(14, cell).chars).eql('e'); - expect(term.buffer.lines.get(0).loadCell(15, cell).chars).eql('0'); - expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql('4'); + expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e'); + expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0'); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4'); }); it('fullwidth - insert', () => { term.write(Array(9).join('0123456789').slice(-80)); @@ -1060,11 +1060,11 @@ describe('term.js addons', () => { term.insertMode = true; term.write('¥¥¥'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(10, cell).chars).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(11, cell).chars).eql(''); - expect(term.buffer.lines.get(0).loadCell(14, cell).chars).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(15, cell).chars).eql(''); - expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql('3'); + expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3'); }); it('fullwidth - right border', () => { term.write(Array(41).join('¥')); @@ -1073,14 +1073,14 @@ describe('term.js addons', () => { term.insertMode = true; term.write('a'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(10, cell).chars).eql('a'); - expect(term.buffer.lines.get(0).loadCell(11, cell).chars).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql(''); // fullwidth char got replaced + expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced term.write('b'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(11, cell).chars).eql('b'); - expect(term.buffer.lines.get(0).loadCell(12, cell).chars).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(79, cell).chars).eql(''); // empty cell after fullwidth + expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b'); + expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth }); }); }); diff --git a/src/Types.ts b/src/Types.ts index 9588505c..a08bd485 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -527,12 +527,12 @@ export interface ICellData { fg: number; bg: number; combinedData: string; - isCombined: number; - width: number; - chars: string; - code: number; + isCombined(): number; + getWidth(): number; + getChars(): string; + getCode(): number; setFromCharData(value: CharData): void; - asCharData: CharData; + getAsCharData(): CharData; } /** diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index f84fbe6b..addd028c 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -239,7 +239,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( - cell.chars, + cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); } diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index 4cad7c72..eb44bb58 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -56,8 +56,8 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { for (let x = 0; x < this._terminal.cols; x++) { line.loadCell(x, this._cell); - const chars = this._cell.chars; - const width = this._cell.width; + const chars = this._cell.getChars(); + const width = this._cell.getWidth(); const attr = this._cell.fg >> 9; if (width === 0) { diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 18ba7ada..c3b751fa 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -143,7 +143,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state.y = viewportRelativeCursorY; this._state.isFocused = false; this._state.style = terminal.options.cursorStyle; - this._state.width = this._cell.width; + this._state.width = this._cell.getWidth(); return; } @@ -159,7 +159,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state.y === viewportRelativeCursorY && this._state.isFocused === terminal.isFocused && this._state.style === terminal.options.cursorStyle && - this._state.width === this._cell.width) { + this._state.width === this._cell.getWidth()) { return; } this._clearCursor(); @@ -173,7 +173,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state.y = viewportRelativeCursorY; this._state.isFocused = false; this._state.style = terminal.options.cursorStyle; - this._state.width = this._cell.width; + this._state.width = this._cell.getWidth(); } private _clearCursor(): void { @@ -199,7 +199,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBlockCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this.fillCells(x, y, cell.width, 1); + this.fillCells(x, y, cell.getWidth(), 1); this._ctx.fillStyle = this._colors.cursorAccent.css; this.fillCharTrueColor(terminal, cell, x, y); this._ctx.restore(); @@ -215,7 +215,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBlurCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.strokeStyle = this._colors.cursor.css; - this.strokeRectAtCell(x, y, cell.width, 1); + this.strokeRectAtCell(x, y, cell.getWidth(), 1); this._ctx.restore(); } } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index bf7c5616..be022239 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -75,13 +75,13 @@ export class TextRenderLayer extends BaseRenderLayer { const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { (line as any).loadCell(x, this._cell); - let code: number = this._cell.code || WHITESPACE_CELL_CODE; + let code: number = this._cell.getCode() || WHITESPACE_CELL_CODE; // Can either represent character(s) for a single cell or multiple cells // if indicated by a character joiner. - let chars = this._cell.chars || WHITESPACE_CELL_CHAR; + let chars = this._cell.getChars() || WHITESPACE_CELL_CHAR; const attr = this._cell.fg; - let width = this._cell.width; + let width = this._cell.getWidth(); // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -127,7 +127,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).code === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).getCode() === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 83a4651e..5bc5fffa 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -33,7 +33,7 @@ export class DomRendererRowFactory { // the viewport). let lineLength = 0; for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { - if (lineData.loadCell(x, this._cell).code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + if (lineData.loadCell(x, this._cell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; break; } @@ -42,7 +42,7 @@ export class DomRendererRowFactory { for (let x = 0; x < lineLength; x++) { lineData.loadCell(x, this._cell); const attr = this._cell.fg; - const width = this._cell.width; + const width = this._cell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -100,7 +100,7 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } - charElement.textContent = this._cell.chars || WHITESPACE_CELL_CHAR; + charElement.textContent = this._cell.getChars() || WHITESPACE_CELL_CHAR; if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); } From 54a1319f57c4a8f38f91209705d4d85f402940b0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Feb 2019 18:23:10 -0800 Subject: [PATCH 097/140] v3.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ed3a4b8..fa9cc070 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.10.0", + "version": "3.11.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", From 1829cb7609029405230e69803c0262f2768a40ff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Feb 2019 19:28:07 -0800 Subject: [PATCH 098/140] Add roadmap wiki link to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35ccc9b6..f22c6c00 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht Xterm.js follows a monthly release cycle roughly. -All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). ## Contributing From f3aac10bbd32efb4c5f648d7fe7b7cdd10a25a55 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Feb 2019 10:46:20 -0800 Subject: [PATCH 099/140] Update license year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 28adbdad..4472336c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js) +Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) From 69d3a4667f6d3ade4a89e71be066de0e69d82d16 Mon Sep 17 00:00:00 2001 From: coderaiser Date: Mon, 4 Feb 2019 13:28:14 +0200 Subject: [PATCH 100/140] fix: Renderer: IntersectionObserver can produce more then 1 entry --- src/renderer/Renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 02328877..b8ef87aa 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -70,7 +70,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 }); + const observer = new IntersectionObserver(e => this.onIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(this._terminal.element); this.register({ dispose: () => observer.disconnect() }); } From b88230db8ba0d54fb194f5b903215a53b017a743 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 5 Feb 2019 08:20:16 -0800 Subject: [PATCH 101/140] Make sure the viewport is filled when reflowing a row change Fixes #1926 --- src/Buffer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7f4b6071..e40fa8b4 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -233,22 +233,22 @@ export class Buffer implements IBuffer { // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { - this._reflowLarger(newCols); + this._reflowLarger(newCols, newRows); } else { this._reflowSmaller(newCols, newRows); } } - private _reflowLarger(newCols: number): void { + private _reflowLarger(newCols: number, newRows: number): void { const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); - this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved); + this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved); } } - private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void { + private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { // Adjust viewport based on number of items removed let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { @@ -256,7 +256,7 @@ export class Buffer implements IBuffer { if (this.y > 0) { this.y--; } - if (this.lines.length < this._rows) { + if (this.lines.length < newRows) { // Add an extra row at the bottom of the viewport this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); } From d17dfbc73fc61d467ad079e4e0b90ae778687a88 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Feb 2019 12:13:00 -0800 Subject: [PATCH 102/140] Cover a case when resizing smaller making y go out of bounds --- src/Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index e40fa8b4..d017aa7e 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -360,7 +360,7 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._rows - 1) { + if (this.y < newRows - 1) { this.y++; this.lines.pop(); } else { From 92be01dd8188b9b2eb730048526f51443b4fac6c Mon Sep 17 00:00:00 2001 From: Ahtsham Raziq Date: Sat, 9 Feb 2019 23:24:32 +0500 Subject: [PATCH 103/140] Compose file: fix variable substitution --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8e6a2f46..6eefed89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: volumes: - ./:/usr/src/app ports: - - ${XTERMJS_PORT:3000}:3000 + - ${XTERMJS_PORT:-3000}:3000 command: ["npm", "start"] watch: From 84d7bfeacce308c0dc762e038a20ea36b03b44cd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 05:14:55 -0800 Subject: [PATCH 104/140] Remove font-family from .css file Fixes #1935 --- src/xterm.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xterm.css b/src/xterm.css index 24cd475f..2e47b1a1 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -36,7 +36,6 @@ */ .xterm { - font-family: courier-new, courier, monospace; font-feature-settings: "liga" 0; position: relative; user-select: none; From 817401bbcd08c45ffa8169341213d49b7de81823 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 05:32:47 -0800 Subject: [PATCH 105/140] Align y draw coord with how cache draws it Fixes #1937 --- src/renderer/BaseRenderLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3e0b8643..a609d79c 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -241,7 +241,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } /** @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); this._ctx.restore(); } From 78426d8a12c56f40cf1c2d74fb53c23f2982ebb5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 06:05:14 -0800 Subject: [PATCH 106/140] Make the composition view use the same font as the terminal --- src/CompositionHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 31ad866b..5f838c7a 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -203,6 +203,7 @@ export class CompositionHelper { this._compositionView.style.top = cursorTop + 'px'; this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.fontFamily = this._terminal.options.fontFamily; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this._compositionView.getBoundingClientRect(); From c934200c86ccaca419cae6aae210880a52791c46 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Feb 2019 06:07:31 -0800 Subject: [PATCH 107/140] Also set font size --- src/CompositionHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 5f838c7a..840bef55 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -204,6 +204,7 @@ export class CompositionHelper { this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px'; this._compositionView.style.fontFamily = this._terminal.options.fontFamily; + this._compositionView.style.fontSize = this._terminal.options.fontSize + 'px'; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this._compositionView.getBoundingClientRect(); From 9f29eeed2338dbc8861c65a1a35631fc34d980e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E8=99=AB?= Date: Tue, 19 Feb 2019 17:19:19 +0800 Subject: [PATCH 108/140] Update README.md (Jumpserver)[https://github.com/jumpserver/] use xterm.js --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f22c6c00..59e406e9 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. From e1c1c7a4f217f75eea4ed71fd0afcf5a1f14a93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E8=99=AB?= Date: Wed, 20 Feb 2019 14:57:16 +0800 Subject: [PATCH 109/140] Update README.md move it to the bottom --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59e406e9..17bc9fb4 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js -- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. @@ -155,6 +154,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. - [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 3285374618a2ea112e5124c3b551ae0ac0761035 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Feb 2019 07:03:40 -0800 Subject: [PATCH 110/140] Disable reflow when winptyCompat is on Fixes #1943 --- src/Buffer.ts | 6 +++++- src/addons/winptyCompat/winptyCompat.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7f4b6071..bf0bfe17 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -211,7 +211,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback) { + if (this._isReflowEnabled) { this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk @@ -226,6 +226,10 @@ export class Buffer implements IBuffer { this._rows = newRows; } + private get _isReflowEnabled(): boolean { + return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled; + } + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index aec580ed..d162f4e9 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -19,6 +19,8 @@ export function winptyCompatInit(terminal: Terminal): void { return; } + (addonTerminal._core as any).isWinptyCompatEnabled = true; + // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening From 0854f846533689b253e3a6b6924216b2b52592f3 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Tue, 26 Feb 2019 11:28:51 +0100 Subject: [PATCH 111/140] actually fix mouse handler before term attached --- src/InputHandler.ts | 8 ++++++-- src/Terminal.ts | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2f53cfcb..7405ff9f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1284,7 +1284,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); } - this._terminal.selectionManager.disable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.disable(); + } this._terminal.log('Binding to mouse events.'); break; case 1004: // send focusin/focusout events @@ -1474,7 +1476,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.remove('enable-mouse-events'); } - this._terminal.selectionManager.enable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.enable(); + } break; case 1004: // send focusin/focusout events this._terminal.sendFocus = false; diff --git a/src/Terminal.ts b/src/Terminal.ts index c1fc8ec8..cb9bc675 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -738,6 +738,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.mouseHelper = new MouseHelper(this.renderer); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); + if (this.mouseEvents) { + this.selectionManager.disable() + } else { + this.selectionManager.enable() + } if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to From e178139907a8a9a098a249849931faf89bdec5dc Mon Sep 17 00:00:00 2001 From: Nick Shaffner Date: Wed, 27 Feb 2019 22:37:22 -0800 Subject: [PATCH 112/140] Fix for issue #812: Xterm.js's encoding of mouse coordinate See: https://github.com/xtermjs/xterm.js/issues/812 Changed the utf-8 mouse encoding to match iTerm --- src/Terminal.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c1fc8ec8..0539a904 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -854,16 +854,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (ch > 127) ch = 127; data.push(ch); } else { - if (ch === 2047) { - data.push(0); + if (ch > 2047) { + data.push(2047); return; - } - if (ch < 127) { - data.push(ch); } else { - if (ch > 2047) ch = 2047; - data.push(0xC0 | (ch >> 6)); - data.push(0x80 | (ch & 0x3F)); + data.push(ch); } } } From 4e479b455a659a53c7a26d05549bd38ba301c9be Mon Sep 17 00:00:00 2001 From: Ioannis Cherouvim <743305+cherouvim@users.noreply.github.com> Date: Fri, 1 Mar 2019 14:40:21 +0200 Subject: [PATCH 113/140] docs: Consistent style on lists. Added some missing dots, capitalized a couple of lines. --- CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7924027..e8102a2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,21 +33,21 @@ opening an issue, read these pointers. 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 -- Make your changes + ([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. +- Make your changes. - If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory. - Follow the general code style of the rest of the project (see below). - Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)). Don't put more than one feature/fix in a single pull request. -By contributing code to xterm.js you +By contributing code to xterm.js you: - - agree to license the contributed code under xterm.js' [MIT + - Agree to license the contributed code under xterm.js' [MIT license](LICENSE). - - confirm that you have the right to contribute and license the code + - Confirm that you have the right to contribute and license the code in question. (Either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct From a8a0344d1ac5ea411ea244a73754a06ffda2d308 Mon Sep 17 00:00:00 2001 From: Ioannis Cherouvim <743305+cherouvim@users.noreply.github.com> Date: Fri, 1 Mar 2019 14:51:19 +0200 Subject: [PATCH 114/140] docs: style improvements - Added a trailing dot at the end of each list line. - Consistent usage of `xterm.js` in "Real-world uses". --- README.md | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 17bc9fb4..d70e8b7e 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Xterm.js is a front-end component written in TypeScript that lets applications b ## Features -- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support -- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer -- **Rich unicode support**: Supports CJK, emojis and IMEs -- **Self-contained**: Requires zero dependencies to work -- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option +- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support. +- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. +- **Rich unicode support**: Supports CJK, emojis and IMEs. +- **Self-contained**: Requires zero dependencies to work. +- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. - **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not -- Xterm.js is not a terminal application that you can download and use on your computer -- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) +- Xterm.js is not a terminal application that you can download and use on your computer. +- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output). ## Getting Started @@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t @@ -106,9 +106,9 @@ Note that some APIs are marked *experimental*, these are added to enable experim ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. -- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js -- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js -- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js +- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on `xterm.js`. +- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on `xterm.js`. +- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on `xterm.js`. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. @@ -125,11 +125,10 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. - [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. -- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible -computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. +- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. -- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. +- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses `xterm.js` for container terminals and the host shell. - [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. - [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017. - [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. @@ -138,23 +137,23 @@ computational environment for Jupyter, supporting interactive data science and s - [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. - [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. - [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. -- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS +- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS. - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. -- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users +- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users. - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. -- [**Hyper**](https://hyper.is): A terminal built on web technologies +- [**Hyper**](https://hyper.is): A terminal built on web technologies. - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. -- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. +- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on `xterm.js`. - [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. - [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice. -- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js +- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to `xterm.js`. - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. - [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. -- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard. -- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. +- [**info-beamer hosted**](https://info-beamer.com): Uses `xterm.js` to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use `xterm.js` for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 09754fddf8e3ba0dbbd3840b65a69822482fb64b Mon Sep 17 00:00:00 2001 From: Ioannis Cherouvim <743305+cherouvim@users.noreply.github.com> Date: Fri, 1 Mar 2019 21:15:20 +0200 Subject: [PATCH 115/140] xterm.js references to the library should not be backticked --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d70e8b7e..5378f443 100644 --- a/README.md +++ b/README.md @@ -106,17 +106,17 @@ Note that some APIs are marked *experimental*, these are added to enable experim ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. -- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on `xterm.js`. -- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on `xterm.js`. -- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on `xterm.js`. +- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js. +- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js. +- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. -- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`. -- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2. +- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js. +- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. -- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`. +- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. @@ -124,11 +124,11 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job. - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. -- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. +- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets. - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. -- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses `xterm.js` for container terminals and the host shell. +- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. - [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. - [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017. - [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. @@ -144,16 +144,16 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. - [**Hyper**](https://hyper.is): A terminal built on web technologies. - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. -- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on `xterm.js`. +- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. - [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. - [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice. -- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to `xterm.js`. +- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js. - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. - [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. -- [**info-beamer hosted**](https://info-beamer.com): Uses `xterm.js` to manage digital signage devices from the web dashboard. -- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use `xterm.js` for web terminal emulation. +- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From c63f15a9b26c770dcbcceca6dfaf33acb3121d67 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Mar 2019 10:00:04 -0800 Subject: [PATCH 116/140] Fix lint --- src/Terminal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index cb9bc675..5de369fe 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -739,9 +739,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { - this.selectionManager.disable() + this.selectionManager.disable(); } else { - this.selectionManager.enable() + this.selectionManager.enable(); } if (this.options.screenReaderMode) { From 32e157bfaa43164171bac02798fc293df30d151c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Mar 2019 10:04:52 -0800 Subject: [PATCH 117/140] Remove unnecessary else --- src/Terminal.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 0539a904..25c92fdf 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -857,9 +857,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (ch > 2047) { data.push(2047); return; - } else { - data.push(ch); } + data.push(ch); } } From 07430a4892365e65946c05b34d51ba668fbc2b9f Mon Sep 17 00:00:00 2001 From: Jesse Stolwijk Date: Tue, 5 Mar 2019 00:00:54 +0100 Subject: [PATCH 118/140] Replace array shift with offset (#1955) --- src/Terminal.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 4d480eb6..19b5f125 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1350,19 +1350,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } - protected _innerWrite(): void { + protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { this.writeBuffer = []; } const startTime = Date.now(); - while (this.writeBuffer.length > 0) { - const data = this.writeBuffer.shift(); + while (this.writeBuffer.length > bufferOffset) { + const data = this.writeBuffer[bufferOffset]; + bufferOffset++; // If XOFF was sent in order to catch up with the pty process, resume it if - // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && this.writeBuffer.length === 0) { + // we reached the end of the writeBuffer to allow more data to come in. + if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1385,11 +1386,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II break; } } - if (this.writeBuffer.length > 0) { + if (this.writeBuffer.length > bufferOffset) { // Allow renderer to catch up before processing the next batch - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; + this.writeBuffer = []; } } From 51e2cdfafb697a8076d9122fcf0e7bbc8f1840e8 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Wed, 6 Mar 2019 21:45:53 +0000 Subject: [PATCH 119/140] WIP: First draft --- src/addons/webLinks/webLinks.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index f0d69cc5..b9f2925a 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -36,6 +36,13 @@ function handleLink(event: MouseEvent, uri: string): void { */ export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { options.matchIndex = 1; + + handler = (event, uri) => { + if (!term.hasSelection()) { + window.open(uri, '_blank'); + } + }; + term.registerLinkMatcher(strictUrlRegex, handler, options); } From bc41cc7d279e7edd2b7f50b8032252efc4d88f31 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Thu, 7 Mar 2019 00:15:03 +0000 Subject: [PATCH 120/140] Fix #1908 --- src/ui/MouseZoneManager.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index a232f5b9..79022723 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -23,6 +23,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _areZonesActive: boolean = false; private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseLeaveListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; private _tooltipTimeout: number = null; @@ -38,6 +39,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); + this._mouseLeaveListener = e => this._onMouseLeave(e); this._clickListener = e => this._onClick(e); } @@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (!this._areZonesActive) { this._areZonesActive = true; this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.addEventListener('click', this._clickListener); } } @@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (this._areZonesActive) { this._areZonesActive = false; this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.removeEventListener('click', this._clickListener); } } @@ -169,6 +173,18 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } } + private _onMouseLeave(e: MouseEvent): void { + // Fire the hover end callback and cancel any existing timer if the mouse + // leaves the terminal element + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + } + private _onClick(e: MouseEvent): void { // Find the active zone and click it if found const zone = this._findZoneEventAt(e); From 5878aa099dcf093a9480bf110cde28f8ee011b69 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Thu, 7 Mar 2019 21:00:07 +0000 Subject: [PATCH 121/140] Cleaner aproach --- src/addons/webLinks/webLinks.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index b9f2925a..19200c90 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -24,9 +24,7 @@ const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); -function handleLink(event: MouseEvent, uri: string): void { - window.open(uri, '_blank'); -} +let handleLink: (event: MouseEvent, uri: string) => void; /** * Initialize the web links addon, registering the link matcher. @@ -37,17 +35,17 @@ function handleLink(event: MouseEvent, uri: string): void { export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { options.matchIndex = 1; - handler = (event, uri) => { - if (!term.hasSelection()) { - window.open(uri, '_blank'); - } - }; - term.registerLinkMatcher(strictUrlRegex, handler, options); } export function apply(terminalConstructor: typeof Terminal): void { (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { + handleLink = (event, uri) => { + if (!this.hasSelection()) { + window.open(uri, '_blank'); + } + }; + webLinksInit(this, handler, options); }; } From 525af9c36a3b57702d6048737d37aeb394464892 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Thu, 7 Mar 2019 21:30:32 +0000 Subject: [PATCH 122/140] Fix #1773 --- src/renderer/dom/DomRenderer.ts | 2 +- src/renderer/dom/DomRendererRowFactory.test.ts | 11 +++++++++-- src/renderer/dom/DomRendererRowFactory.ts | 7 ++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c5ef212d..5bff3d1c 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -75,7 +75,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); - this._rowFactory = new DomRendererRowFactory(document); + this._rowFactory = new DomRendererRowFactory(_terminal, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 67342da0..ab308077 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -9,17 +9,24 @@ import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; import { BufferLine } from '../../BufferLine'; -import { IBufferLine } from '../../Types'; +import { IBufferLine, ITerminal } from '../../Types'; import { DEFAULT_COLOR } from '../atlas/Types'; +import { MockTerminal } from '../../ui/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; + let term: ITerminal; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document); + + term = new MockTerminal(); + term.options.enableBold = true; + term.options.drawBoldTextInBrightColors = true; + + rowFactory = new DomRendererRowFactory(term, dom.window.document); lineData = createEmptyLineData(2); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8bcde39a..f1865175 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,7 @@ import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { IBufferLine } from '../../Types'; +import { IBufferLine, ITerminal } from '../../Types'; import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; @@ -17,6 +17,7 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { constructor( + private _terminal: ITerminal, private _document: Document ) { } @@ -88,10 +89,10 @@ export class DomRendererRowFactory { } } - if (flags & FLAGS.BOLD) { + if (flags & FLAGS.BOLD && this._terminal.options.enableBold) { // 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) { + if (fg < 8 && this._terminal.options.drawBoldTextInBrightColors) { fg += 8; } charElement.classList.add(BOLD_CLASS); From 4046d682c9770276240746cd5d46647ddfb2e10d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Mar 2019 09:21:01 -0800 Subject: [PATCH 123/140] v3.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa9cc070..c5fad515 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.11.0", + "version": "3.12.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", From b4bef0986cb99f2b43e4cb8ab372ec839bdb0422 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Sun, 10 Mar 2019 22:03:38 +0000 Subject: [PATCH 124/140] Remove circular dependency --- src/renderer/dom/DomRenderer.ts | 2 +- src/renderer/dom/DomRendererRowFactory.test.ts | 12 +++++------- src/renderer/dom/DomRendererRowFactory.ts | 8 ++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 5bff3d1c..3a5f29e6 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -75,7 +75,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); - this._rowFactory = new DomRendererRowFactory(_terminal, document); + this._rowFactory = new DomRendererRowFactory(_terminal.options, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index ab308077..76c07781 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -9,24 +9,22 @@ import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; import { BufferLine } from '../../BufferLine'; -import { IBufferLine, ITerminal } from '../../Types'; +import { IBufferLine, ITerminalOptions } from '../../Types'; import { DEFAULT_COLOR } from '../atlas/Types'; -import { MockTerminal } from '../../ui/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; - let term: ITerminal; + const options: ITerminalOptions = {}; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); - term = new MockTerminal(); - term.options.enableBold = true; - term.options.drawBoldTextInBrightColors = true; + options.enableBold = true; + options.drawBoldTextInBrightColors = true; - rowFactory = new DomRendererRowFactory(term, dom.window.document); + rowFactory = new DomRendererRowFactory(options, dom.window.document); lineData = createEmptyLineData(2); }); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index f1865175..206c0c81 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,7 @@ import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { IBufferLine, ITerminal } from '../../Types'; +import { IBufferLine, ITerminalOptions } from '../../Types'; import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; @@ -17,7 +17,7 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { constructor( - private _terminal: ITerminal, + private _terminalOptions: ITerminalOptions, private _document: Document ) { } @@ -89,10 +89,10 @@ export class DomRendererRowFactory { } } - if (flags & FLAGS.BOLD && this._terminal.options.enableBold) { + if (flags & FLAGS.BOLD && this._terminalOptions.enableBold) { // 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 && this._terminal.options.drawBoldTextInBrightColors) { + if (fg < 8 && this._terminalOptions.drawBoldTextInBrightColors) { fg += 8; } charElement.classList.add(BOLD_CLASS); From 19d36f92286267e43fe94ee010e3743c421810a3 Mon Sep 17 00:00:00 2001 From: turtle0x1 <12494629+turtle0x1@users.noreply.github.com> Date: Mon, 11 Mar 2019 13:47:25 +0000 Subject: [PATCH 125/140] Update readme with link for lxdmosaic --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5378f443..481590a7 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. - [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. - [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. +- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 62cfa642cbe5b2bce1d8cc0d71c2a8cdc1198bdb Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Mon, 11 Mar 2019 20:54:17 +0000 Subject: [PATCH 126/140] Better aproach, check i a selection is being performed --- src/addons/webLinks/webLinks.ts | 11 +++-------- src/ui/MouseZoneManager.ts | 12 ++++++++++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 19200c90..f0d69cc5 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -24,7 +24,9 @@ const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); -let handleLink: (event: MouseEvent, uri: string) => void; +function handleLink(event: MouseEvent, uri: string): void { + window.open(uri, '_blank'); +} /** * Initialize the web links addon, registering the link matcher. @@ -34,18 +36,11 @@ let handleLink: (event: MouseEvent, uri: string) => void; */ export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { options.matchIndex = 1; - term.registerLinkMatcher(strictUrlRegex, handler, options); } export function apply(terminalConstructor: typeof Terminal): void { (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { - handleLink = (event, uri) => { - if (!this.hasSelection()) { - window.open(uri, '_blank'); - } - }; - webLinksInit(this, handler, options); }; } diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index 79022723..3b848795 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -29,6 +29,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; + private _initialSelectionLenght: number; constructor( private _terminal: ITerminal @@ -157,6 +158,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onMouseDown(e: MouseEvent): void { + // Store current terminal selection length, to check if we're performing + // a selection operation + this._initialSelectionLenght = this._terminal.getSelection().length; + // Ignore the event if there are no zones active if (!this._areZonesActive) { return; @@ -186,9 +191,12 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onClick(e: MouseEvent): void { - // Find the active zone and click it if found + // Find the active zone and click it if found and no selection was + // being performed const zone = this._findZoneEventAt(e); - if (zone) { + const currentSelectionLength = this._terminal.getSelection().length; + + if (zone && currentSelectionLength === this._initialSelectionLenght) { zone.clickCallback(e); e.preventDefault(); e.stopImmediatePropagation(); From 27ebe8c353cb8988f71e665ce4765e9c5f02c597 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Mon, 11 Mar 2019 20:58:42 +0000 Subject: [PATCH 127/140] Use new vscode serverReadyAction --- .vscode/launch.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c7bf7381..2ec26fca 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,7 +38,11 @@ "run", "start-debug" ], - "port": 9229 + "port": 9229, + "serverReadyAction": { + "action": "openExternally", + "pattern": "App listening to (http://.*?:[0-9]+)" + } } ] } From 0d63cecc6bed2561cae1ca99e6c64d3f35a3cea6 Mon Sep 17 00:00:00 2001 From: Jianhui Zhao Date: Tue, 19 Mar 2019 14:04:16 +0800 Subject: [PATCH 128/140] Modify uses Signed-off-by: Jianhui Zhao --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 481590a7..40de04cf 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters. - [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. - [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. -- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. +- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web. - [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS. - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. From 131e4f78bdc1e036feed1a0aee03cddee25f6e63 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 23 Mar 2019 09:08:55 -0700 Subject: [PATCH 129/140] Fix renderer pausing to not full refresh every time Fixes #1975 --- src/renderer/Renderer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b8ef87aa..2c1b516a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -85,6 +85,7 @@ export class Renderer extends EventEmitter implements IRenderer { this._isPaused = entry.intersectionRatio === 0; if (!this._isPaused && this._needsFullRefresh) { this._terminal.refresh(0, this._terminal.rows - 1); + this._needsFullRefresh = false; } } From 4dbe8ec1db2d6005186b547cd2fb87f710aeb48c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 23 Mar 2019 10:58:26 -0700 Subject: [PATCH 130/140] Let consumers decide whether winptyCompat should be active --- demo/client.ts | 5 ++++- src/addons/winptyCompat/winptyCompat.ts | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index a3a912f6..70996c4a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -30,7 +30,10 @@ Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); Terminal.applyAddon(webLinks); -Terminal.applyAddon(winptyCompat); +const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; +if (isWindows) { + Terminal.applyAddon(winptyCompat); +} let term; diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index d162f4e9..58f59fd9 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -13,12 +13,6 @@ const WHITESPACE_CELL_CODE = 32; export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; - // Don't do anything when the platform is not Windows - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - if (!isWindows) { - return; - } - (addonTerminal._core as any).isWinptyCompatEnabled = true; // Winpty does not support wraparound mode which means that lines will never From f9df863ee05244f34536ae9c64606d38abd04a61 Mon Sep 17 00:00:00 2001 From: Jesse Stolwijk Date: Mon, 25 Mar 2019 22:17:16 +0100 Subject: [PATCH 131/140] Add blinking cursor to DomRenderer --- src/renderer/dom/DomRenderer.ts | 15 +++++++-- .../dom/DomRendererRowFactory.test.ts | 31 ++++++++++++------- src/renderer/dom/DomRendererRowFactory.ts | 7 ++++- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c5ef212d..1da20d95 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -9,7 +9,7 @@ import { ITheme } from 'xterm'; 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 { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_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-'; @@ -165,12 +165,22 @@ export class DomRenderer extends EventEmitter implements IRenderer { `${this._terminalSelector} span.${ITALIC_CLASS} {` + ` font-style: italic;` + `}`; + // Blink animation + styles += + `@keyframes blink {` + + ` 0 % { opacity: 1.0; }` + + ` 50% { opacity: 0.0; }` + + ` 100 % { opacity: 1.0; }` + + `}`; // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + ` outline: 1px solid ${this.colorManager.colors.cursor.css};` + ` outline-offset: -1px;` + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` + + ` animation: blink 1s step-end infinite;` + + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${this.colorManager.colors.cursor.css};` + ` color: ${this.colorManager.colors.cursorAccent.css};` + @@ -328,6 +338,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; const cursorX = this._terminal.buffer.x; + const cursorBlink = this._terminal.options.cursorBlink; for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; @@ -336,7 +347,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, this.dimensions.actualCellWidth, terminal.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, 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 67342da0..07e7686d 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -25,7 +25,7 @@ describe('DomRendererRowFactory', () => { describe('createRow', () => { it('should not create anything for an empty row', () => { - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -35,7 +35,7 @@ describe('DomRendererRowFactory', () => { lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]); // There should be no element for the following "empty" cell lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -43,17 +43,24 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), ` ` ); } }); + it('should add class for cursor blink', () => { + const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20); + assert.equal(getFragmentHtml(fragment), + ` ` + ); + }); + it('should not render cells that go beyond the terminal\'s columns', () => { lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]); lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -62,7 +69,7 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -70,7 +77,7 @@ describe('DomRendererRowFactory', () => { it('should add class for italic', () => { lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -80,7 +87,7 @@ describe('DomRendererRowFactory', () => { 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); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -91,7 +98,7 @@ describe('DomRendererRowFactory', () => { 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); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -100,7 +107,7 @@ describe('DomRendererRowFactory', () => { it('should correctly invert colors', () => { 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); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -108,7 +115,7 @@ describe('DomRendererRowFactory', () => { it('should correctly invert default fg color', () => { 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); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -116,7 +123,7 @@ describe('DomRendererRowFactory', () => { it('should correctly invert default bg color', () => { 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); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -125,7 +132,7 @@ describe('DomRendererRowFactory', () => { it('should turn bold fg text bright', () => { for (let i = 0; i < 8; i++) { 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); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8bcde39a..fd263b21 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -11,6 +11,7 @@ import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; export const CURSOR_CLASS = 'xterm-cursor'; +export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block'; export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; @@ -21,7 +22,7 @@ export class DomRendererRowFactory { ) { } - public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); // Find the line length first, this prevents the need to output a bunch of @@ -62,6 +63,10 @@ export class DomRendererRowFactory { if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); + if (cursorBlink) { + charElement.classList.add(CURSOR_BLINK_CLASS); + } + switch (cursorStyle) { case 'bar': charElement.classList.add(CURSOR_STYLE_BAR_CLASS); From e346f8bba37ee094f4710cd1c8dc43438fa01a79 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 29 Mar 2019 19:46:24 -0700 Subject: [PATCH 132/140] Prevent scroll on focus Fixes #1981 --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 4a85758f..c2497df4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -343,7 +343,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public focus(): void { if (this.textarea) { - this.textarea.focus(); + this.textarea.focus({ preventScroll: true }); } } From 0b78011fa34e2f79dd3f2cd68c111a171f45c68c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:05:24 -0700 Subject: [PATCH 133/140] Adopt project references Recent versions of TypeScript has improved the performance of project references so they are now viable to switch over to. --- demo/index.html | 4 ++-- demo/server.js | 1 + gulpfile.js | 10 +++++----- package.json | 10 +++------- src/common/tsconfig.json | 14 ++------------ src/core/tsconfig.json | 20 ++++++-------------- src/tsconfig-base.json | 15 +++++++++++++++ src/tsconfig-library-base.json | 11 +++++++++++ src/tsconfig.all.json | 16 ++++++++++++++++ tsconfig.json => src/tsconfig.json | 21 ++++++++++++--------- yarn.lock | 8 ++++---- 11 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 src/tsconfig-base.json create mode 100644 src/tsconfig-library-base.json create mode 100644 src/tsconfig.all.json rename tsconfig.json => src/tsconfig.json (53%) diff --git a/demo/index.html b/demo/index.html index 12f7cb98..370a51ed 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,8 +2,8 @@ xterm.js demo - - + + diff --git a/demo/server.js b/demo/server.js index 5ff9ca61..37df9916 100644 --- a/demo/server.js +++ b/demo/server.js @@ -11,6 +11,7 @@ function startServer() { logs = {}; app.use('/build', express.static(__dirname + '/../build')); + app.use('/src', express.static(__dirname + '/../src')); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); diff --git a/gulpfile.js b/gulpfile.js index 9af8d6e4..bbb4d6e1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,9 +16,9 @@ const ts = require('gulp-typescript'); const util = require('gulp-util'); const buildDir = process.env.BUILD_DIR || 'build'; -const tsProject = ts.createProject('tsconfig.json'); -let srcDir = tsProject.config.compilerOptions.rootDir; -let outDir = tsProject.config.compilerOptions.outDir; +const tsProject = ts.createProject('src/tsconfig.json'); +let srcDir = './src'; +let outDir = './lib'; const addons = fs.readdirSync(`${__dirname}/src/addons`); @@ -61,7 +61,7 @@ gulp.task('browserify', function() { }; let bundleStream = browserify(browserifyOptions) .bundle() - .pipe(source('xterm.js')) + .pipe(source(`xterm.js`)) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'})) .pipe(sourcemaps.write('./')) @@ -136,6 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () { }) }); -gulp.task('build', ['sorcery', 'sorcery-addons']); +gulp.task('build', ['css', 'sorcery', 'sorcery-addons']); gulp.task('test', ['mocha']); gulp.task('default', ['build']); diff --git a/package.json b/package.json index c5fad515..946db609 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.1", + "typescript": "3.4", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", @@ -50,20 +50,16 @@ "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", - "pretest": "npm run layering", "test": "npm run mocha", "posttest": "npm run lint", "test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-suite": "gulp mocha-suite --test", "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", "mocha": "gulp test", - "tsc": "tsc", - "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", + "prebuild": "tsc -b ./src/tsconfig.all.json", "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", - "watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", - "layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\"" + "watch": "tsc -b -w ./src/tsconfig.all.json" } } diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 19dd0273..b40bb2f5 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,17 +1,7 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, - "types": [ - "../../node_modules/@types/mocha", - "../../" - ] + "outDir": "../../lib" }, "include": [ "./**/*" diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 4f024a28..41e41f0c 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,20 +1,12 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, - "types": [ - "../../node_modules/@types/mocha", - "../../" - ] + "outDir": "../../lib" }, "include": [ - "./**/*", - "../common/**/*" + "./**/*" + ], + "references": [ + { "path": "../common" } ] } diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json new file mode 100644 index 00000000..5c6afcc5 --- /dev/null +++ b/src/tsconfig-base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ "es5" ], + "rootDir": ".", + + "sourceMap": true, + "removeComments": true, + "pretty": true, + + "incremental": true, + + "skipLibCheck": true + } +} diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json new file mode 100644 index 00000000..c82e0873 --- /dev/null +++ b/src/tsconfig-library-base.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "types": [ + "../../node_modules/@types/mocha", + "../../" + ], + "composite": true, + "strict": true + } +} diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json new file mode 100644 index 00000000..bee5df32 --- /dev/null +++ b/src/tsconfig.all.json @@ -0,0 +1,16 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "." }, + { "path": "./addons/attach" }, + { "path": "./addons/fit" }, + { "path": "./addons/fullscreen" }, + { "path": "./addons/search" }, + { "path": "./addons/terminado" }, + { "path": "./addons/webLinks" }, + { "path": "./addons/winptyCompat" }, + { "path": "./addons/zmodem" } + ] +} + \ No newline at end of file diff --git a/tsconfig.json b/src/tsconfig.json similarity index 53% rename from tsconfig.json rename to src/tsconfig.json index 2d1d6e35..0aa3abb8 100644 --- a/tsconfig.json +++ b/src/tsconfig.json @@ -1,7 +1,7 @@ { + "extends": "./tsconfig-base", "compilerOptions": { "module": "commonjs", - "target": "es5", "lib": [ "dom", "es5", @@ -9,19 +9,22 @@ "scripthost", "es2015.promise" ], - "rootDir": "src", - "outDir": "lib", - "sourceMap": true, - "removeComments": true, - "preserveWatchOutput": true, + "rootDir": ".", + "outDir": "../lib", + "noUnusedLocals": true, "noImplicitAny": true }, "include": [ - "src/**/*", - "typings/xterm.d.ts" + "./**/*", + "../typings/xterm.d.ts" ], "exclude": [ - "src/addons/**/*" + "./addons/**/*" + ], + "references": [ + { "path": "./common" }, + { "path": "./core" } ] } + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5555321d..896db4bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6633,10 +6633,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.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" - integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typescript@3.4: + version "3.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" + integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== uglify-es@^3.3.4: version "3.3.9" From b400f9ca3b74fbcb0c7506ec6b4456d835f1fc5a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:08:24 -0700 Subject: [PATCH 134/140] Don't clear terminal on yarn watch --- package.json | 2 +- src/addons/attach/tsconfig.json | 3 +-- src/addons/fit/tsconfig.json | 1 - src/addons/fullscreen/tsconfig.json | 1 - src/addons/search/tsconfig.json | 1 - src/addons/terminado/tsconfig.json | 1 - src/addons/webLinks/tsconfig.json | 1 - src/addons/winptyCompat/tsconfig.json | 1 - src/addons/zmodem/tsconfig.json | 1 - 9 files changed, 2 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 946db609..d917039b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,6 @@ "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "tsc -b -w ./src/tsconfig.all.json" + "watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput" } } diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json index 359fbd24..2f39102c 100644 --- a/src/addons/attach/tsconfig.json +++ b/src/addons/attach/tsconfig.json @@ -10,8 +10,7 @@ "outDir": "../../../lib/addons/attach/", "sourceMap": true, "removeComments": true, - "declaration": true, - "preserveWatchOutput": true + "declaration": true }, "include": [ "**/*.ts", diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index 489ccdfe..3458d23a 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index 05e6df68..0c74c25c 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 87899cda..6a1611a5 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 91c18314..e2e19445 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json index 18105aa2..9c4f1176 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/src/addons/webLinks/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json index 9fc4d25e..fa48c963 100644 --- a/src/addons/winptyCompat/tsconfig.json +++ b/src/addons/winptyCompat/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index 2b49f537..7d821b7c 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] From 18f4dc6b3eda8432231cc580fa4221c13e1113ec Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:09:51 -0700 Subject: [PATCH 135/140] Remove concurrently --- package.json | 1 - yarn.lock | 87 +++------------------------------------------------- 2 files changed, 4 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index d917039b..539b96da 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "@types/webpack": "^4.4.11", "browserify": "^13.3.0", "chai": "3.5.0", - "concurrently": "^3.5.1", "coveralls": "^3.0.1", "express": "4.13.4", "express-ws": "2.0.0-rc.1", diff --git a/yarn.lock b/yarn.lock index 896db4bf..440aa4f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1275,11 +1275,6 @@ combined-stream@1.0.6, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" - integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0= - commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -1338,21 +1333,6 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" -concurrently@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.6.0.tgz#c25e34b156a9d5bd4f256a0d85f6192438ae481f" - integrity sha512-6XiIYtYzmGEccNZFkih5JOH92jLA4ulZArAYy5j1uDSdrPLB3KzdE8GW7t2fHPcg9ry2+5LP9IEYzXzxw9lFdA== - dependencies: - chalk "^2.4.1" - commander "2.6.0" - date-fns "^1.23.0" - lodash "^4.5.1" - read-pkg "^3.0.0" - rx "2.3.24" - spawn-command "^0.0.2-1" - supports-color "^3.2.3" - tree-kill "^1.1.0" - configstore@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" @@ -1595,11 +1575,6 @@ data-urls@^1.0.0: whatwg-mimetype "^2.0.0" whatwg-url "^6.4.0" -date-fns@^1.23.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" - integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== - date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -1933,7 +1908,7 @@ errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3625,7 +3600,7 @@ jsesc@^1.3.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== @@ -3815,16 +3790,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -4037,7 +4002,7 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" -lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.5.1: +lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== @@ -4984,14 +4949,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -5092,13 +5049,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5384,15 +5334,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -5688,11 +5629,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx@2.3.24: - version "2.3.24" - resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" - integrity sha1-FPlQpCF9fjXapxu8vljv9o6ksrc= - rxjs@^6.1.0: version "6.3.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.1.tgz#878a1a8c64b8a5da11dcf74b5033fe944cdafb84" @@ -6025,11 +5961,6 @@ sparkles@^1.0.0: resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== -spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= - spawn-wrap@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" @@ -6271,11 +6202,6 @@ strip-bom@^1.0.0: first-chunk-stream "^1.0.0" is-utf8 "^0.2.0" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -6305,7 +6231,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.2: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= @@ -6521,11 +6447,6 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tree-kill@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" - integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" From f80924fb8d5155142a3bd258f64a71176680f7de Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:12:15 -0700 Subject: [PATCH 136/140] Remove zmodem demo --- demo/server.js | 1 - demo/zmodem/app.js | 87 --------- demo/zmodem/index.html | 128 -------------- demo/zmodem/main.js | 388 ----------------------------------------- 4 files changed, 604 deletions(-) delete mode 100644 demo/zmodem/app.js delete mode 100644 demo/zmodem/index.html delete mode 100644 demo/zmodem/main.js diff --git a/demo/server.js b/demo/server.js index 37df9916..c41110ff 100644 --- a/demo/server.js +++ b/demo/server.js @@ -10,7 +10,6 @@ function startServer() { var terminals = {}, logs = {}; - app.use('/build', express.static(__dirname + '/../build')); app.use('/src', express.static(__dirname + '/../src')); app.get('/', function(req, res){ diff --git a/demo/zmodem/app.js b/demo/zmodem/app.js deleted file mode 100644 index 7124c222..00000000 --- a/demo/zmodem/app.js +++ /dev/null @@ -1,87 +0,0 @@ -var express = require('express'); -var app = express(); -var expressWs = require('express-ws')(app); -var os = require('os'); -var pty = require('node-pty'); - -var terminals = {}, - logs = {}; - -app.use('/build', express.static(__dirname + '/../../build')); -app.use('/demo', express.static(__dirname + '/../../demo')); -app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist')); - -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); - -app.get('/style.css', function(req, res){ - res.sendFile(__dirname + '../style.css'); -}); - -app.get('/main.js', function(req, res){ - res.sendFile(__dirname + '/main.js'); -}); - -app.post('/terminals', function (req, res) { - var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - encoding: null, - name: 'xterm-color', - cols: cols || 80, - rows: rows || 24, - cwd: process.env.PWD, - env: process.env - }); - - console.log('Created terminal with PID: ' + term.pid); - terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; - }); - res.send(term.pid.toString()); - res.end(); -}); - -app.post('/terminals/:pid/size', function (req, res) { - var pid = parseInt(req.params.pid), - cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = terminals[pid]; - - term.resize(cols, rows); - console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); - res.end(); -}); - -app.ws('/terminals/:pid', function (ws, req) { - var term = terminals[parseInt(req.params.pid)]; - console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); - - term.on('data', function(data) { - try { - ws.send(data); - } catch (ex) { - // The WebSocket is not open, ignore - } - }); - ws.on('message', function(msg) { - term.write(msg); - }); - ws.on('close', function () { - term.kill(); - console.log('Closed terminal ' + term.pid); - // Clean things up - delete terminals[term.pid]; - delete logs[term.pid]; - }); -}); - -var port = process.env.PORT || 3000, - host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; - -console.log('App listening to http://' + host + ':' + port); -app.listen(port, host); diff --git a/demo/zmodem/index.html b/demo/zmodem/index.html deleted file mode 100644 index aee7742a..00000000 --- a/demo/zmodem/index.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - xterm.js demo - - - - - - - - - - - - - - - - -

xterm.js: xterm, in the browser

- -
- -
- - - - - - - - - -
- -
-

Actions

-

- - -

-
-
-

Options

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-
-

Size

-
-
- - -
-
- - -
-
-
-
-

Attention: The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.

-

* ZMODEM file transfers are supported via an addon. To try it out, install lrzsz onto the remote peer, then run rz to send from your browser or sz <file> to send from the remote peer.

- - - diff --git a/demo/zmodem/main.js b/demo/zmodem/main.js deleted file mode 100644 index 619ef2b8..00000000 --- a/demo/zmodem/main.js +++ /dev/null @@ -1,388 +0,0 @@ -"use strict"; - -var term, - protocol, - socketURL, - socket, - pid; - -Terminal.applyAddon(fit); -Terminal.applyAddon(attach); -Terminal.applyAddon(zmodem); -Terminal.applyAddon(search); - -var terminalContainer = document.getElementById('terminal-container'), - actionElements = { - findNext: document.querySelector('#find-next'), - findPrevious: document.querySelector('#find-previous') - }, - optionElements = { - cursorBlink: document.querySelector('#option-cursor-blink'), - cursorStyle: document.querySelector('#option-cursor-style'), - scrollback: document.querySelector('#option-scrollback'), - tabstopwidth: document.querySelector('#option-tabstopwidth'), - bellStyle: document.querySelector('#option-bell-style') - }, - colsElement = document.getElementById('cols'), - rowsElement = document.getElementById('rows'); - -function setTerminalSize() { - var cols = parseInt(colsElement.value, 10); - var rows = parseInt(rowsElement.value, 10); - var viewportElement = document.querySelector('.xterm-viewport'); - var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth; - var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px'; - var height = (rows * term.charMeasure.height).toString() + 'px'; - - terminalContainer.style.width = width; - terminalContainer.style.height = height; - term.resize(cols, rows); -} - -colsElement.addEventListener('change', setTerminalSize); -rowsElement.addEventListener('change', setTerminalSize); - -actionElements.findNext.addEventListener('keypress', function (e) { - if (e.key === "Enter") { - e.preventDefault(); - term.findNext(actionElements.findNext.value); - } -}); -actionElements.findPrevious.addEventListener('keypress', function (e) { - if (e.key === "Enter") { - e.preventDefault(); - term.findPrevious(actionElements.findPrevious.value); - } -}); - -optionElements.cursorBlink.addEventListener('change', function () { - term.setOption('cursorBlink', optionElements.cursorBlink.checked); -}); -optionElements.cursorStyle.addEventListener('change', function () { - term.setOption('cursorStyle', optionElements.cursorStyle.value); -}); -optionElements.bellStyle.addEventListener('change', function () { - term.setOption('bellStyle', optionElements.bellStyle.value); -}); -optionElements.scrollback.addEventListener('change', function () { - term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); -}); -optionElements.tabstopwidth.addEventListener('change', function () { - term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); -}); - -createTerminal(); - -function createTerminal() { - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - term = new Terminal({ - cursorBlink: optionElements.cursorBlink.checked, - scrollback: parseInt(optionElements.scrollback.value, 10), - tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10) - }); - term.on('resize', function (size) { - if (!pid) { - return; - } - var cols = size.cols, - rows = size.rows, - url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - - fetch(url, {method: 'POST'}); - }); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - - term.open(terminalContainer); - term.fit(); - - // fit is called within a setTimeout, cols and rows need this. - setTimeout(function () { - colsElement.value = term.cols; - rowsElement.value = term.rows; - - // Set terminal size again to set the specific dimensions on the demo - setTerminalSize(); - - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { - - res.text().then(function (pid) { - window.pid = pid; - socketURL += pid; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; - - term.zmodemAttach(socket, { - noTerminalWriteOutsideSession: true, - } ); - - term.on("zmodemRetract", () => { - start_form.style.display = "none"; - start_form.onsubmit = null; - }); - - term.on("zmodemDetect", (detection) => { - function do_zmodem() { - term.detach(); - let zsession = detection.confirm(); - - var promise; - - if (zsession.type === "receive") { - promise = _handle_receive_session(zsession); - } - else { - promise = _handle_send_session(zsession); - } - - promise.catch( console.error.bind(console) ).then( () => { - term.attach(socket); - } ); - } - - if (_auto_zmodem()) { - do_zmodem(); - } - else { - start_form.style.display = ""; - start_form.onsubmit = function(e) { - start_form.style.display = "none"; - - if (document.getElementById("zmstart_yes").checked) { - do_zmodem(); - } - else { - detection.deny(); - } - }; - } - }); - }); - }); - }, 0); -} - -//---------------------------------------------------------------------- -// UI STUFF - -function _show_file_info(xfer) { - var file_info = xfer.get_details(); - - document.getElementById("name").textContent = file_info.name; - document.getElementById("size").textContent = file_info.size; - document.getElementById("mtime").textContent = file_info.mtime; - document.getElementById("files_remaining").textContent = file_info.files_remaining; - document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining; - - document.getElementById("mode").textContent = "0" + file_info.mode.toString(8); - - var xfer_opts = xfer.get_options(); - ["conversion", "management", "transport", "sparse"].forEach( (lbl) => { - document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl]; - } ); - - document.getElementById("zm_file").style.display = ""; -} -function _hide_file_info() { - document.getElementById("zm_file").style.display = "none"; -} - -function _save_to_disk(xfer, buffer) { - return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name); -} - -var skipper_button = document.getElementById("zm_progress_skipper"); -var skipper_button_orig_text = skipper_button.textContent; - -function _show_progress() { - skipper_button.disabled = false; - skipper_button.textContent = skipper_button_orig_text; - - document.getElementById("bytes_received").textContent = 0; - document.getElementById("percent_received").textContent = 0; - - document.getElementById("zm_progress").style.display = ""; -} - -function _update_progress(xfer) { - var total_in = xfer.get_offset(); - - document.getElementById("bytes_received").textContent = total_in; - - var percent_received = 100 * total_in / xfer.get_details().size; - document.getElementById("percent_received").textContent = percent_received.toFixed(2); -} - -function _hide_progress() { - document.getElementById("zm_progress").style.display = "none"; -} - -var start_form = document.getElementById("zm_start"); - -function _auto_zmodem() { - return document.getElementById("zmodem-auto").checked; -} - -// END UI STUFF -//---------------------------------------------------------------------- - -function _handle_receive_session(zsession) { - zsession.on("offer", function(xfer) { - current_receive_xfer = xfer; - - _show_file_info(xfer); - - var offer_form = document.getElementById("zm_offer"); - - function on_form_submit() { - offer_form.style.display = "none"; - - //START - //if (offer_form.zmaccept.value) { - if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) { - _show_progress(); - - var FILE_BUFFER = []; - xfer.on("input", (payload) => { - _update_progress(xfer); - FILE_BUFFER.push( new Uint8Array(payload) ); - }); - xfer.accept().then( - () => { - _save_to_disk(xfer, FILE_BUFFER); - }, - console.error.bind(console) - ); - } - else { - xfer.skip(); - } - //END - } - - if (_auto_zmodem()) { - on_form_submit(); - } - else { - offer_form.onsubmit = on_form_submit; - offer_form.style.display = ""; - } - } ); - - var promise = new Promise( (res) => { - zsession.on("session_end", () => { - _hide_file_info(); - _hide_progress(); - res(); - } ); - } ); - - zsession.start(); - - return promise; -} - -function _handle_send_session(zsession) { - var choose_form = document.getElementById("zm_choose"); - choose_form.style.display = ""; - - var file_el = document.getElementById("zm_files"); - - var promise = new Promise( (res) => { - file_el.onchange = function(e) { - choose_form.style.display = "none"; - - var files_obj = file_el.files; - - Zmodem.Browser.send_files( - zsession, - files_obj, - { - on_offer_response(obj, xfer) { - if (xfer) _show_progress(); - //console.log("offer", xfer ? "accepted" : "skipped"); - }, - on_progress(obj, xfer) { - _update_progress(xfer); - }, - on_file_complete(obj) { - //console.log("COMPLETE", obj); - _hide_progress(); - }, - } - ).then(_hide_progress).then( - zsession.close.bind(zsession), - console.error.bind(console) - ).then( () => { - _hide_file_info(); - _hide_progress(); - res(); - } ); - }; - } ); - - return promise; -} - -//This is here to allow canceling of an in-progress ZMODEM transfer. -var current_receive_xfer; - -//Called from HTML directly. -function skip_current_file() { - current_receive_xfer.skip(); - - skipper_button.disabled = true; - skipper_button.textContent = "Waiting for server to acknowledge skip …"; -} - -function runRealTerminal() { - term.attach(socket); - - term._initialized = true; -} - -function runFakeTerminal() { - if (term._initialized) { - return; - } - - term._initialized = true; - - var shellprompt = '$ '; - - term.prompt = function () { - term.write('\r\n' + shellprompt); - }; - - term.writeln('Welcome to xterm.js'); - term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); - term.writeln('Type some keys and commands to play around.'); - term.writeln(''); - term.prompt(); - - term.on('key', function (key, ev) { - var printable = ( - !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey - ); - - if (ev.keyCode == 13) { - term.prompt(); - } else if (ev.keyCode == 8) { - // Do not delete the prompt - if (term.x > 2) { - term.write('\b \b'); - } - } else if (printable) { - term.write(key); - } - }); - - term.on('paste', function (data, ev) { - term.write(data); - }); -} From e182e3d4e43af884a61d0fadf0d5f34792a76cd5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:52:48 -0700 Subject: [PATCH 137/140] Fix demo on non-Windows Broke in #1978 --- demo/client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index 70996c4a..aaaf2829 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -102,7 +102,9 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.winptyCompatInit(); + if (isWindows) { + term.winptyCompatInit(); + } term.webLinksInit(); term.fit(); term.focus(); From bf9d879efa9897827fbeb9f58022cbc0960ad439 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 14:59:31 -0700 Subject: [PATCH 138/140] Fix typo --- src/ui/MouseZoneManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index 3b848795..372dccc5 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -29,7 +29,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; - private _initialSelectionLenght: number; + private _initialSelectionLength: number; constructor( private _terminal: ITerminal @@ -160,7 +160,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _onMouseDown(e: MouseEvent): void { // Store current terminal selection length, to check if we're performing // a selection operation - this._initialSelectionLenght = this._terminal.getSelection().length; + this._initialSelectionLength = this._terminal.getSelection().length; // Ignore the event if there are no zones active if (!this._areZonesActive) { @@ -196,7 +196,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { const zone = this._findZoneEventAt(e); const currentSelectionLength = this._terminal.getSelection().length; - if (zone && currentSelectionLength === this._initialSelectionLenght) { + if (zone && currentSelectionLength === this._initialSelectionLength) { zone.clickCallback(e); e.preventDefault(); e.stopImmediatePropagation(); From 4d008c66f11ed06835e6b2270305397033c81ebe Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Mar 2019 15:06:25 -0700 Subject: [PATCH 139/140] Recommend 127.0.0.1:3000 on mac and linux too Fixes #1986 --- .vscode/launch.json | 5 +---- demo/server.js | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ec26fca..e5bad7b3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,10 +23,7 @@ "type": "chrome", "request": "launch", "name": "Demo Client", - "url": "http://0.0.0.0:3000", - "windows": { - "url": "http://127.0.0.1:3000" - }, + "url": "http://127.0.0.1:3000", "webRoot": "${workspaceFolder}/" }, { diff --git a/demo/server.js b/demo/server.js index 5ff9ca61..0587977c 100644 --- a/demo/server.js +++ b/demo/server.js @@ -99,7 +99,7 @@ function startServer() { var port = process.env.PORT || 3000, host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; - console.log('App listening to http://' + host + ':' + port); + console.log('App listening to http://127.0.0.1:' + port); app.listen(port, host); } From 5c8c680dac3717e927069699168272ebd23ce54e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 1 Apr 2019 00:01:41 -0700 Subject: [PATCH 140/140] Fix feedback --- src/Buffer.ts | 2 +- src/BufferLine.test.ts | 10 +-- src/BufferLine.ts | 101 +++++++++++----------- src/InputHandler.ts | 10 +-- src/Linkifier.ts | 2 +- src/SelectionManager.ts | 22 ++--- src/Terminal.ts | 2 +- src/Types.ts | 4 +- src/renderer/CharacterJoinerRegistry.ts | 12 +-- src/renderer/TextRenderLayer.ts | 14 +-- src/renderer/dom/DomRendererRowFactory.ts | 13 +-- 11 files changed, 98 insertions(+), 94 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 69a36e43..9cc1adba 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -29,7 +29,7 @@ export const NULL_CELL_WIDTH = 1; export const NULL_CELL_CODE = 0; /** - * Whilespace cell. + * Whitespace cell. * This is meant as a replacement for empty cells when needed * during rendering lines to preserve correct aligment. */ diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 7dfcbd2c..29a783ae 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, CellData, Content } from './BufferLine'; +import { BufferLine, CellData, ContentMasks } from './BufferLine'; import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -32,7 +32,7 @@ describe('CellData', () => { // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); @@ -40,7 +40,7 @@ describe('CellData', () => { // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); @@ -350,7 +350,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -363,7 +363,7 @@ describe('BufferLine', function(): void { // width is set to 1 chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.isCombined(), Content.IS_COMBINED); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 14518454..c4aa55be 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -34,9 +34,9 @@ const enum Cell { } /** - * Bitmasks and helper for accessing data in `content`. + * Bitmasks for accessing data in `content`. */ -export const enum Content { +export const enum ContentMasks { /** * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) * read: `codepoint = content & Content.codepointMask;` @@ -44,7 +44,7 @@ export const enum Content { * shortcut if precondition `codepoint <= 0x10FFFF` is met: * `content |= codepoint;` */ - CODEPOINT_MASK = 0x1FFFFF, + CODEPOINT = 0x1FFFFF, /** * bit 22 flag indication whether a cell contains combined content @@ -72,10 +72,11 @@ export const enum Content { * shortcut if precondition `0 <= width <= 3` is met: * `content |= width << Content.widthShift;` */ - WIDTH_MASK = 0xC00000, // 3 << 22 - WIDTH_SHIFT = 22 + WIDTH = 0xC00000 // 3 << 22 } +const WIDTH_MASK_SHIFT = 22; + /** * CellData - represents a single Cell in the terminal buffer. */ @@ -96,21 +97,21 @@ export class CellData implements ICellData { /** Whether cell contains a combined string. */ public isCombined(): number { - return this.content & Content.IS_COMBINED; + return this.content & ContentMasks.IS_COMBINED; } /** Width of the cell. */ public getWidth(): number { - return this.content >> Content.WIDTH_SHIFT; + return this.content >> WIDTH_MASK_SHIFT; } /** JS string of the content. */ public getChars(): string { - if (this.content & Content.IS_COMBINED) { + if (this.content & ContentMasks.IS_COMBINED) { return this.combinedData; } - if (this.content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + if (this.content & ContentMasks.CODEPOINT) { + return stringFromCodePoint(this.content & ContentMasks.CODEPOINT); } return ''; } @@ -124,7 +125,7 @@ export class CellData implements ICellData { public getCode(): number { return (this.isCombined()) ? this.combinedData.charCodeAt(this.combinedData.length - 1) - : this.content & Content.CODEPOINT_MASK; + : this.content & ContentMasks.CODEPOINT; } /** Set data from CharData */ @@ -143,7 +144,7 @@ export class CellData implements ICellData { if (0xD800 <= code && code <= 0xDBFF) { const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); if (0xDC00 <= second && second <= 0xDFFF) { - this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } else { combined = true; } @@ -151,11 +152,11 @@ export class CellData implements ICellData { combined = true; } } else { - this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } if (combined) { this.combinedData = value[CHAR_DATA_CHAR_INDEX]; - this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.content = ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } } @@ -203,14 +204,14 @@ export class BufferLine implements IBufferLine { */ public get(index: number): CharData { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; + const cp = content & ContentMasks.CODEPOINT; return [ this._data[index * CELL_SIZE + Cell.FG], - (content & Content.IS_COMBINED) + (content & ContentMasks.IS_COMBINED) ? this._combined[index] : (cp) ? stringFromCodePoint(cp) : '', - content >> Content.WIDTH_SHIFT, - (content & Content.IS_COMBINED) + content >> WIDTH_MASK_SHIFT, + (content & ContentMasks.IS_COMBINED) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : cp ]; @@ -224,9 +225,9 @@ export class BufferLine implements IBufferLine { this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = index | ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } else { - this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } } @@ -235,21 +236,21 @@ export class BufferLine implements IBufferLine { * use these when only one value is needed, otherwise use `loadCell` */ public getWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; + return this._data[index * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT; } /** Test whether content has width. */ public hasWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.WIDTH; } /** Get FG cell component. */ - public getFG(index: number): number { + public getFg(index: number): number { return this._data[index * CELL_SIZE + Cell.FG]; } /** Get BG cell component. */ - public getBG(index: number): number { + public getBg(index: number): number { return this._data[index * CELL_SIZE + Cell.BG]; } @@ -259,7 +260,7 @@ export class BufferLine implements IBufferLine { * from real empty cells. * */ public hasContent(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT; } /** @@ -269,38 +270,40 @@ export class BufferLine implements IBufferLine { */ public getCodePoint(index: number): number { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & ContentMasks.IS_COMBINED) { return this._combined[index].charCodeAt(this._combined[index].length - 1); } - return content & Content.CODEPOINT_MASK; + return content & ContentMasks.CODEPOINT; } /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.IS_COMBINED; } /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & ContentMasks.IS_COMBINED) { return this._combined[index]; } - if (content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(content & Content.CODEPOINT_MASK); + if (content & ContentMasks.CODEPOINT) { + return stringFromCodePoint(content & ContentMasks.CODEPOINT); } // return empty string for empty cells return ''; } /** - * Load data at `index` into `cell`. + * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly + * to GC as it significantly reduced the amount of new objects/references needed. */ public loadCell(index: number, cell: ICellData): ICellData { - cell.content = this._data[index * CELL_SIZE + Cell.CONTENT]; - cell.fg = this._data[index * CELL_SIZE + Cell.FG]; - cell.bg = this._data[index * CELL_SIZE + Cell.BG]; - if (cell.content & Content.IS_COMBINED) { + const startIndex = index * CELL_SIZE; + cell.content = this._data[startIndex + Cell.CONTENT]; + cell.fg = this._data[startIndex + Cell.FG]; + cell.bg = this._data[startIndex + Cell.BG]; + if (cell.content & ContentMasks.IS_COMBINED) { cell.combinedData = this._combined[index]; } return cell; @@ -310,7 +313,7 @@ export class BufferLine implements IBufferLine { * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - if (cell.content & Content.IS_COMBINED) { + if (cell.content & ContentMasks.IS_COMBINED) { this._combined[index] = cell.combinedData; } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; @@ -324,7 +327,7 @@ export class BufferLine implements IBufferLine { * it gets an optimized access method. */ public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { - this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << WIDTH_MASK_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; } @@ -337,21 +340,21 @@ export class BufferLine implements IBufferLine { */ public addCodepointToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED) { + if (content & ContentMasks.IS_COMBINED) { // we already have a combined string, simply add this._combined[index] += stringFromCodePoint(codePoint); } else { - if (content & Content.CODEPOINT_MASK) { + if (content & ContentMasks.CODEPOINT) { // normal case for combining chars: // - move current leading char + new one into combined string // - set combined flag - this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); - content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 - content |= Content.IS_COMBINED; + this._combined[index] = stringFromCodePoint(content & ContentMasks.CODEPOINT) + stringFromCodePoint(codePoint); + content &= ~ContentMasks.CODEPOINT; // set codepoint in buffer to 0 + content |= ContentMasks.IS_COMBINED; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 - content = codePoint | (1 << Content.WIDTH_SHIFT); + content = codePoint | (1 << WIDTH_MASK_SHIFT); } this._data[index * CELL_SIZE + Cell.CONTENT] = content; } @@ -473,8 +476,8 @@ export class BufferLine implements IBufferLine { public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT); } } return 0; @@ -513,9 +516,9 @@ export class BufferLine implements IBufferLine { let result = ''; while (startCol < endCol) { const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; - result += (content & Content.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; - startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 + const cp = content & ContentMasks.CODEPOINT; + result += (content & ContentMasks.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + startCol += (content >> WIDTH_MASK_SHIFT) || 1; // always advance by 1 } return result; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 65f4aaaf..37c9bbe5 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -106,7 +106,7 @@ class DECRQSS implements IDcsHandler { export class InputHandler extends Disposable implements IInputHandler { private _parseBuffer: Uint32Array = new Uint32Array(4096); private _stringDecoder: StringToUtf32 = new StringToUtf32(); - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); constructor( protected _terminal: IInputHandlingTerminal, @@ -351,7 +351,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - if (!bufferRow.loadCell(buffer.x - 1, this._cell).getWidth()) { + if (!bufferRow.getWidth(buffer.x - 1)) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars @@ -400,7 +400,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell - if (bufferRow.loadCell(cols - 1, this._cell).getWidth() === 2) { + if (bufferRow.getWidth(cols - 1) === 2) { bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } @@ -970,10 +970,10 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); - line.loadCell(buffer.x - 1, this._cell); + line.loadCell(buffer.x - 1, this._workCell); line.replaceCells(buffer.x, buffer.x + (params[0] || 1), - (this._cell.content !== undefined) ? this._cell : buffer.getNullCell(DEFAULT_ATTR) + (this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR) ); // FIXME: no updateRange here? } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a2b9045b..80399904 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -231,7 +231,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { } const line = this._terminal.buffer.lines.get(bufferIndex[0]); - const attr = line.getFG(bufferIndex[1]); + const attr = line.getFg(bufferIndex[1]); let fg: number | undefined; if (attr) { fg = (attr >> 9) & 0x1ff; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 9d49a860..361b1123 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -103,7 +103,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; private _trimListener: XtermListener; - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); private _mouseDownTimeStamp: number; @@ -669,8 +669,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const length = bufferLine.loadCell(i, this._cell).getChars().length; - if (this._cell.getWidth() === 0) { + const length = bufferLine.loadCell(i, this._workCell).getChars().length; + if (this._workCell.getWidth() === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. charIndex--; @@ -755,10 +755,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._cell))) { - bufferLine.loadCell(startCol - 1, this._cell); - const length = this._cell.getChars().length; - if (this._cell.getWidth() === 0) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) { + bufferLine.loadCell(startCol - 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; @@ -771,10 +771,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._cell))) { - bufferLine.loadCell(endCol + 1, this._cell); - const length = this._cell.getChars().length; - if (this._cell.getWidth() === 2) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) { + bufferLine.loadCell(endCol + 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; diff --git a/src/Terminal.ts b/src/Terminal.ts index f3ec6e31..db696bd6 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1181,7 +1181,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFG(0) !== this.eraseAttr()) { + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); this._blankLine = newLine; } diff --git a/src/Types.ts b/src/Types.ts index a08bd485..10665f25 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -560,8 +560,8 @@ export interface IBufferLine { /* direct access to cell attrs */ getWidth(index: number): number; hasWidth(index: number): number; - getFG(index: number): number; - getBG(index: number): number; + getFg(index: number): number; + getBg(index: number): number; hasContent(index: number): number; getCodePoint(index: number): number; isCombined(index: number): number; diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index eb44bb58..4a899d72 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -6,7 +6,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { private _characterJoiners: ICharacterJoiner[] = []; private _nextCharacterJoinerId: number = 0; - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); constructor(private _terminal: ITerminal) { } @@ -52,13 +52,13 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line.getFG(0) >> 9; + let rangeAttr = line.getFg(0) >> 9; for (let x = 0; x < this._terminal.cols; x++) { - line.loadCell(x, this._cell); - const chars = this._cell.getChars(); - const width = this._cell.getWidth(); - const attr = this._cell.fg >> 9; + line.loadCell(x, this._workCell); + const chars = this._workCell.getChars(); + const width = this._workCell.getWidth(); + const attr = this._workCell.fg >> 9; if (width === 0) { // If this character is of width 0, skip it. diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index be022239..f56ccf3a 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -25,7 +25,7 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterFont: string; private _characterOverlapCache: { [key: string]: boolean } = {}; private _characterJoinerRegistry: ICharacterJoinerRegistry; - private _cell = new CellData(); + private _workCell = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) { super(container, 'text', zIndex, alpha, colors); @@ -74,14 +74,14 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - (line as any).loadCell(x, this._cell); - let code: number = this._cell.getCode() || WHITESPACE_CELL_CODE; + line.loadCell(x, this._workCell); + let code: number = this._workCell.getCode() || WHITESPACE_CELL_CODE; // Can either represent character(s) for a single cell or multiple cells // if indicated by a character joiner. - let chars = this._cell.getChars() || WHITESPACE_CELL_CHAR; - const attr = this._cell.fg; - let width = this._cell.getWidth(); + let chars = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + const attr = this._workCell.fg; + let width = this._workCell.getWidth(); // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -127,7 +127,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._cell).getCode() === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._workCell).getCode() === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index dfd3a154..47232981 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -18,7 +18,8 @@ export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { - private _cell: CellData = new CellData(); + private _workCell: CellData = new CellData(); + constructor( private _terminalOptions: ITerminalOptions, private _document: Document @@ -35,16 +36,16 @@ export class DomRendererRowFactory { // the viewport). let lineLength = 0; for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { - if (lineData.loadCell(x, this._cell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; break; } } for (let x = 0; x < lineLength; x++) { - lineData.loadCell(x, this._cell); - const attr = this._cell.fg; - const width = this._cell.getWidth(); + lineData.loadCell(x, this._workCell); + const attr = this._workCell.fg; + const width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -106,7 +107,7 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } - charElement.textContent = this._cell.getChars() || WHITESPACE_CELL_CHAR; + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); }