From d8364d749ca02e4f79dab581546c3d38daff057c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Jul 2019 01:15:18 +0200 Subject: [PATCH 01/41] remove null type from dcsHandler --- src/common/parser/EscapeSequenceParser.ts | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 1d2b7e1e..99af8a10 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -251,7 +251,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _escHandlers: any; protected _oscHandlers: IHandlerCollection; protected _dcsHandlers: any; - protected _activeDcsHandler: IDcsHandler | null; + protected _activeDcsHandler: IDcsHandler; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers @@ -288,7 +288,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlers = Object.create(null); this._oscHandlers = Object.create(null); this._dcsHandlers = Object.create(null); - this._activeDcsHandler = null; + this._activeDcsHandler = this._dcsHandlerFb; this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) @@ -299,7 +299,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlers = null; this._escHandlers = null; this._dcsHandlers = null; - this._activeDcsHandler = null; + this._activeDcsHandler = new DcsDummy(); } setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { @@ -387,7 +387,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag]; } setDcsHandlerFallback(handler: IDcsHandler): void { - this._dcsHandlerFb = handler; + if (this._activeDcsHandler === this._dcsHandlerFb) { + this._dcsHandlerFb = handler; + this._activeDcsHandler = handler; + } else { + this._dcsHandlerFb = handler; + } } setErrorHandler(callback: (state: IParsingState) => IParsingState): void { @@ -403,7 +408,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.reset(); this._params.addParam(0); // ZDM this._collect = ''; - this._activeDcsHandler = null; + this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } @@ -429,7 +434,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - let dcsHandler: IDcsHandler | null = this._activeDcsHandler; + let dcsHandler: IDcsHandler = this._activeDcsHandler; let callback: Function | null = null; // process input string @@ -547,19 +552,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // unhook triggered by: 0x1b, 0x9c for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - if (dcsHandler) { - dcsHandler.put(data, i, j); - } + dcsHandler.put(data, i, j); i = j - 1; break; } } break; case ParserAction.DCS_UNHOOK: - if (dcsHandler) { - dcsHandler.unhook(); - dcsHandler = null; - } + dcsHandler.unhook(); + dcsHandler = this._dcsHandlerFb; if (code === 0x1b) transition |= ParserState.ESCAPE; osc = ''; params.reset(); From f626e1c93d09da42bb74cc5d0e29d9bb645ebd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Jul 2019 20:38:14 +0200 Subject: [PATCH 02/41] fix OSC parsing --- src/InputHandler.ts | 14 +- src/common/parser/Constants.ts | 10 + .../parser/EscapeSequenceParser.test.ts | 96 +++++--- src/common/parser/EscapeSequenceParser.ts | 90 ++----- src/common/parser/OscParser.test.ts | 222 ++++++++++++++++++ src/common/parser/OscParser.ts | 186 +++++++++++++++ src/common/parser/Types.d.ts | 46 +++- .../EscapeSequenceParser.benchmark.ts | 5 +- typings/xterm.d.ts | 2 +- 9 files changed, 561 insertions(+), 110 deletions(-) create mode 100644 src/common/parser/OscParser.test.ts create mode 100644 src/common/parser/OscParser.ts diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f47085d4..0e322370 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -21,6 +21,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; +import { OscHandlerFactory } from 'common/parser/OscParser'; /** * Map collect to glevel. Used in `selectCharset`. @@ -152,9 +153,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandlerFallback((code: number) => { this._logService.debug('Unknown EXECUTE code: ', { code }); }); - this._parser.setOscHandlerFallback((identifier: number, data: string) => { - this._logService.debug('Unknown OSC code: ', { identifier, data }); - }); + this._parser.setOscHandlerFallback((identifier, action, data) => { + this._logService.debug('Unknown OSC code: ', { identifier, action, data }); + } + ); /** * print handler @@ -224,10 +226,10 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC handler */ // 0 - icon name + title - this._parser.setOscHandler(0, (data) => this.setTitle(data)); + this._parser.setOscHandler(0, new OscHandlerFactory((data: string) => this.setTitle(data))); // 1 - icon name // 2 - title - this._parser.setOscHandler(2, (data) => this.setTitle(data)); + this._parser.setOscHandler(2, new OscHandlerFactory((data: string) => this.setTitle(data))); // 3 - set property X in the form "prop=value" // 4 - Change Color Number // 5 - Change Special Color Number @@ -485,7 +487,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Forward addOscHandler from parser. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, callback); + return this._parser.addOscHandler(ident, new OscHandlerFactory(callback)); } /** diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index 55e4a005..9f28a27a 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -43,3 +43,13 @@ export const enum ParserAction { DCS_PUT = 13, DCS_UNHOOK = 14 } + +/** + * Internal states of OscParser. + */ +export const enum OscState { + START = 0, + ID = 1, + PAYLOAD = 2, + ABORT = 3 +} diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 90ec0ee0..a2b4b499 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,12 +3,14 @@ * @license MIT */ -import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types'; +import { IDcsHandler, IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; -import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; import { ParserState } from 'common/parser/Constants'; import { Params } from 'common/parser/Params'; +import { OscHandlerFactory } from 'common/parser/OscParser'; +import { IDisposable } from 'common/Types'; function r(a: number, b: number): string[] { @@ -20,13 +22,45 @@ function r(a: number, b: number): string[] { return arr; } +class MockOscPutParser implements IOscParser { + private _fallback: OscFallbackHandler = () => {}; + public data = ''; + public reset(): void { + this.data = ''; + } + public put(data: Uint32Array, start: number, end: number): void { + this.data += utf32ToString(data, start, end); + } + public dispose(): void { } + public start(): void { } + public end(): void { + const id = parseInt(this.data.slice(0, this.data.indexOf(';'))); + if (!isNaN(id)) { + this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1)); + } + } + addOscHandler(ident: number, handler: IOscHandler): IDisposable { + throw new Error('not implemented'); + } + setOscHandler(ident: number, handler: IOscHandler): void { + throw new Error('not implemented'); + } + clearOscHandler(ident: number): void { + throw new Error('not implemented'); + } + setOscHandlerFallback(handler: OscFallbackHandler): void { + this._fallback = handler; + } +} +const oscPutParser = new MockOscPutParser(); + // derived parser with access to internal states class TestEscapeSequenceParser extends EscapeSequenceParser { public get osc(): string { - return this._osc; + return (this._oscParser as MockOscPutParser).data; } public set osc(value: string) { - this._osc = value; + (this._oscParser as MockOscPutParser).data = value; } public get params(): ParamsArray { return this._params.toArray(); @@ -46,6 +80,9 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public mockActiveDcsHandler(): void { this._activeDcsHandler = this._dcsHandlerFb; } + public mockOscParser(): void { + this._oscParser = oscPutParser; + } } // test object to collect parser actions and compare them with expected values @@ -124,6 +161,7 @@ let state: any; // parser with Uint8Array based transition table const testParser = new TestEscapeSequenceParser(); +testParser.mockOscParser(); testParser.setPrintHandler(testTerminal.print.bind(testTerminal)); testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); @@ -134,9 +172,9 @@ testParser.setEscHandlerFallback((collect: string, flag: number) => { testParser.setExecuteHandlerFallback((code: number) => { testTerminal.actionExecute(String.fromCharCode(code)); }); -testParser.setOscHandlerFallback((identifier: number, data: string) => { +testParser.setOscHandlerFallback((identifier, action, data) => { if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently - else testTerminal.actionOSC('' + identifier + ';' + data); + else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END }); testParser.setDcsHandlerFallback(new DcsTest()); @@ -1026,9 +1064,9 @@ describe('EscapeSequenceParser', function (): void { ], null); }); it('print + OSC(C1) + print', function (): void { - test('abc\x9d123tzf\x9cdefg', [ + test('abc\x9d123;tzf\x9cdefg', [ ['print', 'abc'], - ['osc', '123tzf'], + ['osc', '123;tzf'], ['print', 'defg'] ], null); }); @@ -1039,9 +1077,9 @@ describe('EscapeSequenceParser', function (): void { ], null); }); it('7bit ST should be swallowed', function (): void { - test('abc\x9d123tzf\x1b\\defg', [ + test('abc\x9d123;tzf\x1b\\defg', [ ['print', 'abc'], - ['osc', '123tzf'], + ['osc', '123;tzf'], ['print', 'defg'] ], null); }); @@ -1256,9 +1294,9 @@ describe('EscapeSequenceParser', function (): void { chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { - parser2.setOscHandler(1, function (data: string): void { + parser2.setOscHandler(1, new OscHandlerFactory(function (data: string): void { osc.push([1, data]); - }); + })); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); parser2.clearOscHandler(1); @@ -1270,16 +1308,16 @@ describe('EscapeSequenceParser', function (): void { describe('OSC custom handlers', () => { it('Prevent fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); it('Allow fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); + parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return false; })); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); @@ -1287,9 +1325,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - 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.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return false; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); @@ -1298,9 +1336,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - 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.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return true; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([], 'Should not fallback once'); @@ -1308,16 +1346,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setOscHandler(1, () => order.push(1)); - parser2.addOscHandler(1, () => { order.push(2); return false; }); - parser2.addOscHandler(1, () => { order.push(3); return false; }); + parser2.setOscHandler(1, new OscHandlerFactory(() => order.push(1))); + parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(2); return false; })); + parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(3); return false; })); parse(parser2, '\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); + const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); @@ -1325,8 +1363,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); + const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 99af8a10..e2e88728 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,20 +3,13 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; -import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; import { Params } from 'common/parser/Params'; - -interface IHandlerCollection { - [key: string]: T[]; -} - -type CsiHandler = (params: IParams, collect: string) => boolean | void; -type OscHandler = (data: string) => boolean | void; +import { OscParser } from 'common/parser/OscParser'; /** * Table values are generated like this: @@ -240,7 +233,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public precedingCodepoint: number; // buffers over several parse calls - protected _osc: string; protected _params: Params; protected _collect: string; @@ -249,7 +241,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _executeHandlers: any; protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; - protected _oscHandlers: IHandlerCollection; + protected _oscParser: IOscParser; protected _dcsHandlers: any; protected _activeDcsHandler: IDcsHandler; protected _errorHandler: (state: IParsingState) => IParsingState; @@ -259,7 +251,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; - protected _oscHandlerFb: (identifier: number, data: string) => void; protected _dcsHandlerFb: IDcsHandler; protected _errorHandlerFb: (state: IParsingState) => IParsingState; @@ -268,7 +259,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.initialState = ParserState.GROUND; this.currentState = this.initialState; - this._osc = ''; this._params = new Params(); // defaults to 32 storable params/subparams this._params.addParam(0); // ZDM this._collect = ''; @@ -279,14 +269,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = (code: number): void => { }; this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { }; this._escHandlerFb = (collect: string, flag: number): void => { }; - this._oscHandlerFb = (identifier: number, data: string): void => { }; this._dcsHandlerFb = new DcsDummy(); this._errorHandlerFb = (state: IParsingState): IParsingState => state; this._printHandler = this._printHandlerFb; this._executeHandlers = Object.create(null); this._csiHandlers = Object.create(null); this._escHandlers = Object.create(null); - this._oscHandlers = Object.create(null); + this._oscParser = new OscParser(); this._dcsHandlers = Object.create(null); this._activeDcsHandler = this._dcsHandlerFb; this._errorHandler = this._errorHandlerFb; @@ -300,6 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = new DcsDummy(); + this._oscParser.dispose(); } setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { @@ -355,29 +345,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - if (this._oscHandlers[ident] === undefined) { - this._oscHandlers[ident] = []; - } - const handlerList = this._oscHandlers[ident]; - handlerList.push(callback); - return { - dispose: () => { - const handlerIndex = handlerList.indexOf(callback); - if (handlerIndex !== -1) { - handlerList.splice(handlerIndex, 1); - } - } - }; + addOscHandler(ident: number, handler: IOscHandler): IDisposable { + return this._oscParser.addOscHandler(ident, handler); } - setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = [callback]; + setOscHandler(ident: number, handler: IOscHandler): void { + this._oscParser.setOscHandler(ident, handler); } clearOscHandler(ident: number): void { - if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; + this._oscParser.clearOscHandler(ident); } - setOscHandlerFallback(callback: (identifier: number, data: string) => void): void { - this._oscHandlerFb = callback; + setOscHandlerFallback(handler: OscFallbackHandler): void { + this._oscParser.setOscHandlerFallback(handler); } setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { @@ -404,7 +382,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP reset(): void { this.currentState = this.initialState; - this._osc = ''; + this._oscParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM this._collect = ''; @@ -430,7 +408,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let code = 0; let transition = 0; let currentState = this.currentState; - let osc = this._osc; + const osc = this._oscParser; let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; @@ -484,7 +462,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP position: i, code, currentState, - osc, + osc: '', // FIXME: what to send here? collect, params, abort: false @@ -537,7 +515,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.precedingCodepoint = 0; break; case ParserAction.CLEAR: - osc = ''; + osc.reset(); params.reset(); params.addParam(0); // ZDM collect = ''; @@ -562,54 +540,29 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP dcsHandler.unhook(); dcsHandler = this._dcsHandlerFb; if (code === 0x1b) transition |= ParserState.ESCAPE; - osc = ''; + osc.reset(); params.reset(); params.addParam(0); // ZDM collect = ''; this.precedingCodepoint = 0; break; case ParserAction.OSC_START: - osc = ''; + osc.start(); break; case ParserAction.OSC_PUT: // inner loop: 0x20 (SP) included, 0x7F (DEL) included for (let j = i + 1; ; j++) { if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc += utf32ToString(data, i, j); + osc.put(data, i, j); i = j - 1; break; } } break; case ParserAction.OSC_END: - if (osc && code !== 0x18 && code !== 0x1a) { - // NOTE: OSC subparsing is not part of the original parser - // we do basic identifier parsing here to offer a jump table for OSC as well - const idx = osc.indexOf(';'); - if (idx === -1) { - this._oscHandlerFb(-1, osc); // this is an error (malformed OSC) - } else { - // Note: NaN is not handled here - // either catch it with the fallback handler - // or with an explicit NaN OSC handler - const identifier = parseInt(osc.substring(0, idx)); - const content = osc.substring(idx + 1); - // Trigger OSC Handler - const handlers = this._oscHandlers[identifier]; - let j = handlers ? handlers.length - 1 : -1; - for (; j >= 0; j--) { - // undefined or true means success and to stop bubbling - if (handlers[j](content) !== false) { - break; - } - } - if (j < 0) { - this._oscHandlerFb(identifier, content); - } - } - } + osc.end(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc = ''; + osc.reset(); params.reset(); params.addParam(0); // ZDM collect = ''; @@ -620,7 +573,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } // save non pushable buffers - this._osc = osc; this._collect = collect; this._params = params; diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts new file mode 100644 index 00000000..9b5de5fd --- /dev/null +++ b/src/common/parser/OscParser.test.ts @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { OscParser, OscHandlerFactory } from 'common/parser/OscParser'; +import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder'; +import { IOscHandler } from 'common/parser/Types'; + +function toUtf32(s: string): Uint32Array { + const utf32 = new Uint32Array(s.length); + const decoder = new StringToUtf32(); + const length = decoder.decode(s, utf32); + return utf32.subarray(0, length); +} + +class TestHandler implements IOscHandler { + constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {} + start(): void { + this.output.push([this.msg, this.id, 'START']); + } + put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); + } + end(success: boolean): void | boolean { + this.output.push([this.msg, this.id, 'END', success]); + if (this.returnFalse) { + return false; + } + } +} + +describe('OscParser', () => { + let parser: OscParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new OscParser(); + parser.setOscHandlerFallback((id, action, data) => { + reports.push([id, action, data]); + }); + }); + describe('identifier parsing', () => { + it('no report for illegal ids', () => { + const data = toUtf32('hello world!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, []); + }); + it('no payload', () => { + parser.start(); + let data = toUtf32('12'); + parser.put(data, 0, data.length); + data = toUtf32('34'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]); + }); + it('with payload', () => { + parser.start(); + let data = toUtf32('12'); + parser.put(data, 0, data.length); + data = toUtf32('34'); + parser.put(data, 0, data.length); + data = toUtf32(';h'); + parser.put(data, 0, data.length); + data = toUtf32('ello'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + [1234, 'START', undefined], + [1234, 'PUT', 'h'], + [1234, 'PUT', 'ello'], + [1234, 'END', true] + ]); + }); + }); + describe('handler registration', () => { + it('setOscHandler', () => { + parser.setOscHandler(1234, new TestHandler(1234, reports, 'th')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['th', 1234, 'START'], + ['th', 1234, 'PUT', 'Here comes'], + ['th', 1234, 'PUT', 'the mouse!'], + ['th', 1234, 'END', true] + ]); + }); + it('clearOscHandler', () => { + parser.setOscHandler(1234, new TestHandler(1234, reports, 'th')); + parser.clearOscHandler(1234); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + // messages from fallback handler + [1234, 'START', undefined], + [1234, 'PUT', 'Here comes'], + [1234, 'PUT', 'the mouse!'], + [1234, 'END', true] + ]); + }); + it('addOscHandler', () => { + parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + ['th2', 1234, 'START'], + ['th1', 1234, 'START'], + ['th2', 1234, 'PUT', 'Here comes'], + ['th1', 1234, 'PUT', 'Here comes'], + ['th2', 1234, 'PUT', 'the mouse!'], + ['th1', 1234, 'PUT', 'the mouse!'], + ['th2', 1234, 'END', true], + ['th1', 1234, 'END', false] // false due being already handled by th2! + ]); + }); + it('addOscHandler with return false', () => { + parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + ['th2', 1234, 'START'], + ['th1', 1234, 'START'], + ['th2', 1234, 'PUT', 'Here comes'], + ['th1', 1234, 'PUT', 'Here comes'], + ['th2', 1234, 'PUT', 'the mouse!'], + ['th1', 1234, 'PUT', 'the mouse!'], + ['th2', 1234, 'END', true], + ['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling + ]); + }); + it('dispose handlers', () => { + parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); + const dispo = parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true)); + dispo.dispose(); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [ + ['th1', 1234, 'START'], + ['th1', 1234, 'PUT', 'Here comes'], + ['th1', 1234, 'PUT', 'the mouse!'], + ['th1', 1234, 'END', true] + ]); + }); + }); + describe('OscHandlerFactory', () => { + it('should be called once on end(true)', () => { + parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', () => { + parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(false); + assert.deepEqual(reports, []); + }); + it('should be disposable', () => { + parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data]))); + const dispo = parser.addOscHandler(1234, new OscHandlerFactory(data => reports.push(['two', data]))); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!']]); + dispo.dispose(); + parser.start(); + data = toUtf32('1234;some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); + }); + it('should respect return false', () => { + parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data]))); + parser.addOscHandler(1234, new OscHandlerFactory(data => { reports.push(['two', data]); return false; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]); + }); + }); +}); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts new file mode 100644 index 00000000..1163fd20 --- /dev/null +++ b/src/common/parser/OscParser.ts @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOscHandler, IHandlerCollection, OscFallbackHandler } from 'common/parser/Types'; +import { OscState } from 'common/parser/Constants'; +import { Disposable } from 'common/Lifecycle'; +import { utf32ToString } from 'common/input/TextDecoder'; +import { IDisposable } from 'common/Types'; + + +export class OscParser extends Disposable { + private _state = OscState.START; + private _id = -1; + private _handlers: IHandlerCollection = Object.create(null); + private _handlerFb: OscFallbackHandler = () => { }; + + addOscHandler(ident: number, handler: IOscHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; + } + const handlerList = this._handlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + setOscHandler(ident: number, handler: IOscHandler): void { + this._handlers[ident] = [handler]; + } + clearOscHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; + } + setOscHandlerFallback(handler: OscFallbackHandler): void { + this._handlerFb = handler; + } + + public dispose(): void { + this._handlers = {}; + } + + public reset(): void { + // cleanup handlers if payload was already sent + if (this._state === OscState.PAYLOAD) { + this.end(false); + } + this._id = -1; + this._state = OscState.START; + } + + private _start(): void { + const handlers = this._handlers[this._id]; + if (!handlers) { + this._handlerFb(this._id, 'START'); + } else { + for (let j = handlers.length - 1; j >= 0; j--) { + handlers[j].start(); + } + } + } + + private _put(data: Uint32Array, start: number, end: number): void { + const handlers = this._handlers[this._id]; + if (!handlers) { + this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = handlers.length - 1; j >= 0; j--) { + handlers[j].put(data, start, end); + } + } + } + + private _end(success: boolean): void { + // other than the old code we always have to call .end + // to keep the bubbling we use `success` to indicate + // whether a handler should execute + const handlers = this._handlers[this._id]; + if (!handlers) { + this._handlerFb(this._id, 'END', success); + } else { + let j = handlers.length - 1; + for (; j >= 0; j--) { + if (handlers[j].end(success) !== false) { + break; + } + } + j--; + // cleanup left over handlers + for (; j >= 0; j--) { + handlers[j].end(false); + } + } + } + + public start(): void { + this._id = -1; + this._state = OscState.ID; + } + + /** + * Put data to current OSC command. + * Expects the identifier of the OSC command in the form + * OSC id ; payload ST/BEL + * Payload chunks are not further processed and get + * directly passed to the handlers. + */ + public put(data: Uint32Array, start: number, end: number): void { + if (this._state === OscState.ABORT) { + return; + } + if (this._state === OscState.ID) { + while (start < end) { + const code = data[start++]; + if (code === 0x3b) { + this._state = OscState.PAYLOAD; + this._start(); + break; + } + if (code < 0x30 || 0x39 < code) { + this._state = OscState.ABORT; + return; + } + if (this._id === -1) { + this._id = 0; + } + this._id = this._id * 10 + code - 48; + } + } + if (this._state === OscState.PAYLOAD && end - start > 0) { + this._put(data, start, end); + } + } + + /** + * Indicates end of an OSC command. + * Whether the OSC got aborted or finished normally + * is indicated by `success`. + */ + public end(success: boolean): void { + if (this._state === OscState.START) { + return; + } + // do nothing if command was faulty + if (this._state !== OscState.ABORT) { + // if we are still in ID state and get an early end + // means that the command has no payload thus we still have + // to announce START and send END right after + if (this._state === OscState.ID) { + this._start(); + } + this._end(success); + } + this._id = -1; + this._state = OscState.START; + } +} + + +/** + * Convenient class to allow attaching string based handler functions + * as OSC handlers. + */ +export class OscHandlerFactory implements IOscHandler { + private _data = ''; + constructor(private _handler: (data: string) => any) {} + public start(): void { + this._data = ''; + } + public put(data: Uint32Array, start: number, end: number): void { + this._data += utf32ToString(data, start, end); + } + public end(success: boolean): any { + let ret; + if (success) { + ret = this._handler(this._data); + } + this._data = ''; + return ret; + } +} diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index d432800d..2b21f893 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -63,6 +63,12 @@ export interface IParsingState { abort: boolean; } +export interface IHandlerCollection { + [key: string]: T[]; +} + +export type CsiHandler = (params: IParams, collect: string) => boolean | void; + /** * DCS handler signature for EscapeSequenceParser. * EscapeSequenceParser handles DCS commands via separate @@ -90,6 +96,29 @@ export interface IDcsHandler { unhook(): void; } +export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; + +export interface IOscHandler { + /** + * Announces start of this OSC command. + * Prepare needed data structures here. + */ + start(): void; + + /** + * Incoming data chunk. + */ + put(data: Uint32Array, start: number, end: number): void; + + /** + * End of OSC command. `success` indicates whether the + * command finished normally or got aborted, thus execution + * of the command should depend on `success`. + * To save memory cleanup data structures in `.end`. + */ + end(success: boolean): void | boolean; +} + /** * EscapeSequenceParser interface. */ @@ -123,15 +152,15 @@ export interface IEscapeSequenceParser extends IDisposable { clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void; addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + addOscHandler(ident: number, handler: IOscHandler): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; - setOscHandler(ident: number, callback: (data: string) => void): void; + setOscHandler(ident: number, handler: IOscHandler): void; clearOscHandler(ident: number): void; - setOscHandlerFallback(callback: (identifier: number, data: string) => void): void; + setOscHandlerFallback(handler: OscFallbackHandler): void; setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; clearDcsHandler(collectAndFlag: string): void; @@ -140,3 +169,14 @@ export interface IEscapeSequenceParser extends IDisposable { setErrorHandler(callback: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; } + +export interface IOscParser extends IDisposable { + addOscHandler(ident: number, handler: IOscHandler): IDisposable; + setOscHandler(ident: number, handler: IOscHandler): void; + clearOscHandler(ident: number): void; + setOscHandlerFallback(handler: OscFallbackHandler): void; + reset(): void; + start(): void; + put(data: Uint32Array, start: number, end: number): void; + end(success: boolean): void; +} diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index b9a6b0df..19b135c7 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -7,6 +7,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { C0, C1 } from 'common/data/EscapeSequences'; import { IDcsHandler, IParams } from 'common/parser/Types'; +import { OscHandlerFactory } from 'common/parser/OscParser'; function toUtf32(s: string): Uint32Array { @@ -79,8 +80,8 @@ perfContext('Parser throughput - 50MB data', () => { parser.setExecuteHandler(C1.IND, () => {}); parser.setExecuteHandler(C1.NEL, () => {}); parser.setExecuteHandler(C1.HTS, () => {}); - parser.setOscHandler(0, (data) => {}); - parser.setOscHandler(2, (data) => {}); + parser.setOscHandler(0, new OscHandlerFactory((data) => {})); + parser.setOscHandler(2, new OscHandlerFactory((data) => {})); parser.setEscHandler('7', () => {}); parser.setEscHandler('8', () => {}); parser.setEscHandler('D', () => {}); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d9e28b26..e0041dea 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -516,7 +516,7 @@ declare module 'xterm' { addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; /** - * (EXPERIMENTAL) Adds a handler for OSC escape sequences. + * Adds a handler for OSC escape sequences. * @param ident The number (first parameter) of the sequence. * @param callback The function to handle the escape sequence. The callback * is called with OSC data string. Return true if the sequence was handled; From c87b3fd9424d7f19df51210d06cc95d5f0f7e061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Jul 2019 21:37:51 +0200 Subject: [PATCH 03/41] integration test for addOscHandler --- test/api/InputHandler.api.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index dfd16ddb..6011b2ab 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -351,6 +351,30 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); }); }); + describe('addOscHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customOscHandlerCallStack = []; + const _customOscHandlerA = window.term.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['A', data]); + return false; + }); + const _customOscHandlerB = window.term.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['B', data]); + return true; + }); + const _customOscHandlerC = window.term.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['C', data]); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1b]1234;some data\x07'); + `); + assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { From 1f4dde601e3c8eca6f8639a3e89532b26353acc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Jul 2019 21:38:50 +0200 Subject: [PATCH 04/41] remove experimental from addCsiHandler --- typings/xterm.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index e0041dea..2e9cafd2 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -501,7 +501,7 @@ declare module 'xterm' { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; /** - * (EXPERIMENTAL) Adds a handler for CSI escape sequences. + * Adds a handler for CSI escape sequences. * @param flag The flag should be one-character string, which specifies the * final character (e.g "m" for SGR) of the CSI sequence. * @param callback The function to handle the escape sequence. The callback From f985c30847f1c02481d8846b17869119aa49e646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Jul 2019 22:44:08 +0200 Subject: [PATCH 05/41] add addEscHandler --- src/InputHandler.ts | 7 +++ src/Terminal.ts | 5 ++ src/TestUtils.test.ts | 7 ++- src/Types.d.ts | 1 + .../parser/EscapeSequenceParser.test.ts | 51 +++++++++++++++++++ src/common/parser/EscapeSequenceParser.ts | 38 +++++++++++--- src/common/parser/Types.d.ts | 4 +- src/public/Terminal.ts | 3 ++ test/api/InputHandler.api.ts | 24 +++++++++ typings/xterm.d.ts | 13 +++++ 10 files changed, 143 insertions(+), 10 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 0e322370..6912404b 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -476,6 +476,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowService.markDirty(buffer.y); } + /** + * Forward addEscHandler from parser. + */ + public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + return this._parser.addEscHandler(collectAndFlag, handler); + } + /** * Forward addCsiHandler from parser. */ diff --git a/src/Terminal.ts b/src/Terminal.ts index d08ae73c..8057362a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1392,6 +1392,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._customKeyEventHandler = customKeyEventHandler; } + /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ + public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + return this._inputHandler.addEscHandler(collectAndFlag, handler); + } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { return this._inputHandler.addCsiHandler(flag, callback); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 0ee948d5..efe41858 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -75,10 +75,13 @@ export class MockTerminal implements ITerminal { throw new Error('Method not implemented.'); } addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - throw new Error('Method not implemented.'); + throw new Error('Method not implemented.'); + } + addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + throw new Error('Method not implemented.'); } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - throw new Error('Method not implemented.'); + throw new Error('Method not implemented.'); } registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { throw new Error('Method not implemented.'); diff --git a/src/Types.d.ts b/src/Types.d.ts index f96ba07d..be488304 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -201,6 +201,7 @@ export interface IPublicTerminal extends IDisposable { open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; + addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index a2b4b499..0d038ee0 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -1199,6 +1199,57 @@ describe('EscapeSequenceParser', function (): void { parse(parser2, INPUT); chai.expect(esc).eql([]); }); + describe('ESC custom handlers', () => { + it('prevent fallback', () => { + parser2.setEscHandler('%G', () => esc.push('default - %G')); + parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parse(parser2, INPUT); + chai.expect(esc).eql(['custom - %G']); + }); + it('allow fallback', () => { + parser2.setEscHandler('%G', () => esc.push('default - %G')); + parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return false; }); + parse(parser2, INPUT); + chai.expect(esc).eql(['custom - %G', 'default - %G']); + }); + it('Multiple custom handlers fallback once', () => { + parser2.setEscHandler('%G', () => esc.push('default - %G')); + parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler('%G', () => { esc.push('custom2 - %G'); return false; }); + parse(parser2, INPUT); + chai.expect(esc).eql(['custom2 - %G', 'custom - %G']); + }); + it('Multiple custom handlers no fallback', () => { + parser2.setEscHandler('%G', () => esc.push('default - %G')); + parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler('%G', () => { esc.push('custom2 - %G'); return true; }); + parse(parser2, INPUT); + chai.expect(esc).eql(['custom2 - %G']); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setEscHandler('%G', () => order.push(1)); + parser2.addEscHandler('%G', () => { order.push(2); return false; }); + parser2.addEscHandler('%G', () => { order.push(3); return false; }); + parse(parser2, '\x1b%G'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + parser2.setEscHandler('%G', () => esc.push('default - %G')); + const dispo = parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + dispo.dispose(); + parse(parser2, INPUT); + chai.expect(esc).eql(['default - %G']); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + parser2.setEscHandler('%G', () => esc.push('default - %G')); + const dispo = parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + dispo.dispose(); + dispo.dispose(); + parse(parser2, INPUT); + chai.expect(esc).eql(['default - %G']); + }); + }); it('CSI handler', function (): void { parser2.setCsiHandler('m', function (params: IParams, collect: string): void { csi.push(['m', params.toArray(), collect]); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index e2e88728..355fbecd 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -240,7 +240,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _printHandler: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlers: any; protected _csiHandlers: IHandlerCollection; - protected _escHandlers: any; + protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; protected _dcsHandlers: any; protected _activeDcsHandler: IDcsHandler; @@ -286,7 +286,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public dispose(): void { this._executeHandlers = null; - this._escHandlers = null; + this._escHandlers = Object.create(null); this._dcsHandlers = null; this._activeDcsHandler = new DcsDummy(); this._oscParser.dispose(); @@ -335,8 +335,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._csiHandlerFb = callback; } + addEscHandler(collectAndFlag: string, callback: EscHandler): IDisposable { + if (this._escHandlers[collectAndFlag] === undefined) { + this._escHandlers[collectAndFlag] = []; + } + const handlerList = this._escHandlers[collectAndFlag]; + handlerList.push(callback); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } setEscHandler(collectAndFlag: string, callback: () => void): void { - this._escHandlers[collectAndFlag] = callback; + this._escHandlers[collectAndFlag] = [callback]; } clearEscHandler(collectAndFlag: string): void { if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag]; @@ -509,9 +524,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP collect += String.fromCharCode(code); break; case ParserAction.ESC_DISPATCH: - callback = this._escHandlers[collect + String.fromCharCode(code)]; - if (callback) callback(collect, code); - else this._escHandlerFb(collect, code); + const handlersEsc = this._escHandlers[collect + String.fromCharCode(code)]; + let jj = handlersEsc ? handlersEsc.length - 1 : -1; + for (; jj >= 0; jj--) { + // undefined or true means success and to stop bubbling + if (handlersEsc[jj]() !== false) { + break; + } + } + if (jj < 0) { + this._escHandlerFb(collect, code); + } + this.precedingCodepoint = 0; break; case ParserAction.CLEAR: diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 2b21f893..8c65f332 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -68,6 +68,7 @@ export interface IHandlerCollection { } export type CsiHandler = (params: IParams, collect: string) => boolean | void; +export type EscHandler = () => boolean | void; /** * DCS handler signature for EscapeSequenceParser. @@ -152,15 +153,16 @@ export interface IEscapeSequenceParser extends IDisposable { clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void; addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; - addOscHandler(ident: number, handler: IOscHandler): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; + addEscHandler(collectAndFlag: string, handler: EscHandler): IDisposable; setOscHandler(ident: number, handler: IOscHandler): void; clearOscHandler(ident: number): void; setOscHandlerFallback(handler: OscFallbackHandler): void; + addOscHandler(ident: number, handler: IOscHandler): IDisposable; setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; clearDcsHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 5d270ca7..391501e0 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -60,6 +60,9 @@ export class Terminal implements ITerminalApi { public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable { return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect)); } + public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + return this._core.addEscHandler(collectAndFlag, handler); + } public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { return this._core.addOscHandler(ident, callback); } diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 6011b2ab..95502c3e 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -351,6 +351,30 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); }); }); + describe('addEscHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customEscHandlerCallStack = []; + const _customEscHandlerA = window.term.addEscHandler('(B', () => { + _customEscHandlerCallStack.push('A'); + return false; + }); + const _customEscHandlerB = window.term.addEscHandler('(B', () => { + _customEscHandlerCallStack.push('B'); + return true; + }); + const _customEscHandlerC = window.term.addEscHandler('(B', () => { + _customEscHandlerCallStack.push('C'); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1b(B'); + `); + assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']); + }); + }); describe('addOscHandler', () => { it('should respects return value', async () => { await page.evaluate(` diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2e9cafd2..7582f6d2 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -515,6 +515,19 @@ declare module 'xterm' { */ addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; + /** + * Adds a handler for ESC escape sequences. + * @param flag The flag should be a string, which specifies the + * collect and the final character (e.g "%G" for default charset selection) + * of the ESC sequence. + * @param callback The function to handle the escape sequence. + * Return true if the sequence was handled; false if + * we should try a previous handler (set by addEscHandler or setEscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addEscHandler(collect: string, handler: () => boolean): IDisposable; + /** * Adds a handler for OSC escape sequences. * @param ident The number (first parameter) of the sequence. From 6606024c2ba9c5bae0934f9a8b0524d5582ae71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 01:58:42 +0200 Subject: [PATCH 06/41] support for multiple DCS handler --- src/InputHandler.ts | 15 ++- src/common/parser/DcsParser.ts | 110 ++++++++++++++++++ .../parser/EscapeSequenceParser.test.ts | 38 +++--- src/common/parser/EscapeSequenceParser.ts | 58 +++------ src/common/parser/OscParser.ts | 3 +- src/common/parser/Types.d.ts | 23 +++- 6 files changed, 176 insertions(+), 71 deletions(-) create mode 100644 src/common/parser/DcsParser.ts diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 6912404b..04d433ab 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -57,7 +57,11 @@ class DECRQSS implements IDcsHandler { this._data = concat(this._data, data.subarray(start, end)); } - unhook(): void { + unhook(success: boolean): void { + if (!success) { + this._data = new Uint32Array(0); + return; + } const data = utf32ToString(this._data); this._data = new Uint32Array(0); switch (data) { @@ -155,8 +159,13 @@ export class InputHandler extends Disposable implements IInputHandler { }); this._parser.setOscHandlerFallback((identifier, action, data) => { this._logService.debug('Unknown OSC code: ', { identifier, action, data }); - } - ); + }); + this._parser.setDcsHandlerFallback((identifier, action, payload) => { + if (payload.params) { + payload.params = payload.params.toArray(); + } + this._logService.debug('Unknown DCS code: ', { identifier, action, payload }); + }); /** * print handler diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts new file mode 100644 index 00000000..6c645a71 --- /dev/null +++ b/src/common/parser/DcsParser.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { utf32ToString } from 'common/input/TextDecoder'; + + +export class DcsParser implements IDcsParser { + private _handlers: IHandlerCollection = Object.create(null); + private _active: IDcsHandler[] = []; + private _collectAndFlag: string = ''; + private _handlerFb: DcsFallbackHandler = () => {}; + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => {}; + } + public addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { + if (this._handlers[collectAndFlag] === undefined) { + this._handlers[collectAndFlag] = []; + } + const handlerList = this._handlers[collectAndFlag]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { + this._handlers[collectAndFlag] = [handler]; + } + public clearDcsHandler(collectAndFlag: string): void { + if (this._handlers[collectAndFlag]) delete this._handlers[collectAndFlag]; + } + public setOscHandlerFallback(handler: DcsFallbackHandler): void { + this._handlerFb = handler; + } + public reset(): void { + if (this._active.length) { + this.unhook(false); + } + this._active = []; + this._collectAndFlag = ''; + } + public hook(collect: string, params: IParams, flag: number): void { + this._collectAndFlag = collect + String.fromCharCode(flag); + this._active = this._handlers[this._collectAndFlag] || []; + if (!this._active.length) { + this._handlerFb(this._collectAndFlag, 'HOOK', {collect, params, flag}); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].hook(collect, params, flag); + } + } + } + public put(data: Uint32Array, start: number, end: number): void { + if (!this._active.length) { + this._handlerFb(this._collectAndFlag, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); + } + } + } + public unhook(success: boolean): void { + if (!this._active.length) { + this._handlerFb(this._collectAndFlag, 'UNHOOK', success); + } else { + let j = this._active.length - 1; + for (; j >= 0; j--) { + if (this._active[j].unhook(success) !== false) { + break; + } + } + j--; + // cleanup left over handlers + for (; j >= 0; j--) { + this._active[j].unhook(false); + } + } + } +} + +export class DcsHandlerFactory implements IDcsHandler { + private _data = ''; + private _params: IParams | undefined; + constructor(private _handler: (params: ParamsArray, data: string) => any) {} + public hook(collect: string, params: IParams, flag: number): void { + this._params = params.clone(); + this._data = ''; + } + public put(data: Uint32Array, start: number, end: number): void { + this._data += utf32ToString(data, start, end); + } + public unhook(success: boolean): any { + let ret; + if (success) { + ret = this._handler(this._params ? this._params.toArray() : [], this._data); + } + this._params = undefined; + this._data = ''; + return ret; + } +} diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 0d038ee0..87946dc5 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDcsHandler, IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; @@ -77,9 +77,6 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public set collect(value: string) { this._collect = value; } - public mockActiveDcsHandler(): void { - this._activeDcsHandler = this._dcsHandlerFb; - } public mockOscParser(): void { this._oscParser = oscPutParser; } @@ -116,11 +113,7 @@ const testTerminal: any = { actionDCSHook: function (collect: string, params: IParams, flag: string): void { this.calls.push(['dcs hook', collect, params.toArray(), flag]); }, - actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { - let s = ''; - for (let i = start; i < end; ++i) { - s += stringFromCodePoint(data[i]); - } + actionDCSPrint: function (s: string): void { this.calls.push(['dcs put', s]); }, actionDCSUnhook: function (): void { @@ -128,19 +121,6 @@ const testTerminal: any = { } }; -// dcs handler to map dcs actions into the test object `testTerminal` -class DcsTest implements IDcsHandler { - hook(collect: string, params: IParams, flag: number): void { - testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); - } - put(data: Uint32Array, start: number, end: number): void { - testTerminal.actionDCSPrint(data, start, end); - } - unhook(): void { - testTerminal.actionDCSUnhook(); - } -} - const states: number[] = [ ParserState.GROUND, ParserState.ESCAPE, @@ -176,7 +156,18 @@ testParser.setOscHandlerFallback((identifier, action, data) => { if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END }); -testParser.setDcsHandlerFallback(new DcsTest()); +testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => { + switch (action) { + case 'HOOK': + testTerminal.actionDCSHook(payload.collect, payload.params, String.fromCharCode(payload.flag)); + break; + case 'PUT': + testTerminal.actionDCSPrint(payload); + break; + case 'UNHOOK': + testTerminal.actionDCSUnhook(); + } +}); // translate string based parse calls into typed array based @@ -987,7 +978,6 @@ describe('EscapeSequenceParser', function (): void { puts = puts.concat(r(0x20, 0x7f)); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.mockActiveDcsHandler(); parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 355fbecd..dcd242f5 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; import { Params } from 'common/parser/Params'; import { OscParser } from 'common/parser/OscParser'; +import { DcsParser } from 'common/parser/DcsParser'; /** * Table values are generated like this: @@ -196,14 +197,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { return table; })(); -/** - * Dummy DCS handler as default fallback. - */ -class DcsDummy implements IDcsHandler { - hook(collect: string, params: IParams, flag: number): void { } - put(data: Uint32Array, start: number, end: number): void { } - unhook(): void { } -} /** * EscapeSequenceParser. @@ -242,8 +235,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; - protected _dcsHandlers: any; - protected _activeDcsHandler: IDcsHandler; + protected _dcsParser: IDcsParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers @@ -251,7 +243,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; - protected _dcsHandlerFb: IDcsHandler; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -269,15 +260,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = (code: number): void => { }; this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { }; this._escHandlerFb = (collect: string, flag: number): void => { }; - this._dcsHandlerFb = new DcsDummy(); this._errorHandlerFb = (state: IParsingState): IParsingState => state; this._printHandler = this._printHandlerFb; this._executeHandlers = Object.create(null); this._csiHandlers = Object.create(null); this._escHandlers = Object.create(null); this._oscParser = new OscParser(); - this._dcsHandlers = Object.create(null); - this._activeDcsHandler = this._dcsHandlerFb; + this._dcsParser = new DcsParser(); this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) @@ -287,9 +276,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public dispose(): void { this._executeHandlers = null; this._escHandlers = Object.create(null); - this._dcsHandlers = null; - this._activeDcsHandler = new DcsDummy(); this._oscParser.dispose(); + this._dcsParser.dispose(); } setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { @@ -373,19 +361,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._oscParser.setOscHandlerFallback(handler); } + addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { + return this._dcsParser.addDcsHandler(collectAndFlag, handler); + } setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._dcsHandlers[collectAndFlag] = handler; + this._dcsParser.setDcsHandler(collectAndFlag, handler); } clearDcsHandler(collectAndFlag: string): void { - if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag]; + this._dcsParser.clearDcsHandler(collectAndFlag); } - setDcsHandlerFallback(handler: IDcsHandler): void { - if (this._activeDcsHandler === this._dcsHandlerFb) { - this._dcsHandlerFb = handler; - this._activeDcsHandler = handler; - } else { - this._dcsHandlerFb = handler; - } + setDcsHandlerFallback(handler: DcsFallbackHandler): void { + this._dcsParser.setOscHandlerFallback(handler); } setErrorHandler(callback: (state: IParsingState) => IParsingState): void { @@ -398,10 +384,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP reset(): void { this.currentState = this.initialState; this._oscParser.reset(); + this._dcsParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM this._collect = ''; - this._activeDcsHandler = this._dcsHandlerFb; + // this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } @@ -424,10 +411,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let transition = 0; let currentState = this.currentState; const osc = this._oscParser; + const dcs = this._dcsParser; let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - let dcsHandler: IDcsHandler = this._activeDcsHandler; + // let dcsHandler: IDcsHandler = this._activeDcsHandler; let callback: Function | null = null; // process input string @@ -535,7 +523,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (jj < 0) { this._escHandlerFb(collect, code); } - this.precedingCodepoint = 0; break; case ParserAction.CLEAR: @@ -545,24 +532,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP collect = ''; break; case ParserAction.DCS_HOOK: - dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)]; - if (!dcsHandler) dcsHandler = this._dcsHandlerFb; - dcsHandler.hook(collect, params, code); + dcs.hook(collect, params, code); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f // unhook triggered by: 0x1b, 0x9c for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - dcsHandler.put(data, i, j); + dcs.put(data, i, j); i = j - 1; break; } } break; case ParserAction.DCS_UNHOOK: - dcsHandler.unhook(); - dcsHandler = this._dcsHandlerFb; + dcs.unhook(true); // FIXME: apply abort vs. success exit rules if (code === 0x1b) transition |= ParserState.ESCAPE; osc.reset(); params.reset(); @@ -598,10 +582,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // save non pushable buffers this._collect = collect; - this._params = params; - - // save active dcs handler reference - this._activeDcsHandler = dcsHandler; // save state this.currentState = currentState; diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 1163fd20..d6b83d64 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -42,7 +42,8 @@ export class OscParser extends Disposable { } public dispose(): void { - this._handlers = {}; + this._handlers = Object.create(null); + this._handlerFb = () => {}; } public reset(): void { diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 8c65f332..33849c4e 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -89,15 +89,16 @@ export type EscHandler = () => boolean | void; * 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. +* `unhook` marks the end of the current DCS sequence. `success` +* indicates whether the command was aborted. */ export interface IDcsHandler { hook(collect: string, params: IParams, flag: number): void; put(data: Uint32Array, start: number, end: number): void; - unhook(): void; + unhook(success: boolean): void | boolean; } -export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; +export type DcsFallbackHandler = (collectAndFlag: string, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; export interface IOscHandler { /** @@ -120,6 +121,8 @@ export interface IOscHandler { end(success: boolean): void | boolean; } +export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; + /** * EscapeSequenceParser interface. */ @@ -166,7 +169,8 @@ export interface IEscapeSequenceParser extends IDisposable { setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; clearDcsHandler(collectAndFlag: string): void; - setDcsHandlerFallback(handler: IDcsHandler): void; + setDcsHandlerFallback(handler: DcsFallbackHandler): void; + addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; setErrorHandler(callback: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; @@ -182,3 +186,14 @@ export interface IOscParser extends IDisposable { put(data: Uint32Array, start: number, end: number): void; end(success: boolean): void; } + +export interface IDcsParser extends IDisposable { + addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; + setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; + clearDcsHandler(collectAndFlag: string): void; + setOscHandlerFallback(handler: DcsFallbackHandler): void; + reset(): void; + hook(collect: string, params: IParams, flag: number): void; + put(data: Uint32Array, start: number, end: number): void; + unhook(success: boolean): void; +} From c9b7aefb880ebf48e17d5478994896fdeb847e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 02:56:48 +0200 Subject: [PATCH 07/41] reset internal state on unhook --- src/common/parser/DcsParser.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 6c645a71..46f5d501 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -13,10 +13,12 @@ export class DcsParser implements IDcsParser { private _active: IDcsHandler[] = []; private _collectAndFlag: string = ''; private _handlerFb: DcsFallbackHandler = () => {}; + public dispose(): void { this._handlers = Object.create(null); this._handlerFb = () => {}; } + public addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { if (this._handlers[collectAndFlag] === undefined) { this._handlers[collectAndFlag] = []; @@ -32,15 +34,19 @@ export class DcsParser implements IDcsParser { } }; } + public setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { this._handlers[collectAndFlag] = [handler]; } + public clearDcsHandler(collectAndFlag: string): void { if (this._handlers[collectAndFlag]) delete this._handlers[collectAndFlag]; } + public setOscHandlerFallback(handler: DcsFallbackHandler): void { this._handlerFb = handler; } + public reset(): void { if (this._active.length) { this.unhook(false); @@ -48,6 +54,7 @@ export class DcsParser implements IDcsParser { this._active = []; this._collectAndFlag = ''; } + public hook(collect: string, params: IParams, flag: number): void { this._collectAndFlag = collect + String.fromCharCode(flag); this._active = this._handlers[this._collectAndFlag] || []; @@ -59,6 +66,7 @@ export class DcsParser implements IDcsParser { } } } + public put(data: Uint32Array, start: number, end: number): void { if (!this._active.length) { this._handlerFb(this._collectAndFlag, 'PUT', utf32ToString(data, start, end)); @@ -68,6 +76,7 @@ export class DcsParser implements IDcsParser { } } } + public unhook(success: boolean): void { if (!this._active.length) { this._handlerFb(this._collectAndFlag, 'UNHOOK', success); @@ -84,6 +93,8 @@ export class DcsParser implements IDcsParser { this._active[j].unhook(false); } } + this._active = []; + this._collectAndFlag = ''; } } From 82aaef29f14e3c3006bccd759cbcba5ded46d614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 15:07:24 +0200 Subject: [PATCH 08/41] dcs parser tests --- src/common/parser/DcsParser.test.ts | 180 ++++++++++++++++++++++ src/common/parser/DcsParser.ts | 2 +- src/common/parser/EscapeSequenceParser.ts | 2 +- src/common/parser/Types.d.ts | 2 +- 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/common/parser/DcsParser.test.ts diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts new file mode 100644 index 00000000..d45d2770 --- /dev/null +++ b/src/common/parser/DcsParser.test.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { DcsParser, DcsHandlerFactory } from 'common/parser/DcsParser'; +import { IDcsHandler, IParams } from 'common/parser/Types'; +import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder'; +import { Params } from 'common/parser/Params'; + +function toUtf32(s: string): Uint32Array { + const utf32 = new Uint32Array(s.length); + const decoder = new StringToUtf32(); + const length = decoder.decode(s, utf32); + return utf32.subarray(0, length); +} + +class TestHandler implements IDcsHandler { + constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} + hook(collect: string, params: IParams, flag: number): void { + this.output.push([this.msg, 'HOOK', params.toArray(), collect, flag]); + } + put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); + } + unhook(success: boolean): void | boolean { + this.output.push([this.msg, 'UNHOOK', success]); + if (this.returnFalse) { + return false; + } + } +} + +describe('DcsParser', () => { + let parser: DcsParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new DcsParser(); + parser.setDcsHandlerFallback((id, action, data) => { + if (data.params) { + data.params = data.params.toArray(); + } + reports.push([id, action, data]); + }); + }); + describe('handler registration', () => { + it('setDcsHandler', () => { + parser.setDcsHandler('+p', new TestHandler(reports, 'th')); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['th', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th', 'PUT', 'Here comes'], + ['th', 'PUT', 'the mouse!'], + ['th', 'UNHOOK', true] + ]); + }); + it('clearDcsHandler', () => { + parser.setDcsHandler('+p', new TestHandler(reports, 'th')); + parser.clearDcsHandler('+p'); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + // messages from fallback handler + ['+p', 'HOOK', {collect: '+', params: [1, 2, 3], flag: 'p'.charCodeAt(0)}], + ['+p', 'PUT', 'Here comes'], + ['+p', 'PUT', 'the mouse!'], + ['+p', 'UNHOOK', true] + ]); + }); + it('addDcsHandler', () => { + parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); + parser.addDcsHandler('+p', new TestHandler(reports, 'th2')); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + ['th2', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th2', 'PUT', 'Here comes'], + ['th1', 'PUT', 'Here comes'], + ['th2', 'PUT', 'the mouse!'], + ['th1', 'PUT', 'the mouse!'], + ['th2', 'UNHOOK', true], + ['th1', 'UNHOOK', false] // false due being already handled by th2! + ]); + }); + it('addDcsHandler with return false', () => { + parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); + parser.addDcsHandler('+p', new TestHandler(reports, 'th2', true)); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + ['th2', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th2', 'PUT', 'Here comes'], + ['th1', 'PUT', 'Here comes'], + ['th2', 'PUT', 'the mouse!'], + ['th1', 'PUT', 'the mouse!'], + ['th2', 'UNHOOK', true], + ['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling + ]); + }); + it('dispose handlers', () => { + parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); + const dispo = parser.addDcsHandler('+p', new TestHandler(reports, 'th2', true)); + dispo.dispose(); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [ + ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th1', 'PUT', 'Here comes'], + ['th1', 'PUT', 'the mouse!'], + ['th1', 'UNHOOK', true] + ]); + }); + }); + describe('DcsHandlerFactory', () => { + it('should be called once on end(true)', () => { + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params, data]))); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', () => { + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params, data]))); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(false); + assert.deepEqual(reports, []); + }); + it('should be disposable', () => { + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params, data]))); + const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params, data]))); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); + dispo.dispose(); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + data = toUtf32('some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); + }); + }); +}); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 46f5d501..aef8c694 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -43,7 +43,7 @@ export class DcsParser implements IDcsParser { if (this._handlers[collectAndFlag]) delete this._handlers[collectAndFlag]; } - public setOscHandlerFallback(handler: DcsFallbackHandler): void { + public setDcsHandlerFallback(handler: DcsFallbackHandler): void { this._handlerFb = handler; } diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index dcd242f5..4d30a549 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -371,7 +371,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.clearDcsHandler(collectAndFlag); } setDcsHandlerFallback(handler: DcsFallbackHandler): void { - this._dcsParser.setOscHandlerFallback(handler); + this._dcsParser.setDcsHandlerFallback(handler); } setErrorHandler(callback: (state: IParsingState) => IParsingState): void { diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 33849c4e..4ae7ebcc 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -191,7 +191,7 @@ export interface IDcsParser extends IDisposable { addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; clearDcsHandler(collectAndFlag: string): void; - setOscHandlerFallback(handler: DcsFallbackHandler): void; + setDcsHandlerFallback(handler: DcsFallbackHandler): void; reset(): void; hook(collect: string, params: IParams, flag: number): void; put(data: Uint32Array, start: number, end: number): void; From a9b2728c076ecfa3454bfebf53a96d972c3e42c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 15:59:24 +0200 Subject: [PATCH 09/41] add DCS interface, payload limits for DCS/OSC --- src/InputHandler.ts | 22 +++++++++++++------- src/Terminal.ts | 5 +++++ src/TestUtils.test.ts | 3 +++ src/Types.d.ts | 1 + src/common/parser/DcsParser.test.ts | 8 ++++---- src/common/parser/DcsParser.ts | 30 +++++++++++++++++++++++++--- src/common/parser/OscParser.ts | 21 ++++++++++++++++++- src/public/Terminal.ts | 3 +++ typings/xterm.d.ts | 31 +++++++++++++++++++++++++---- 9 files changed, 105 insertions(+), 19 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 04d433ab..62d95cdc 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -22,6 +22,7 @@ import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; import { OscHandlerFactory } from 'common/parser/OscParser'; +import { DcsHandlerFactory } from 'common/parser/DcsParser'; /** * Map collect to glevel. Used in `selectCharset`. @@ -485,13 +486,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowService.markDirty(buffer.y); } - /** - * Forward addEscHandler from parser. - */ - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._parser.addEscHandler(collectAndFlag, handler); - } - /** * Forward addCsiHandler from parser. */ @@ -499,6 +493,20 @@ export class InputHandler extends Disposable implements IInputHandler { return this._parser.addCsiHandler(flag, callback); } + /** + * Forward addDcsHandler from parser. + */ + public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + return this._parser.addDcsHandler(collectAndFlag, new DcsHandlerFactory(callback)); + } + + /** + * Forward addEscHandler from parser. + */ + public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + return this._parser.addEscHandler(collectAndFlag, handler); + } + /** * Forward addOscHandler from parser. */ diff --git a/src/Terminal.ts b/src/Terminal.ts index 0de1b3d5..bd5ddcc3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1397,6 +1397,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this._inputHandler.addEscHandler(collectAndFlag, handler); } + /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ + public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(collectAndFlag, callback); + } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { return this._inputHandler.addCsiHandler(flag, callback); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index efe41858..6552c3e3 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -77,6 +77,9 @@ export class MockTerminal implements ITerminal { addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { throw new Error('Method not implemented.'); } + addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { throw new Error('Method not implemented.'); } diff --git a/src/Types.d.ts b/src/Types.d.ts index be488304..e6e540a3 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -201,6 +201,7 @@ export interface IPublicTerminal extends IDisposable { open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; + addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable; addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index d45d2770..a48b99dd 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -138,7 +138,7 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params, data]))); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -148,7 +148,7 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params, data]))); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -158,8 +158,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params, data]))); - const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params, data]))); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params.toArray(), data]))); parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index aef8c694..d3f777dd 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -6,6 +6,7 @@ import { IDisposable } from 'common/Types'; import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; +import { Params } from './Params'; export class DcsParser implements IDcsParser { @@ -98,24 +99,47 @@ export class DcsParser implements IDcsParser { } } +// limit allowed payload for DcsHandlerFactory +const PAYLOAD_LIMIT = 50000000; + +/** + * Convenient class to create a DCS handler from a single callback function. + * Note: The payload is currently limited to 50 MB (hardcoded). + */ export class DcsHandlerFactory implements IDcsHandler { private _data = ''; private _params: IParams | undefined; - constructor(private _handler: (params: ParamsArray, data: string) => any) {} + private _hitLimit: boolean = false; + + constructor(private _handler: (params: IParams, data: string) => any) {} + public hook(collect: string, params: IParams, flag: number): void { this._params = params.clone(); this._data = ''; + this._hitLimit = false; } + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } } + public unhook(success: boolean): any { let ret; - if (success) { - ret = this._handler(this._params ? this._params.toArray() : [], this._data); + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._params ? this._params : new Params(), this._data); } this._params = undefined; this._data = ''; + this._hitLimit = false; return ret; } } diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index d6b83d64..f0c9c93c 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -163,25 +163,44 @@ export class OscParser extends Disposable { } +// limit allowed payload for OscHandlerFactory +const PAYLOAD_LIMIT = 50000000; + /** * Convenient class to allow attaching string based handler functions * as OSC handlers. */ export class OscHandlerFactory implements IOscHandler { private _data = ''; + private _hitLimit: boolean = false; + constructor(private _handler: (data: string) => any) {} + public start(): void { this._data = ''; + this._hitLimit = false; } + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } } + public end(success: boolean): any { let ret; - if (success) { + if (this._hitLimit) { + ret = false; + } else if (success) { ret = this._handler(this._data); } this._data = ''; + this._hitLimit = false; return ret; } } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 391501e0..c9513f53 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -60,6 +60,9 @@ export class Terminal implements ITerminalApi { public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable { return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect)); } + public addDcsHandler(collectAndFlag: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable { + return this._core.addDcsHandler(collectAndFlag, (params: IParams, data: string) => callback(params.toArray(), data)); + } public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { return this._core.addEscHandler(collectAndFlag, handler); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7582f6d2..a0810309 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -515,10 +515,28 @@ declare module 'xterm' { */ addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; + /** + * Adds a handler for DCS escape sequences. + * @param collect Should be a string, which specifies the collect and the + * final character (e.g "$q" for DECRQSS) of the DCS sequence. + * @param callback The function to handle the escape sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, those will be stored up + * until the sequence is finished. Since DCS sequences are not limited by the amount + * of data this might impose a problem for big payloads. Currently xterm.js limits + * DCS payload to 50 MB which should give enough room for most use cases. + * The function gets numerical parameter and the data as arguments. + * Return true if the sequence was handled; false if + * we should try a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addDcsHandler(collect: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable; + /** * Adds a handler for ESC escape sequences. - * @param flag The flag should be a string, which specifies the - * collect and the final character (e.g "%G" for default charset selection) + * @param collect Should be a string, which specifies the collect and the + * final character (e.g "%G" for default charset selection) * of the ESC sequence. * @param callback The function to handle the escape sequence. * Return true if the sequence was handled; false if @@ -531,8 +549,13 @@ declare module 'xterm' { /** * Adds a handler for OSC escape sequences. * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the escape sequence. The callback - * is called with OSC data string. Return true if the sequence was handled; + * @param callback The function to handle the escape sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, those will be stored up + * until the sequence is finished. Since OSC sequences are not limited by the amount + * of data this might impose a problem for big payloads. Currently xterm.js limits + * OSC payload to 50 MB which should give enough room for most use cases. + * The callback is called with OSC data string. Return true if the sequence was handled; * false if we should try a previous handler (set by addOscHandler or * setOscHandler). The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. From bffa78585e1d4fd0ce02329936e2aa56aa2c6c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 17:35:37 +0200 Subject: [PATCH 10/41] test cases for payload limits --- src/common/parser/Constants.ts | 3 +++ src/common/parser/DcsParser.test.ts | 36 +++++++++++++++++++++++++++++ src/common/parser/DcsParser.ts | 6 ++--- src/common/parser/OscParser.test.ts | 29 +++++++++++++++++++++++ src/common/parser/OscParser.ts | 6 +---- typings/xterm.d.ts | 4 ++-- 6 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts index 9f28a27a..85156c3e 100644 --- a/src/common/parser/Constants.ts +++ b/src/common/parser/Constants.ts @@ -53,3 +53,6 @@ export const enum OscState { PAYLOAD = 2, ABORT = 3 } + +// payload limit for OSC and DCS +export const PAYLOAD_LIMIT = 10000000; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index a48b99dd..172f51bc 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -7,6 +7,7 @@ import { DcsParser, DcsHandlerFactory } from 'common/parser/DcsParser'; import { IDcsHandler, IParams } from 'common/parser/Types'; import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; +import { PAYLOAD_LIMIT } from 'common/parser/Constants'; function toUtf32(s: string): Uint32Array { const utf32 = new Uint32Array(s.length); @@ -176,5 +177,40 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); + it('should respect return false', () => { + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); + parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]); + }); + it('should work up to payload limit', function(): void { + this.timeout(10000); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + const data = toUtf32('A'.repeat(1000)); + for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { + parser.put(data, 0, data.length); + } + parser.unhook(true); + assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(PAYLOAD_LIMIT)]]); + }); + it('should abort for payload limit +1', function(): void { + this.timeout(10000); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); + parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + let data = toUtf32('A'.repeat(1000)); + for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { + parser.put(data, 0, data.length); + } + data = toUtf32('A'); + parser.put(data, 0, data.length); + parser.unhook(true); + assert.deepEqual(reports, []); + }); }); }); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index d3f777dd..f89edcfc 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -6,7 +6,8 @@ import { IDisposable } from 'common/Types'; import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; -import { Params } from './Params'; +import { Params } from 'common/parser/Params'; +import { PAYLOAD_LIMIT } from 'common/parser/Constants'; export class DcsParser implements IDcsParser { @@ -99,9 +100,6 @@ export class DcsParser implements IDcsParser { } } -// limit allowed payload for DcsHandlerFactory -const PAYLOAD_LIMIT = 50000000; - /** * Convenient class to create a DCS handler from a single callback function. * Note: The payload is currently limited to 50 MB (hardcoded). diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index 9b5de5fd..2d1180d2 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -6,6 +6,7 @@ import { assert } from 'chai'; import { OscParser, OscHandlerFactory } from 'common/parser/OscParser'; import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder'; import { IOscHandler } from 'common/parser/Types'; +import { PAYLOAD_LIMIT } from 'common/parser/Constants'; function toUtf32(s: string): Uint32Array { const utf32 = new Uint32Array(s.length); @@ -218,5 +219,33 @@ describe('OscParser', () => { parser.end(true); assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]); }); + it('should work up to payload limit', function(): void { + this.timeout(10000); + parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;'); + parser.put(data, 0, data.length); + data = toUtf32('A'.repeat(1000)); + for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { + parser.put(data, 0, data.length); + } + parser.end(true); + assert.deepEqual(reports, [[1234, 'A'.repeat(PAYLOAD_LIMIT)]]); + }); + it('should abort for payload limit +1', function(): void { + this.timeout(10000); + parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.start(); + let data = toUtf32('1234;'); + parser.put(data, 0, data.length); + data = toUtf32('A'.repeat(1000)); + for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { + parser.put(data, 0, data.length); + } + data = toUtf32('A'); + parser.put(data, 0, data.length); + parser.end(true); + assert.deepEqual(reports, []); + }); }); }); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index f0c9c93c..cf363714 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -4,7 +4,7 @@ */ import { IOscHandler, IHandlerCollection, OscFallbackHandler } from 'common/parser/Types'; -import { OscState } from 'common/parser/Constants'; +import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; @@ -162,10 +162,6 @@ export class OscParser extends Disposable { } } - -// limit allowed payload for OscHandlerFactory -const PAYLOAD_LIMIT = 50000000; - /** * Convenient class to allow attaching string based handler functions * as OSC handlers. diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a0810309..806074a2 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -524,7 +524,7 @@ declare module 'xterm' { * There is currently no way to intercept smaller data chunks, those will be stored up * until the sequence is finished. Since DCS sequences are not limited by the amount * of data this might impose a problem for big payloads. Currently xterm.js limits - * DCS payload to 50 MB which should give enough room for most use cases. + * DCS payload to 10 MB which should give enough room for most use cases. * The function gets numerical parameter and the data as arguments. * Return true if the sequence was handled; false if * we should try a previous handler (set by addDcsHandler or setDcsHandler). @@ -554,7 +554,7 @@ declare module 'xterm' { * There is currently no way to intercept smaller data chunks, those will be stored up * until the sequence is finished. Since OSC sequences are not limited by the amount * of data this might impose a problem for big payloads. Currently xterm.js limits - * OSC payload to 50 MB which should give enough room for most use cases. + * OSC payload to 10 MB which should give enough room for most use cases. * The callback is called with OSC data string. Return true if the sequence was handled; * false if we should try a previous handler (set by addOscHandler or * setOscHandler). The most recently-added handler is tried first. From 0a2d9a212069a8ab0778f598303f201a20e30e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 17:48:36 +0200 Subject: [PATCH 11/41] addOscHandler integration test --- test/api/InputHandler.api.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 95502c3e..fe43617d 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -351,6 +351,30 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); }); }); + describe('addDcsHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customDcsHandlerCallStack = []; + const _customDcsHandlerA = window.term.addDcsHandler('+p', (params, data) => { + _customDcsHandlerCallStack.push(['A', params, data]); + return false; + }); + const _customDcsHandlerB = window.term.addDcsHandler('+p', (params, data) => { + _customDcsHandlerCallStack.push(['B', params, data]); + return true; + }); + const _customDcsHandlerC = window.term.addDcsHandler('+p', (params, data) => { + _customDcsHandlerCallStack.push(['C', params, data]); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1bP1;2+psome data\x1b\\\\'); + `); + assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); + }); + }); describe('addEscHandler', () => { it('should respects return value', async () => { await page.evaluate(` From 0106bdcb732c84b9b56b9fab2604d4ca41d3b9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 18:11:31 +0200 Subject: [PATCH 12/41] parser tests for addDcsHandler --- .../parser/EscapeSequenceParser.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 87946dc5..6b14e41b 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -11,6 +11,7 @@ import { ParserState } from 'common/parser/Constants'; import { Params } from 'common/parser/Params'; import { OscHandlerFactory } from 'common/parser/OscParser'; import { IDisposable } from 'common/Types'; +import { DcsHandlerFactory } from 'common/parser/DcsParser'; function r(a: number, b: number): string[] { @@ -1443,6 +1444,64 @@ describe('EscapeSequenceParser', function (): void { parse(parser2, ';de\x9c'); chai.expect(dcs).eql([]); }); + describe('DCS custom handlers', () => { + const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; + it('Prevent fallback', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parse(parser2, DCS_INPUT); + chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]); + }); + it('Allow fallback', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parse(parser2, DCS_INPUT); + chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); + }); + it('Multiple custom handlers fallback once', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parse(parser2, DCS_INPUT); + chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); + }); + it('Multiple custom handlers no fallback', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parse(parser2, DCS_INPUT); + chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory(() => order.push(1))); + parser2.addDcsHandler('+p', new DcsHandlerFactory(() => { order.push(2); return false; })); + parser2.addDcsHandler('+p', new DcsHandlerFactory(() => { order.push(3); return false; })); + parse(parser2, DCS_INPUT); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + dispo.dispose(); + parse(parser2, DCS_INPUT); + chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const dcsCustom: [string, (number | number[])[], string][] = []; + parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + dispo.dispose(); + dispo.dispose(); + parse(parser2, DCS_INPUT); + chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); + }); + }); it('ERROR handler', function (): void { let errorState: IParsingState | null = null; parser2.setErrorHandler(function (state: IParsingState): IParsingState { From 812fc5e6ec4071b963b6d97a2d69bba5123a5d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 31 Jul 2019 04:49:01 +0200 Subject: [PATCH 13/41] register functions by prefix/intermediate/final bytes --- src/InputHandler.test.ts | 20 +- src/InputHandler.ts | 417 +++++++++--------- src/Terminal.ts | 14 +- src/TestUtils.test.ts | 8 +- src/Types.d.ts | 11 +- src/common/parser/DcsParser.test.ts | 125 ++++-- src/common/parser/DcsParser.ts | 44 +- .../parser/EscapeSequenceParser.test.ts | 175 ++++---- src/common/parser/EscapeSequenceParser.ts | 153 +++++-- src/common/parser/Types.d.ts | 51 ++- src/public/Terminal.ts | 14 +- test/api/InputHandler.api.ts | 14 +- .../EscapeSequenceParser.benchmark.ts | 108 ++--- typings/xterm.d.ts | 37 +- 14 files changed, 652 insertions(+), 539 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 38645a0b..8752e0a3 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -53,39 +53,38 @@ describe('InputHandler', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService); - const collect = ' '; - inputHandler.setCursorStyle(Params.fromArray([0]), collect); + inputHandler.setCursorStyle(Params.fromArray([0])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([1]), collect); + inputHandler.setCursorStyle(Params.fromArray([1])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([2]), collect); + inputHandler.setCursorStyle(Params.fromArray([2])); assert.equal(optionsService.options['cursorStyle'], 'block'); assert.equal(optionsService.options['cursorBlink'], false); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([3]), collect); + inputHandler.setCursorStyle(Params.fromArray([3])); assert.equal(optionsService.options['cursorStyle'], 'underline'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([4]), collect); + inputHandler.setCursorStyle(Params.fromArray([4])); assert.equal(optionsService.options['cursorStyle'], 'underline'); assert.equal(optionsService.options['cursorBlink'], false); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([5]), collect); + inputHandler.setCursorStyle(Params.fromArray([5])); assert.equal(optionsService.options['cursorStyle'], 'bar'); assert.equal(optionsService.options['cursorBlink'], true); optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([6]), collect); + inputHandler.setCursorStyle(Params.fromArray([6])); assert.equal(optionsService.options['cursorStyle'], 'bar'); assert.equal(optionsService.options['cursorBlink'], false); }); @@ -93,14 +92,13 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle Terminal.bracketedPasteMode', () => { const terminal = new MockInputHandlingTerminal(); - const collect = '?'; terminal.bracketedPasteMode = false; const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // Set bracketed paste mode - inputHandler.setMode(Params.fromArray([2004]), collect); + inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, true); // Reset bracketed paste mode - inputHandler.resetMode(Params.fromArray([2004]), collect); + inputHandler.resetModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, false); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 62d95cdc..02e20961 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,7 +14,7 @@ import { concat } from 'common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -50,7 +50,7 @@ class DECRQSS implements IDcsHandler { private _optionsService: IOptionsService ) { } - hook(collect: string, params: IParams, flag: number): void { + hook(params: IParams): void { this._data = new Uint32Array(0); } @@ -149,69 +149,74 @@ export class InputHandler extends Disposable implements IInputHandler { /** * custom fallback handlers */ - this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - this._logService.debug('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) }); + this._parser.setCsiHandlerFallback((ident, params) => { + this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() }); }); - this._parser.setEscHandlerFallback((collect: string, flag: number) => { - this._logService.debug('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) }); + this._parser.setEscHandlerFallback(ident => { + this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) }); }); - this._parser.setExecuteHandlerFallback((code: number) => { + this._parser.setExecuteHandlerFallback(code => { this._logService.debug('Unknown EXECUTE code: ', { code }); }); this._parser.setOscHandlerFallback((identifier, action, data) => { this._logService.debug('Unknown OSC code: ', { identifier, action, data }); }); - this._parser.setDcsHandlerFallback((identifier, action, payload) => { - if (payload.params) { - payload.params = payload.params.toArray(); + this._parser.setDcsHandlerFallback((ident, action, payload) => { + if (action === 'HOOK') { + payload = payload.toArray(); } - this._logService.debug('Unknown DCS code: ', { identifier, action, payload }); + this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload }); }); /** * print handler */ - this._parser.setPrintHandler((data, start, end): void => this.print(data, start, end)); + this._parser.setPrintHandler((data, start, end) => this.print(data, start, end)); /** * CSI handler */ - this._parser.setCsiHandler('@', (params, collect) => this.insertChars(params)); - this._parser.setCsiHandler('A', (params, collect) => this.cursorUp(params)); - this._parser.setCsiHandler('B', (params, collect) => this.cursorDown(params)); - this._parser.setCsiHandler('C', (params, collect) => this.cursorForward(params)); - this._parser.setCsiHandler('D', (params, collect) => this.cursorBackward(params)); - this._parser.setCsiHandler('E', (params, collect) => this.cursorNextLine(params)); - this._parser.setCsiHandler('F', (params, collect) => this.cursorPrecedingLine(params)); - this._parser.setCsiHandler('G', (params, collect) => this.cursorCharAbsolute(params)); - this._parser.setCsiHandler('H', (params, collect) => this.cursorPosition(params)); - this._parser.setCsiHandler('I', (params, collect) => this.cursorForwardTab(params)); - this._parser.setCsiHandler('J', (params, collect) => this.eraseInDisplay(params)); - this._parser.setCsiHandler('K', (params, collect) => this.eraseInLine(params)); - this._parser.setCsiHandler('L', (params, collect) => this.insertLines(params)); - this._parser.setCsiHandler('M', (params, collect) => this.deleteLines(params)); - this._parser.setCsiHandler('P', (params, collect) => this.deleteChars(params)); - this._parser.setCsiHandler('S', (params, collect) => this.scrollUp(params)); - this._parser.setCsiHandler('T', (params, collect) => this.scrollDown(params, collect)); - this._parser.setCsiHandler('X', (params, collect) => this.eraseChars(params)); - this._parser.setCsiHandler('Z', (params, collect) => this.cursorBackwardTab(params)); - this._parser.setCsiHandler('`', (params, collect) => this.charPosAbsolute(params)); - this._parser.setCsiHandler('a', (params, collect) => this.hPositionRelative(params)); - this._parser.setCsiHandler('b', (params, collect) => this.repeatPrecedingCharacter(params)); - this._parser.setCsiHandler('c', (params, collect) => this.sendDeviceAttributes(params, collect)); - this._parser.setCsiHandler('d', (params, collect) => this.linePosAbsolute(params)); - this._parser.setCsiHandler('e', (params, collect) => this.vPositionRelative(params)); - this._parser.setCsiHandler('f', (params, collect) => this.hVPosition(params)); - this._parser.setCsiHandler('g', (params, collect) => this.tabClear(params)); - this._parser.setCsiHandler('h', (params, collect) => this.setMode(params, collect)); - this._parser.setCsiHandler('l', (params, collect) => this.resetMode(params, collect)); - this._parser.setCsiHandler('m', (params, collect) => this.charAttributes(params)); - this._parser.setCsiHandler('n', (params, collect) => this.deviceStatus(params, collect)); - this._parser.setCsiHandler('p', (params, collect) => this.softReset(params, collect)); - this._parser.setCsiHandler('q', (params, collect) => this.setCursorStyle(params, collect)); - this._parser.setCsiHandler('r', (params, collect) => this.setScrollRegion(params, collect)); - this._parser.setCsiHandler('s', (params, collect) => this.saveCursor(params)); - this._parser.setCsiHandler('u', (params, collect) => this.restoreCursor(params)); + this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params)); + this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params)); + this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params)); + this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params)); + this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params)); + this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params)); + this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params)); + this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params)); + this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params)); + this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params)); + this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params)); + this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params)); + this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params)); + this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params)); + this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params)); + this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params)); + this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params)); + this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params)); + this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params)); + this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params)); + this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params)); + this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params)); + this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params)); + this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params)); + this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params)); + this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params)); + this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params)); + this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params)); + this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params)); + this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params)); + this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params)); + this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params)); + this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params)); + this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params)); + this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params)); + this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params)); + this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params)); + this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params)); + this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params)); + this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); + this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); /** * execute handler @@ -276,32 +281,32 @@ export class InputHandler extends Disposable implements IInputHandler { /** * ESC handlers */ - this._parser.setEscHandler('7', () => this.saveCursor()); - this._parser.setEscHandler('8', () => this.restoreCursor()); - this._parser.setEscHandler('D', () => this.index()); - this._parser.setEscHandler('E', () => this.nextLine()); - this._parser.setEscHandler('H', () => this.tabSet()); - this._parser.setEscHandler('M', () => this.reverseIndex()); - this._parser.setEscHandler('=', () => this.keypadApplicationMode()); - this._parser.setEscHandler('>', () => this.keypadNumericMode()); - this._parser.setEscHandler('c', () => this.reset()); - this._parser.setEscHandler('n', () => this.setgLevel(2)); - this._parser.setEscHandler('o', () => this.setgLevel(3)); - this._parser.setEscHandler('|', () => this.setgLevel(3)); - this._parser.setEscHandler('}', () => this.setgLevel(2)); - this._parser.setEscHandler('~', () => this.setgLevel(1)); - this._parser.setEscHandler('%@', () => this.selectDefaultCharset()); - this._parser.setEscHandler('%G', () => this.selectDefaultCharset()); + this._parser.setEscHandler({final: '7'}, () => this.saveCursor()); + this._parser.setEscHandler({final: '8'}, () => this.restoreCursor()); + this._parser.setEscHandler({final: 'D'}, () => this.index()); + this._parser.setEscHandler({final: 'E'}, () => this.nextLine()); + this._parser.setEscHandler({final: 'H'}, () => this.tabSet()); + this._parser.setEscHandler({final: 'M'}, () => this.reverseIndex()); + this._parser.setEscHandler({final: '='}, () => this.keypadApplicationMode()); + this._parser.setEscHandler({final: '>'}, () => this.keypadNumericMode()); + this._parser.setEscHandler({final: 'c'}, () => this.reset()); + this._parser.setEscHandler({final: 'n'}, () => this.setgLevel(2)); + this._parser.setEscHandler({final: 'o'}, () => this.setgLevel(3)); + this._parser.setEscHandler({final: '|'}, () => this.setgLevel(3)); + this._parser.setEscHandler({final: '}'}, () => this.setgLevel(2)); + this._parser.setEscHandler({final: '~'}, () => this.setgLevel(1)); + this._parser.setEscHandler({intermediates: '%', final: '@'}, () => this.selectDefaultCharset()); + this._parser.setEscHandler({intermediates: '%', final: 'G'}, () => this.selectDefaultCharset()); for (const flag in CHARSETS) { - this._parser.setEscHandler('(' + flag, () => this.selectCharset('(' + flag)); - this._parser.setEscHandler(')' + flag, () => this.selectCharset(')' + flag)); - this._parser.setEscHandler('*' + flag, () => this.selectCharset('*' + flag)); - this._parser.setEscHandler('+' + flag, () => this.selectCharset('+' + flag)); - this._parser.setEscHandler('-' + flag, () => this.selectCharset('-' + flag)); - this._parser.setEscHandler('.' + flag, () => this.selectCharset('.' + flag)); - this._parser.setEscHandler('/' + flag, () => this.selectCharset('/' + flag)); // TODO: supported? + this._parser.setEscHandler({intermediates: '(', final: flag}, () => this.selectCharset('(' + flag)); + this._parser.setEscHandler({intermediates: ')', final: flag}, () => this.selectCharset(')' + flag)); + this._parser.setEscHandler({intermediates: '*', final: flag}, () => this.selectCharset('*' + flag)); + this._parser.setEscHandler({intermediates: '+', final: flag}, () => this.selectCharset('+' + flag)); + this._parser.setEscHandler({intermediates: '-', final: flag}, () => this.selectCharset('-' + flag)); + this._parser.setEscHandler({intermediates: '.', final: flag}, () => this.selectCharset('.' + flag)); + this._parser.setEscHandler({intermediates: '/', final: flag}, () => this.selectCharset('/' + flag)); // TODO: supported? } - this._parser.setEscHandler('#8', () => this.screenAlignmentPattern()); + this._parser.setEscHandler({intermediates: '#', final: '8'}, () => this.screenAlignmentPattern()); /** * error handler @@ -314,7 +319,7 @@ export class InputHandler extends Disposable implements IInputHandler { /** * DCS handler */ - this._parser.setDcsHandler('$q', new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); + this._parser.setDcsHandler({intermediates: '$', final: 'q'}, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); } public dispose(): void { @@ -489,22 +494,22 @@ export class InputHandler extends Disposable implements IInputHandler { /** * Forward addCsiHandler from parser. */ - public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - return this._parser.addCsiHandler(flag, callback); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._parser.addCsiHandler(id, callback); } /** * Forward addDcsHandler from parser. */ - public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { - return this._parser.addDcsHandler(collectAndFlag, new DcsHandlerFactory(callback)); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._parser.addDcsHandler(id, new DcsHandlerFactory(callback)); } /** * Forward addEscHandler from parser. */ - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._parser.addEscHandler(collectAndFlag, handler); + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._parser.addEscHandler(id, callback); } /** @@ -1030,8 +1035,8 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: IParams, collect?: string): void { - if (params.length < 2 && !collect) { + public scrollDown(params: IParams): void { + if (params.length < 2) { let param = params.params[0] || 1; // make buffer local for faster access @@ -1134,32 +1139,33 @@ export class InputHandler extends Disposable implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: IParams, collect?: string): void { + public sendDeviceAttributesPrimary(params: IParams): void { if (params.params[0] > 0) { return; } - - if (!collect) { - if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { - this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); - } else if (this._terminal.is('linux')) { - this._coreService.triggerDataEvent(C0.ESC + '[?6c'); - } - } else if (collect === '>') { - // xterm and urxvt - // seem to spit this - // out around ~370 times (?). - if (this._terminal.is('xterm')) { - this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); - } else if (this._terminal.is('rxvt-unicode')) { - this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); - } else if (this._terminal.is('linux')) { - // not supported by linux console. - // linux console echoes parameters. - this._coreService.triggerDataEvent(params.params[0] + 'c'); - } else if (this._terminal.is('screen')) { - this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); - } + if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); + } else if (this._terminal.is('linux')) { + this._coreService.triggerDataEvent(C0.ESC + '[?6c'); + } + } + public sendDeviceAttributesSecondary(params: IParams): void { + if (params.params[0] > 0) { + return; + } + // xterm and urxvt + // seem to spit this + // out around ~370 times (?). + if (this._terminal.is('xterm')) { + this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); + } else if (this._terminal.is('rxvt-unicode')) { + this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); + } else if (this._terminal.is('linux')) { + // not supported by linux console. + // linux console echoes parameters. + this._coreService.triggerDataEvent(params.params[0] + 'c'); + } else if (this._terminal.is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } } @@ -1249,15 +1255,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: IParams, collect?: string): void { + public setMode(params: IParams): void { for (let i = 0; i < params.length; i++) { - this._setMode(params.params[i], collect); - } - } - - private _setMode(param: number, collect?: string): void { - if (!collect) { - switch (param) { + switch (params.params[i]) { case 4: this._terminal.insertMode = true; break; @@ -1265,8 +1265,11 @@ export class InputHandler extends Disposable implements IInputHandler { // this._t.convertEol = true; break; } - } else if (collect === '?') { - switch (param) { + } + } + public setModePrivate(params: IParams): void { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = true; break; @@ -1312,9 +1315,9 @@ export class InputHandler extends Disposable implements IInputHandler { // TODO: Why are params[0] compares nested within a switch for params[0]? - this._terminal.x10Mouse = param === 9; - this._terminal.vt200Mouse = param === 1000; - this._terminal.normalMouse = param > 1000; + this._terminal.x10Mouse = params.params[i] === 9; + this._terminal.vt200Mouse = params.params[i] === 1000; + this._terminal.normalMouse = params.params[i] > 1000; this._terminal.mouseEvents = true; if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); @@ -1373,6 +1376,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** * CSI Pm l Reset Mode (RM). * Ps = 2 -> Keyboard Action Mode (AM). @@ -1455,15 +1459,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: IParams, collect?: string): void { + public resetMode(params: IParams): void { for (let i = 0; i < params.length; i++) { - this._resetMode(params.params[i], collect); - } - } - - private _resetMode(param: number, collect?: string): void { - if (!collect) { - switch (param) { + switch (params.params[i]) { case 4: this._terminal.insertMode = false; break; @@ -1471,8 +1469,11 @@ export class InputHandler extends Disposable implements IInputHandler { // this._t.convertEol = false; break; } - } else if (collect === '?') { - switch (param) { + } + } + public resetModePrivate(params: IParams): void { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { case 1: this._coreService.decPrivateModes.applicationCursorKeys = false; break; @@ -1542,7 +1543,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 1047: // normal screen buffer - clearing it first // Ensure the selection manager has the correct buffer this._bufferService.buffers.activateNormalBuffer(); - if (param === 1049) { + if (params.params[i] === 1049) { this.restoreCursor(); } this._terminal.refresh(0, this._bufferService.rows - 1); @@ -1558,6 +1559,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** * Helper to extract and apply color params/subparams. * Returns advance for params index. @@ -1816,47 +1818,46 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: IParams, collect?: string): void { - if (!collect) { - switch (params.params[0]) { - case 5: - // status report - this._coreService.triggerDataEvent(`${C0.ESC}[0n`); - break; - case 6: - // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; - this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); - break; - } - } else if (collect === '?') { - // modern xterm doesnt seem to - // respond to any of these except ?6, 6, and 5 - switch (params.params[0]) { - case 6: - // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; - this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); - break; - case 15: - // no printer - // this.handler(C0.ESC + '[?11n'); - break; - case 25: - // dont support user defined keys - // this.handler(C0.ESC + '[?21n'); - break; - case 26: - // north american keyboard - // this.handler(C0.ESC + '[?27;1;0;0n'); - break; - case 53: - // no dec locator/mouse - // this.handler(C0.ESC + '[?50n'); - break; - } + public deviceStatus(params: IParams): void { + switch (params.params[0]) { + case 5: + // status report + this._coreService.triggerDataEvent(`${C0.ESC}[0n`); + break; + case 6: + // cursor position + const y = this._bufferService.buffer.y + 1; + const x = this._bufferService.buffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); + break; + } + } + public deviceStatusPrivate(params: IParams): void { + // modern xterm doesnt seem to + // respond to any of these except ?6, 6, and 5 + switch (params.params[0]) { + case 6: + // cursor position + const y = this._bufferService.buffer.y + 1; + const x = this._bufferService.buffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); + break; + case 15: + // no printer + // this.handler(C0.ESC + '[?11n'); + break; + case 25: + // dont support user defined keys + // this.handler(C0.ESC + '[?21n'); + break; + case 26: + // north american keyboard + // this.handler(C0.ESC + '[?27;1;0;0n'); + break; + case 53: + // no dec locator/mouse + // this.handler(C0.ESC + '[?50n'); + break; } } @@ -1864,25 +1865,23 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: IParams, collect?: string): void { - if (collect === '!') { - this._terminal.cursorHidden = false; - this._terminal.insertMode = false; - this._terminal.originMode = false; - this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false - this._terminal.applicationKeypad = false; // ? - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } - this._coreService.decPrivateModes.applicationCursorKeys = false; - this._bufferService.buffer.scrollTop = 0; - this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; - this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); - this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + public softReset(params: IParams): void { + this._terminal.cursorHidden = false; + this._terminal.insertMode = false; + this._terminal.originMode = false; + this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false + this._terminal.applicationKeypad = false; // ? + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); } + this._coreService.decPrivateModes.applicationCursorKeys = false; + this._bufferService.buffer.scrollTop = 0; + this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; + this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? + this._terminal.charset = null; + this._terminal.glevel = 0; // ?? + this._terminal.charsets = [null]; // ?? } /** @@ -1895,40 +1894,32 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: IParams, collect?: string): void { - if (collect === ' ') { - const param = params.params[0] || 1; - switch (param) { - case 1: - case 2: - this._optionsService.options.cursorStyle = 'block'; - break; - case 3: - case 4: - this._optionsService.options.cursorStyle = 'underline'; - break; - case 5: - case 6: - this._optionsService.options.cursorStyle = 'bar'; - break; - } - const isBlinking = param % 2 === 1; - this._optionsService.options.cursorBlink = isBlinking; + public setCursorStyle(params: IParams): void { + const param = params.params[0] || 1; + switch (param) { + case 1: + case 2: + this._optionsService.options.cursorStyle = 'block'; + break; + case 3: + case 4: + this._optionsService.options.cursorStyle = 'underline'; + break; + case 5: + case 6: + this._optionsService.options.cursorStyle = 'bar'; + break; } + const isBlinking = param % 2 === 1; + this._optionsService.options.cursorBlink = isBlinking; } /** * CSI Ps ; Ps r * Set Scrolling Region [top;bottom] (default = full size of win- * dow) (DECSTBM). - * CSI ? Pm r - * currently skipped */ - public setScrollRegion(params: IParams, collect?: string): void { - if (collect) { - return; - } - + public setScrollRegion(params: IParams): void { const top = params.params[0] || 1; let bottom: number; diff --git a/src/Terminal.ts b/src/Terminal.ts index b0dedf33..8dd684a2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -55,7 +55,7 @@ import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; import { LogService } from 'common/services/LogService'; import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; @@ -1395,18 +1395,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._inputHandler.addEscHandler(collectAndFlag, handler); + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._inputHandler.addEscHandler(id, callback); } /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ - public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { - return this._inputHandler.addDcsHandler(collectAndFlag, callback); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(id, callback); } /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { - return this._inputHandler.addCsiHandler(flag, callback); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(id, callback); } /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 6552c3e3..7d03126d 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -15,7 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { @@ -74,13 +74,13 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { throw new Error('Method not implemented.'); } - addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { throw new Error('Method not implemented.'); } - addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { throw new Error('Method not implemented.'); } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { diff --git a/src/Types.d.ts b/src/Types.d.ts index e6e540a3..e3a9771d 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -9,7 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -113,7 +113,8 @@ export interface IInputHandler { /** CSI ` */ charPosAbsolute(params: IParams): void; /** CSI a */ hPositionRelative(params: IParams): void; /** CSI b */ repeatPrecedingCharacter(params: IParams): void; - /** CSI c */ sendDeviceAttributes(params: IParams, collect?: string): void; + /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void; + sendDeviceAttributesSecondary(params: IParams): void; /** CSI d */ linePosAbsolute(params: IParams): void; /** CSI e */ vPositionRelative(params: IParams): void; /** CSI f */ hVPosition(params: IParams): void; @@ -200,9 +201,9 @@ export interface IPublicTerminal extends IDisposable { writeln(data: string): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; - addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable; - addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index 172f51bc..3ef42a68 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; import { DcsParser, DcsHandlerFactory } from 'common/parser/DcsParser'; -import { IDcsHandler, IParams } from 'common/parser/Types'; +import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -16,10 +16,47 @@ function toUtf32(s: string): Uint32Array { return utf32.subarray(0, length); } +function identifier(id: IFunctionIdentifier): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (0x40 > finalCode || finalCode > 0x7e) { + throw new Error('final must be in range 0x40 .. 0x7e'); + } + res <<= 8; + res |= finalCode; + + return res; +} + class TestHandler implements IDcsHandler { constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} - hook(collect: string, params: IParams, flag: number): void { - this.output.push([this.msg, 'HOOK', params.toArray(), collect, flag]); + hook(params: IParams): void { + this.output.push([this.msg, 'HOOK', params.toArray()]); } put(data: Uint32Array, start: number, end: number): void { this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); @@ -39,16 +76,16 @@ describe('DcsParser', () => { reports = []; parser = new DcsParser(); parser.setDcsHandlerFallback((id, action, data) => { - if (data.params) { - data.params = data.params.toArray(); + if (action === 'HOOK') { + data = data.toArray(); } reports.push([id, action, data]); }); }); describe('handler registration', () => { it('setDcsHandler', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th')); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); @@ -56,16 +93,16 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [ // messages from TestHandler - ['th', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th', 'HOOK', [1, 2, 3]], ['th', 'PUT', 'Here comes'], ['th', 'PUT', 'the mouse!'], ['th', 'UNHOOK', true] ]); }); it('clearDcsHandler', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th')); - parser.clearDcsHandler('+p'); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.clearDcsHandler(identifier({intermediates: '+', final: 'p'})); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); @@ -73,24 +110,24 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [ // messages from fallback handler - ['+p', 'HOOK', {collect: '+', params: [1, 2, 3], flag: 'p'.charCodeAt(0)}], - ['+p', 'PUT', 'Here comes'], - ['+p', 'PUT', 'the mouse!'], - ['+p', 'UNHOOK', true] + [identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]], + [identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'], + [identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'], + [identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true] ]); }); it('addDcsHandler', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); - parser.addDcsHandler('+p', new TestHandler(reports, 'th2')); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); parser.put(data, 0, data.length); parser.unhook(true); assert.deepEqual(reports, [ - ['th2', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], - ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th2', 'HOOK', [1, 2, 3]], + ['th1', 'HOOK', [1, 2, 3]], ['th2', 'PUT', 'Here comes'], ['th1', 'PUT', 'Here comes'], ['th2', 'PUT', 'the mouse!'], @@ -100,17 +137,17 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler with return false', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); - parser.addDcsHandler('+p', new TestHandler(reports, 'th2', true)); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); parser.put(data, 0, data.length); parser.unhook(true); assert.deepEqual(reports, [ - ['th2', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], - ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th2', 'HOOK', [1, 2, 3]], + ['th1', 'HOOK', [1, 2, 3]], ['th2', 'PUT', 'Here comes'], ['th1', 'PUT', 'Here comes'], ['th2', 'PUT', 'the mouse!'], @@ -120,17 +157,17 @@ describe('DcsParser', () => { ]); }); it('dispose handlers', () => { - parser.setDcsHandler('+p', new TestHandler(reports, 'th1')); - const dispo = parser.addDcsHandler('+p', new TestHandler(reports, 'th2', true)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); dispo.dispose(); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32('the mouse!'); parser.put(data, 0, data.length); parser.unhook(true); assert.deepEqual(reports, [ - ['th1', 'HOOK', [1, 2, 3], '+', 'p'.charCodeAt(0)], + ['th1', 'HOOK', [1, 2, 3]], ['th1', 'PUT', 'Here comes'], ['th1', 'PUT', 'the mouse!'], ['th1', 'UNHOOK', true] @@ -139,8 +176,8 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -149,8 +186,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -159,9 +196,9 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); - const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['two', params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -169,7 +206,7 @@ describe('DcsParser', () => { parser.unhook(true); assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); dispo.dispose(); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); data = toUtf32('some other'); parser.put(data, 0, data.length); data = toUtf32(' data'); @@ -178,9 +215,9 @@ describe('DcsParser', () => { assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); it('should respect return false', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); - parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { reports.push(['two', params.toArray(), data]); return false; })); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); data = toUtf32(' the mouse!'); @@ -190,8 +227,8 @@ describe('DcsParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); const data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { parser.put(data, 0, data.length); @@ -201,8 +238,8 @@ describe('DcsParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); - parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { parser.put(data, 0, data.length); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index f89edcfc..a22ca696 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -13,7 +13,7 @@ import { PAYLOAD_LIMIT } from 'common/parser/Constants'; export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); private _active: IDcsHandler[] = []; - private _collectAndFlag: string = ''; + private _ident: number = 0; private _handlerFb: DcsFallbackHandler = () => {}; public dispose(): void { @@ -21,11 +21,11 @@ export class DcsParser implements IDcsParser { this._handlerFb = () => {}; } - public addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { - if (this._handlers[collectAndFlag] === undefined) { - this._handlers[collectAndFlag] = []; + public addDcsHandler(ident: number, handler: IDcsHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; } - const handlerList = this._handlers[collectAndFlag]; + const handlerList = this._handlers[ident]; handlerList.push(handler); return { dispose: () => { @@ -37,12 +37,12 @@ export class DcsParser implements IDcsParser { }; } - public setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._handlers[collectAndFlag] = [handler]; + public setDcsHandler(ident: number, handler: IDcsHandler): void { + this._handlers[ident] = [handler]; } - public clearDcsHandler(collectAndFlag: string): void { - if (this._handlers[collectAndFlag]) delete this._handlers[collectAndFlag]; + public clearDcsHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; } public setDcsHandlerFallback(handler: DcsFallbackHandler): void { @@ -54,24 +54,24 @@ export class DcsParser implements IDcsParser { this.unhook(false); } this._active = []; - this._collectAndFlag = ''; + this._ident = 0; } - public hook(collect: string, params: IParams, flag: number): void { - this._collectAndFlag = collect + String.fromCharCode(flag); - this._active = this._handlers[this._collectAndFlag] || []; + public hook(ident: number, params: IParams): void { + this._ident = ident; + this._active = this._handlers[ident] || []; if (!this._active.length) { - this._handlerFb(this._collectAndFlag, 'HOOK', {collect, params, flag}); + this._handlerFb(this._ident, 'HOOK', params); } else { for (let j = this._active.length - 1; j >= 0; j--) { - this._active[j].hook(collect, params, flag); + this._active[j].hook(params); } } } public put(data: Uint32Array, start: number, end: number): void { if (!this._active.length) { - this._handlerFb(this._collectAndFlag, 'PUT', utf32ToString(data, start, end)); + this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end)); } else { for (let j = this._active.length - 1; j >= 0; j--) { this._active[j].put(data, start, end); @@ -81,7 +81,7 @@ export class DcsParser implements IDcsParser { public unhook(success: boolean): void { if (!this._active.length) { - this._handlerFb(this._collectAndFlag, 'UNHOOK', success); + this._handlerFb(this._ident, 'UNHOOK', success); } else { let j = this._active.length - 1; for (; j >= 0; j--) { @@ -96,7 +96,7 @@ export class DcsParser implements IDcsParser { } } this._active = []; - this._collectAndFlag = ''; + this._ident = 0; } } @@ -109,9 +109,9 @@ export class DcsHandlerFactory implements IDcsHandler { private _params: IParams | undefined; private _hitLimit: boolean = false; - constructor(private _handler: (params: IParams, data: string) => any) {} + constructor(private _handler: (data: string, params: IParams) => any) {} - public hook(collect: string, params: IParams, flag: number): void { + public hook(params: IParams): void { this._params = params.clone(); this._data = ''; this._hitLimit = false; @@ -133,7 +133,7 @@ export class DcsHandlerFactory implements IDcsHandler { if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._params ? this._params : new Params(), this._data); + ret = this._handler(this._data, this._params ? this._params : new Params()); } this._params = undefined; this._data = ''; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 6b14e41b..a5bcbfb7 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -73,10 +73,14 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { return this._params; } public get collect(): string { - return this._collect; + return this.identToString(this._collect); } public set collect(value: string) { - this._collect = value; + this._collect = 0; + for (let i = 0; i < value.length; ++i) { + this._collect <<= 8; + this._collect |= value.charCodeAt(i); + } } public mockOscParser(): void { this._oscParser = oscPutParser; @@ -111,8 +115,8 @@ const testTerminal: any = { actionESC: function (collect: string, flag: string): void { this.calls.push(['esc', collect, flag]); }, - actionDCSHook: function (collect: string, params: IParams, flag: string): void { - this.calls.push(['dcs hook', collect, params.toArray(), flag]); + actionDCSHook: function (params: IParams): void { + this.calls.push(['dcs hook', params.toArray()]); }, actionDCSPrint: function (s: string): void { this.calls.push(['dcs put', s]); @@ -144,11 +148,13 @@ let state: any; const testParser = new TestEscapeSequenceParser(); testParser.mockOscParser(); testParser.setPrintHandler(testTerminal.print.bind(testTerminal)); -testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => { - testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +testParser.setCsiHandlerFallback((ident: number, params: IParams) => { + const id = testParser.identToString(ident); + testTerminal.actionCSI(id.slice(0, -1), params, id.slice(-1)); }); -testParser.setEscHandlerFallback((collect: string, flag: number) => { - testTerminal.actionESC(collect, String.fromCharCode(flag)); +testParser.setEscHandlerFallback((ident: number) => { + const id = testParser.identToString(ident); + testTerminal.actionESC(id.slice(0, -1), id.slice(-1)); }); testParser.setExecuteHandlerFallback((code: number) => { testTerminal.actionExecute(String.fromCharCode(code)); @@ -160,7 +166,7 @@ testParser.setOscHandlerFallback((identifier, action, data) => { testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => { switch (action) { case 'HOOK': - testTerminal.actionDCSHook(payload.collect, payload.params, String.fromCharCode(payload.flag)); + testTerminal.actionDCSHook(payload); break; case 'PUT': testTerminal.actionDCSPrint(payload); @@ -939,7 +945,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_ENTRY; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -952,7 +958,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_PARAM; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -965,7 +971,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } @@ -1024,14 +1030,14 @@ describe('EscapeSequenceParser', function (): void { }); it('single DCS', function (): void { test('\x1bP1;2;3+$aäbc;däe\x9c', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'äbc;däe'], ['dcs unhook'] ], null); }); it('multi DCS', function (): void { test('\x1bP1;2;3+$abc;de', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'] ], null); testTerminal.clear(); @@ -1043,7 +1049,7 @@ describe('EscapeSequenceParser', function (): void { it('print + DCS(C1)', function (): void { test('abc\x901;2;3+$abc;de\x9c', [ ['print', 'abc'], - ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'], ['dcs unhook'] ], null); @@ -1086,7 +1092,7 @@ describe('EscapeSequenceParser', function (): void { it('colon notation in DCS params', function (): void { test('abc\x901;2::55;3+$abc;de\x9c', [ ['print', 'abc'], - ['dcs hook', '+$', [1, 2, [-1, 55], 3], 'a'], + ['dcs hook', [1, 2, [-1, 55], 3]], ['dcs put', 'bc;de'], ['dcs unhook'] ], null); @@ -1120,7 +1126,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = ParserState.DCS_PASSTHROUGH; parse(parser, '\x901;2;3+$a€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); + testTerminal.compare([['dcs hook', [1, 2, 3]], ['dcs put', '€öäü']]); parser.reset(); testTerminal.clear(); }); @@ -1172,69 +1178,69 @@ describe('EscapeSequenceParser', function (): void { chai.expect(print).equal(''); }); it('ESC handler', function (): void { - parser2.setEscHandler('%G', function (): void { + parser2.setEscHandler({intermediates: '%', final: 'G'}, function (): void { esc.push('%G'); }); - parser2.setEscHandler('E', function (): void { + parser2.setEscHandler({final: 'E'}, function (): void { esc.push('E'); }); parse(parser2, INPUT); chai.expect(esc).eql(['%G', 'E']); - parser2.clearEscHandler('%G'); - parser2.clearEscHandler('%G'); // should not throw + parser2.clearEscHandler({intermediates: '%', final: 'G'}); + parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw clearAccu(); parse(parser2, INPUT); chai.expect(esc).eql(['E']); - parser2.clearEscHandler('E'); + parser2.clearEscHandler({final: 'E'}); clearAccu(); parse(parser2, INPUT); chai.expect(esc).eql([]); }); describe('ESC custom handlers', () => { it('prevent fallback', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G']); }); it('allow fallback', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return false; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G', 'default - %G']); }); it('Multiple custom handlers fallback once', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); - parser2.addEscHandler('%G', () => { esc.push('custom2 - %G'); return false; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom2 - %G', 'custom - %G']); }); it('Multiple custom handlers no fallback', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); - parser2.addEscHandler('%G', () => { esc.push('custom2 - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom2 - %G']); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setEscHandler('%G', () => order.push(1)); - parser2.addEscHandler('%G', () => { order.push(2); return false; }); - parser2.addEscHandler('%G', () => { order.push(3); return false; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => order.push(1)); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; }); + parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; }); parse(parser2, '\x1b%G'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - const dispo = parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); parse(parser2, INPUT); chai.expect(esc).eql(['default - %G']); }); it('Should not corrupt the parser when dispose is called twice', () => { - parser2.setEscHandler('%G', () => esc.push('default - %G')); - const dispo = parser2.addEscHandler('%G', () => { esc.push('custom - %G'); return true; }); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); dispo.dispose(); parse(parser2, INPUT); @@ -1242,13 +1248,13 @@ describe('EscapeSequenceParser', function (): void { }); }); it('CSI handler', function (): void { - parser2.setCsiHandler('m', function (params: IParams, collect: string): void { - csi.push(['m', params.toArray(), collect]); + parser2.setCsiHandler({final: 'm'}, function (params: IParams): void { + csi.push(['m', params.toArray(), '']); }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - parser2.clearCsiHandler('m'); - parser2.clearCsiHandler('m'); // should not throw + parser2.clearCsiHandler({final: 'm'}); + parser2.clearCsiHandler({final: 'm'}); // should not throw clearAccu(); parse(parser2, INPUT); chai.expect(csi).eql([]); @@ -1256,16 +1262,16 @@ describe('EscapeSequenceParser', function (): void { describe('CSI custom handlers', () => { it('Prevent fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); it('Allow fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return false; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); 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], '']]); @@ -1273,9 +1279,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return false; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1284,9 +1290,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([], 'Should not fallback once'); @@ -1294,16 +1300,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setCsiHandler('m', () => order.push(1)); - parser2.addCsiHandler('m', () => { order.push(2); return false; }); - parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parser2.setCsiHandler({final: 'm'}, () => order.push(1)); + parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; }); + parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); @@ -1311,8 +1317,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; }); + parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1415,9 +1421,9 @@ describe('EscapeSequenceParser', function (): void { }); }); it('DCS handler', function (): void { - parser2.setDcsHandler('+p', { - hook: function (collect: string, params: IParams, flag: number): void { - dcs.push(['hook', collect, params.toArray(), flag]); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, { + hook: function (params: IParams): void { + dcs.push(['hook', '', params.toArray(), 0]); }, put: function (data: Uint32Array, start: number, end: number): void { let s = ''; @@ -1433,12 +1439,12 @@ describe('EscapeSequenceParser', function (): void { parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); chai.expect(dcs).eql([ - ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], + ['hook', '', [1, 2, 3], 0], ['put', 'abc'], ['put', ';de'], ['unhook'] ]); - parser2.clearDcsHandler('+p'); - parser2.clearDcsHandler('+p'); // should not throw + parser2.clearDcsHandler({intermediates: '+', final: 'p'}); + parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw clearAccu(); parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); @@ -1448,54 +1454,54 @@ describe('EscapeSequenceParser', function (): void { const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; it('Prevent fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]); }); it('Allow fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers fallback once', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers no fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory(() => order.push(1))); - parser2.addDcsHandler('+p', new DcsHandlerFactory(() => { order.push(2); return false; })); - parser2.addDcsHandler('+p', new DcsHandlerFactory(() => { order.push(3); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => order.push(1))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(2); return false; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(3); return false; })); parse(parser2, DCS_INPUT); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); }); it('Should not corrupt the parser when dispose is called twice', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler('+p', new DcsHandlerFactory((params, data) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler('+p', new DcsHandlerFactory((params, data) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); dispo.dispose(); parse(parser2, DCS_INPUT); @@ -1513,8 +1519,7 @@ describe('EscapeSequenceParser', function (): void { position: 6, code: '€'.charCodeAt(0), currentState: ParserState.CSI_PARAM, - osc: '', - collect: '', + collect: 0, params: Params.fromArray([1, 2, 0]), // extra zero here abort: false }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 4d30a549..8342de4c 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler, IFunctionIdentifier } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -206,17 +206,25 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { * To implement custom ANSI compliant escape sequences it is not needed to * alter this parser, instead consider registering a custom handler. * For non ANSI compliant sequences change the transition table with - * the optional `transitions` contructor argument and + * the optional `transitions` constructor argument and * reimplement the `parse` method. * * This parser is currently hardcoded to operate in ZDM (Zero Default Mode) * as suggested by the original parser, thus empty parameters are set to 0. - * This this is not in line with the latest ECMA specification + * This this is not in line with the latest ECMA-48 specification * (ZDM was part of the early specs and got completely removed later on). * * Other than the original parser from vt100.net this parser supports * sub parameters in digital parameters separated by colons. Empty sub parameters - * are set to -1. + * are set to -1 (no ZDM for sub parameters). + * + * About prefix and intermediate bytes: + * This parser follows the assumptions of the vt100.net parser with these restrictions: + * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f + * - max. two intermediates are respected, byte range 0x20 .. 0x2f + * Note that this is not in line with ECMA-48 which does not limit either of those. + * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently + * there are no known sequences that follow the broader definition of the specification. * * TODO: implement error recovery hook via error handler return values */ @@ -227,7 +235,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // buffers over several parse calls protected _params: Params; - protected _collect: string; + protected _collect: number; // handler lookup containers protected _printHandler: (data: Uint32Array, start: number, end: number) => void; @@ -241,8 +249,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // fallback handlers protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlerFb: (code: number) => void; - protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void; - protected _escHandlerFb: (collect: string, flag: number) => void; + protected _csiHandlerFb: (ident: number, params: IParams) => void; + protected _escHandlerFb: (ident: number) => void; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -252,14 +260,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.currentState = this.initialState; this._params = new Params(); // defaults to 32 storable params/subparams this._params.addParam(0); // ZDM - this._collect = ''; + this._collect = 0; this.precedingCodepoint = 0; // set default fallback handlers and handler lookup containers this._printHandlerFb = (data, start, end): void => { }; this._executeHandlerFb = (code: number): void => { }; - this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { }; - this._escHandlerFb = (collect: string, flag: number): void => { }; + this._csiHandlerFb = (ident: number, params: IParams): void => { }; + this._escHandlerFb = (ident: number): void => { }; this._errorHandlerFb = (state: IParsingState): IParsingState => state; this._printHandler = this._printHandlerFb; this._executeHandlers = Object.create(null); @@ -270,7 +278,53 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) - this.setEscHandler('\\', () => {}); + this.setEscHandler({final: '\\'}, () => {}); + } + + private _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (finalRange[0] > finalCode || finalCode > finalRange[1]) { + throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`); + } + res <<= 8; + res |= finalCode; + + return res; + } + + public identToString(ident: number): string { + const res: string[] = []; + while (ident) { + res.push(String.fromCharCode(ident & 0xFF)); + ident >>= 8; + } + return res.reverse().join(''); } public dispose(): void { @@ -297,12 +351,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - addCsiHandler(flag: string, callback: CsiHandler): IDisposable { - const index = flag.charCodeAt(0); - if (this._csiHandlers[index] === undefined) { - this._csiHandlers[index] = []; + addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { + const ident = this._identifier(id); + if (this._csiHandlers[ident] === undefined) { + this._csiHandlers[ident] = []; } - const handlerList = this._csiHandlers[index]; + const handlerList = this._csiHandlers[ident]; handlerList.push(callback); return { dispose: () => { @@ -313,21 +367,22 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = [callback]; + setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { + this._csiHandlers[this._identifier(id)] = [callback]; } - clearCsiHandler(flag: string): void { - if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; + clearCsiHandler(id: IFunctionIdentifier): void { + if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; } - setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void { + setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { this._csiHandlerFb = callback; } - addEscHandler(collectAndFlag: string, callback: EscHandler): IDisposable { - if (this._escHandlers[collectAndFlag] === undefined) { - this._escHandlers[collectAndFlag] = []; + addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { + const ident = this._identifier(id, [0x30, 0x7e]); + if (this._escHandlers[ident] === undefined) { + this._escHandlers[ident] = []; } - const handlerList = this._escHandlers[collectAndFlag]; + const handlerList = this._escHandlers[ident]; handlerList.push(callback); return { dispose: () => { @@ -338,13 +393,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setEscHandler(collectAndFlag: string, callback: () => void): void { - this._escHandlers[collectAndFlag] = [callback]; + setEscHandler(id: IFunctionIdentifier, callback: () => void): void { + this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [callback]; } - clearEscHandler(collectAndFlag: string): void { - if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag]; + clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } - setEscHandlerFallback(callback: (collect: string, flag: number) => void): void { + setEscHandlerFallback(callback: (ident: number) => void): void { this._escHandlerFb = callback; } @@ -361,14 +416,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._oscParser.setOscHandlerFallback(handler); } - addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { - return this._dcsParser.addDcsHandler(collectAndFlag, handler); + addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.addDcsHandler(this._identifier(id), handler); } - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._dcsParser.setDcsHandler(collectAndFlag, handler); + setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { + this._dcsParser.setDcsHandler(this._identifier(id), handler); } - clearDcsHandler(collectAndFlag: string): void { - this._dcsParser.clearDcsHandler(collectAndFlag); + clearDcsHandler(id: IFunctionIdentifier): void { + this._dcsParser.clearDcsHandler(this._identifier(id)); } setDcsHandlerFallback(handler: DcsFallbackHandler): void { this._dcsParser.setDcsHandlerFallback(handler); @@ -387,11 +442,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM - this._collect = ''; + this._collect = 0; // this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } + + /** * Parse UTF32 codepoints in `data` up to `length`. * @@ -415,7 +472,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - // let dcsHandler: IDcsHandler = this._activeDcsHandler; let callback: Function | null = null; // process input string @@ -465,7 +521,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP position: i, code, currentState, - osc: '', // FIXME: what to send here? collect, params, abort: false @@ -475,16 +530,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.CSI_DISPATCH: // Trigger CSI Handler - const handlers = this._csiHandlers[code]; + const handlers = this._csiHandlers[collect << 8 | code]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { // undefined or true means success and to stop bubbling - if (handlers[j](params, collect) !== false) { + if (handlers[j](params) !== false) { break; } } if (j < 0) { - this._csiHandlerFb(collect, params, code); + this._csiHandlerFb(collect << 8 | code, params); } this.precedingCodepoint = 0; break; @@ -509,10 +564,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP i--; break; case ParserAction.COLLECT: - collect += String.fromCharCode(code); + collect |= code; break; case ParserAction.ESC_DISPATCH: - const handlersEsc = this._escHandlers[collect + String.fromCharCode(code)]; + const handlersEsc = this._escHandlers[collect << 8 | code]; let jj = handlersEsc ? handlersEsc.length - 1 : -1; for (; jj >= 0; jj--) { // undefined or true means success and to stop bubbling @@ -521,7 +576,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } if (jj < 0) { - this._escHandlerFb(collect, code); + this._escHandlerFb(collect << 8 | code); } this.precedingCodepoint = 0; break; @@ -529,10 +584,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc.reset(); params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; break; case ParserAction.DCS_HOOK: - dcs.hook(collect, params, code); + dcs.hook(collect << 8 | code, params); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f @@ -551,7 +606,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc.reset(); params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; this.precedingCodepoint = 0; break; case ParserAction.OSC_START: @@ -573,7 +628,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc.reset(); params.reset(); params.addParam(0); // ZDM - collect = ''; + collect = 0; this.precedingCodepoint = 0; break; } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 4ae7ebcc..b237cd10 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -53,10 +53,8 @@ export interface IParsingState { code: number; // current parser state currentState: ParserState; - // osc string buffer - osc: string; // collect buffer with intermediate characters - collect: string; + collect: number; // params buffer params: IParams; // should abort (default: false) @@ -67,7 +65,7 @@ export interface IHandlerCollection { [key: string]: T[]; } -export type CsiHandler = (params: IParams, collect: string) => boolean | void; +export type CsiHandler = (params: IParams) => boolean | void; export type EscHandler = () => boolean | void; /** @@ -93,12 +91,12 @@ export type EscHandler = () => boolean | void; * indicates whether the command was aborted. */ export interface IDcsHandler { - hook(collect: string, params: IParams, flag: number): void; + hook(params: IParams): void; put(data: Uint32Array, start: number, end: number): void; unhook(success: boolean): void | boolean; } -export type DcsFallbackHandler = (collectAndFlag: string, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; +export type DcsFallbackHandler = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; export interface IOscHandler { /** @@ -145,6 +143,11 @@ export interface IEscapeSequenceParser extends IDisposable { */ parse(data: Uint32Array, length: number): void; + /** + * Get string from ident number. + */ + identToString(ident: number): string; + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; clearPrintHandler(): void; @@ -152,25 +155,25 @@ export interface IEscapeSequenceParser extends IDisposable { clearExecuteHandler(flag: string): void; setExecuteHandlerFallback(callback: (code: number) => void): void; - setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void; - clearCsiHandler(flag: string): void; - setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void; - addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; + setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void; + clearCsiHandler(id: IFunctionIdentifier): void; + setCsiHandlerFallback(callback: (identifier: number, params: IParams) => void): void; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - setEscHandler(collectAndFlag: string, callback: () => void): void; - clearEscHandler(collectAndFlag: string): void; - setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; - addEscHandler(collectAndFlag: string, handler: EscHandler): IDisposable; + setEscHandler(id: IFunctionIdentifier, callback: () => void): void; + clearEscHandler(id: IFunctionIdentifier): void; + setEscHandlerFallback(callback: (identifier: number) => void): void; + addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable; setOscHandler(ident: number, handler: IOscHandler): void; clearOscHandler(ident: number): void; setOscHandlerFallback(handler: OscFallbackHandler): void; addOscHandler(ident: number, handler: IOscHandler): IDisposable; - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; - clearDcsHandler(collectAndFlag: string): void; + setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; + clearDcsHandler(id: IFunctionIdentifier): void; setDcsHandlerFallback(handler: DcsFallbackHandler): void; - addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; + addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; setErrorHandler(callback: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; @@ -188,12 +191,18 @@ export interface IOscParser extends IDisposable { } export interface IDcsParser extends IDisposable { - addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; - setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; - clearDcsHandler(collectAndFlag: string): void; + addDcsHandler(ident: number, handler: IDcsHandler): IDisposable; + setDcsHandler(ident: number, handler: IDcsHandler): void; + clearDcsHandler(ident: number): void; setDcsHandlerFallback(handler: DcsFallbackHandler): void; reset(): void; - hook(collect: string, params: IParams, flag: number): void; + hook(ident: number, params: IParams): void; put(data: Uint32Array, start: number, end: number): void; unhook(success: boolean): void; } + +export interface IFunctionIdentifier { + prefix?: string; + intermediates?: string; + final: string; +} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index c9513f53..4ea65c10 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -11,7 +11,7 @@ import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; -import { IParams } from 'common/parser/Types'; +import { IParams, IFunctionIdentifier } from 'common/parser/Types'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -57,14 +57,14 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable { - return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect)); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); } - public addDcsHandler(collectAndFlag: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable { - return this._core.addDcsHandler(collectAndFlag, (params: IParams, data: string) => callback(params.toArray(), data)); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); } - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._core.addEscHandler(collectAndFlag, handler); + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + return this._core.addEscHandler(id, handler); } public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { return this._core.addOscHandler(ident, callback); diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index fe43617d..4b784642 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -340,7 +340,7 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate(` window.term.reset(); const _customCsiHandlerParams = []; - const _customCsiHandler = window.term.addCsiHandler('m', (params, collect) => { + const _customCsiHandler = window.term.addCsiHandler({final: 'm'}, (params, collect) => { _customCsiHandlerParams.push(params); return false; }, ''); @@ -356,15 +356,15 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate(` window.term.reset(); const _customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.addDcsHandler('+p', (params, data) => { + const _customDcsHandlerA = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { _customDcsHandlerCallStack.push(['A', params, data]); return false; }); - const _customDcsHandlerB = window.term.addDcsHandler('+p', (params, data) => { + const _customDcsHandlerB = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { _customDcsHandlerCallStack.push(['B', params, data]); return true; }); - const _customDcsHandlerC = window.term.addDcsHandler('+p', (params, data) => { + const _customDcsHandlerC = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { _customDcsHandlerCallStack.push(['C', params, data]); return false; }); @@ -380,15 +380,15 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate(` window.term.reset(); const _customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.addEscHandler('(B', () => { + const _customEscHandlerA = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { _customEscHandlerCallStack.push('A'); return false; }); - const _customEscHandlerB = window.term.addEscHandler('(B', () => { + const _customEscHandlerB = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { _customEscHandlerCallStack.push('B'); return true; }); - const _customEscHandlerC = window.term.addEscHandler('(B', () => { + const _customEscHandlerC = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { _customEscHandlerCallStack.push('C'); return false; }); diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index 19b135c7..a8b9fde9 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -19,7 +19,7 @@ function toUtf32(s: string): Uint32Array { } class DcsHandler implements IDcsHandler { - hook(collect: string, params: IParams, flag: number) : void {} + hook(params: IParams) : void {} put(data: Uint32Array, start: number, end: number) : void {} unhook() :void {} } @@ -32,42 +32,42 @@ perfContext('Parser throughput - 50MB data', () => { beforeEach(() => { parser = new EscapeSequenceParser(); parser.setPrintHandler((data, start, end) => {}); - parser.setCsiHandler('@', (params, collect) => {}); - parser.setCsiHandler('A', (params, collect) => {}); - parser.setCsiHandler('B', (params, collect) => {}); - parser.setCsiHandler('C', (params, collect) => {}); - parser.setCsiHandler('D', (params, collect) => {}); - parser.setCsiHandler('E', (params, collect) => {}); - parser.setCsiHandler('F', (params, collect) => {}); - parser.setCsiHandler('G', (params, collect) => {}); - parser.setCsiHandler('H', (params, collect) => {}); - parser.setCsiHandler('I', (params, collect) => {}); - parser.setCsiHandler('J', (params, collect) => {}); - parser.setCsiHandler('K', (params, collect) => {}); - parser.setCsiHandler('L', (params, collect) => {}); - parser.setCsiHandler('M', (params, collect) => {}); - parser.setCsiHandler('P', (params, collect) => {}); - parser.setCsiHandler('S', (params, collect) => {}); - parser.setCsiHandler('T', (params, collect) => {}); - parser.setCsiHandler('X', (params, collect) => {}); - parser.setCsiHandler('Z', (params, collect) => {}); - parser.setCsiHandler('`', (params, collect) => {}); - parser.setCsiHandler('a', (params, collect) => {}); - parser.setCsiHandler('b', (params, collect) => {}); - parser.setCsiHandler('c', (params, collect) => {}); - parser.setCsiHandler('d', (params, collect) => {}); - parser.setCsiHandler('e', (params, collect) => {}); - parser.setCsiHandler('f', (params, collect) => {}); - parser.setCsiHandler('g', (params, collect) => {}); - parser.setCsiHandler('h', (params, collect) => {}); - parser.setCsiHandler('l', (params, collect) => {}); - parser.setCsiHandler('m', (params, collect) => {}); - parser.setCsiHandler('n', (params, collect) => {}); - parser.setCsiHandler('p', (params, collect) => {}); - parser.setCsiHandler('q', (params, collect) => {}); - parser.setCsiHandler('r', (params, collect) => {}); - parser.setCsiHandler('s', (params, collect) => {}); - parser.setCsiHandler('u', (params, collect) => {}); + parser.setCsiHandler({final: '@'}, params => {}); + parser.setCsiHandler({final: 'A'}, params => {}); + parser.setCsiHandler({final: 'B'}, params => {}); + parser.setCsiHandler({final: 'C'}, params => {}); + parser.setCsiHandler({final: 'D'}, params => {}); + parser.setCsiHandler({final: 'E'}, params => {}); + parser.setCsiHandler({final: 'F'}, params => {}); + parser.setCsiHandler({final: 'G'}, params => {}); + parser.setCsiHandler({final: 'H'}, params => {}); + parser.setCsiHandler({final: 'I'}, params => {}); + parser.setCsiHandler({final: 'J'}, params => {}); + parser.setCsiHandler({final: 'K'}, params => {}); + parser.setCsiHandler({final: 'L'}, params => {}); + parser.setCsiHandler({final: 'M'}, params => {}); + parser.setCsiHandler({final: 'P'}, params => {}); + parser.setCsiHandler({final: 'S'}, params => {}); + parser.setCsiHandler({final: 'T'}, params => {}); + parser.setCsiHandler({final: 'X'}, params => {}); + parser.setCsiHandler({final: 'Z'}, params => {}); + parser.setCsiHandler({final: '`'}, params => {}); + parser.setCsiHandler({final: 'a'}, params => {}); + parser.setCsiHandler({final: 'b'}, params => {}); + parser.setCsiHandler({final: 'c'}, params => {}); + parser.setCsiHandler({final: 'd'}, params => {}); + parser.setCsiHandler({final: 'e'}, params => {}); + parser.setCsiHandler({final: 'f'}, params => {}); + parser.setCsiHandler({final: 'g'}, params => {}); + parser.setCsiHandler({final: 'h'}, params => {}); + parser.setCsiHandler({final: 'l'}, params => {}); + parser.setCsiHandler({final: 'm'}, params => {}); + parser.setCsiHandler({final: 'n'}, params => {}); + parser.setCsiHandler({final: 'p'}, params => {}); + parser.setCsiHandler({final: 'q'}, params => {}); + parser.setCsiHandler({final: 'r'}, params => {}); + parser.setCsiHandler({final: 's'}, params => {}); + parser.setCsiHandler({final: 'u'}, params => {}); parser.setExecuteHandler(C0.BEL, () => {}); parser.setExecuteHandler(C0.LF, () => {}); parser.setExecuteHandler(C0.VT, () => {}); @@ -82,23 +82,23 @@ perfContext('Parser throughput - 50MB data', () => { parser.setExecuteHandler(C1.HTS, () => {}); parser.setOscHandler(0, new OscHandlerFactory((data) => {})); parser.setOscHandler(2, new OscHandlerFactory((data) => {})); - parser.setEscHandler('7', () => {}); - parser.setEscHandler('8', () => {}); - parser.setEscHandler('D', () => {}); - parser.setEscHandler('E', () => {}); - parser.setEscHandler('H', () => {}); - parser.setEscHandler('M', () => {}); - parser.setEscHandler('=', () => {}); - parser.setEscHandler('>', () => {}); - parser.setEscHandler('c', () => {}); - parser.setEscHandler('n', () => {}); - parser.setEscHandler('o', () => {}); - parser.setEscHandler('|', () => {}); - parser.setEscHandler('}', () => {}); - parser.setEscHandler('~', () => {}); - parser.setEscHandler('%@', () => {}); - parser.setEscHandler('%G', () => {}); - parser.setDcsHandler('q', new DcsHandler()); + parser.setEscHandler({final: '7'}, () => {}); + parser.setEscHandler({final: '8'}, () => {}); + parser.setEscHandler({final: 'D'}, () => {}); + parser.setEscHandler({final: 'E'}, () => {}); + parser.setEscHandler({final: 'H'}, () => {}); + parser.setEscHandler({final: 'M'}, () => {}); + parser.setEscHandler({final: '='}, () => {}); + parser.setEscHandler({final: '>'}, () => {}); + parser.setEscHandler({final: 'c'}, () => {}); + parser.setEscHandler({final: 'n'}, () => {}); + parser.setEscHandler({final: 'o'}, () => {}); + parser.setEscHandler({final: '|'}, () => {}); + parser.setEscHandler({final: '}'}, () => {}); + parser.setEscHandler({final: '~'}, () => {}); + parser.setEscHandler({intermediates: '%', final: '@'}, () => {}); + parser.setEscHandler({intermediates: '%', final: 'G'}, () => {}); + parser.setDcsHandler({final: 'q'}, new DcsHandler()); }); perfContext('PRINT - a', () => { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 806074a2..417e03a9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -502,8 +502,8 @@ declare module 'xterm' { /** * Adds a handler for CSI escape sequences. - * @param flag The flag should be one-character string, which specifies the - * final character (e.g "m" for SGR) of the CSI sequence. + * @param id Specifies the function identifier under which the callback gets registered, + * e.g. {final: 'm'} for SGR. * @param callback The function to handle the escape sequence. The callback * is called with the numerical params, as well as the special characters * (e.g. "$" for DECSCPP). If the sequence has subparams the array will @@ -513,12 +513,12 @@ declare module 'xterm' { * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; + addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; /** * Adds a handler for DCS escape sequences. - * @param collect Should be a string, which specifies the collect and the - * final character (e.g "$q" for DECRQSS) of the DCS sequence. + * @param id Specifies the function identifier under which the callback gets registered, + * e.g. {intermediates: '$' final: 'q'} for DECRQSS. * @param callback The function to handle the escape sequence. Note that the * function will only be called once if the sequence finished sucessfully. * There is currently no way to intercept smaller data chunks, those will be stored up @@ -531,20 +531,19 @@ declare module 'xterm' { * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addDcsHandler(collect: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; /** * Adds a handler for ESC escape sequences. - * @param collect Should be a string, which specifies the collect and the - * final character (e.g "%G" for default charset selection) - * of the ESC sequence. + * @param id Specifies the function identifier under which the callback gets registered, + * e.g. {intermediates: '%' final: 'G'} for default charset selection. * @param callback The function to handle the escape sequence. * Return true if the sequence was handled; false if * we should try a previous handler (set by addEscHandler or setEscHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - addEscHandler(collect: string, handler: () => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; /** * Adds a handler for OSC escape sequences. @@ -987,4 +986,22 @@ declare module 'xterm' { */ readonly width: number; } + + /** + * Data type to register a CSI, DCS or ESC callback in the parser. + */ + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e (\x30 .. \x7e for ESC). + */ + final: string; + } } From 83a1340cc4dbc44104f13828e9cf8507b45ce2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 31 Jul 2019 12:03:12 +0200 Subject: [PATCH 14/41] abort and cleanup subparsers for CAN and SUB --- .../parser/EscapeSequenceParser.test.ts | 52 ++++++++++++++----- src/common/parser/EscapeSequenceParser.ts | 6 +-- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index a5bcbfb7..5c4fe5b0 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -34,7 +34,8 @@ class MockOscPutParser implements IOscParser { } public dispose(): void { } public start(): void { } - public end(): void { + public end(success: boolean): void { + this.data += `, success: ${success}`; const id = parseInt(this.data.slice(0, this.data.indexOf(';'))); if (!isNaN(id)) { this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1)); @@ -121,8 +122,8 @@ const testTerminal: any = { actionDCSPrint: function (s: string): void { this.calls.push(['dcs put', s]); }, - actionDCSUnhook: function (): void { - this.calls.push(['dcs unhook']); + actionDCSUnhook: function (success: boolean): void { + this.calls.push(['dcs unhook', success]); } }; @@ -172,7 +173,7 @@ testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => { testTerminal.actionDCSPrint(payload); break; case 'UNHOOK': - testTerminal.actionDCSUnhook(); + testTerminal.actionDCSUnhook(payload); } }); @@ -253,7 +254,8 @@ describe('EscapeSequenceParser', function (): void { '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' ]; const exceptions: { [key: number]: { [key: string]: any[] } } = { - 8: { '\x18': [], '\x1a': [] } // simply abort osc state + 8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING + 13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] } // abort DCS_PASSTHROUGH }; parser.reset(); testTerminal.clear(); @@ -1025,14 +1027,14 @@ describe('EscapeSequenceParser', function (): void { }); it('OSC', function (): void { test('\x1b]0;abc123€öäü\x07', [ - ['osc', '0;abc123€öäü'] + ['osc', '0;abc123€öäü, success: true'] ], null); }); it('single DCS', function (): void { test('\x1bP1;2;3+$aäbc;däe\x9c', [ ['dcs hook', [1, 2, 3]], ['dcs put', 'äbc;däe'], - ['dcs unhook'] + ['dcs unhook', true] ], null); }); it('multi DCS', function (): void { @@ -1043,7 +1045,7 @@ describe('EscapeSequenceParser', function (): void { testTerminal.clear(); test('abc\x9c', [ ['dcs put', 'abc'], - ['dcs unhook'] + ['dcs unhook', true] ], true); }); it('print + DCS(C1)', function (): void { @@ -1051,7 +1053,7 @@ describe('EscapeSequenceParser', function (): void { ['print', 'abc'], ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'], - ['dcs unhook'] + ['dcs unhook', true] ], null); }); it('print + PM(C1) + print', function (): void { @@ -1063,7 +1065,7 @@ describe('EscapeSequenceParser', function (): void { it('print + OSC(C1) + print', function (): void { test('abc\x9d123;tzf\x9cdefg', [ ['print', 'abc'], - ['osc', '123;tzf'], + ['osc', '123;tzf, success: true'], ['print', 'defg'] ], null); }); @@ -1076,7 +1078,7 @@ describe('EscapeSequenceParser', function (): void { it('7bit ST should be swallowed', function (): void { test('abc\x9d123;tzf\x1b\\defg', [ ['print', 'abc'], - ['osc', '123;tzf'], + ['osc', '123;tzf, success: true'], ['print', 'defg'] ], null); }); @@ -1094,7 +1096,33 @@ describe('EscapeSequenceParser', function (): void { ['print', 'abc'], ['dcs hook', [1, 2, [-1, 55], 3]], ['dcs put', 'bc;de'], - ['dcs unhook'] + ['dcs unhook', true] + ], null); + }); + it('CAN should abort DCS', () => { + test('abc\x901;2::55;3+$abc;de\x18', [ + ['print', 'abc'], + ['dcs hook', [1, 2, [-1, 55], 3]], + ['dcs put', 'bc;de'], + ['dcs unhook', false] // false for abort + ], null); + }); + it('SUB should abort DCS', () => { + test('abc\x901;2::55;3+$abc;de\x1a', [ + ['print', 'abc'], + ['dcs hook', [1, 2, [-1, 55], 3]], + ['dcs put', 'bc;de'], + ['dcs unhook', false] // false for abort + ], null); + }); + it('CAN should abort OSC', () => { + test('\x1b]0;abc123€öäü\x18', [ + ['osc', '0;abc123€öäü, success: false'] + ], null); + }); + it('SUB should abort OSC', () => { + test('\x1b]0;abc123€öäü\x1a', [ + ['osc', '0;abc123€öäü, success: false'] ], null); }); }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 8342de4c..d40f39de 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -187,7 +187,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); - table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); // special handling of unicode chars table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); @@ -591,7 +591,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f - // unhook triggered by: 0x1b, 0x9c + // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort) for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { dcs.put(data, i, j); @@ -601,7 +601,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.DCS_UNHOOK: - dcs.unhook(true); // FIXME: apply abort vs. success exit rules + dcs.unhook(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; osc.reset(); params.reset(); From 7252a5921d05ba87de97a36e3d012de6853f0e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 31 Jul 2019 14:20:36 +0200 Subject: [PATCH 15/41] cleanup parser rules and docs --- src/InputHandler.ts | 3 +- src/common/parser/DcsParser.ts | 11 ++- .../parser/EscapeSequenceParser.test.ts | 6 -- src/common/parser/EscapeSequenceParser.ts | 59 ++++++------ src/common/parser/OscParser.ts | 10 +- typings/xterm.d.ts | 94 ++++++++++++------- 6 files changed, 102 insertions(+), 81 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 02e20961..aed32ab1 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -231,8 +231,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandler(C0.SO, () => this.shiftOut()); this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); // FIXME: What do to with missing? Old code just added those to print. - - // some C1 control codes - FIXME: should those be enabled by default? + this._parser.setExecuteHandler(C1.IND, () => this.index()); this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index a22ca696..0112e935 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -12,7 +12,8 @@ import { PAYLOAD_LIMIT } from 'common/parser/Constants'; export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); - private _active: IDcsHandler[] = []; + private _empty: IDcsHandler[] = []; + private _active: IDcsHandler[] = this._empty; private _ident: number = 0; private _handlerFb: DcsFallbackHandler = () => {}; @@ -53,13 +54,15 @@ export class DcsParser implements IDcsParser { if (this._active.length) { this.unhook(false); } - this._active = []; + this._active = this._empty; this._ident = 0; } public hook(ident: number, params: IParams): void { + // always reset leftover handlers + this.reset(); this._ident = ident; - this._active = this._handlers[ident] || []; + this._active = this._handlers[ident] || this._empty; if (!this._active.length) { this._handlerFb(this._ident, 'HOOK', params); } else { @@ -95,7 +98,7 @@ export class DcsParser implements IDcsParser { this._active[j].unhook(false); } } - this._active = []; + this._active = this._empty; this._ident = 0; } } diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 5c4fe5b0..40ce38b1 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -279,12 +279,10 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); for (state in states) { parser.currentState = state; - parser.osc = '#'; parser.params = [23]; parser.collect = '#'; parse(parser, '\x1b'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); @@ -396,24 +394,20 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); // C0 parser.currentState = ParserState.ESCAPE; - parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parse(parser, '['); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parse(parser, '\x9b'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index d40f39de..30aabc6d 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -334,24 +334,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.dispose(); } - setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { + public setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { this._printHandler = callback; } - clearPrintHandler(): void { + public clearPrintHandler(): void { this._printHandler = this._printHandlerFb; } - setExecuteHandler(flag: string, callback: () => void): void { + public setExecuteHandler(flag: string, callback: () => void): void { this._executeHandlers[flag.charCodeAt(0)] = callback; } - clearExecuteHandler(flag: string): void { + public clearExecuteHandler(flag: string): void { if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; } - setExecuteHandlerFallback(callback: (code: number) => void): void { + public setExecuteHandlerFallback(callback: (code: number) => void): void { this._executeHandlerFb = callback; } - addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { + public addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { const ident = this._identifier(id); if (this._csiHandlers[ident] === undefined) { this._csiHandlers[ident] = []; @@ -367,17 +367,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { + public setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { this._csiHandlers[this._identifier(id)] = [callback]; } - clearCsiHandler(id: IFunctionIdentifier): void { + public clearCsiHandler(id: IFunctionIdentifier): void { if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; } - setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { + public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { this._csiHandlerFb = callback; } - addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { + public addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { const ident = this._identifier(id, [0x30, 0x7e]); if (this._escHandlers[ident] === undefined) { this._escHandlers[ident] = []; @@ -393,50 +393,50 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setEscHandler(id: IFunctionIdentifier, callback: () => void): void { + public setEscHandler(id: IFunctionIdentifier, callback: () => void): void { this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [callback]; } - clearEscHandler(id: IFunctionIdentifier): void { + public clearEscHandler(id: IFunctionIdentifier): void { if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } - setEscHandlerFallback(callback: (ident: number) => void): void { + public setEscHandlerFallback(callback: (ident: number) => void): void { this._escHandlerFb = callback; } - addOscHandler(ident: number, handler: IOscHandler): IDisposable { + public addOscHandler(ident: number, handler: IOscHandler): IDisposable { return this._oscParser.addOscHandler(ident, handler); } - setOscHandler(ident: number, handler: IOscHandler): void { + public setOscHandler(ident: number, handler: IOscHandler): void { this._oscParser.setOscHandler(ident, handler); } - clearOscHandler(ident: number): void { + public clearOscHandler(ident: number): void { this._oscParser.clearOscHandler(ident); } - setOscHandlerFallback(handler: OscFallbackHandler): void { + public setOscHandlerFallback(handler: OscFallbackHandler): void { this._oscParser.setOscHandlerFallback(handler); } - addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { return this._dcsParser.addDcsHandler(this._identifier(id), handler); } - setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { + public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { this._dcsParser.setDcsHandler(this._identifier(id), handler); } - clearDcsHandler(id: IFunctionIdentifier): void { + public clearDcsHandler(id: IFunctionIdentifier): void { this._dcsParser.clearDcsHandler(this._identifier(id)); } - setDcsHandlerFallback(handler: DcsFallbackHandler): void { + public setDcsHandlerFallback(handler: DcsFallbackHandler): void { this._dcsParser.setDcsHandlerFallback(handler); } - setErrorHandler(callback: (state: IParsingState) => IParsingState): void { + public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { this._errorHandler = callback; } - clearErrorHandler(): void { + public clearErrorHandler(): void { this._errorHandler = this._errorHandlerFb; } - reset(): void { + public reset(): void { this.currentState = this.initialState; this._oscParser.reset(); this._dcsParser.reset(); @@ -463,7 +463,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * - OSC_STRING:OSC_PUT * - DCS_PASSTHROUGH:DCS_PUT */ - parse(data: Uint32Array, length: number): void { + public parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; let currentState = this.currentState; @@ -472,7 +472,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - let callback: Function | null = null; // process input string for (let i = 0; i < length; ++i) { @@ -508,8 +507,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.EXECUTE: - callback = this._executeHandlers[code]; - if (callback) callback(); + if (this._executeHandlers[code]) this._executeHandlers[code](); else this._executeHandlerFb(code); this.precedingCodepoint = 0; break; @@ -581,7 +579,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.precedingCodepoint = 0; break; case ParserAction.CLEAR: - osc.reset(); params.reset(); params.addParam(0); // ZDM collect = 0; @@ -603,7 +600,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.DCS_UNHOOK: dcs.unhook(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc.reset(); params.reset(); params.addParam(0); // ZDM collect = 0; @@ -625,7 +621,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.OSC_END: osc.end(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc.reset(); params.reset(); params.addParam(0); // ZDM collect = 0; @@ -635,7 +630,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP currentState = transition & TableAccess.TRANSITION_STATE_MASK; } - // save non pushable buffers + // save collected intermediates this._collect = collect; // save state diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index cf363714..ed549deb 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -16,7 +16,7 @@ export class OscParser extends Disposable { private _handlers: IHandlerCollection = Object.create(null); private _handlerFb: OscFallbackHandler = () => { }; - addOscHandler(ident: number, handler: IOscHandler): IDisposable { + public addOscHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -31,13 +31,13 @@ export class OscParser extends Disposable { } }; } - setOscHandler(ident: number, handler: IOscHandler): void { + public setOscHandler(ident: number, handler: IOscHandler): void { this._handlers[ident] = [handler]; } - clearOscHandler(ident: number): void { + public clearOscHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - setOscHandlerFallback(handler: OscFallbackHandler): void { + public setOscHandlerFallback(handler: OscFallbackHandler): void { this._handlerFb = handler; } @@ -100,6 +100,8 @@ export class OscParser extends Disposable { } public start(): void { + // always reset leftover handlers + this.reset(); this._id = -1; this._state = OscState.ID; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 417e03a9..3891004e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -502,14 +502,13 @@ declare module 'xterm' { /** * Adds a handler for CSI escape sequences. - * @param id Specifies the function identifier under which the callback gets registered, - * e.g. {final: 'm'} for SGR. - * @param callback The function to handle the escape sequence. The callback - * is called with the numerical params, as well as the special characters - * (e.g. "$" for DECSCPP). If the sequence has subparams the array will - * contain subarrays with their numercial values. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addCsiHandler or setCsiHandler). + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. If the sequence has subparams the + * array will contain subarrays with their numercial values. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addCsiHandler or setCsiHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ @@ -517,17 +516,18 @@ declare module 'xterm' { /** * Adds a handler for DCS escape sequences. - * @param id Specifies the function identifier under which the callback gets registered, - * e.g. {intermediates: '$' final: 'q'} for DECRQSS. - * @param callback The function to handle the escape sequence. Note that the + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, those will be stored up - * until the sequence is finished. Since DCS sequences are not limited by the amount - * of data this might impose a problem for big payloads. Currently xterm.js limits - * DCS payload to 10 MB which should give enough room for most use cases. - * The function gets numerical parameter and the data as arguments. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addDcsHandler or setDcsHandler). + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ @@ -535,11 +535,12 @@ declare module 'xterm' { /** * Adds a handler for ESC escape sequences. - * @param id Specifies the function identifier under which the callback gets registered, - * e.g. {intermediates: '%' final: 'G'} for default charset selection. - * @param callback The function to handle the escape sequence. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addEscHandler or setEscHandler). + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ @@ -548,15 +549,17 @@ declare module 'xterm' { /** * Adds a handler for OSC escape sequences. * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the escape sequence. Note that the + * @param callback The function to handle the sequence. Note that the * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, those will be stored up - * until the sequence is finished. Since OSC sequences are not limited by the amount - * of data this might impose a problem for big payloads. Currently xterm.js limits - * OSC payload to 10 MB which should give enough room for most use cases. - * The callback is called with OSC data string. Return true if the sequence was handled; - * false if we should try a previous handler (set by addOscHandler or - * setOscHandler). The most recently-added handler is tried first. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addOscHandler or setOscHandler). + * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; @@ -988,19 +991,44 @@ declare module 'xterm' { } /** - * Data type to register a CSI, DCS or ESC callback in the parser. + * Data type to register a CSI, DCS or ESC callback in the parser in the form: + * ESC I..I F + * CSI Prefix P..P I..I F + * DCS Prefix P..P I..I F data_bytes ST + * + * with these rules/restrictions: + * - prefix can only be used with CSI and DCS + * - only one leading prefix byte is recognized by the parser + * before any other parameter bytes (P..P) + * - intermediate bytes are recognized up to 2 + * + * For custom sequences make sure to read ECMA-48 and the resources at + * vt100.net to not clash with existing sequences or reserved address space. + * General recommendations: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * thus parsers might get confused with more intermediates) + * - test against other common emulators to check whether they escape/ignore + * the sequence correctly + * + * Notes: OSC command registration is handled differently (see addOscHandler) + * APC, PM or SOS is currently not supported */ export interface IFunctionIdentifier { /** * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. */ prefix?: string; /** * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. */ intermediates?: string; /** - * Final byte, must be in range \x40 .. \x7e (\x30 .. \x7e for ESC). + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. */ final: string; } From bbd7275ad727e4f9bb8963172b3c4b7d87f3306e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 31 Jul 2019 14:44:50 +0200 Subject: [PATCH 16/41] make linter happy --- src/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index aed32ab1..5e138d91 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -231,7 +231,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandler(C0.SO, () => this.shiftOut()); this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); // FIXME: What do to with missing? Old code just added those to print. - + this._parser.setExecuteHandler(C1.IND, () => this.index()); this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); From 0958f0ef912265281c4483683fac02f30d7f5a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 2 Aug 2019 15:29:26 +0200 Subject: [PATCH 17/41] fix DECSTBM with LF --- .../escape_sequence_files/t0070-DECSTBM_LF.in | 2 +- .../t0070-DECSTBM_LF.text | 44 +++++++++---------- src/InputHandler.ts | 9 +++- src/Terminal2.test.ts | 4 +- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in index 571f2233..c56b99a0 100644 --- a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in +++ b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in @@ -27,5 +27,5 @@ r s t uvwxyz - + The end. diff --git a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text index 0940b073..940bc86d 100644 --- a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text +++ b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text @@ -1,25 +1,25 @@ -1 - 2 - 6 - 7 - 8 - 9 ABC +6 +7 +8 +9 ABC DEF a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p -yz qrstu vwx +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +yz vwx The end. + diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f47085d4..0b011e34 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -412,10 +412,13 @@ export class InputHandler extends Disposable implements IInputHandler { if (wraparoundMode) { buffer.x = 0; buffer.y++; - if (buffer.y > buffer.scrollBottom) { + if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; this._terminal.scroll(true); } else { + if (buffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; + } // The line already exists (eg. the initial viewport), mark it as a // wrapped line buffer.lines.get(buffer.y).isWrapped = true; @@ -508,9 +511,11 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.x = 0; } buffer.y++; - if (buffer.y > buffer.scrollBottom) { + if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; this._terminal.scroll(); + } else if (buffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; } // If the end of the line is hit, prevent this action from wrapping around to the next line. if (buffer.x >= this._bufferService.cols) { diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index cbd790d5..05018f0f 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -17,7 +17,7 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const SKIP_FILES = [ - 't0070-DECSTBM_LF.in', // lineFeed not working correctly + // 't0070-DECSTBM_LF.in', // lineFeed not working correctly 't0071-DECSTBM_IND.in', 't0072-DECSTBM_NEL.in', 't0075-DECSTBM_CUU_CUD.in', @@ -42,7 +42,7 @@ const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slic describe('Escape Sequence Files', function(): void { - this.timeout(20000); + this.timeout(100); let ptyTerm: any; let slaveEnd: any; From 84d759150650fceb2324f6ab0180ab7fdc290580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 2 Aug 2019 21:06:16 +0200 Subject: [PATCH 18/41] increase test timeout --- src/Terminal2.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index 05018f0f..594558fb 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -42,7 +42,7 @@ const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slic describe('Escape Sequence Files', function(): void { - this.timeout(100); + this.timeout(1000); let ptyTerm: any; let slaveEnd: any; From 497a5e6e84011fbb7719b7d47a904be15fd6373f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 3 Aug 2019 15:00:30 +0200 Subject: [PATCH 19/41] fix DECSTBM with IND --- fixtures/escape_sequence_files/t0071-DECSTBM_IND.in | 2 +- fixtures/escape_sequence_files/t0071-DECSTBM_IND.text | 4 ++-- src/InputHandler.ts | 7 +++++-- src/Terminal2.test.ts | 2 -- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in index f9f6a424..a6aa1810 100644 --- a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in +++ b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in @@ -1,3 +1,3 @@ 1D2D3D4D5D6D7D8D9ABCDEFaDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDqDrDsDtDuvwxyz - + The end. diff --git a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text index f6644fb5..bc7344f9 100644 --- a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text +++ b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text @@ -1,4 +1,3 @@ - 2 6 7 8 @@ -21,5 +20,6 @@ DEF o p q -The end. rstu vwx +yz rstu vwx +The end. diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 0b011e34..f243433a 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -2048,10 +2048,13 @@ export class InputHandler extends Disposable implements IInputHandler { */ public index(): void { this._restrictCursor(); + const buffer = this._bufferService.buffer; this._bufferService.buffer.y++; - if (this._bufferService.buffer.y > this._bufferService.buffer.scrollBottom) { - this._bufferService.buffer.y--; + if (buffer.y === buffer.scrollBottom + 1) { + buffer.y--; this._terminal.scroll(); + } else if (buffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; } this._restrictCursor(); } diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index 594558fb..0638e316 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -17,8 +17,6 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const SKIP_FILES = [ - // 't0070-DECSTBM_LF.in', // lineFeed not working correctly - 't0071-DECSTBM_IND.in', 't0072-DECSTBM_NEL.in', 't0075-DECSTBM_CUU_CUD.in', 't0076-DECSTBM_IL_DL.in', // not working due to lineFeed From eccd6d7bdf822d58bc14c97706b3fe3d29f3a5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 3 Aug 2019 15:05:44 +0200 Subject: [PATCH 20/41] enable NEL test --- src/Terminal2.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index 0638e316..f87ecd3a 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -17,7 +17,6 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const SKIP_FILES = [ - 't0072-DECSTBM_NEL.in', 't0075-DECSTBM_CUU_CUD.in', 't0076-DECSTBM_IL_DL.in', // not working due to lineFeed 't0077-DECSTBM_quirks.in', From 2a533e7236f62e85632ce717f1db117b6274941f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 4 Aug 2019 09:00:07 +0200 Subject: [PATCH 21/41] map DECEL back to eraseInLine --- src/InputHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 5e138d91..83019c18 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -189,6 +189,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params)); this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params)); this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params)); + this._parser.setCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params)); this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params)); this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params)); this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params)); From 00298531ed9b1a742c8161baaadf427551e4976d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 4 Aug 2019 12:20:44 +0200 Subject: [PATCH 22/41] respect scroll margins in CUU/CUD/CPL/CNL; test files --- .../t0078-DECSTBM_CPL_CNL.in | 24 ++++++++++++++++++ .../t0078-DECSTBM_CPL_CNL.text | 25 +++++++++++++++++++ .../t0079-DECSTBM_VPR.in | 24 ++++++++++++++++++ .../t0079-DECSTBM_VPR.text | 25 +++++++++++++++++++ src/InputHandler.ts | 20 ++++++++++++--- src/Terminal2.test.ts | 1 - 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in create mode 100644 fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text create mode 100644 fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in create mode 100644 fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text diff --git a/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in new file mode 100644 index 00000000..b279f92a --- /dev/null +++ b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in @@ -0,0 +1,24 @@ +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x12 diff --git a/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text new file mode 100644 index 00000000..3acccf1a --- /dev/null +++ b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text @@ -0,0 +1,25 @@ +a +b +c +d +e +f +g +h +i +2 +k +l +m +n +o +p +q +r +1 +t +u +v +w +x + diff --git a/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in new file mode 100644 index 00000000..f818ad7f --- /dev/null +++ b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in @@ -0,0 +1,24 @@ +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x1 diff --git a/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text new file mode 100644 index 00000000..a2d66f8e --- /dev/null +++ b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text @@ -0,0 +1,25 @@ +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +1 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f243433a..746f4345 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -616,7 +616,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Up Ps Times (default = 1) (CUU). */ public cursorUp(params: IParams): void { - this._moveCursor(0, -(params.params[0] || 1)); + // stop at scrollTop + const diffToTop = this._bufferService.buffer.y - this._bufferService.buffer.scrollTop; + if (diffToTop >= 0) { + this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1)); + } else { + this._moveCursor(0, -(params.params[0] || 1)); + } } /** @@ -624,7 +630,13 @@ export class InputHandler extends Disposable implements IInputHandler { * Cursor Down Ps Times (default = 1) (CUD). */ public cursorDown(params: IParams): void { - this._moveCursor(0, params.params[0] || 1); + // stop at scrollBottom + const diffToBottom = this._bufferService.buffer.scrollBottom - this._bufferService.buffer.y; + if (diffToBottom >= 0) { + this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1)); + } else { + this._moveCursor(0, params.params[0] || 1); + } } /** @@ -649,7 +661,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Other than cursorDown (CUD) also set the cursor to first column. */ public cursorNextLine(params: IParams): void { - this._moveCursor(0, params.params[0] || 1); + this.cursorDown(params); this._bufferService.buffer.x = 0; } @@ -659,7 +671,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Other than cursorUp (CUU) also set the cursor to first column. */ public cursorPrecedingLine(params: IParams): void { - this._moveCursor(0, -(params.params[0] || 1)); + this.cursorUp(params); this._bufferService.buffer.x = 0; } diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index f87ecd3a..2462fb19 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -17,7 +17,6 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const SKIP_FILES = [ - 't0075-DECSTBM_CUU_CUD.in', 't0076-DECSTBM_IL_DL.in', // not working due to lineFeed 't0077-DECSTBM_quirks.in', 't0084-CBT.in', From 6285cbf9764e4c93ff87beec65de90dfceb3c064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 4 Aug 2019 13:46:37 +0200 Subject: [PATCH 23/41] fix IL/DL test --- .../escape_sequence_files/t0076-DECSTBM_IL_DL.text | 11 +++++------ src/Terminal2.test.ts | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text index f89893ba..92c10331 100644 --- a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text +++ b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text @@ -1,4 +1,3 @@ - 6 C 8 ^^^^ 9 vvvv DL on line 11, expected: ACD_ 10 A @@ -13,14 +12,14 @@ 19 vvvv IL on line 21, expected: A_ 20 A - 22 ^^^^ -24 A -25 B -27 vvvv DL on line 28, expected: B_ +23 vvvv IL on line 24, expected: _A +25 B +26 ^^^^ 28 A + 29 B 30 ^^^^ 31 -32 \ No newline at end of file +32 diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index 2462fb19..efb2a446 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -17,8 +17,6 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const SKIP_FILES = [ - 't0076-DECSTBM_IL_DL.in', // not working due to lineFeed - 't0077-DECSTBM_quirks.in', 't0084-CBT.in', 't0101-NLM.in', 't0103-reverse_wrap.in', From 4200e8356bbfec6314ec412941472bbb0ecf1671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 4 Aug 2019 17:07:56 +0200 Subject: [PATCH 24/41] apply BCE to SU/SD --- src/InputHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 746f4345..381671e0 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1013,7 +1013,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } @@ -1030,7 +1030,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } From 29d8493a7a085263dd11422d72410dcaf5012fd6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 7 Aug 2019 07:18:27 -0700 Subject: [PATCH 25/41] Don't throw if first click is incremental Fixes #2365 --- src/browser/services/SelectionService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index e4c703eb..90c85ecd 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -563,9 +563,10 @@ export class SelectionService implements ISelectionService { // to be sent to the pty. event.stopImmediatePropagation(); - // Something went wrong + // Do nothing if there is no selection start, this can happen if the first + // click in the terminal is an incremental click if (!this._model.selectionStart) { - throw new Error('Selection start position was not set before mousemove event'); + return; } // Record the previous position so we know whether to redraw the selection From 20fa44703310cc38237afbe515a0beed68565e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 8 Aug 2019 19:34:00 +0200 Subject: [PATCH 26/41] namespace parser related stuff --- src/public/Terminal.ts | 36 +++--- test/api/InputHandler.api.ts | 89 --------------- test/api/Parser.api.ts | 134 ++++++++++++++++++++++ typings/xterm.d.ts | 209 ++++++++++++++++++----------------- 4 files changed, 266 insertions(+), 202 deletions(-) create mode 100644 test/api/Parser.api.ts diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 4ea65c10..0a91aa74 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, Parser } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -11,7 +11,9 @@ import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; -import { IParams, IFunctionIdentifier } from 'common/parser/Types'; +import { IParams, IEscapeSequenceParser } from 'common/parser/Types'; +import { OscHandlerFactory } from 'common/parser/OscParser'; +import { DcsHandlerFactory } from '../../out/common/parser/DcsParser'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -33,6 +35,7 @@ export class Terminal implements ITerminalApi { public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } public get element(): HTMLElement { return this._core.element; } + public get parser(): Parser.IParser { return new ParserApi((this._core as any)._inputHandler._parser); } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } @@ -57,18 +60,6 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { - return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); - } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); - } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { - return this._core.addEscHandler(id, handler); - } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._core.addOscHandler(ident, callback); - } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { return this._core.registerLinkMatcher(regex, handler, options); } @@ -226,3 +217,20 @@ class BufferCellApiView implements IBufferCellApi { public get char(): string { return this._line.getString(this._x); } public get width(): number { return this._line.getWidth(this._x); } } + +class ParserApi implements Parser.IParser { + constructor(private _parser: IEscapeSequenceParser) {} + + public addCsiHandler(id: Parser.IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this._parser.addCsiHandler(id, (params: IParams) => callback(params.toArray())); + } + public addDcsHandler(id: Parser.IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this._parser.addDcsHandler(id, new DcsHandlerFactory((data: string, params: IParams) => callback(data, params.toArray()))); + } + public addEscHandler(id: Parser.IFunctionIdentifier, handler: () => boolean): IDisposable { + return this._parser.addEscHandler(id, handler); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._parser.addOscHandler(ident, new OscHandlerFactory(callback)); + } +} diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 4b784642..9ca0c943 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -334,95 +334,6 @@ describe('InputHandler Integration Tests', function(): void { }); }); }); - - describe('addCsiHandler', () => { - it('should call custom CSI handler with js array params', async () => { - await page.evaluate(` - window.term.reset(); - const _customCsiHandlerParams = []; - const _customCsiHandler = window.term.addCsiHandler({final: 'm'}, (params, collect) => { - _customCsiHandlerParams.push(params); - return false; - }, ''); - `); - await page.evaluate(` - window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - `); - assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); - }); - }); - describe('addDcsHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - const _customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - _customDcsHandlerCallStack.push(['A', params, data]); - return false; - }); - const _customDcsHandlerB = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - _customDcsHandlerCallStack.push(['B', params, data]); - return true; - }); - const _customDcsHandlerC = window.term.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - _customDcsHandlerCallStack.push(['C', params, data]); - return false; - }); - `); - await page.evaluate(` - window.term.write('\x1bP1;2+psome data\x1b\\\\'); - `); - assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); - }); - }); - describe('addEscHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - const _customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { - _customEscHandlerCallStack.push('A'); - return false; - }); - const _customEscHandlerB = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { - _customEscHandlerCallStack.push('B'); - return true; - }); - const _customEscHandlerC = window.term.addEscHandler({intermediates:'(', final: 'B'}, () => { - _customEscHandlerCallStack.push('C'); - return false; - }); - `); - await page.evaluate(` - window.term.write('\x1b(B'); - `); - assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']); - }); - }); - describe('addOscHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - const _customOscHandlerCallStack = []; - const _customOscHandlerA = window.term.addOscHandler(1234, data => { - _customOscHandlerCallStack.push(['A', data]); - return false; - }); - const _customOscHandlerB = window.term.addOscHandler(1234, data => { - _customOscHandlerCallStack.push(['B', data]); - return true; - }); - const _customOscHandlerC = window.term.addOscHandler(1234, data => { - _customOscHandlerCallStack.push(['C', data]); - return false; - }); - `); - await page.evaluate(` - window.term.write('\x1b]1234;some data\x07'); - `); - assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); - }); - }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts new file mode 100644 index 00000000..28f7d8b3 --- /dev/null +++ b/test/api/Parser.api.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('Parser Integration Tests', function(): void { + this.timeout(20000); + + before(async function(): Promise { + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + await page.goto(APP); + await openTerminal(); + }); + + after(() => { + browser.close(); + }); + + describe('addCsiHandler', () => { + it('should call custom CSI handler with js array params', async () => { + await page.evaluate(` + window.term.reset(); + const _customCsiHandlerParams = []; + const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => { + _customCsiHandlerParams.push(params); + return false; + }, ''); + `); + await page.evaluate(` + window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); + `); + assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); + }); + }); + describe('addDcsHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customDcsHandlerCallStack = []; + const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + _customDcsHandlerCallStack.push(['A', params, data]); + return false; + }); + const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + _customDcsHandlerCallStack.push(['B', params, data]); + return true; + }); + const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + _customDcsHandlerCallStack.push(['C', params, data]); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1bP1;2+psome data\x1b\\\\'); + `); + assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); + }); + }); + describe('addEscHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customEscHandlerCallStack = []; + const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + _customEscHandlerCallStack.push('A'); + return false; + }); + const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + _customEscHandlerCallStack.push('B'); + return true; + }); + const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + _customEscHandlerCallStack.push('C'); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1b(B'); + `); + assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']); + }); + }); + describe('addOscHandler', () => { + it('should respects return value', async () => { + await page.evaluate(` + window.term.reset(); + const _customOscHandlerCallStack = []; + const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['A', data]); + return false; + }); + const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['B', data]); + return true; + }); + const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => { + _customOscHandlerCallStack.push(['C', data]); + return false; + }); + `); + await page.evaluate(` + window.term.write('\x1b]1234;some data\x07'); + `); + assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); + }); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3891004e..de767f59 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -500,70 +500,6 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - /** - * Adds a handler for CSI escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {final: 'm'} for SGR. - * @param callback The function to handle the sequence. The callback is - * called with the numerical params. If the sequence has subparams the - * array will contain subarrays with their numercial values. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; - - /** - * Adds a handler for DCS escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. - * @param callback The function to handle the sequence. Note that the - * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since DCS sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits DCS payload to 10 MB - * which should give enough room for most use cases. - * The function gets the payload and numerical parameters as arguments. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addDcsHandler or setDcsHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; - - /** - * Adds a handler for ESC escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '%' final: 'G'} for - * default charset selection. - * @param callback The function to handle the sequence. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addEscHandler or setEscHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; - - /** - * Adds a handler for OSC escape sequences. - * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the sequence. Note that the - * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since OSC sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits OSC payload to 10 MB - * which should give enough room for most use cases. - * The callback is called with OSC data string. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addOscHandler or setOscHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; - /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. @@ -991,45 +927,120 @@ declare module 'xterm' { } /** - * Data type to register a CSI, DCS or ESC callback in the parser in the form: - * ESC I..I F - * CSI Prefix P..P I..I F - * DCS Prefix P..P I..I F data_bytes ST - * - * with these rules/restrictions: - * - prefix can only be used with CSI and DCS - * - only one leading prefix byte is recognized by the parser - * before any other parameter bytes (P..P) - * - intermediate bytes are recognized up to 2 - * - * For custom sequences make sure to read ECMA-48 and the resources at - * vt100.net to not clash with existing sequences or reserved address space. - * General recommendations: - * - use private address space (see ECMA-48) - * - use max one intermediate byte (technically not limited by the spec, - * in practice there are no sequences with more than one intermediate byte, - * thus parsers might get confused with more intermediates) - * - test against other common emulators to check whether they escape/ignore - * the sequence correctly - * - * Notes: OSC command registration is handled differently (see addOscHandler) - * APC, PM or SOS is currently not supported + * Parser namespace, contains all parser related bits. */ - export interface IFunctionIdentifier { + export namespace Parser { + /** - * Optional prefix byte, must be in range \x3c .. \x3f. - * Usable in CSI and DCS. + * Data type to register a CSI, DCS or ESC callback in the parser in the form: + * ESC I..I F + * CSI Prefix P..P I..I F + * DCS Prefix P..P I..I F data_bytes ST + * + * with these rules/restrictions: + * - prefix can only be used with CSI and DCS + * - only one leading prefix byte is recognized by the parser + * before any other parameter bytes (P..P) + * - intermediate bytes are recognized up to 2 + * + * For custom sequences make sure to read ECMA-48 and the resources at + * vt100.net to not clash with existing sequences or reserved address space. + * General recommendations: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * thus parsers might get confused with more intermediates) + * - test against other common emulators to check whether they escape/ignore + * the sequence correctly + * + * Notes: OSC command registration is handled differently (see addOscHandler) + * APC, PM or SOS is currently not supported. */ - prefix?: string; + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. + */ + final: string; + } + /** - * Optional intermediate bytes, must be in range \x20 .. \x2f. - * Usable in CSI, DCS and ESC. + * Parser interface. */ - intermediates?: string; - /** - * Final byte, must be in range \x40 .. \x7e for CSI and DCS, - * \x30 .. \x7e for ESC. - */ - final: string; + export interface IParser { + /** + * Adds a handler for CSI escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. If the sequence has subparams the + * array will contain subarrays with their numercial values. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for DCS escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for ESC escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + + /** + * Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addOscHandler or setOscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + } } } From 1286eb618a6274d7e3b9208f622c1ee35b5d0127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 8 Aug 2019 19:41:12 +0200 Subject: [PATCH 27/41] rename to OscHandler and DcsHandler --- src/InputHandler.ts | 12 +-- src/common/parser/DcsParser.test.ts | 18 ++--- src/common/parser/DcsParser.ts | 2 +- .../parser/EscapeSequenceParser.test.ts | 74 +++++++++---------- src/common/parser/OscParser.test.ts | 18 ++--- src/common/parser/OscParser.ts | 2 +- src/public/Terminal.ts | 8 +- .../EscapeSequenceParser.benchmark.ts | 6 +- 8 files changed, 70 insertions(+), 70 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 6a8e0ac6..ecd5bad9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -21,8 +21,8 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; -import { OscHandlerFactory } from 'common/parser/OscParser'; -import { DcsHandlerFactory } from 'common/parser/DcsParser'; +import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from 'common/parser/DcsParser'; /** * Map collect to glevel. Used in `selectCharset`. @@ -241,10 +241,10 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC handler */ // 0 - icon name + title - this._parser.setOscHandler(0, new OscHandlerFactory((data: string) => this.setTitle(data))); + this._parser.setOscHandler(0, new OscHandler((data: string) => this.setTitle(data))); // 1 - icon name // 2 - title - this._parser.setOscHandler(2, new OscHandlerFactory((data: string) => this.setTitle(data))); + this._parser.setOscHandler(2, new OscHandler((data: string) => this.setTitle(data))); // 3 - set property X in the form "prop=value" // 4 - Change Color Number // 5 - Change Special Color Number @@ -505,7 +505,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Forward addDcsHandler from parser. */ public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - return this._parser.addDcsHandler(id, new DcsHandlerFactory(callback)); + return this._parser.addDcsHandler(id, new DcsHandler(callback)); } /** @@ -519,7 +519,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Forward addOscHandler from parser. */ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, new OscHandlerFactory(callback)); + return this._parser.addOscHandler(ident, new OscHandler(callback)); } /** diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index 3ef42a68..1507df86 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { DcsParser, DcsHandlerFactory } from 'common/parser/DcsParser'; +import { DcsParser, DcsHandler } from 'common/parser/DcsParser'; import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; @@ -176,7 +176,7 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -186,7 +186,7 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -196,8 +196,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); - const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['two', params.toArray(), data]))); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -215,8 +215,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); it('should respect return false', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push(['one', params.toArray(), data]))); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); + parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -227,7 +227,7 @@ describe('DcsParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); const data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { @@ -238,7 +238,7 @@ describe('DcsParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandlerFactory((data, params) => reports.push([params.toArray(), data]))); + parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 0112e935..b2485d92 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -107,7 +107,7 @@ export class DcsParser implements IDcsParser { * Convenient class to create a DCS handler from a single callback function. * Note: The payload is currently limited to 50 MB (hardcoded). */ -export class DcsHandlerFactory implements IDcsHandler { +export class DcsHandler implements IDcsHandler { private _data = ''; private _params: IParams | undefined; private _hitLimit: boolean = false; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 40ce38b1..8019b09e 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -9,9 +9,9 @@ import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; import { ParserState } from 'common/parser/Constants'; import { Params } from 'common/parser/Params'; -import { OscHandlerFactory } from 'common/parser/OscParser'; +import { OscHandler } from 'common/parser/OscParser'; import { IDisposable } from 'common/Types'; -import { DcsHandlerFactory } from 'common/parser/DcsParser'; +import { DcsHandler } from 'common/parser/DcsParser'; function r(a: number, b: number): string[] { @@ -1364,7 +1364,7 @@ describe('EscapeSequenceParser', function (): void { chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { - parser2.setOscHandler(1, new OscHandlerFactory(function (data: string): void { + parser2.setOscHandler(1, new OscHandler(function (data: string): void { osc.push([1, data]); })); parse(parser2, INPUT); @@ -1378,16 +1378,16 @@ describe('EscapeSequenceParser', function (): void { describe('OSC custom handlers', () => { it('Prevent fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); it('Allow fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return false; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return false; })); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); @@ -1395,9 +1395,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return false; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return false; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); @@ -1406,9 +1406,9 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); - parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return true; })); parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([], 'Should not fallback once'); @@ -1416,16 +1416,16 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setOscHandler(1, new OscHandlerFactory(() => order.push(1))); - parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(2); return false; })); - parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(3); return false; })); + parser2.setOscHandler(1, new OscHandler(() => order.push(1))); + parser2.addOscHandler(1, new OscHandler(() => { order.push(2); return false; })); + parser2.addOscHandler(1, new OscHandler(() => { order.push(3); return false; })); parse(parser2, '\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); @@ -1433,8 +1433,8 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data]))); - const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; })); + parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); + const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1476,54 +1476,54 @@ describe('EscapeSequenceParser', function (): void { const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; it('Prevent fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]); }); it('Allow fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers fallback once', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers no fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => order.push(1))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(2); return false; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory(() => { order.push(3); return false; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => order.push(1))); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; })); + parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; })); parse(parser2, DCS_INPUT); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); parse(parser2, DCS_INPUT); chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); }); it('Should not corrupt the parser when dispose is called twice', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandlerFactory((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); + const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); dispo.dispose(); parse(parser2, DCS_INPUT); diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index 2d1180d2..288bddd3 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { OscParser, OscHandlerFactory } from 'common/parser/OscParser'; +import { OscParser, OscHandler } from 'common/parser/OscParser'; import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder'; import { IOscHandler } from 'common/parser/Types'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -170,7 +170,7 @@ describe('OscParser', () => { }); describe('OscHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -180,7 +180,7 @@ describe('OscParser', () => { assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -190,8 +190,8 @@ describe('OscParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data]))); - const dispo = parser.addOscHandler(1234, new OscHandlerFactory(data => reports.push(['two', data]))); + parser.setOscHandler(1234, new OscHandler(data => reports.push(['one', data]))); + const dispo = parser.addOscHandler(1234, new OscHandler(data => reports.push(['two', data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -209,8 +209,8 @@ describe('OscParser', () => { assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); }); it('should respect return false', () => { - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data]))); - parser.addOscHandler(1234, new OscHandlerFactory(data => { reports.push(['two', data]); return false; })); + parser.setOscHandler(1234, new OscHandler(data => reports.push(['one', data]))); + parser.addOscHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -221,7 +221,7 @@ describe('OscParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); @@ -234,7 +234,7 @@ describe('OscParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data]))); + parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index ed549deb..10b3470d 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -168,7 +168,7 @@ export class OscParser extends Disposable { * Convenient class to allow attaching string based handler functions * as OSC handlers. */ -export class OscHandlerFactory implements IOscHandler { +export class OscHandler implements IOscHandler { private _data = ''; private _hitLimit: boolean = false; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 0a91aa74..673e56f0 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -12,8 +12,8 @@ import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; import { IParams, IEscapeSequenceParser } from 'common/parser/Types'; -import { OscHandlerFactory } from 'common/parser/OscParser'; -import { DcsHandlerFactory } from '../../out/common/parser/DcsParser'; +import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from 'common/parser/DcsParser'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -225,12 +225,12 @@ class ParserApi implements Parser.IParser { return this._parser.addCsiHandler(id, (params: IParams) => callback(params.toArray())); } public addDcsHandler(id: Parser.IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this._parser.addDcsHandler(id, new DcsHandlerFactory((data: string, params: IParams) => callback(data, params.toArray()))); + return this._parser.addDcsHandler(id, new DcsHandler((data: string, params: IParams) => callback(data, params.toArray()))); } public addEscHandler(id: Parser.IFunctionIdentifier, handler: () => boolean): IDisposable { return this._parser.addEscHandler(id, handler); } public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, new OscHandlerFactory(callback)); + return this._parser.addOscHandler(ident, new OscHandler(callback)); } } diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index a8b9fde9..fa22dc58 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -7,7 +7,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { C0, C1 } from 'common/data/EscapeSequences'; import { IDcsHandler, IParams } from 'common/parser/Types'; -import { OscHandlerFactory } from 'common/parser/OscParser'; +import { OscHandler } from 'common/parser/OscParser'; function toUtf32(s: string): Uint32Array { @@ -80,8 +80,8 @@ perfContext('Parser throughput - 50MB data', () => { parser.setExecuteHandler(C1.IND, () => {}); parser.setExecuteHandler(C1.NEL, () => {}); parser.setExecuteHandler(C1.HTS, () => {}); - parser.setOscHandler(0, new OscHandlerFactory((data) => {})); - parser.setOscHandler(2, new OscHandlerFactory((data) => {})); + parser.setOscHandler(0, new OscHandler((data) => {})); + parser.setOscHandler(2, new OscHandler((data) => {})); parser.setEscHandler({final: '7'}, () => {}); parser.setEscHandler({final: '8'}, () => {}); parser.setEscHandler({final: 'D'}, () => {}); From 0469e271da21b578651a7f8ccbf6c11b5c2cf362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 8 Aug 2019 21:58:02 +0200 Subject: [PATCH 28/41] cache ParserApi, remove namespace --- src/public/Terminal.ts | 28 ++--- typings/xterm.d.ts | 252 ++++++++++++++++++++--------------------- 2 files changed, 137 insertions(+), 143 deletions(-) diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 673e56f0..f19c8ce3 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, Parser } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -11,17 +11,17 @@ import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; -import { IParams, IEscapeSequenceParser } from 'common/parser/Types'; -import { OscHandler } from 'common/parser/OscParser'; -import { DcsHandler } from 'common/parser/DcsParser'; +import { IParams } from 'common/parser/Types'; export class Terminal implements ITerminalApi { private _core: ITerminal; private _addonManager: AddonManager; + private _parser: IParser; constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); this._addonManager = new AddonManager(); + this._parser = new ParserApi(this._core); } public get onCursorMove(): IEvent { return this._core.onCursorMove; } @@ -35,7 +35,7 @@ export class Terminal implements ITerminalApi { public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } public get element(): HTMLElement { return this._core.element; } - public get parser(): Parser.IParser { return new ParserApi((this._core as any)._inputHandler._parser); } + public get parser(): IParser { return this._parser; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } @@ -218,19 +218,19 @@ class BufferCellApiView implements IBufferCellApi { public get width(): number { return this._line.getWidth(this._x); } } -class ParserApi implements Parser.IParser { - constructor(private _parser: IEscapeSequenceParser) {} +class ParserApi implements IParser { + constructor(private _core: ITerminal) {} - public addCsiHandler(id: Parser.IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { - return this._parser.addCsiHandler(id, (params: IParams) => callback(params.toArray())); + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); } - public addDcsHandler(id: Parser.IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this._parser.addDcsHandler(id, new DcsHandler((data: string, params: IParams) => callback(data, params.toArray()))); + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); } - public addEscHandler(id: Parser.IFunctionIdentifier, handler: () => boolean): IDisposable { - return this._parser.addEscHandler(id, handler); + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + return this._core.addEscHandler(id, handler); } public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, new OscHandler(callback)); + return this._core.addOscHandler(ident, callback); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index de767f59..1ba4be1f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -207,47 +207,47 @@ declare module 'xterm' { */ export interface ITheme { /** The default foreground color */ - foreground?: string, + foreground?: string; /** The default background color */ - background?: string, + background?: string; /** The cursor color */ - cursor?: string, + cursor?: string; /** The accent color of the cursor (fg color for a block cursor) */ - cursorAccent?: string, + cursorAccent?: string; /** The selection background color (can be transparent) */ - selection?: string, + selection?: string; /** ANSI black (eg. `\x1b[30m`) */ - black?: string, + black?: string; /** ANSI red (eg. `\x1b[31m`) */ - red?: string, + red?: string; /** ANSI green (eg. `\x1b[32m`) */ - green?: string, + green?: string; /** ANSI yellow (eg. `\x1b[33m`) */ - yellow?: string, + yellow?: string; /** ANSI blue (eg. `\x1b[34m`) */ - blue?: string, + blue?: string; /** ANSI magenta (eg. `\x1b[35m`) */ - magenta?: string, + magenta?: string; /** ANSI cyan (eg. `\x1b[36m`) */ - cyan?: string, + cyan?: string; /** ANSI white (eg. `\x1b[37m`) */ - white?: string, + white?: string; /** ANSI bright black (eg. `\x1b[1;30m`) */ - brightBlack?: string, + brightBlack?: string; /** ANSI bright red (eg. `\x1b[1;31m`) */ - brightRed?: string, + brightRed?: string; /** ANSI bright green (eg. `\x1b[1;32m`) */ - brightGreen?: string, + brightGreen?: string; /** ANSI bright yellow (eg. `\x1b[1;33m`) */ - brightYellow?: string, + brightYellow?: string; /** ANSI bright blue (eg. `\x1b[1;34m`) */ - brightBlue?: string, + brightBlue?: string; /** ANSI bright magenta (eg. `\x1b[1;35m`) */ - brightMagenta?: string, + brightMagenta?: string; /** ANSI bright cyan (eg. `\x1b[1;36m`) */ - brightCyan?: string, + brightCyan?: string; /** ANSI bright white (eg. `\x1b[1;37m`) */ - brightWhite?: string + brightWhite?: string; } /** @@ -778,7 +778,7 @@ declare module 'xterm' { /** * Perform a full reset (RIS, aka '\x1bc'). */ - reset(): void + reset(): void; /** * Applies an addon to the Terminal prototype, making it available to all @@ -927,120 +927,114 @@ declare module 'xterm' { } /** - * Parser namespace, contains all parser related bits. + * Data type to register a CSI, DCS or ESC callback in the parser in the form: + * ESC I..I F + * CSI Prefix P..P I..I F + * DCS Prefix P..P I..I F data_bytes ST + * + * with these rules/restrictions: + * - prefix can only be used with CSI and DCS + * - only one leading prefix byte is recognized by the parser + * before any other parameter bytes (P..P) + * - intermediate bytes are recognized up to 2 + * + * For custom sequences make sure to read ECMA-48 and the resources at + * vt100.net to not clash with existing sequences or reserved address space. + * General recommendations: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * thus parsers might get confused with more intermediates) + * - test against other common emulators to check whether they escape/ignore + * the sequence correctly + * + * Notes: OSC command registration is handled differently (see addOscHandler) + * APC, PM or SOS is currently not supported. */ - export namespace Parser { + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. + */ + final: string; + } + + /** + * Parser interface. + */ + export interface IParser { + /** + * Adds a handler for CSI escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. If the sequence has subparams the + * array will contain subarrays with their numercial values. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; /** - * Data type to register a CSI, DCS or ESC callback in the parser in the form: - * ESC I..I F - * CSI Prefix P..P I..I F - * DCS Prefix P..P I..I F data_bytes ST - * - * with these rules/restrictions: - * - prefix can only be used with CSI and DCS - * - only one leading prefix byte is recognized by the parser - * before any other parameter bytes (P..P) - * - intermediate bytes are recognized up to 2 - * - * For custom sequences make sure to read ECMA-48 and the resources at - * vt100.net to not clash with existing sequences or reserved address space. - * General recommendations: - * - use private address space (see ECMA-48) - * - use max one intermediate byte (technically not limited by the spec, - * in practice there are no sequences with more than one intermediate byte, - * thus parsers might get confused with more intermediates) - * - test against other common emulators to check whether they escape/ignore - * the sequence correctly - * - * Notes: OSC command registration is handled differently (see addOscHandler) - * APC, PM or SOS is currently not supported. + * Adds a handler for DCS escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. */ - export interface IFunctionIdentifier { - /** - * Optional prefix byte, must be in range \x3c .. \x3f. - * Usable in CSI and DCS. - */ - prefix?: string; - /** - * Optional intermediate bytes, must be in range \x20 .. \x2f. - * Usable in CSI, DCS and ESC. - */ - intermediates?: string; - /** - * Final byte, must be in range \x40 .. \x7e for CSI and DCS, - * \x30 .. \x7e for ESC. - */ - final: string; - } + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; /** - * Parser interface. + * Adds a handler for ESC escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. */ - export interface IParser { - /** - * Adds a handler for CSI escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {final: 'm'} for SGR. - * @param callback The function to handle the sequence. The callback is - * called with the numerical params. If the sequence has subparams the - * array will contain subarrays with their numercial values. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; - /** - * Adds a handler for DCS escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. - * @param callback The function to handle the sequence. Note that the - * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since DCS sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits DCS payload to 10 MB - * which should give enough room for most use cases. - * The function gets the payload and numerical parameters as arguments. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addDcsHandler or setDcsHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; - - /** - * Adds a handler for ESC escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '%' final: 'G'} for - * default charset selection. - * @param callback The function to handle the sequence. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addEscHandler or setEscHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; - - /** - * Adds a handler for OSC escape sequences. - * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the sequence. Note that the - * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since OSC sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits OSC payload to 10 MB - * which should give enough room for most use cases. - * The callback is called with OSC data string. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addOscHandler or setOscHandler). - * The most recently-added handler is tried first. - * @return An IDisposable you can call to remove this handler. - */ - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; - } + /** + * Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addOscHandler or setOscHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; } } From 811c24b866cc5aaff12e3358efb6ffcd9d2d91b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 9 Aug 2019 09:55:31 -0700 Subject: [PATCH 29/41] Fix mouse events class apply on open Fixes #2370 --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index f857130b..bb014100 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -662,9 +662,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.linkifier.attachToDom(this.element, this._mouseZoneManager); // 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._selectionService.disable(); + this.element.classList.add('enable-mouse-events'); } else { this._selectionService.enable(); } From 598d37a0233f22f14ead47fff2b7eecf482a6c6a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 10 Aug 2019 07:38:08 -0700 Subject: [PATCH 30/41] Fix DOM blink for non-block cursors Fixes #2373 --- src/renderer/dom/DomRenderer.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 22b50349..d2c80235 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -165,7 +165,13 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Blink animation styles += - `@keyframes blink {` + + `@keyframes blink_box_shadow {` + + ` 50% {` + + ` box-shadow: none;` + + ` }` + + `}`; + styles += + `@keyframes blink_block {` + ` 0% {` + ` background-color: ${this._colors.cursor.css};` + ` color: ${this._colors.cursorAccent.css};` + @@ -181,8 +187,11 @@ export class DomRenderer extends Disposable implements IRenderer { ` outline: 1px solid ${this._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_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` + + ` animation: blink_box_shadow 1s step-end infinite;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + + ` animation: blink_block 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${this._colors.cursor.css};` + From 13dde8061fd3189ab776c7addbcacfc889eea3ee Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 10 Aug 2019 14:23:17 -0700 Subject: [PATCH 31/41] Move buffer tests to Terminal.test.ts and re-enable Fixes #2361 --- src/Terminal.test.ts | 235 +++++++++++++++++++++++++++++++ src/common/buffer/Buffer.test.ts | 233 ------------------------------ src/common/buffer/Types.d.ts | 2 +- 3 files changed, 236 insertions(+), 234 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 714257f9..e0f9f878 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -1123,6 +1123,241 @@ describe('Terminal', () => { }); }); }); + + describe('Buffer.stringIndexToBufferIndex', () => { + let terminal: TestTerminal; + + beforeEach(() => { + terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + }); + + it('multiline ascii', () => { + const input = 'This is ASCII text spanning multiple lines.'; + terminal.writeSync(input); + 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); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + }); + + it('combining e\u0301 in a sentence', () => { + const input = 'Sitting in the cafe\u0301 drinking coffee.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 19; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 18), + terminal.buffer.stringIndexToBufferIndex(0, 19)); + // after the combining char every string index has an offset of -1 + for (let i = 19; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline combining e\u0301', () => { + const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 2 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + it('surrogate char in a sentence', () => { + const input = 'The 𝄞 is a clef widely used in modern notation.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 5; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 4), + terminal.buffer.stringIndexToBufferIndex(0, 5)); + // after the combining char every string index has an offset of -1 + for (let i = 5; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate char', () => { + const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 2 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + it('surrogate char with combining', () => { + // eye of Ra with acute accent - string length of 3 + const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // index 0..2 should map to 0 + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); + for (let i = 2; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate with combining', () => { + const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 3 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth chars', () => { + const input = 'These 123 are some fat numbers.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 6; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 6, 7, 8 take 2 cells + assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); + assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); + // rest of the string has offset of +3 + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); + } + }); + + it('multiline fullwidth chars', () => { + const input = '12345678901234567890'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth combining with emoji - match emoji cell', () => { + const input = 'Lots of ¥\u0301 make me 😃.'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + 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()).getChars(), '😃'); + }); + + it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { + const input = 'a12345678901234567890'; + // the 'a' at the beginning moves all fullwidth chars one to the right + // now the end of the line contains a dangling empty cell since + // the next fullwidth char has to wrap early + // the dangling last cell is wrongly added in the string + // --> fixable after resolving #1685 + terminal.writeSync(input); + 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, true); + const j = (i - 0) << 1; + assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); + } + }); + + it('test fully wrapped buffer up to last char', () => { + const input = Array(6).join('1234567890'); + terminal.writeSync(input); + 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, true); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + it('test fully wrapped buffer up to last char with full width odd', () => { + const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' + + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; + terminal.writeSync(input); + 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, true); + assert.equal( + (!(i % 3)) + ? input[i] + : (i % 3 === 1) + ? input.substr(i, 2) + : input.substr(i - 1, 2), + terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + 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.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); + }); + }); + + describe('BufferStringIterator', function(): void { + it('iterator does not overflow buffer limits', function(): void { + const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + const data = [ + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa' + ]; + terminal.writeSync(data.join('')); + // brute force test with insane values + expect(() => { + for (let overscan = 0; overscan < 20; ++overscan) { + for (let start = -10; start < 20; ++start) { + for (let end = -10; end < 20; ++end) { + const it = terminal.buffer.iterator(false, start, end, overscan, overscan); + while (it.hasNext()) { + it.next(); + } + } + } + } + }).to.not.throw(); + }); + }); }); class TestLinkifier extends Linkifier { diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index af5abeca..35aade4a 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -1163,237 +1163,4 @@ describe('Buffer', () => { assert.equal(str3, '😁a'); }); }); - // describe('stringIndexToBufferIndex', () => { - // let terminal: TestTerminal; - - // beforeEach(() => { - // terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - // }); - - // it('multiline ascii', () => { - // const input = 'This is ASCII text spanning multiple lines.'; - // terminal.writeSync(input); - // 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); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // }); - - // it('combining e\u0301 in a sentence', () => { - // const input = 'Sitting in the cafe\u0301 drinking coffee.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < 19; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index - // assert.deepEqual( - // terminal.buffer.stringIndexToBufferIndex(0, 18), - // terminal.buffer.stringIndexToBufferIndex(0, 19)); - // // after the combining char every string index has an offset of -1 - // for (let i = 19; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline combining e\u0301', () => { - // const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // every buffer cell index contains 2 string indices - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('surrogate char in a sentence', () => { - // const input = 'The 𝄞 is a clef widely used in modern notation.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < 5; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index - // assert.deepEqual( - // terminal.buffer.stringIndexToBufferIndex(0, 4), - // terminal.buffer.stringIndexToBufferIndex(0, 5)); - // // after the combining char every string index has an offset of -1 - // for (let i = 5; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline surrogate char', () => { - // const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // every buffer cell index contains 2 string indices - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('surrogate char with combining', () => { - // // eye of Ra with acute accent - string length of 3 - // const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // index 0..2 should map to 0 - // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); - // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); - // for (let i = 2; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline surrogate with combining', () => { - // const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // // every buffer cell index contains 3 string indices - // for (let i = 0; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); - // } - // }); - - // it('fullwidth chars', () => { - // const input = 'These 123 are some fat numbers.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 0; i < 6; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - // } - // // string index 6, 7, 8 take 2 cells - // assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); - // assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); - // // rest of the string has offset of +3 - // for (let i = 9; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); - // } - // }); - - // it('multiline fullwidth chars', () => { - // const input = '12345678901234567890'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // assert.equal(input, s); - // for (let i = 9; i < input.length; ++i) { - // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - // assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); - // } - // }); - - // it('fullwidth combining with emoji - match emoji cell', () => { - // const input = 'Lots of ¥\u0301 make me 😃.'; - // terminal.writeSync(input); - // const s = terminal.buffer.iterator(true).next().content; - // 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()).getChars(), '😃'); - // }); - - // it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { - // const input = 'a12345678901234567890'; - // // the 'a' at the beginning moves all fullwidth chars one to the right - // // now the end of the line contains a dangling empty cell since - // // the next fullwidth char has to wrap early - // // the dangling last cell is wrongly added in the string - // // --> fixable after resolving #1685 - // terminal.writeSync(input); - // 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, true); - // const j = (i - 0) << 1; - // assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); - // } - // }); - - // it('test fully wrapped buffer up to last char', () => { - // const input = Array(6).join('1234567890'); - // terminal.writeSync(input); - // 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, true); - // assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - // } - // }); - - // it('test fully wrapped buffer up to last char with full width odd', () => { - // const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' - // + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; - // terminal.writeSync(input); - // 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, true); - // assert.equal( - // (!(i % 3)) - // ? input[i] - // : (i % 3 === 1) - // ? input.substr(i, 2) - // : input.substr(i - 1, 2), - // terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - // } - // }); - - // 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(optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); - // }); - // }); - // describe('BufferStringIterator', function(): void { - // it('iterator does not overflow buffer limits', function(): void { - // const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - // const data = [ - // 'aaaaaaaaaa', - // 'aaaaaaaaa\n', - // 'aaaaaaaaaa', - // 'aaaaaaaaa\n', - // 'aaaaaaaaaa', - // 'aaaaaaaaaa', - // 'aaaaaaaaaa', - // 'aaaaaaaaa\n', - // 'aaaaaaaaaa', - // 'aaaaaaaaaa' - // ]; - // terminal.writeSync(data.join('')); - // // brute force test with insane values - // expect(() => { - // for (let overscan = 0; overscan < 20; ++overscan) { - // for (let start = -10; start < 20; ++start) { - // for (let end = -10; end < 20; ++end) { - // const it = terminal.buffer.iterator(false, start, end, overscan, overscan); - // while (it.hasNext()) { - // it.next(); - // } - // } - // } - // } - // }).to.not.throw(); - // }); - // }); }); diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 532230ec..e229d69c 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -40,7 +40,7 @@ export interface IBuffer { nextStop(x?: number): number; prevStop(x?: number): number; getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; - stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; getNullCell(attr?: IAttributeData): ICellData; getWhitespaceCell(attr?: IAttributeData): ICellData; From ba1274ba129541a44c2c6ea264f82eb796e675a3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 10 Aug 2019 14:31:14 -0700 Subject: [PATCH 32/41] Remove applyAddon, stabilize loadAddon Fixes #2076 --- demo/start.js | 3 --- src/public/Terminal.ts | 3 --- typings/xterm.d.ts | 10 +--------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/demo/start.js b/demo/start.js index a054e939..a14627c1 100644 --- a/demo/start.js +++ b/demo/start.js @@ -20,9 +20,6 @@ startServer(); * For production builds see `webpack.config.js` in the root directory. If that is built the demo * can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for * validating that the packaged version works correctly. - * - * The addons are not webpacked right now and are built directly to `lib/` via `tsc` as they are - * the legacy format (`applyAddon`) and will be removed soon anyway. */ const clientConfig = { entry: path.resolve(__dirname, 'client.ts'), diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 5d270ca7..6c12866c 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -164,9 +164,6 @@ export class Terminal implements ITerminalApi { public reset(): void { this._core.reset(); } - public static applyAddon(addon: any): void { - addon.apply(Terminal); - } public loadAddon(addon: ITerminalAddon): void { return this._addonManager.loadAddon(this, addon); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d9e28b26..e9c83ad2 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -807,15 +807,7 @@ declare module 'xterm' { reset(): void /** - * Applies an addon to the Terminal prototype, making it available to all - * newly created Terminals. - * @param addon The addon to apply. - * @deprecated Use the new loadAddon API/addon format. - */ - static applyAddon(addon: any): void; - - /** - * (EXPERIMENTAL) Loads an addon into this instance of xterm.js. + * Loads an addon into this instance of xterm.js. * @param addon The addon to load. */ loadAddon(addon: ITerminalAddon): void; From dd42eccb915e1261d8e6183830a402979171a166 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 10 Aug 2019 17:51:17 -0700 Subject: [PATCH 33/41] Fix dom -> canvas renderer switch not drawing viewport Fixes #2204 --- src/Terminal.ts | 1 + src/browser/services/RenderService.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index ef05c664..6192102a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -394,6 +394,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp case 'rendererType': if (this._renderService) { this._renderService.setRenderer(this._createRenderer()); + this._renderService.onResize(this.cols, this.rows); } break; case 'scrollback': diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index c0b3c0e5..63084f54 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -108,7 +108,7 @@ export class RenderService extends Disposable implements IRenderService { } public setRenderer(renderer: IRenderer): void { - // TODO: RenderCoordinator should be the only one to dispose the renderer + // TODO: RenderService should be the only one to dispose the renderer this._renderer.dispose(); this._renderer = renderer; this.refreshRows(0, this._rowCount - 1); From f677516e950dc9c6f6794e0c7c1706a9742726b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 11 Aug 2019 16:45:35 +0200 Subject: [PATCH 34/41] better interfaces, cleanup --- src/InputHandler.ts | 2 +- src/Types.d.ts | 2 +- src/common/parser/DcsParser.test.ts | 36 +++++++++---------- src/common/parser/DcsParser.ts | 18 +++++----- .../parser/EscapeSequenceParser.test.ts | 8 ++--- src/common/parser/EscapeSequenceParser.ts | 17 +++++---- src/common/parser/OscParser.test.ts | 36 +++++++++---------- src/common/parser/OscParser.ts | 13 ++++--- src/common/parser/Types.d.ts | 35 +++++++++++------- src/public/Terminal.ts | 8 +++-- typings/xterm.d.ts | 2 +- 11 files changed, 94 insertions(+), 83 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ecd5bad9..90067772 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1576,7 +1576,6 @@ export class InputHandler extends Disposable implements IInputHandler { } } - /** * Helper to extract and apply color params/subparams. * Returns advance for params index. @@ -1849,6 +1848,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + public deviceStatusPrivate(params: IParams): void { // modern xterm doesnt seem to // respond to any of these except ?6, 6, and 5 diff --git a/src/Types.d.ts b/src/Types.d.ts index e3a9771d..bb203b52 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -114,7 +114,7 @@ export interface IInputHandler { /** CSI a */ hPositionRelative(params: IParams): void; /** CSI b */ repeatPrecedingCharacter(params: IParams): void; /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void; - sendDeviceAttributesSecondary(params: IParams): void; + /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void; /** CSI d */ linePosAbsolute(params: IParams): void; /** CSI e */ vPositionRelative(params: IParams): void; /** CSI f */ hVPosition(params: IParams): void; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index 1507df86..4d6dce11 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -75,7 +75,7 @@ describe('DcsParser', () => { beforeEach(() => { reports = []; parser = new DcsParser(); - parser.setDcsHandlerFallback((id, action, data) => { + parser.setHandlerFallback((id, action, data) => { if (action === 'HOOK') { data = data.toArray(); } @@ -84,7 +84,7 @@ describe('DcsParser', () => { }); describe('handler registration', () => { it('setDcsHandler', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -100,8 +100,8 @@ describe('DcsParser', () => { ]); }); it('clearDcsHandler', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); - parser.clearDcsHandler(identifier({intermediates: '+', final: 'p'})); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.clearHandler(identifier({intermediates: '+', final: 'p'})); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -117,8 +117,8 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -137,8 +137,8 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler with return false', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -157,8 +157,8 @@ describe('DcsParser', () => { ]); }); it('dispose handlers', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); dispo.dispose(); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); @@ -176,7 +176,7 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -186,7 +186,7 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -196,8 +196,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); - const dispo = parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -215,8 +215,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); it('should respect return false', () => { - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); - parser.addDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); + parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -227,7 +227,7 @@ describe('DcsParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); const data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { @@ -238,7 +238,7 @@ describe('DcsParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setDcsHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index b2485d92..a6d2a6d7 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -9,11 +9,11 @@ import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; +const EMPTY_HANDLERS: IDcsHandler[] = []; export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); - private _empty: IDcsHandler[] = []; - private _active: IDcsHandler[] = this._empty; + private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; private _handlerFb: DcsFallbackHandler = () => {}; @@ -22,7 +22,7 @@ export class DcsParser implements IDcsParser { this._handlerFb = () => {}; } - public addDcsHandler(ident: number, handler: IDcsHandler): IDisposable { + public addHandler(ident: number, handler: IDcsHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -38,15 +38,15 @@ export class DcsParser implements IDcsParser { }; } - public setDcsHandler(ident: number, handler: IDcsHandler): void { + public setHandler(ident: number, handler: IDcsHandler): void { this._handlers[ident] = [handler]; } - public clearDcsHandler(ident: number): void { + public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - public setDcsHandlerFallback(handler: DcsFallbackHandler): void { + public setHandlerFallback(handler: DcsFallbackHandler): void { this._handlerFb = handler; } @@ -54,7 +54,7 @@ export class DcsParser implements IDcsParser { if (this._active.length) { this.unhook(false); } - this._active = this._empty; + this._active = EMPTY_HANDLERS; this._ident = 0; } @@ -62,7 +62,7 @@ export class DcsParser implements IDcsParser { // always reset leftover handlers this.reset(); this._ident = ident; - this._active = this._handlers[ident] || this._empty; + this._active = this._handlers[ident] || EMPTY_HANDLERS; if (!this._active.length) { this._handlerFb(this._ident, 'HOOK', params); } else { @@ -98,7 +98,7 @@ export class DcsParser implements IDcsParser { this._active[j].unhook(false); } } - this._active = this._empty; + this._active = EMPTY_HANDLERS; this._ident = 0; } } diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 8019b09e..e960387f 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -41,16 +41,16 @@ class MockOscPutParser implements IOscParser { this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1)); } } - addOscHandler(ident: number, handler: IOscHandler): IDisposable { + addHandler(ident: number, handler: IOscHandler): IDisposable { throw new Error('not implemented'); } - setOscHandler(ident: number, handler: IOscHandler): void { + setHandler(ident: number, handler: IOscHandler): void { throw new Error('not implemented'); } - clearOscHandler(ident: number): void { + clearHandler(ident: number): void { throw new Error('not implemented'); } - setOscHandlerFallback(handler: OscFallbackHandler): void { + setHandlerFallback(handler: OscFallbackHandler): void { this._fallback = handler; } } diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 30aabc6d..2411ebdb 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -404,29 +404,29 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } public addOscHandler(ident: number, handler: IOscHandler): IDisposable { - return this._oscParser.addOscHandler(ident, handler); + return this._oscParser.addHandler(ident, handler); } public setOscHandler(ident: number, handler: IOscHandler): void { - this._oscParser.setOscHandler(ident, handler); + this._oscParser.setHandler(ident, handler); } public clearOscHandler(ident: number): void { - this._oscParser.clearOscHandler(ident); + this._oscParser.clearHandler(ident); } public setOscHandlerFallback(handler: OscFallbackHandler): void { - this._oscParser.setOscHandlerFallback(handler); + this._oscParser.setHandlerFallback(handler); } public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { - return this._dcsParser.addDcsHandler(this._identifier(id), handler); + return this._dcsParser.addHandler(this._identifier(id), handler); } public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { - this._dcsParser.setDcsHandler(this._identifier(id), handler); + this._dcsParser.setHandler(this._identifier(id), handler); } public clearDcsHandler(id: IFunctionIdentifier): void { - this._dcsParser.clearDcsHandler(this._identifier(id)); + this._dcsParser.clearHandler(this._identifier(id)); } public setDcsHandlerFallback(handler: DcsFallbackHandler): void { - this._dcsParser.setDcsHandlerFallback(handler); + this._dcsParser.setHandlerFallback(handler); } public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { @@ -443,7 +443,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; - // this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index 288bddd3..6969d6f3 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -37,7 +37,7 @@ describe('OscParser', () => { beforeEach(() => { reports = []; parser = new OscParser(); - parser.setOscHandlerFallback((id, action, data) => { + parser.setHandlerFallback((id, action, data) => { reports.push([id, action, data]); }); }); @@ -78,7 +78,7 @@ describe('OscParser', () => { }); describe('handler registration', () => { it('setOscHandler', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th')); + parser.setHandler(1234, new TestHandler(1234, reports, 'th')); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -94,8 +94,8 @@ describe('OscParser', () => { ]); }); it('clearOscHandler', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th')); - parser.clearOscHandler(1234); + parser.setHandler(1234, new TestHandler(1234, reports, 'th')); + parser.clearHandler(1234); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -111,8 +111,8 @@ describe('OscParser', () => { ]); }); it('addOscHandler', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); - parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2')); + parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.addHandler(1234, new TestHandler(1234, reports, 'th2')); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -131,8 +131,8 @@ describe('OscParser', () => { ]); }); it('addOscHandler with return false', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); - parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true)); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -151,8 +151,8 @@ describe('OscParser', () => { ]); }); it('dispose handlers', () => { - parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1')); - const dispo = parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); + const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true)); dispo.dispose(); parser.start(); let data = toUtf32('1234;Here comes'); @@ -170,7 +170,7 @@ describe('OscParser', () => { }); describe('OscHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -180,7 +180,7 @@ describe('OscParser', () => { assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -190,8 +190,8 @@ describe('OscParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setOscHandler(1234, new OscHandler(data => reports.push(['one', data]))); - const dispo = parser.addOscHandler(1234, new OscHandler(data => reports.push(['two', data]))); + parser.setHandler(1234, new OscHandler(data => reports.push(['one', data]))); + const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data]))); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -209,8 +209,8 @@ describe('OscParser', () => { assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); }); it('should respect return false', () => { - parser.setOscHandler(1234, new OscHandler(data => reports.push(['one', data]))); - parser.addOscHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; })); + parser.setHandler(1234, new OscHandler(data => reports.push(['one', data]))); + parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -221,7 +221,7 @@ describe('OscParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); @@ -234,7 +234,7 @@ describe('OscParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setOscHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 10b3470d..9f600e97 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -3,20 +3,19 @@ * @license MIT */ -import { IOscHandler, IHandlerCollection, OscFallbackHandler } from 'common/parser/Types'; +import { IOscHandler, IHandlerCollection, OscFallbackHandler, IOscParser } from 'common/parser/Types'; import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; -import { Disposable } from 'common/Lifecycle'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; -export class OscParser extends Disposable { +export class OscParser implements IOscParser { private _state = OscState.START; private _id = -1; private _handlers: IHandlerCollection = Object.create(null); private _handlerFb: OscFallbackHandler = () => { }; - public addOscHandler(ident: number, handler: IOscHandler): IDisposable { + public addHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -31,13 +30,13 @@ export class OscParser extends Disposable { } }; } - public setOscHandler(ident: number, handler: IOscHandler): void { + public setHandler(ident: number, handler: IOscHandler): void { this._handlers[ident] = [handler]; } - public clearOscHandler(ident: number): void { + public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - public setOscHandlerFallback(handler: OscFallbackHandler): void { + public setHandlerFallback(handler: OscFallbackHandler): void { this._handlerFb = handler; } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index b237cd10..1a43f773 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -179,28 +179,37 @@ export interface IEscapeSequenceParser extends IDisposable { clearErrorHandler(): void; } -export interface IOscParser extends IDisposable { - addOscHandler(ident: number, handler: IOscHandler): IDisposable; - setOscHandler(ident: number, handler: IOscHandler): void; - clearOscHandler(ident: number): void; - setOscHandlerFallback(handler: OscFallbackHandler): void; +/** + * Subparser interfaces. + * The subparsers are instantiated in `EscapeSequenceParser` and + * called during `EscapeSequenceParser.parse`. + */ +export interface ISubParser extends IDisposable { reset(): void; - start(): void; + addHandler(ident: number, handler: T): IDisposable; + setHandler(ident: number, handler: T): void; + clearHandler(ident: number): void; + setHandlerFallback(handler: U): void; put(data: Uint32Array, start: number, end: number): void; +} + +export interface IOscParser extends ISubParser { + start(): void; end(success: boolean): void; } -export interface IDcsParser extends IDisposable { - addDcsHandler(ident: number, handler: IDcsHandler): IDisposable; - setDcsHandler(ident: number, handler: IDcsHandler): void; - clearDcsHandler(ident: number): void; - setDcsHandlerFallback(handler: DcsFallbackHandler): void; - reset(): void; +export interface IDcsParser extends ISubParser { hook(ident: number, params: IParams): void; - put(data: Uint32Array, start: number, end: number): void; unhook(success: boolean): void; } +/** + * Interface to denote a specific ESC, CSI or DCS handler slot. + * The values are used to create an integer respresentation during handler + * regristation before passed to the subparsers as `ident`. + * The integer translation is made to allow faster key-value access + * in `EscapeSequenceParser.parse`. + */ export interface IFunctionIdentifier { prefix?: string; intermediates?: string; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index f19c8ce3..4b352210 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -21,7 +21,6 @@ export class Terminal implements ITerminalApi { constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); this._addonManager = new AddonManager(); - this._parser = new ParserApi(this._core); } public get onCursorMove(): IEvent { return this._core.onCursorMove; } @@ -35,7 +34,12 @@ export class Terminal implements ITerminalApi { public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } public get element(): HTMLElement { return this._core.element; } - public get parser(): IParser { return this._parser; } + public get parser(): IParser { + if (!this._parser) { + this._parser = new ParserApi(this._core); + } + return this._parser; + } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 1ba4be1f..c4050675 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -970,7 +970,7 @@ declare module 'xterm' { } /** - * Parser interface. + * (EXPERIMENTAL) Parser interface. */ export interface IParser { /** From b35383915d54e6e52c9334dd4bf72d5f52dacaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 11 Aug 2019 17:55:24 +0200 Subject: [PATCH 35/41] cleanup interfaces --- .../parser/EscapeSequenceParser.test.ts | 28 ++-- src/common/parser/EscapeSequenceParser.ts | 132 ++++++++--------- src/common/parser/Types.d.ts | 133 +++++++++++------- 3 files changed, 160 insertions(+), 133 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index e960387f..6835866e 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -1220,26 +1220,26 @@ describe('EscapeSequenceParser', function (): void { }); describe('ESC custom handlers', () => { it('prevent fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G']); }); it('allow fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom - %G', 'default - %G']); }); it('Multiple custom handlers fallback once', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; }); parse(parser2, INPUT); chai.expect(esc).eql(['custom2 - %G', 'custom - %G']); }); it('Multiple custom handlers no fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); parse(parser2, INPUT); @@ -1247,21 +1247,21 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => order.push(1)); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; }); parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; }); parse(parser2, '\x1b%G'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); parse(parser2, INPUT); chai.expect(esc).eql(['default - %G']); }); it('Should not corrupt the parser when dispose is called twice', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => esc.push('default - %G')); + parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); dispo.dispose(); @@ -1284,7 +1284,7 @@ describe('EscapeSequenceParser', function (): void { describe('CSI custom handlers', () => { it('Prevent fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); @@ -1292,7 +1292,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Allow fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); @@ -1301,7 +1301,7 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers fallback once', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); @@ -1312,7 +1312,7 @@ describe('EscapeSequenceParser', function (): void { it('Multiple custom handlers no fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); @@ -1322,7 +1322,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setCsiHandler({final: 'm'}, () => order.push(1)); + parser2.setCsiHandler({final: 'm'}, () => { order.push(1); }); parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; }); parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); @@ -1330,7 +1330,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Dispose should work', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); @@ -1339,7 +1339,7 @@ describe('EscapeSequenceParser', function (): void { }); it('Should not corrupt the parser when dispose is called twice', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => csi.push(['m', params.toArray(), ''])); + parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 2411ebdb..96392eda 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler, IFunctionIdentifier } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler, IFunctionIdentifier, ExecuteFallbackHandler, CsiFallbackHandler, EscFallbackHandler, PrintHandler, PrintFallbackHandler, ExecuteHandler } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -238,7 +238,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: number; // handler lookup containers - protected _printHandler: (data: Uint32Array, start: number, end: number) => void; + protected _printHandler: PrintHandler; protected _executeHandlers: any; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; @@ -247,10 +247,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; - protected _executeHandlerFb: (code: number) => void; - protected _csiHandlerFb: (ident: number, params: IParams) => void; - protected _escHandlerFb: (ident: number) => void; + protected _printHandlerFb: PrintFallbackHandler; + protected _executeHandlerFb: ExecuteFallbackHandler; + protected _csiHandlerFb: CsiFallbackHandler; + protected _escHandlerFb: EscFallbackHandler; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -334,41 +334,67 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.dispose(); } - public setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { - this._printHandler = callback; + public setPrintHandler(handler: PrintHandler): void { + this._printHandler = handler; } public clearPrintHandler(): void { this._printHandler = this._printHandlerFb; } - public setExecuteHandler(flag: string, callback: () => void): void { - this._executeHandlers[flag.charCodeAt(0)] = callback; - } - public clearExecuteHandler(flag: string): void { - if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; - } - public setExecuteHandlerFallback(callback: (code: number) => void): void { - this._executeHandlerFb = callback; - } - - public addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { - const ident = this._identifier(id); - if (this._csiHandlers[ident] === undefined) { - this._csiHandlers[ident] = []; + public addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable { + const ident = this._identifier(id, [0x30, 0x7e]); + if (this._escHandlers[ident] === undefined) { + this._escHandlers[ident] = []; } - const handlerList = this._csiHandlers[ident]; - handlerList.push(callback); + const handlerList = this._escHandlers[ident]; + handlerList.push(handler); return { dispose: () => { - const handlerIndex = handlerList.indexOf(callback); + const handlerIndex = handlerList.indexOf(handler); if (handlerIndex !== -1) { handlerList.splice(handlerIndex, 1); } } }; } - public setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { - this._csiHandlers[this._identifier(id)] = [callback]; + public setEscHandler(id: IFunctionIdentifier, handler: EscHandler): void { + this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler]; + } + public clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; + } + public setEscHandlerFallback(handler: EscFallbackHandler): void { + this._escHandlerFb = handler; + } + + public setExecuteHandler(flag: string, handler: ExecuteHandler): void { + this._executeHandlers[flag.charCodeAt(0)] = handler; + } + public clearExecuteHandler(flag: string): void { + if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; + } + public setExecuteHandlerFallback(handler: ExecuteFallbackHandler): void { + this._executeHandlerFb = handler; + } + + public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): IDisposable { + const ident = this._identifier(id); + if (this._csiHandlers[ident] === undefined) { + this._csiHandlers[ident] = []; + } + const handlerList = this._csiHandlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): void { + this._csiHandlers[this._identifier(id)] = [handler]; } public clearCsiHandler(id: IFunctionIdentifier): void { if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; @@ -377,45 +403,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._csiHandlerFb = callback; } - public addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { - const ident = this._identifier(id, [0x30, 0x7e]); - if (this._escHandlers[ident] === undefined) { - this._escHandlers[ident] = []; - } - const handlerList = this._escHandlers[ident]; - handlerList.push(callback); - return { - dispose: () => { - const handlerIndex = handlerList.indexOf(callback); - if (handlerIndex !== -1) { - handlerList.splice(handlerIndex, 1); - } - } - }; - } - public setEscHandler(id: IFunctionIdentifier, callback: () => void): void { - this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [callback]; - } - public clearEscHandler(id: IFunctionIdentifier): void { - if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; - } - public setEscHandlerFallback(callback: (ident: number) => void): void { - this._escHandlerFb = callback; - } - - public addOscHandler(ident: number, handler: IOscHandler): IDisposable { - return this._oscParser.addHandler(ident, handler); - } - public setOscHandler(ident: number, handler: IOscHandler): void { - this._oscParser.setHandler(ident, handler); - } - public clearOscHandler(ident: number): void { - this._oscParser.clearHandler(ident); - } - public setOscHandlerFallback(handler: OscFallbackHandler): void { - this._oscParser.setHandlerFallback(handler); - } - public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { return this._dcsParser.addHandler(this._identifier(id), handler); } @@ -429,6 +416,19 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.setHandlerFallback(handler); } + public addOscHandler(ident: number, handler: IOscHandler): IDisposable { + return this._oscParser.addHandler(ident, handler); + } + public setOscHandler(ident: number, handler: IOscHandler): void { + this._oscParser.setHandler(ident, handler); + } + public clearOscHandler(ident: number): void { + this._oscParser.clearHandler(ident); + } + public setOscHandlerFallback(handler: OscFallbackHandler): void { + this._oscParser.setHandlerFallback(handler); + } + public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { this._errorHandler = callback; } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 1a43f773..6a165bd1 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -61,66 +61,85 @@ export interface IParsingState { abort: boolean; } -export interface IHandlerCollection { - [key: string]: T[]; -} - -export type CsiHandler = (params: IParams) => boolean | void; -export type EscHandler = () => boolean | void; +/** + * Command handler interfaces. + */ /** -* DCS handler signature for EscapeSequenceParser. -* EscapeSequenceParser handles DCS commands via separate -* subparsers that get hook/unhooked and can handle -* 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 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. `success` -* indicates whether the command was aborted. -*/ + * DCS handler types. + */ export interface IDcsHandler { + /** + * Called when a DCS command starts. + * Prepare needed data structures here. + * Note: `params` is borrowed. + */ hook(params: IParams): void; + /** + * Incoming payload chunk. + * Note: `params` is borrowed. + */ put(data: Uint32Array, start: number, end: number): void; + /** + * End of DCS command. `success` indicates whether the + * command finished normally or got aborted, thus final + * execution of the command should depend on `success`. + * To save memory also cleanup data structures here. + */ unhook(success: boolean): void | boolean; } - export type DcsFallbackHandler = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; +/** + * ESC handler types. + */ +export type EscHandler = () => boolean | void; +export type EscFallbackHandler = (identifier: number) => void; + +/** + * EXECUTE handler types. + */ +export type ExecuteHandler = () => boolean | void; +export type ExecuteFallbackHandler = (ident: number) => void; + +/** + * CSI handler types. + * Note: `params` is borrowed. + */ +export type CsiHandler = (params: IParams) => boolean | void; +export type CsiFallbackHandler = (ident: number, params: IParams) => void; + +/** + * OSC handler types. + */ export interface IOscHandler { /** * Announces start of this OSC command. * Prepare needed data structures here. */ start(): void; - /** * Incoming data chunk. + * Note: Data is borrowed. */ put(data: Uint32Array, start: number, end: number): void; - /** * End of OSC command. `success` indicates whether the - * command finished normally or got aborted, thus execution - * of the command should depend on `success`. - * To save memory cleanup data structures in `.end`. + * command finished normally or got aborted, thus final + * execution of the command should depend on `success`. + * To save memory also cleanup data structures here. */ end(success: boolean): void | boolean; } - export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; +/** + * PRINT handler types. + */ +export type PrintHandler = (data: Uint32Array, start: number, end: number) => void; +export type PrintFallbackHandler = PrintHandler; + + /** * EscapeSequenceParser interface. */ @@ -144,38 +163,42 @@ export interface IEscapeSequenceParser extends IDisposable { parse(data: Uint32Array, length: number): void; /** - * Get string from ident number. + * Get string from numercial function identifier `ident`. + * Useful in fallback handlers which expose the low level + * numcerical function identifier for debugging purposes. + * Note: A full back translation to `IFunctionIdentifier` + * is not implemented. */ identToString(ident: number): string; - setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; + setPrintHandler(handler: PrintHandler): void; clearPrintHandler(): void; - setExecuteHandler(flag: string, callback: () => void): void; - clearExecuteHandler(flag: string): void; - setExecuteHandlerFallback(callback: (code: number) => void): void; - - setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void; - clearCsiHandler(id: IFunctionIdentifier): void; - setCsiHandlerFallback(callback: (identifier: number, params: IParams) => void): void; - addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - - setEscHandler(id: IFunctionIdentifier, callback: () => void): void; + setEscHandler(id: IFunctionIdentifier, handler: EscHandler): void; clearEscHandler(id: IFunctionIdentifier): void; - setEscHandlerFallback(callback: (identifier: number) => void): void; + setEscHandlerFallback(handler: EscFallbackHandler): void; addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable; - setOscHandler(ident: number, handler: IOscHandler): void; - clearOscHandler(ident: number): void; - setOscHandlerFallback(handler: OscFallbackHandler): void; - addOscHandler(ident: number, handler: IOscHandler): IDisposable; + setExecuteHandler(flag: string, handler: ExecuteHandler): void; + clearExecuteHandler(flag: string): void; + setExecuteHandlerFallback(handler: ExecuteFallbackHandler): void; + + setCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): void; + clearCsiHandler(id: IFunctionIdentifier): void; + setCsiHandlerFallback(callback: CsiFallbackHandler): void; + addCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): IDisposable; setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; clearDcsHandler(id: IFunctionIdentifier): void; setDcsHandlerFallback(handler: DcsFallbackHandler): void; addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; - setErrorHandler(callback: (state: IParsingState) => IParsingState): void; + setOscHandler(ident: number, handler: IOscHandler): void; + clearOscHandler(ident: number): void; + setOscHandlerFallback(handler: OscFallbackHandler): void; + addOscHandler(ident: number, handler: IOscHandler): IDisposable; + + setErrorHandler(handler: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; } @@ -207,7 +230,7 @@ export interface IDcsParser extends ISubParser * Interface to denote a specific ESC, CSI or DCS handler slot. * The values are used to create an integer respresentation during handler * regristation before passed to the subparsers as `ident`. - * The integer translation is made to allow faster key-value access + * The integer translation is made to allow a faster handler access * in `EscapeSequenceParser.parse`. */ export interface IFunctionIdentifier { @@ -215,3 +238,7 @@ export interface IFunctionIdentifier { intermediates?: string; final: string; } + +export interface IHandlerCollection { + [key: string]: T[]; +} From 3a5d0bd368b353a438a3cff8dd8b0012d5b24d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 11 Aug 2019 18:38:35 +0200 Subject: [PATCH 36/41] fix escHandlers type and disposing --- src/common/parser/EscapeSequenceParser.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 96392eda..c8f4d119 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: PrintHandler; - protected _executeHandlers: any; + protected _executeHandlers: {[flag: number]: ExecuteHandler}; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; @@ -328,7 +328,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } public dispose(): void { - this._executeHandlers = null; + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); this._escHandlers = Object.create(null); this._oscParser.dispose(); this._dcsParser.dispose(); From a2c266a0052f54d543356a6cb791ff99735dcfb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 11 Aug 2019 18:45:34 +0200 Subject: [PATCH 37/41] rename handler type aliases to XyType to avoid ambiguity with class based types --- src/common/parser/DcsParser.ts | 6 +- .../parser/EscapeSequenceParser.test.ts | 6 +- src/common/parser/EscapeSequenceParser.ts | 38 ++++++------- src/common/parser/OscParser.ts | 6 +- src/common/parser/Types.d.ts | 56 +++++++++---------- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index a6d2a6d7..4622c4ad 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -15,7 +15,7 @@ export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; - private _handlerFb: DcsFallbackHandler = () => {}; + private _handlerFb: DcsFallbackHandlerType = () => {}; public dispose(): void { this._handlers = Object.create(null); @@ -46,7 +46,7 @@ export class DcsParser implements IDcsParser { if (this._handlers[ident]) delete this._handlers[ident]; } - public setHandlerFallback(handler: DcsFallbackHandler): void { + public setHandlerFallback(handler: DcsFallbackHandlerType): void { this._handlerFb = handler; } diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 6835866e..b9a5ae4e 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; @@ -24,7 +24,7 @@ function r(a: number, b: number): string[] { } class MockOscPutParser implements IOscParser { - private _fallback: OscFallbackHandler = () => {}; + private _fallback: OscFallbackHandlerType = () => {}; public data = ''; public reset(): void { this.data = ''; @@ -50,7 +50,7 @@ class MockOscPutParser implements IOscParser { clearHandler(ident: number): void { throw new Error('not implemented'); } - setHandlerFallback(handler: OscFallbackHandler): void { + setHandlerFallback(handler: OscFallbackHandlerType): void { this._fallback = handler; } } diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index c8f4d119..55bac11e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler, IFunctionIdentifier, ExecuteFallbackHandler, CsiFallbackHandler, EscFallbackHandler, PrintHandler, PrintFallbackHandler, ExecuteHandler } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -238,19 +238,19 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: number; // handler lookup containers - protected _printHandler: PrintHandler; - protected _executeHandlers: {[flag: number]: ExecuteHandler}; - protected _csiHandlers: IHandlerCollection; - protected _escHandlers: IHandlerCollection; + protected _printHandler: PrintHandlerType; + protected _executeHandlers: {[flag: number]: ExecuteHandlerType}; + protected _csiHandlers: IHandlerCollection; + protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; protected _dcsParser: IDcsParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: PrintFallbackHandler; - protected _executeHandlerFb: ExecuteFallbackHandler; - protected _csiHandlerFb: CsiFallbackHandler; - protected _escHandlerFb: EscFallbackHandler; + protected _printHandlerFb: PrintFallbackHandlerType; + protected _executeHandlerFb: ExecuteFallbackHandlerType; + protected _csiHandlerFb: CsiFallbackHandlerType; + protected _escHandlerFb: EscFallbackHandlerType; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -335,14 +335,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.dispose(); } - public setPrintHandler(handler: PrintHandler): void { + public setPrintHandler(handler: PrintHandlerType): void { this._printHandler = handler; } public clearPrintHandler(): void { this._printHandler = this._printHandlerFb; } - public addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable { + public addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { const ident = this._identifier(id, [0x30, 0x7e]); if (this._escHandlers[ident] === undefined) { this._escHandlers[ident] = []; @@ -358,27 +358,27 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - public setEscHandler(id: IFunctionIdentifier, handler: EscHandler): void { + public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void { this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler]; } public clearEscHandler(id: IFunctionIdentifier): void { if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } - public setEscHandlerFallback(handler: EscFallbackHandler): void { + public setEscHandlerFallback(handler: EscFallbackHandlerType): void { this._escHandlerFb = handler; } - public setExecuteHandler(flag: string, handler: ExecuteHandler): void { + public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void { this._executeHandlers[flag.charCodeAt(0)] = handler; } public clearExecuteHandler(flag: string): void { if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; } - public setExecuteHandlerFallback(handler: ExecuteFallbackHandler): void { + public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void { this._executeHandlerFb = handler; } - public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): IDisposable { + public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { const ident = this._identifier(id); if (this._csiHandlers[ident] === undefined) { this._csiHandlers[ident] = []; @@ -394,7 +394,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): void { + public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void { this._csiHandlers[this._identifier(id)] = [handler]; } public clearCsiHandler(id: IFunctionIdentifier): void { @@ -413,7 +413,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public clearDcsHandler(id: IFunctionIdentifier): void { this._dcsParser.clearHandler(this._identifier(id)); } - public setDcsHandlerFallback(handler: DcsFallbackHandler): void { + public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void { this._dcsParser.setHandlerFallback(handler); } @@ -426,7 +426,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public clearOscHandler(ident: number): void { this._oscParser.clearHandler(ident); } - public setOscHandlerFallback(handler: OscFallbackHandler): void { + public setOscHandlerFallback(handler: OscFallbackHandlerType): void { this._oscParser.setHandlerFallback(handler); } diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 9f600e97..e8c5a801 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IOscHandler, IHandlerCollection, OscFallbackHandler, IOscParser } from 'common/parser/Types'; +import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types'; import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; @@ -13,7 +13,7 @@ export class OscParser implements IOscParser { private _state = OscState.START; private _id = -1; private _handlers: IHandlerCollection = Object.create(null); - private _handlerFb: OscFallbackHandler = () => { }; + private _handlerFb: OscFallbackHandlerType = () => { }; public addHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { @@ -36,7 +36,7 @@ export class OscParser implements IOscParser { public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - public setHandlerFallback(handler: OscFallbackHandler): void { + public setHandlerFallback(handler: OscFallbackHandlerType): void { this._handlerFb = handler; } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 6a165bd1..e2fac89f 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -65,6 +65,13 @@ export interface IParsingState { * Command handler interfaces. */ +/** + * CSI handler types. + * Note: `params` is borrowed. + */ +export type CsiHandlerType = (params: IParams) => boolean | void; +export type CsiFallbackHandlerType = (ident: number, params: IParams) => void; + /** * DCS handler types. */ @@ -88,26 +95,19 @@ export interface IDcsHandler { */ unhook(success: boolean): void | boolean; } -export type DcsFallbackHandler = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; +export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; /** * ESC handler types. */ -export type EscHandler = () => boolean | void; -export type EscFallbackHandler = (identifier: number) => void; +export type EscHandlerType = () => boolean | void; +export type EscFallbackHandlerType = (identifier: number) => void; /** * EXECUTE handler types. */ -export type ExecuteHandler = () => boolean | void; -export type ExecuteFallbackHandler = (ident: number) => void; - -/** - * CSI handler types. - * Note: `params` is borrowed. - */ -export type CsiHandler = (params: IParams) => boolean | void; -export type CsiFallbackHandler = (ident: number, params: IParams) => void; +export type ExecuteHandlerType = () => boolean | void; +export type ExecuteFallbackHandlerType = (ident: number) => void; /** * OSC handler types. @@ -131,13 +131,13 @@ export interface IOscHandler { */ end(success: boolean): void | boolean; } -export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; +export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; /** * PRINT handler types. */ -export type PrintHandler = (data: Uint32Array, start: number, end: number) => void; -export type PrintFallbackHandler = PrintHandler; +export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void; +export type PrintFallbackHandlerType = PrintHandlerType; /** @@ -171,31 +171,31 @@ export interface IEscapeSequenceParser extends IDisposable { */ identToString(ident: number): string; - setPrintHandler(handler: PrintHandler): void; + setPrintHandler(handler: PrintHandlerType): void; clearPrintHandler(): void; - setEscHandler(id: IFunctionIdentifier, handler: EscHandler): void; + setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void; clearEscHandler(id: IFunctionIdentifier): void; - setEscHandlerFallback(handler: EscFallbackHandler): void; - addEscHandler(id: IFunctionIdentifier, handler: EscHandler): IDisposable; + setEscHandlerFallback(handler: EscFallbackHandlerType): void; + addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable; - setExecuteHandler(flag: string, handler: ExecuteHandler): void; + setExecuteHandler(flag: string, handler: ExecuteHandlerType): void; clearExecuteHandler(flag: string): void; - setExecuteHandlerFallback(handler: ExecuteFallbackHandler): void; + setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void; - setCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): void; + setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void; clearCsiHandler(id: IFunctionIdentifier): void; - setCsiHandlerFallback(callback: CsiFallbackHandler): void; - addCsiHandler(id: IFunctionIdentifier, handler: CsiHandler): IDisposable; + setCsiHandlerFallback(callback: CsiFallbackHandlerType): void; + addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable; setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; clearDcsHandler(id: IFunctionIdentifier): void; - setDcsHandlerFallback(handler: DcsFallbackHandler): void; + setDcsHandlerFallback(handler: DcsFallbackHandlerType): void; addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; setOscHandler(ident: number, handler: IOscHandler): void; clearOscHandler(ident: number): void; - setOscHandlerFallback(handler: OscFallbackHandler): void; + setOscHandlerFallback(handler: OscFallbackHandlerType): void; addOscHandler(ident: number, handler: IOscHandler): IDisposable; setErrorHandler(handler: (state: IParsingState) => IParsingState): void; @@ -216,12 +216,12 @@ export interface ISubParser extends IDisposable { put(data: Uint32Array, start: number, end: number): void; } -export interface IOscParser extends ISubParser { +export interface IOscParser extends ISubParser { start(): void; end(success: boolean): void; } -export interface IDcsParser extends ISubParser { +export interface IDcsParser extends ISubParser { hook(ident: number, params: IParams): void; unhook(success: boolean): void; } From 63fd3be67acc74048a29369c770cda867ceefa01 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 12 Aug 2019 17:56:26 -0700 Subject: [PATCH 38/41] Ensure selection service mousedown happens after mouse zone manager Fixes #2380 --- src/Terminal.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index ef05c664..07150aad 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -643,7 +643,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.screenElement); this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); - this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this._selectionService.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it @@ -664,6 +663,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); + // This event listener must be registered aftre MouseZoneManager is created + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); + // apply mouse event classes set by escape codes before terminal was attached if (this.mouseEvents) { this._selectionService.disable(); From 85444def9f10da776916a35c52b5f995d74ca253 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 16 Aug 2019 10:50:44 -0400 Subject: [PATCH 39/41] Don't scroll terminal search result if within viewport --- addons/xterm-addon-search/src/SearchAddon.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index e9f8a9b4..caf2915e 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -343,9 +343,13 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.term.length); - let scroll = result.row - terminal.buffer.viewportY; - scroll = scroll - Math.floor(terminal.rows / 2); - terminal.scrollLines(scroll); + // If it is not in the viewport then we scroll else it just gets selected + if (result.row > (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { + let scroll = result.row - terminal.buffer.viewportY; + scroll = scroll - Math.floor(terminal.rows / 2); + terminal.scrollLines(scroll); + console.log('scrolling'); + } return true; } } From f3d8e74d6f4f0b11df4d93a1a019bf0de476398b Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 16 Aug 2019 10:55:16 -0400 Subject: [PATCH 40/41] Remove forgotten console .log --- addons/xterm-addon-search/src/SearchAddon.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index caf2915e..4d3e8841 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -348,7 +348,6 @@ export class SearchAddon implements ITerminalAddon { let scroll = result.row - terminal.buffer.viewportY; scroll = scroll - Math.floor(terminal.rows / 2); terminal.scrollLines(scroll); - console.log('scrolling'); } return true; } From 1c20057ed3bd772146ac2377044459729d815884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 16 Aug 2019 21:14:49 +0200 Subject: [PATCH 41/41] add parser property to interface --- typings/xterm.d.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d2c33c87..4ec6fc1c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -386,6 +386,12 @@ declare module 'xterm' { */ readonly markers: ReadonlyArray; + /** + * (EXPERIMENTAL) Get the parser interface to register + * custom escape sequence handlers. + */ + readonly parser: IParser; + /** * Natural language strings that can be localized. */ @@ -919,7 +925,8 @@ declare module 'xterm' { } /** - * Data type to register a CSI, DCS or ESC callback in the parser in the form: + * (EXPERIMENTAL) Data type to register a CSI, DCS or ESC callback in the parser + * in the form: * ESC I..I F * CSI Prefix P..P I..I F * DCS Prefix P..P I..I F data_bytes ST