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] 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.