From 4d660deff3db9f3eea3923dcaa970fde0e9deaf8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 4 Dec 2018 19:00:44 -0800 Subject: [PATCH 01/21] 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 | 78 ++++++++++++++++++++++++++++++++----- src/InputHandler.ts | 17 +++++++- src/Terminal.ts | 6 ++- src/Types.ts | 14 +++++++ src/public/Terminal.ts | 5 ++- 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index b38c50f5..7a5da2c8 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -301,9 +301,39 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } - setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = 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; + }); + } + /* 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; } - setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = 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; + }); + } + /* 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 7604b01f..2c931a2b 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 2cfc1ca8..fea0cb69 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 430c6575..93dd7c8a 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 8ff7cf2b..de15fad0 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/21] 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 7a5da2c8..5a2c6cd3 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/21] 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 5a2c6cd3..28a946d1 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/21] 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 28a946d1..5a2c6cd3 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/21] 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 76b9dd68..90405a42 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/21] 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 90405a42..f4898841 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 2c931a2b..7604b01f 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 8f66ebe9..bc8fb103 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 22eff1c8..a5aa8add 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 de15fad0..8ff7cf2b 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/21] 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 f4898841..28562939 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 7604b01f..2e016296 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 bc8fb103..8f66ebe9 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 a5aa8add..e150b90b 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 8ff7cf2b..de15fad0 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/21] 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 28562939..a104d583 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/21] 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 a104d583..c85f670d 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/21] 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 13da6961..87d911c2 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 2e016296..eb1ca105 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 8f66ebe9..bed45e46 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 e150b90b..93ba01f6 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 de15fad0..87fcfaef 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 10033a33..e6e4aaa3 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 7528bb55..cf489a61 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/21] 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 2afdebb5..3e0b8643 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 e40215cf..cadcce2e 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 72010768..e311c369 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/21] 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 c85f670d..32155130 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/21] 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 a0cefd67..c5ef212d 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/21] 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 32155130..52007244 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/21] 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 e92f4125..3b01cb8a 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 52007244..acb5a42d 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/21] 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 3b01cb8a..9216dd4c 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/21] 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 cf489a61..0ceab01d 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/21] 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 9216dd4c..0f1d63cc 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 acb5a42d..70c1c6c8 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/21] 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 70c1c6c8..6d3de0e0 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); } } }; From 740ca9ce6d172c9bd37ca9bca82195cc9c36afcf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 2 Jan 2019 14:23:09 -0800 Subject: [PATCH 20/21] v3.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d797f78..5ed3a4b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.9.0", + "version": "3.10.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", From c668bb1243eb35008fe5f405537ef400caf69297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Jan 2019 16:08:40 +0100 Subject: [PATCH 21/21] remove js array based buffer line --- demo/client.ts | 3 +- src/Buffer.ts | 36 ++----------- src/BufferLine.ts | 127 +-------------------------------------------- src/Terminal.ts | 35 ++++--------- src/Types.ts | 4 -- typings/xterm.d.ts | 11 ---- 6 files changed, 16 insertions(+), 200 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index acc83cb8..a3a912f6 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -205,8 +205,7 @@ function initOptions(term: TerminalType): void { fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - rendererType: ['dom', 'canvas'], - experimentalBufferLineImpl: ['JsArray', 'TypedArray'] + rendererType: ['dom', 'canvas'] }; const options = Object.keys((term)._core.options); const booleanOptions = []; diff --git a/src/Buffer.ts b/src/Buffer.ts index 74750a8b..625a2497 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,10 +4,10 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine, BufferLineJSArray } from './BufferLine'; +import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); @@ -45,7 +45,6 @@ export class Buffer implements IBuffer { public savedX: number; public savedCurAttr: number; public markers: Marker[] = []; - private _bufferLineConstructor: IBufferLineConstructor; /** * Create a new Buffer. @@ -60,35 +59,9 @@ export class Buffer implements IBuffer { this.clear(); } - public setBufferLineFactory(type: string): void { - if (type === 'JsArray') { - if (this._bufferLineConstructor !== BufferLineJSArray) { - this._bufferLineConstructor = BufferLineJSArray; - this._recreateLines(); - } - } else { - if (this._bufferLineConstructor !== BufferLine) { - this._bufferLineConstructor = BufferLine; - this._recreateLines(); - } - } - } - - private _recreateLines(): void { - if (!this.lines) return; - for (let i = 0; i < this.lines.length; ++i) { - const oldLine = this.lines.get(i); - const newLine = new this._bufferLineConstructor(oldLine.length); - for (let j = 0; j < oldLine.length; ++j) { - newLine.set(j, oldLine.get(j)); - } - this.lines.set(i, newLine); - } - } - public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new this._bufferLineConstructor(this._terminal.cols, fillCharData, isWrapped); + return new BufferLine(this._terminal.cols, fillCharData, isWrapped); } public get hasScrollback(): boolean { @@ -135,7 +108,6 @@ export class Buffer implements IBuffer { * Clears the buffer to it's initial state, discarding all previous data. */ public clear(): void { - this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl); this.ydisp = 0; this.ybase = 0; this.y = 0; @@ -188,7 +160,7 @@ export class Buffer implements IBuffer { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + this.lines.push(new BufferLine(newCols, fillCharData)); } } } diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 3f93af62..226c4a87 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -3,133 +3,8 @@ * @license MIT */ import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer'; -/** - * Class representing a terminal line. - * - * @deprecated to be removed with one of the next releases - */ -export class BufferLineJSArray implements IBufferLine { - protected _data: CharData[]; - public isWrapped = false; - public length: number; - - constructor(cols: number, fillCharData?: CharData, isWrapped?: boolean) { - this._data = []; - if (!fillCharData) { - fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - for (let i = 0; i < cols; i++) { - this._push(fillCharData); // Note: the ctor ch is not cloned (resembles old behavior) - } - if (isWrapped) { - this.isWrapped = true; - } - this.length = this._data.length; - } - - private _pop(): CharData | undefined { - const data = this._data.pop(); - this.length = this._data.length; - return data; - } - - private _push(data: CharData): void { - this._data.push(data); - this.length = this._data.length; - } - - private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { - const removed = this._data.splice(start, deleteCount, ...items); - this.length = this._data.length; - return removed; - } - - public get(index: number): CharData { - return this._data[index]; - } - - public set(index: number, data: CharData): void { - this._data[index] = data; - } - - /** insert n cells ch at pos, right cells are lost (stable length) */ - public insertCells(pos: number, n: number, ch: CharData): void { - while (n--) { - this._splice(pos, 0, ch); - this._pop(); - } - } - - /** delete n cells at pos, right side is filled with fill (stable length) */ - public deleteCells(pos: number, n: number, fillCharData: CharData): void { - while (n--) { - this._splice(pos, 1); - this._push(fillCharData); - } - } - - /** replace cells from pos to pos + n - 1 with fill */ - public replaceCells(start: number, end: number, fillCharData: CharData): void { - while (start < end && start < this.length) { - this.set(start++, fillCharData); // Note: fill is not cloned (resembles old behavior) - } - } - - /** resize line to cols filling new cells with fill */ - public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { - while (this._data.length < cols) { - this._data.push(fillCharData); - } - if (shrink) { - while (this._data.length > cols) { - this._data.pop(); - } - } - this.length = this._data.length; - } - - public fill(fillCharData: CharData): void { - for (let i = 0; i < this.length; ++i) { - this.set(i, fillCharData); - } - } - - public copyFrom(line: BufferLineJSArray): void { - this._data = line._data.slice(0); - this.length = line.length; - this.isWrapped = line.isWrapped; - } - - public clone(): IBufferLine { - const newLine = new BufferLineJSArray(0); - newLine.copyFrom(this); - return newLine; - } - - public getTrimmedLength(): number { - for (let i = this.length - 1; i >= 0; --i) { - const ch = this.get(i); - if (ch[CHAR_DATA_CHAR_INDEX] !== '') { - return i + ch[CHAR_DATA_WIDTH_INDEX]; - } - } - return 0; - } - - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { - if (trimRight) { - endCol = Math.min(endCol, this.getTrimmedLength()); - } - let result = ''; - while (startCol < endCol) { - result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; - startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1; - } - return result; - } -} /** typed array slots taken by one cell */ const CELL_SIZE = 3; diff --git a/src/Terminal.ts b/src/Terminal.ts index bed45e46..bc97de29 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -105,8 +105,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas', - experimentalBufferLineImpl: 'TypedArray' + rendererType: 'canvas' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -497,11 +496,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'experimentalBufferLineImpl': - this.buffers.normal.setBufferLineFactory(value); - this.buffers.alt.setBufferLineFactory(value); - this._blankLine = null; - break; } // Inform renderer of changes if (this.renderer) { @@ -1180,17 +1174,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; - const useRecycling = this.options.experimentalBufferLineImpl !== 'JsArray'; - if (useRecycling) { - newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { - newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); - this._blankLine = newLine; - } - newLine.isWrapped = isWrapped; - } else { + newLine = this._blankLine; + if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); + this._blankLine = newLine; } + newLine.isWrapped = isWrapped; const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1201,17 +1190,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { - if (useRecycling) { - if (willBufferBeTrimmed) { - this.buffer.lines.recycle().copyFrom(newLine); - } else { - this.buffer.lines.push(newLine.clone()); - } + if (willBufferBeTrimmed) { + this.buffer.lines.recycle().copyFrom(newLine); } else { - this.buffer.lines.push(newLine); + this.buffer.lines.push(newLine.clone()); } } else { - this.buffer.lines.splice(bottomRow + 1, 0, (useRecycling) ? newLine.clone() : newLine); + this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); } // Only adjust ybase and ydisp when the buffer is not trimmed @@ -1233,7 +1218,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, (useRecycling) ? newLine.clone() : newLine); + this.buffer.lines.set(bottomRow, newLine.clone()); } // Move the viewport to the bottom of the buffer unless the user is diff --git a/src/Types.ts b/src/Types.ts index b54b9d66..c6395ea3 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -529,7 +529,3 @@ export interface IBufferLine { getTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; } - -export interface IBufferLineConstructor { - new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine; -} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0ceab01d..c5d2b020 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -101,17 +101,6 @@ declare module 'xterm' { */ experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; - /** - * (EXPERIMENTAL) Defines which implementation to use for buffer lines. - * - * - 'JsArray': The default/stable implementation. - * - 'TypedArray': The new experimental implementation based on TypedArrays that is expected to - * significantly boost performance and memory consumption. Use at your own risk. - * - * @deprecated This option will be removed in the future. - */ - experimentalBufferLineImpl?: 'JsArray' | 'TypedArray'; - /** * The font size used to render text. */