From 4d660deff3db9f3eea3923dcaa970fde0e9deaf8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 4 Dec 2018 19:00:44 -0800 Subject: [PATCH 01/19] hooks for custom control sequences This fixes (at least partially) issue #1176 "Add a way to plugin a custom control sequence handler". --- src/EscapeSequenceParser.ts | 74 +++++++++++++++++++++++++++++++++---- src/InputHandler.ts | 17 ++++++++- src/Terminal.ts | 6 ++- src/Types.ts | 14 +++++++ src/public/Terminal.ts | 5 ++- 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index b38c50f543..7a5da2c851 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -301,9 +301,39 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } + private _removeHandler(array: any[], callback: any): void { + if (array) { + for (let i = array.length; --i >= 0; ) { + if (array[i] == callback) { + array.splice(i, 1); + return; + } + } + } + } + + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + let index = flag.charCodeAt(0); + let array = this._csiHandlers[index]; + if (! array) { this._csiHandlers[index] = array = new Array(); } + array.push(callback); + } + + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + let index = flag.charCodeAt(0); + let array = this._csiHandlers[index]; + this._removeHandler(array, callback); + if (array && array.length == 0) + delete this._csiHandlers[index]; + } + /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = callback; + this.clearCsiHandler(flag); + this.addCsiHandler(flag, (params: number[], collect: string): boolean => { + callback(params, collect); return true; + }); } + /* deprecated */ clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; } @@ -321,9 +351,25 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } + addOscHandler(ident: number, callback: (data: string) => boolean): void { + let array = this._oscHandlers[ident]; + if (! array) { this._oscHandlers[ident] = array = new Array(); } + array.push(callback); + } + removeOscHandler(ident: number, callback: (data: string) => boolean): void { + let array = this._oscHandlers[ident]; + this._removeHandler(array, callback); + if (array && array.length == 0) + delete this._oscHandlers[ident]; + } + /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = callback; + this.clearOscHandler(ident); + this.addOscHandler(ident, (data: string): boolean => { + callback(data); return true; + }); } + /* deprecated */ clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; } @@ -463,9 +509,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - callback = this._csiHandlers[code]; - if (callback) callback(params, collect); - else this._csiHandlerFb(collect, params, code); + let cHandler = this._csiHandlers[code]; + if (cHandler) { + for (let i = cHandler.length; ;) { + if (--i < 0) { cHandler = null; break; } + if ((cHandler[i])(params, collect)) + break; + } + } + if (! cHandler) this._csiHandlerFb(collect, params, code); break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -532,9 +584,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - callback = this._oscHandlers[identifier]; - if (callback) callback(content); - else this._oscHandlerFb(identifier, content); + let oHandler = this._oscHandlers[identifier]; + if (oHandler) { + for (let i = oHandler.length; ;) { + if (--i < 0) { oHandler = null; break; } + if ((oHandler[i])(content)) + break; + } + } + if (! oHandler) this._oscHandlerFb(identifier, content); } } if (code === 0x1b) transition |= ParserState.ESCAPE; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f69..2c931a2bb0 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -112,7 +112,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IInputHandler { +export class InputHandler extends Disposable implements IVtInputHandler { private _surrogateFirst: string; constructor( @@ -465,6 +465,19 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + this._parser.addCsiHandler(flag, callback); + } + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { + this._parser.removeCsiHandler(flag, callback); + } + addOscHandler(ident: number, callback: (data: string) => boolean): void { + this._parser.setOscHandler(ident, callback); + } + removeOscHandler(ident: number, callback: (data: string) => boolean): void { + this._parser.removeOscHandler(ident, callback); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index 2cfc1ca88b..fea0cb699d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1286,6 +1286,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } + public get inputHandler(): IInputHandler { + return this._inputHandler; + } + /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). diff --git a/src/Types.ts b/src/Types.ts index 430c6575bd..93dd7c8aa8 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,6 +182,16 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } +/* + * An InputHandler for VT-style terminals + */ +export interface IVtInputHandler extends IInputHandler { + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + addOscHandler(ident: number, callback: (data: string) => boolean): void; + removeOscHandler(ident: number, callback: (data: string) => boolean): void; +} + export interface ILinkMatcher { id: number; regex: RegExp; @@ -492,6 +502,10 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; + addOscHandler(ident: number, callback: (data: string) => boolean): void; + removeOscHandler(ident: number, callback: (data: string) => boolean): void; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 8ff7cf2b3b..de15fad08c 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal } from '../Types'; +import { ITerminal, IInputHandler } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,6 +15,9 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } + public get inputHandler(): IInputHandler { + return (this._core as TerminalCore).inputHandler; + } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } From b68974517e2f9f02b0304db064020e25afc527fd Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 5 Dec 2018 14:45:57 -0800 Subject: [PATCH 02/19] Cleanups required by tslink. --- src/EscapeSequenceParser.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 7a5da2c851..5a2c6cd37a 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -304,7 +304,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP private _removeHandler(array: any[], callback: any): void { if (array) { for (let i = array.length; --i >= 0; ) { - if (array[i] == callback) { + if (array[i] === callback) { array.splice(i, 1); return; } @@ -313,18 +313,19 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); + const index = flag.charCodeAt(0); let array = this._csiHandlers[index]; if (! array) { this._csiHandlers[index] = array = new Array(); } array.push(callback); } removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); - let array = this._csiHandlers[index]; + const index = flag.charCodeAt(0); + const array = this._csiHandlers[index]; this._removeHandler(array, callback); - if (array && array.length == 0) + if (array && array.length === 0) { delete this._csiHandlers[index]; + } } /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { @@ -357,10 +358,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP array.push(callback); } removeOscHandler(ident: number, callback: (data: string) => boolean): void { - let array = this._oscHandlers[ident]; + const array = this._oscHandlers[ident]; this._removeHandler(array, callback); - if (array && array.length == 0) + if (array && array.length === 0) { delete this._oscHandlers[ident]; + } } /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { @@ -513,8 +515,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (cHandler) { for (let i = cHandler.length; ;) { if (--i < 0) { cHandler = null; break; } - if ((cHandler[i])(params, collect)) - break; + if ((cHandler[i])(params, collect)) { break; } } } if (! cHandler) this._csiHandlerFb(collect, params, code); @@ -588,8 +589,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (oHandler) { for (let i = oHandler.length; ;) { if (--i < 0) { oHandler = null; break; } - if ((oHandler[i])(content)) - break; + if ((oHandler[i])(content)) { break; } } } if (! oHandler) this._oscHandlerFb(identifier, content); From 03115639a959c19ad9a5b6e112cab2066cb46c7f Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 9 Dec 2018 10:56:34 -0800 Subject: [PATCH 03/19] Optimize parsing of OSC_STRING to minimize string concatenation. --- src/EscapeSequenceParser.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 5a2c6cd37a..28a946d1dc 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -439,7 +439,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } // normal transition & action lookup - transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; + transition = (code < 0xa0 + ? (table[currentState << 8 | code]) + : currentState === ParserState.OSC_STRING + ? (ParserAction.OSC_PUT << 4) | ParserState.OSC_STRING + : DEFAULT_TRANSITION); switch (transition >> 4) { case ParserAction.PRINT: print = (~print) ? print : i; @@ -471,10 +475,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserState.GROUND: print = (~print) ? print : i; break; - case ParserState.OSC_STRING: - osc += String.fromCharCode(code); - transition |= ParserState.OSC_STRING; - break; case ParserState.CSI_IGNORE: transition |= ParserState.CSI_IGNORE; break; @@ -570,7 +570,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; break; case ParserAction.OSC_PUT: - osc += data.charAt(i); + for (let j = i + 1; ; j++) { + if (j >= l + || ((code = data.charCodeAt(j)) <= 0x9f + && (table[ParserState.OSC_STRING << 8 | code] >> 4 + !== ParserAction.OSC_PUT))) { + osc += data.substring(i, j); + i = j - 1; + break; + } + } break; case ParserAction.OSC_END: if (osc && code !== 0x18 && code !== 0x1a) { From 30a667c3be32cbccc57405b58a787a41668491e4 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 9 Dec 2018 13:11:17 -0800 Subject: [PATCH 04/19] Revert "Optimize parsing of OSC_STRING to minimize string concatenation." This reverts commit 03115639a959c19ad9a5b6e112cab2066cb46c7f. --- src/EscapeSequenceParser.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 28a946d1dc..5a2c6cd37a 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -439,11 +439,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } // normal transition & action lookup - transition = (code < 0xa0 - ? (table[currentState << 8 | code]) - : currentState === ParserState.OSC_STRING - ? (ParserAction.OSC_PUT << 4) | ParserState.OSC_STRING - : DEFAULT_TRANSITION); + transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; switch (transition >> 4) { case ParserAction.PRINT: print = (~print) ? print : i; @@ -475,6 +471,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserState.GROUND: print = (~print) ? print : i; break; + case ParserState.OSC_STRING: + osc += String.fromCharCode(code); + transition |= ParserState.OSC_STRING; + break; case ParserState.CSI_IGNORE: transition |= ParserState.CSI_IGNORE; break; @@ -570,16 +570,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; break; case ParserAction.OSC_PUT: - for (let j = i + 1; ; j++) { - if (j >= l - || ((code = data.charCodeAt(j)) <= 0x9f - && (table[ParserState.OSC_STRING << 8 | code] >> 4 - !== ParserAction.OSC_PUT))) { - osc += data.substring(i, j); - i = j - 1; - break; - } - } + osc += data.charAt(i); break; case ParserAction.OSC_END: if (osc && code !== 0x18 && code !== 0x1a) { From ffb2708a8128a0cf1637c1f57de3c35ceca6029b Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 11 Dec 2018 16:50:14 -0800 Subject: [PATCH 05/19] Revert "Cleanups required by tslink." This reverts commit b68974517e2f9f02b0304db064020e25afc527fd. --- src/EscapeSequenceParser.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 76b9dd686f..90405a4275 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -306,7 +306,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP private _removeHandler(array: any[], callback: any): void { if (array) { for (let i = array.length; --i >= 0; ) { - if (array[i] === callback) { + if (array[i] == callback) { array.splice(i, 1); return; } @@ -315,19 +315,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - const index = flag.charCodeAt(0); + let index = flag.charCodeAt(0); let array = this._csiHandlers[index]; if (! array) { this._csiHandlers[index] = array = new Array(); } array.push(callback); } removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - const index = flag.charCodeAt(0); - const array = this._csiHandlers[index]; + let index = flag.charCodeAt(0); + let array = this._csiHandlers[index]; this._removeHandler(array, callback); - if (array && array.length === 0) { + if (array && array.length == 0) delete this._csiHandlers[index]; - } } /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { @@ -360,11 +359,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP array.push(callback); } removeOscHandler(ident: number, callback: (data: string) => boolean): void { - const array = this._oscHandlers[ident]; + let array = this._oscHandlers[ident]; this._removeHandler(array, callback); - if (array && array.length === 0) { + if (array && array.length == 0) delete this._oscHandlers[ident]; - } } /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { @@ -513,7 +511,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (cHandler) { for (let i = cHandler.length; ;) { if (--i < 0) { cHandler = null; break; } - if ((cHandler[i])(params, collect)) { break; } + if ((cHandler[i])(params, collect)) + break; } } if (! cHandler) this._csiHandlerFb(collect, params, code); @@ -595,7 +594,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (oHandler) { for (let i = oHandler.length; ;) { if (--i < 0) { oHandler = null; break; } - if ((oHandler[i])(content)) { break; } + if ((oHandler[i])(content)) + break; } } if (! oHandler) this._oscHandlerFb(identifier, content); From 53fd04a8867a9e67519df38600d2a4c08b0e3065 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 11 Dec 2018 16:50:40 -0800 Subject: [PATCH 06/19] Revert "hooks for custom control sequences" This reverts commit 4d660deff3db9f3eea3923dcaa970fde0e9deaf8. --- src/EscapeSequenceParser.ts | 74 ++++--------------------------------- src/InputHandler.ts | 17 +-------- src/Terminal.ts | 6 +-- src/Types.ts | 14 ------- src/public/Terminal.ts | 5 +-- 5 files changed, 12 insertions(+), 104 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 90405a4275..f489884177 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -303,39 +303,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - private _removeHandler(array: any[], callback: any): void { - if (array) { - for (let i = array.length; --i >= 0; ) { - if (array[i] == callback) { - array.splice(i, 1); - return; - } - } - } - } - - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); - let array = this._csiHandlers[index]; - if (! array) { this._csiHandlers[index] = array = new Array(); } - array.push(callback); - } - - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - let index = flag.charCodeAt(0); - let array = this._csiHandlers[index]; - this._removeHandler(array, callback); - if (array && array.length == 0) - delete this._csiHandlers[index]; - } - /* deprecated */ setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this.clearCsiHandler(flag); - this.addCsiHandler(flag, (params: number[], collect: string): boolean => { - callback(params, collect); return true; - }); + this._csiHandlers[flag.charCodeAt(0)] = callback; } - /* deprecated */ clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; } @@ -353,25 +323,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } - addOscHandler(ident: number, callback: (data: string) => boolean): void { - let array = this._oscHandlers[ident]; - if (! array) { this._oscHandlers[ident] = array = new Array(); } - array.push(callback); - } - removeOscHandler(ident: number, callback: (data: string) => boolean): void { - let array = this._oscHandlers[ident]; - this._removeHandler(array, callback); - if (array && array.length == 0) - delete this._oscHandlers[ident]; - } - /* deprecated */ setOscHandler(ident: number, callback: (data: string) => void): void { - this.clearOscHandler(ident); - this.addOscHandler(ident, (data: string): boolean => { - callback(data); return true; - }); + this._oscHandlers[ident] = callback; } - /* deprecated */ clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; } @@ -507,15 +461,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - let cHandler = this._csiHandlers[code]; - if (cHandler) { - for (let i = cHandler.length; ;) { - if (--i < 0) { cHandler = null; break; } - if ((cHandler[i])(params, collect)) - break; - } - } - if (! cHandler) this._csiHandlerFb(collect, params, code); + callback = this._csiHandlers[code]; + if (callback) callback(params, collect); + else this._csiHandlerFb(collect, params, code); break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -590,15 +538,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - let oHandler = this._oscHandlers[identifier]; - if (oHandler) { - for (let i = oHandler.length; ;) { - if (--i < 0) { oHandler = null; break; } - if ((oHandler[i])(content)) - break; - } - } - if (! oHandler) this._oscHandlerFb(identifier, content); + callback = this._oscHandlers[identifier]; + if (callback) callback(content); + else this._oscHandlerFb(identifier, content); } } if (code === 0x1b) transition |= ParserState.ESCAPE; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2c931a2bb0..7604b01f69 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -112,7 +112,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IVtInputHandler { +export class InputHandler extends Disposable implements IInputHandler { private _surrogateFirst: string; constructor( @@ -465,19 +465,6 @@ export class InputHandler extends Disposable implements IVtInputHandler { this._terminal.updateRange(buffer.y); } - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - this._parser.addCsiHandler(flag, callback); - } - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void { - this._parser.removeCsiHandler(flag, callback); - } - addOscHandler(ident: number, callback: (data: string) => boolean): void { - this._parser.setOscHandler(ident, callback); - } - removeOscHandler(ident: number, callback: (data: string) => boolean): void { - this._parser.removeOscHandler(ident, callback); - } - /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index 8f66ebe905..bc8fb103a2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1287,10 +1287,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } - public get inputHandler(): IInputHandler { - return this._inputHandler; - } - /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). diff --git a/src/Types.ts b/src/Types.ts index 22eff1c853..a5aa8add70 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,16 +182,6 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } -/* - * An InputHandler for VT-style terminals - */ -export interface IVtInputHandler extends IInputHandler { - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - addOscHandler(ident: number, callback: (data: string) => boolean): void; - removeOscHandler(ident: number, callback: (data: string) => boolean): void; -} - export interface ILinkMatcher { id: number; regex: RegExp; @@ -502,10 +492,6 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - removeCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): void; - addOscHandler(ident: number, callback: (data: string) => boolean): void; - removeOscHandler(ident: number, callback: (data: string) => boolean): void; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index de15fad08c..8ff7cf2b3b 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal, IInputHandler } from '../Types'; +import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,9 +15,6 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } - public get inputHandler(): IInputHandler { - return (this._core as TerminalCore).inputHandler; - } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } From 8ceea112f7a4d11532b03d39a06d319c08c13f18 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 11 Dec 2018 17:18:10 -0800 Subject: [PATCH 07/19] hooks for custom control sequences This re-implements addCsiHandler/addOscHandler to return an IDisposable. --- src/EscapeSequenceParser.ts | 53 ++++++++++++++++++++++++++++++++++++- src/InputHandler.ts | 12 +++++++-- src/Terminal.ts | 6 ++++- src/Types.ts | 10 +++++++ src/public/Terminal.ts | 5 +++- 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index f489884177..285629391e 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -4,6 +4,7 @@ */ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; /** @@ -41,7 +42,7 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - add(code: number, state: number, action: number | null, next: number | null): void { + add(code: number, state: number, action: number | null, next: number | null): void { this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); } @@ -303,6 +304,32 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + const index = flag.charCodeAt(0); + const oldHead = this._csiHandlers[index]; + const newHead = Object.assign( + (params: number[], collect: string): void => { + if (callback(params, collect)) { } + else if (newHead.nextHandler) { newHead.nextHandler(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + }, + { nextHandler: oldHead, + dispose(): void { + let previous = null; let cur = this._csiHandlers[index]; + for (; cur && cur.nextHandler; + previous = cur, cur = cur.nextHandler) { + if (cur === newHead) { + if (previous) { previous.nextHandler = cur.nextHandler; } + else { this._csiHandlers[index] = cur.nextHandler; } + break; + } + } + } + }); + this._csiHandlers[index] = newHead; + return newHead; + } + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { this._csiHandlers[flag.charCodeAt(0)] = callback; } @@ -323,6 +350,30 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + const oldHead = this._oscHandlers[ident]; + const newHead = Object.assign( + (data: string): void => { + if (callback(data)) { } + else if (newHead.nextHandler) { newHead.nextHandler(data); } + else { this._oscHandlerFb(ident, data); } + }, + { nextHandler: oldHead, + dispose(): void { + let previous = null; let cur = this._oscHandlers[ident]; + for (; cur && cur.nextHandler; + previous = cur, cur = cur.nextHandler) { + if (cur === newHead) { + if (previous) { previous.nextHandler = cur.nextHandler; } + else { this._oscHandlers[ident] = cur.nextHandler; } + break; + } + } + } + }); + this._oscHandlers[ident] = newHead; + return newHead; + } setOscHandler(ident: number, callback: (data: string) => void): void { this._oscHandlers[ident] = callback; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7604b01f69..2e016296f7 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; /** @@ -112,7 +113,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IInputHandler { +export class InputHandler extends Disposable implements IVtInputHandler { private _surrogateFirst: string; constructor( @@ -465,6 +466,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._parser.addCsiHandler(flag, callback); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._parser.addOscHandler(ident, callback); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Terminal.ts b/src/Terminal.ts index bc8fb103a2..8f66ebe905 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1287,6 +1287,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } + public get inputHandler(): IInputHandler { + return this._inputHandler; + } + /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). diff --git a/src/Types.ts b/src/Types.ts index a5aa8add70..e150b90b55 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,6 +182,14 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } +/* + * An InputHandler for VT-style terminals + */ +export interface IVtInputHandler extends IInputHandler { + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; +} + export interface ILinkMatcher { id: number; regex: RegExp; @@ -492,6 +500,8 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 8ff7cf2b3b..de15fad08c 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal } from '../Types'; +import { ITerminal, IInputHandler } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,6 +15,9 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } + public get inputHandler(): IInputHandler { + return (this._core as TerminalCore).inputHandler; + } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } From 5af4626ec7d44f0536024a7a9af66b463ee5d7d9 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 13 Dec 2018 17:33:33 -0800 Subject: [PATCH 08/19] Change addCsiHandler/addOscHandler to not use Object.assign. --- src/EscapeSequenceParser.ts | 47 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 285629391e..a104d58362 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -307,25 +307,26 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { const index = flag.charCodeAt(0); const oldHead = this._csiHandlers[index]; - const newHead = Object.assign( + const parser = this; + const newHead = (params: number[], collect: string): void => { - if (callback(params, collect)) { } - else if (newHead.nextHandler) { newHead.nextHandler(params, collect); } - else { this._csiHandlerFb(collect, params, index); } - }, - { nextHandler: oldHead, - dispose(): void { - let previous = null; let cur = this._csiHandlers[index]; + if (! callback(params, collect)) { + if (newHead.nextHandler) { newHead.nextHandler(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + } + }; + newHead.nextHandler = oldHead; + newHead.dispose = function (): void { + let previous = null; let cur = parser._csiHandlers[index]; for (; cur && cur.nextHandler; previous = cur, cur = cur.nextHandler) { if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } - else { this._csiHandlers[index] = cur.nextHandler; } + else { parser._csiHandlers[index] = cur.nextHandler; } break; } } - } - }); + }; this._csiHandlers[index] = newHead; return newHead; } @@ -352,25 +353,27 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { const oldHead = this._oscHandlers[ident]; - const newHead = Object.assign( + const parser = this; + const newHead = (data: string): void => { - if (callback(data)) { } - else if (newHead.nextHandler) { newHead.nextHandler(data); } - else { this._oscHandlerFb(ident, data); } - }, - { nextHandler: oldHead, - dispose(): void { - let previous = null; let cur = this._oscHandlers[ident]; + if (! callback(data)) { + if (newHead.nextHandler) { newHead.nextHandler(data); } + else { this._oscHandlerFb(ident, data); } + } + }; + newHead.nextHandler = oldHead; + newHead.dispose = + function (): void { + let previous = null; let cur = parser._oscHandlers[ident]; for (; cur && cur.nextHandler; previous = cur, cur = cur.nextHandler) { if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } - else { this._oscHandlers[ident] = cur.nextHandler; } + else { parser._oscHandlers[ident] = cur.nextHandler; } break; } } - } - }); + }; this._oscHandlers[ident] = newHead; return newHead; } From 8a5a03238fedded95e4cedfc9e80e3e0aec2ecc4 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 15 Dec 2018 09:42:19 -0800 Subject: [PATCH 09/19] New method _linkHandler used by both addCsiHandler and addOscHandler. --- src/EscapeSequenceParser.ts | 60 +++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index a104d58362..c85f670d4c 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -7,6 +7,10 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceP import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +interface IHandlerLink extends IDisposable { + nextHandler: IHandlerLink | null; +} + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -304,33 +308,38 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { - const index = flag.charCodeAt(0); - const oldHead = this._csiHandlers[index]; - const parser = this; - const newHead = - (params: number[], collect: string): void => { - if (! callback(params, collect)) { - if (newHead.nextHandler) { newHead.nextHandler(params, collect); } - else { this._csiHandlerFb(collect, params, index); } - } - }; - newHead.nextHandler = oldHead; + private _linkHandler(handlers: object[], index: number, newCallback: object): IDisposable { + const newHead: any = newCallback; + newHead.nextHandler = handlers[index] as IHandlerLink; newHead.dispose = function (): void { - let previous = null; let cur = parser._csiHandlers[index]; + let previous = null; + let cur = handlers[index] as IHandlerLink; for (; cur && cur.nextHandler; previous = cur, cur = cur.nextHandler) { if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } - else { parser._csiHandlers[index] = cur.nextHandler; } + else { handlers[index] = cur.nextHandler; } break; } } }; - this._csiHandlers[index] = newHead; + handlers[index] = newHead; return newHead; } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + const index = flag.charCodeAt(0); + const newHead = + (params: number[], collect: string): void => { + if (! callback(params, collect)) { + const next = (newHead as unknown as IHandlerLink).nextHandler; + if (next) { (next as any)(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + } + }; + return this._linkHandler(this._csiHandlers, index, newHead); + } + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { this._csiHandlers[flag.charCodeAt(0)] = callback; } @@ -352,30 +361,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - const oldHead = this._oscHandlers[ident]; - const parser = this; const newHead = (data: string): void => { if (! callback(data)) { - if (newHead.nextHandler) { newHead.nextHandler(data); } + const next = (newHead as unknown as IHandlerLink).nextHandler; + if (next) { (next as any)(data); } else { this._oscHandlerFb(ident, data); } } }; - newHead.nextHandler = oldHead; - newHead.dispose = - function (): void { - let previous = null; let cur = parser._oscHandlers[ident]; - for (; cur && cur.nextHandler; - previous = cur, cur = cur.nextHandler) { - if (cur === newHead) { - if (previous) { previous.nextHandler = cur.nextHandler; } - else { parser._oscHandlers[ident] = cur.nextHandler; } - break; - } - } - }; - this._oscHandlers[ident] = newHead; - return newHead; + return this._linkHandler(this._oscHandlers, ident, newHead); } setOscHandler(ident: number, callback: (data: string) => void): void { this._oscHandlers[ident] = callback; From 6b65ebd4aace41ddf0f7d663e5ff6c92a4a37948 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 15 Dec 2018 11:34:15 -0800 Subject: [PATCH 10/19] Various typing and API fixes, doc comments, typing test etc. --- fixtures/typings-test/typings-test.ts | 8 +++++++- src/InputHandler.ts | 4 ++-- src/Terminal.ts | 15 ++++++++++----- src/Types.ts | 8 -------- src/public/Terminal.ts | 11 +++++++---- src/ui/TestUtils.test.ts | 6 ++++++ typings/xterm.d.ts | 25 +++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 13da69616b..87d911c2a4 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -4,7 +4,7 @@ /// -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; namespace constructor { { @@ -119,6 +119,12 @@ namespace methods_core { const t: Terminal = new Terminal(); t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); + const d1: IDisposable = t.addCsiHandler("x", + (params: number[], collect: string): boolean => params[0]===1); + d1.dispose(); + const d2: IDisposable = t.addOscHandler(199, + (data: string): boolean => true); + d2.dispose(); } namespace options { { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2e016296f7..eb1ca1050b 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IVtInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -113,7 +113,7 @@ class DECRQSS implements IDcsHandler { * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand * each function's header comment. */ -export class InputHandler extends Disposable implements IVtInputHandler { +export class InputHandler extends Disposable implements IInputHandler { private _surrogateFirst: string; constructor( diff --git a/src/Terminal.ts b/src/Terminal.ts index 8f66ebe905..bed45e46ec 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IInputHandler, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -1287,10 +1287,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.refresh(0, this.rows - 1); } - public get inputHandler(): IInputHandler { - return this._inputHandler; - } - /** * Scroll the display of the terminal by a number of pages. * @param pageCount The number of pages to scroll (negative scrolls up). @@ -1417,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._customKeyEventHandler = customKeyEventHandler; } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(flag, callback); + } + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._inputHandler.addOscHandler(ident, callback); + } + /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. diff --git a/src/Types.ts b/src/Types.ts index e150b90b55..93ba01f64b 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -182,14 +182,6 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } -/* - * An InputHandler for VT-style terminals - */ -export interface IVtInputHandler extends IInputHandler { - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; -} - export interface ILinkMatcher { id: number; regex: RegExp; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index de15fad08c..87fcfaef99 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,7 +4,7 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; -import { ITerminal, IInputHandler } from '../Types'; +import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -15,9 +15,6 @@ export class Terminal implements ITerminalApi { this._core = new TerminalCore(options); } - public get inputHandler(): IInputHandler { - return (this._core as TerminalCore).inputHandler; - } public get element(): HTMLElement { return this._core.element; } public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } @@ -62,6 +59,12 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._core.addCsiHandler(flag, callback); + } + 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); } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 10033a3355..e6e4aaa32f 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + 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/typings/xterm.d.ts b/typings/xterm.d.ts index 7528bb5540..cf489a610c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -481,6 +481,31 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + /** + * (EXPERIMENTAL) 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 is called with the numerical params, + * as well as the special characters (e.g. "$" for DECSCPP). + * 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(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + + /** + * (EXPERIMENTAL) 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; 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. From c6e6c519291be333792f0469b205f4522abd1814 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 23 Dec 2018 10:51:41 -0800 Subject: [PATCH 11/19] Use textBaseline middle to draw instead of top Chrome and Firefox behavior differs, see: - https://bugzilla.mozilla.org/show_bug.cgi?id=737852 - https://bugs.chromium.org/p/chromium/issues/detail?id=607053 Fixes #1858 --- src/renderer/BaseRenderLayer.ts | 8 ++++---- src/renderer/atlas/CharAtlasGenerator.ts | 10 +++++----- src/renderer/atlas/DynamicCharAtlas.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 2afdebb54e..3e0b864307 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -236,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); } /** @@ -295,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { this._ctx.save(); this._ctx.font = this._getFont(terminal, bold, italic); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; if (fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } diff --git a/src/renderer/atlas/CharAtlasGenerator.ts b/src/renderer/atlas/CharAtlasGenerator.ts index e40215cfe8..cadcce2ec1 100644 --- a/src/renderer/atlas/CharAtlasGenerator.ts +++ b/src/renderer/atlas/CharAtlasGenerator.ts @@ -29,7 +29,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.save(); ctx.fillStyle = config.colors.foreground.css; ctx.font = getFont(config.fontWeight, config); - ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle'; // Default color for (let i = 0; i < 256; i++) { @@ -37,7 +37,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2); ctx.restore(); } // Default color bold @@ -48,7 +48,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5); ctx.restore(); } ctx.restore(); @@ -64,7 +64,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } @@ -80,7 +80,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 72010768a3..e311c369a3 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -250,7 +250,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'top'; + this._tmpCtx.textBaseline = 'middle'; this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; @@ -259,7 +259,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { this._tmpCtx.globalAlpha = DIM_OPACITY; } // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, 0); + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous From 48ff841d6ab23744570326201c4294a25ec16474 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 23 Dec 2018 12:27:26 -0800 Subject: [PATCH 12/19] Be more paranoid about cleaning up escape sequence handlers. --- src/EscapeSequenceParser.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index c85f670d4c..3215513065 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -283,8 +283,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandlerFb = null; this._printHandler = null; this._executeHandlers = null; - this._csiHandlers = null; this._escHandlers = null; + let handlers; + while ((handlers = this._csiHandlers) && handlers.dispose !== undefined) { + handlers.dispose(); + if (handlers === this._csiHandlers) { break; } // sanity check + } + this._csiHandlers = null; + while ((handlers = this._oscHandlers) && handlers.dispose !== undefined) { + handlers.dispose(); + if (handlers === this._oscHandlers) { break; } // sanity check + } this._oscHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = null; @@ -319,6 +328,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (cur === newHead) { if (previous) { previous.nextHandler = cur.nextHandler; } else { handlers[index] = cur.nextHandler; } + cur.nextHandler = null; + handlers = null; newCallback = null; // just in case break; } } From 9e3e724f63f0de83a86997041395bea58ad8009e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 10:53:40 -0800 Subject: [PATCH 13/19] Add sanity checks to dom renderer underline code Fixes #1860 --- src/renderer/dom/DomRenderer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index a0cefd677c..c5ef212da6 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -364,9 +364,11 @@ export class DomRenderer extends EventEmitter implements IRenderer { return; } const span = row.children[x]; - span.style.textDecoration = enabled ? 'underline' : 'none'; - x = (x + 1) % cols; - if (x === 0) { + if (span) { + span.style.textDecoration = enabled ? 'underline' : 'none'; + } + if (++x >= cols) { + x = 0; y++; } } From d01efdda270f0d9c2a0c55ec78bc40e7213e1d11 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:08:53 -0800 Subject: [PATCH 14/19] Use array instead of linkedlist, add typings --- src/EscapeSequenceParser.ts | 110 ++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 61 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 3215513065..52007244d6 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -11,6 +11,13 @@ interface IHandlerLink extends IDisposable { nextHandler: IHandlerLink | null; } +interface IHandlerCollection { + [key: string]: T[]; +} + +type CsiHandler = (params: number[], collect: string) => boolean | void; +type OscHandler = (data: string) => boolean | void; + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -46,7 +53,7 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - add(code: number, state: number, action: number | null, next: number | null): void { + add(code: number, state: number, action: number | null, next: number | null): void { this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); } @@ -227,9 +234,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: (data: string, start: number, end: number) => void; protected _executeHandlers: any; - protected _csiHandlers: any; + protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; - protected _oscHandlers: any; + protected _oscHandlers: IHandlerCollection; protected _dcsHandlers: any; protected _activeDcsHandler: IDcsHandler | null; protected _errorHandler: (state: IParsingState) => IParsingState; @@ -284,16 +291,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._printHandler = null; this._executeHandlers = null; this._escHandlers = null; - let handlers; - while ((handlers = this._csiHandlers) && handlers.dispose !== undefined) { - handlers.dispose(); - if (handlers === this._csiHandlers) { break; } // sanity check - } this._csiHandlers = null; - while ((handlers = this._oscHandlers) && handlers.dispose !== undefined) { - handlers.dispose(); - if (handlers === this._oscHandlers) { break; } // sanity check - } this._oscHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = null; @@ -317,42 +315,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - private _linkHandler(handlers: object[], index: number, newCallback: object): IDisposable { - const newHead: any = newCallback; - newHead.nextHandler = handlers[index] as IHandlerLink; - newHead.dispose = function (): void { - let previous = null; - let cur = handlers[index] as IHandlerLink; - for (; cur && cur.nextHandler; - previous = cur, cur = cur.nextHandler) { - if (cur === newHead) { - if (previous) { previous.nextHandler = cur.nextHandler; } - else { handlers[index] = cur.nextHandler; } - cur.nextHandler = null; - handlers = null; newCallback = null; // just in case - break; - } - } - }; - handlers[index] = newHead; - return newHead; - } - - addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + addCsiHandler(flag: string, callback: CsiHandler): IDisposable { const index = flag.charCodeAt(0); - const newHead = - (params: number[], collect: string): void => { - if (! callback(params, collect)) { - const next = (newHead as unknown as IHandlerLink).nextHandler; - if (next) { (next as any)(params, collect); } - else { this._csiHandlerFb(collect, params, index); } - } - }; - return this._linkHandler(this._csiHandlers, index, newHead); + if (this._csiHandlers[index] === undefined) { + this._csiHandlers[index] = []; + } + this._csiHandlers[index].push(callback); + return { + dispose: () => this._csiHandlers[index].splice(this._csiHandlers[index].indexOf(callback)) + }; } - setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = callback; + this._csiHandlers[flag.charCodeAt(0)] = [callback]; } clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; @@ -372,18 +346,16 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - const newHead = - (data: string): void => { - if (! callback(data)) { - const next = (newHead as unknown as IHandlerLink).nextHandler; - if (next) { (next as any)(data); } - else { this._oscHandlerFb(ident, data); } - } - }; - return this._linkHandler(this._oscHandlers, ident, newHead); + if (this._oscHandlers[ident] === undefined) { + this._oscHandlers[ident] = []; + } + this._oscHandlers[ident].push(callback); + return { + dispose: () => this._oscHandlers[ident].splice(this._oscHandlers[ident].indexOf(callback)) + }; } setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = callback; + this._oscHandlers[ident] = [callback]; } clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; @@ -520,9 +492,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - callback = this._csiHandlers[code]; - if (callback) callback(params, collect); - else this._csiHandlerFb(collect, params, code); + // Trigger CSI Handler + const handlers = this._csiHandlers[code]; + let j: number; + for (j = handlers.length - 1; j >= 0; j--) { + if (handlers[j](params, collect)) { + break; + } + } + if (j < 0) { + this._csiHandlerFb(collect, params, code); + } break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -597,9 +577,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - callback = this._oscHandlers[identifier]; - if (callback) callback(content); - else this._oscHandlerFb(identifier, content); + // Trigger OSC Handler + const handlers = this._oscHandlers[identifier]; + let j: number; + for (j = handlers.length - 1; j >= 0; j--) { + if (handlers[j](content)) { + break; + } + } + if (j < 0) { + this._oscHandlerFb(identifier, content); + } } } if (code === 0x1b) transition |= ParserState.ESCAPE; From 38796a0f748340044f875580870764f80b5535f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:40:23 -0800 Subject: [PATCH 15/19] Add tests, fix NPE --- src/EscapeSequenceParser.test.ts | 96 ++++++++++++++++++++++++++++++++ src/EscapeSequenceParser.ts | 12 ++-- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index e92f412574..3b01cb8a17 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1169,6 +1169,54 @@ describe('EscapeSequenceParser', function (): void { parser2.parse(INPUT); chai.expect(csi).eql([]); }); + describe('CSI custom handlers', () => { + it('Prevent fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + 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, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers fallback once', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers no fallback', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([], 'Should not fallback once'); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + 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.parse('\x1b[0m'); + chai.expect(order).eql([3, 2, 1]); + }); + }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { exe.push('\n'); @@ -1196,6 +1244,54 @@ describe('EscapeSequenceParser', function (): void { parser2.parse(INPUT); chai.expect(osc).eql([]); }); + 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.parse(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.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + 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.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + 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.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([], 'Should not fallback once'); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + 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.parse('\x1b]1;foo=bar\x1b\\'); + chai.expect(order).eql([3, 2, 1]); + }); + }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { hook: function (collect: string, params: number[], flag: number): void { diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 52007244d6..acb5a42d29 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -7,10 +7,6 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceP import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -interface IHandlerLink extends IDisposable { - nextHandler: IHandlerLink | null; -} - interface IHandlerCollection { [key: string]: T[]; } @@ -494,8 +490,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.CSI_DISPATCH: // Trigger CSI Handler const handlers = this._csiHandlers[code]; - let j: number; - for (j = handlers.length - 1; j >= 0; j--) { + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { if (handlers[j](params, collect)) { break; } @@ -579,8 +575,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP const content = osc.substring(idx + 1); // Trigger OSC Handler const handlers = this._oscHandlers[identifier]; - let j: number; - for (j = handlers.length - 1; j >= 0; j--) { + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { if (handlers[j](content)) { break; } From c045c803d60dc569baab8efd53b6722faebd3e81 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:48:35 -0800 Subject: [PATCH 16/19] Add tests for dispose --- src/EscapeSequenceParser.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 3b01cb8a17..9216dd4cc2 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1216,6 +1216,15 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); + it('Dispose', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { @@ -1291,6 +1300,15 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); + it('Dispose', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { From adbb929f7881d6f678cdde8b8736bfd113565204 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 14:49:49 -0800 Subject: [PATCH 17/19] Wrap .d.ts comments to 80 chars --- typings/xterm.d.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index cf489a610c..0ceab01dcc 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -483,14 +483,13 @@ declare module 'xterm' { /** * (EXPERIMENTAL) 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 is called with the numerical params, - * as well as the special characters (e.g. "$" for DECSCPP). - * 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. + * @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 + * is called with the numerical params, as well as the special characters + * (e.g. "$" for DECSCPP). 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(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; @@ -498,11 +497,10 @@ declare module 'xterm' { /** * (EXPERIMENTAL) 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; false if we should - * try a previous handler (set by addOscHandler or setOscHandler). - * The most recently-added handler is tried first. + * @param callback The function to handle the escape sequence. 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 51e1f49bf36085e46d58ce98c07e115d74c112a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 10:26:40 -0800 Subject: [PATCH 18/19] Make dispose more resilient --- src/EscapeSequenceParser.test.ts | 24 ++++++++++++++++++++++-- src/EscapeSequenceParser.ts | 20 ++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 9216dd4cc2..0f1d63cc0d 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1216,7 +1216,7 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); - it('Dispose', () => { + it('Dispose should work', () => { const csiCustom: [string, number[], string][] = []; parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); @@ -1225,6 +1225,16 @@ describe('EscapeSequenceParser', function (): void { chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); }); + it('Should not corrupt the parser when dispose is called twice', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { @@ -1300,7 +1310,7 @@ describe('EscapeSequenceParser', function (): void { parser2.parse('\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); - it('Dispose', () => { + 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; }); @@ -1309,6 +1319,16 @@ describe('EscapeSequenceParser', function (): void { chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); + 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; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index acb5a42d29..70c1c6c8ef 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -316,9 +316,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (this._csiHandlers[index] === undefined) { this._csiHandlers[index] = []; } - this._csiHandlers[index].push(callback); + const handlerList = this._csiHandlers[index]; + handlerList.push(callback); return { - dispose: () => this._csiHandlers[index].splice(this._csiHandlers[index].indexOf(callback)) + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex); + } + } }; } setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { @@ -345,9 +351,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (this._oscHandlers[ident] === undefined) { this._oscHandlers[ident] = []; } - this._oscHandlers[ident].push(callback); + const handlerList = this._oscHandlers[ident]; + handlerList.push(callback); return { - dispose: () => this._oscHandlers[ident].splice(this._oscHandlers[ident].indexOf(callback)) + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex); + } + } }; } setOscHandler(ident: number, callback: (data: string) => void): void { From 8fbeadd5f9f1b3c3eff1a82ce869d1e0c9e1a2f8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 1 Jan 2019 09:45:23 -0800 Subject: [PATCH 19/19] Add missing deleteCount argument to Array.splice calls. --- src/EscapeSequenceParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 70c1c6c8ef..6d3de0e0ed 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -322,7 +322,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP dispose: () => { const handlerIndex = handlerList.indexOf(callback); if (handlerIndex !== -1) { - handlerList.splice(handlerIndex); + handlerList.splice(handlerIndex, 1); } } }; @@ -357,7 +357,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP dispose: () => { const handlerIndex = handlerList.indexOf(callback); if (handlerIndex !== -1) { - handlerList.splice(handlerIndex); + handlerList.splice(handlerIndex, 1); } } };