diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 6912404b..04d433ab 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -57,7 +57,11 @@ class DECRQSS implements IDcsHandler { this._data = concat(this._data, data.subarray(start, end)); } - unhook(): void { + unhook(success: boolean): void { + if (!success) { + this._data = new Uint32Array(0); + return; + } const data = utf32ToString(this._data); this._data = new Uint32Array(0); switch (data) { @@ -155,8 +159,13 @@ export class InputHandler extends Disposable implements IInputHandler { }); this._parser.setOscHandlerFallback((identifier, action, data) => { this._logService.debug('Unknown OSC code: ', { identifier, action, data }); - } - ); + }); + this._parser.setDcsHandlerFallback((identifier, action, payload) => { + if (payload.params) { + payload.params = payload.params.toArray(); + } + this._logService.debug('Unknown DCS code: ', { identifier, action, payload }); + }); /** * print handler diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts new file mode 100644 index 00000000..6c645a71 --- /dev/null +++ b/src/common/parser/DcsParser.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; +import { utf32ToString } from 'common/input/TextDecoder'; + + +export class DcsParser implements IDcsParser { + private _handlers: IHandlerCollection = Object.create(null); + private _active: IDcsHandler[] = []; + private _collectAndFlag: string = ''; + private _handlerFb: DcsFallbackHandler = () => {}; + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => {}; + } + public addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { + if (this._handlers[collectAndFlag] === undefined) { + this._handlers[collectAndFlag] = []; + } + const handlerList = this._handlers[collectAndFlag]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { + this._handlers[collectAndFlag] = [handler]; + } + public clearDcsHandler(collectAndFlag: string): void { + if (this._handlers[collectAndFlag]) delete this._handlers[collectAndFlag]; + } + public setOscHandlerFallback(handler: DcsFallbackHandler): void { + this._handlerFb = handler; + } + public reset(): void { + if (this._active.length) { + this.unhook(false); + } + this._active = []; + this._collectAndFlag = ''; + } + public hook(collect: string, params: IParams, flag: number): void { + this._collectAndFlag = collect + String.fromCharCode(flag); + this._active = this._handlers[this._collectAndFlag] || []; + if (!this._active.length) { + this._handlerFb(this._collectAndFlag, 'HOOK', {collect, params, flag}); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].hook(collect, params, flag); + } + } + } + public put(data: Uint32Array, start: number, end: number): void { + if (!this._active.length) { + this._handlerFb(this._collectAndFlag, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); + } + } + } + public unhook(success: boolean): void { + if (!this._active.length) { + this._handlerFb(this._collectAndFlag, 'UNHOOK', success); + } else { + let j = this._active.length - 1; + for (; j >= 0; j--) { + if (this._active[j].unhook(success) !== false) { + break; + } + } + j--; + // cleanup left over handlers + for (; j >= 0; j--) { + this._active[j].unhook(false); + } + } + } +} + +export class DcsHandlerFactory implements IDcsHandler { + private _data = ''; + private _params: IParams | undefined; + constructor(private _handler: (params: ParamsArray, data: string) => any) {} + public hook(collect: string, params: IParams, flag: number): void { + this._params = params.clone(); + this._data = ''; + } + public put(data: Uint32Array, start: number, end: number): void { + this._data += utf32ToString(data, start, end); + } + public unhook(success: boolean): any { + let ret; + if (success) { + ret = this._handler(this._params ? this._params.toArray() : [], this._data); + } + this._params = undefined; + this._data = ''; + return ret; + } +} diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 0d038ee0..87946dc5 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDcsHandler, IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; @@ -77,9 +77,6 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public set collect(value: string) { this._collect = value; } - public mockActiveDcsHandler(): void { - this._activeDcsHandler = this._dcsHandlerFb; - } public mockOscParser(): void { this._oscParser = oscPutParser; } @@ -116,11 +113,7 @@ const testTerminal: any = { actionDCSHook: function (collect: string, params: IParams, flag: string): void { this.calls.push(['dcs hook', collect, params.toArray(), flag]); }, - actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { - let s = ''; - for (let i = start; i < end; ++i) { - s += stringFromCodePoint(data[i]); - } + actionDCSPrint: function (s: string): void { this.calls.push(['dcs put', s]); }, actionDCSUnhook: function (): void { @@ -128,19 +121,6 @@ const testTerminal: any = { } }; -// dcs handler to map dcs actions into the test object `testTerminal` -class DcsTest implements IDcsHandler { - hook(collect: string, params: IParams, flag: number): void { - testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); - } - put(data: Uint32Array, start: number, end: number): void { - testTerminal.actionDCSPrint(data, start, end); - } - unhook(): void { - testTerminal.actionDCSUnhook(); - } -} - const states: number[] = [ ParserState.GROUND, ParserState.ESCAPE, @@ -176,7 +156,18 @@ testParser.setOscHandlerFallback((identifier, action, data) => { if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END }); -testParser.setDcsHandlerFallback(new DcsTest()); +testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => { + switch (action) { + case 'HOOK': + testTerminal.actionDCSHook(payload.collect, payload.params, String.fromCharCode(payload.flag)); + break; + case 'PUT': + testTerminal.actionDCSPrint(payload); + break; + case 'UNHOOK': + testTerminal.actionDCSUnhook(); + } +}); // translate string based parse calls into typed array based @@ -987,7 +978,6 @@ describe('EscapeSequenceParser', function (): void { puts = puts.concat(r(0x20, 0x7f)); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.mockActiveDcsHandler(); parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 355fbecd..dcd242f5 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser, EscHandler, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; import { Params } from 'common/parser/Params'; import { OscParser } from 'common/parser/OscParser'; +import { DcsParser } from 'common/parser/DcsParser'; /** * Table values are generated like this: @@ -196,14 +197,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { return table; })(); -/** - * Dummy DCS handler as default fallback. - */ -class DcsDummy implements IDcsHandler { - hook(collect: string, params: IParams, flag: number): void { } - put(data: Uint32Array, start: number, end: number): void { } - unhook(): void { } -} /** * EscapeSequenceParser. @@ -242,8 +235,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; - protected _dcsHandlers: any; - protected _activeDcsHandler: IDcsHandler; + protected _dcsParser: IDcsParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers @@ -251,7 +243,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; - protected _dcsHandlerFb: IDcsHandler; protected _errorHandlerFb: (state: IParsingState) => IParsingState; constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { @@ -269,15 +260,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = (code: number): void => { }; this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { }; this._escHandlerFb = (collect: string, flag: number): void => { }; - this._dcsHandlerFb = new DcsDummy(); this._errorHandlerFb = (state: IParsingState): IParsingState => state; this._printHandler = this._printHandlerFb; this._executeHandlers = Object.create(null); this._csiHandlers = Object.create(null); this._escHandlers = Object.create(null); this._oscParser = new OscParser(); - this._dcsHandlers = Object.create(null); - this._activeDcsHandler = this._dcsHandlerFb; + this._dcsParser = new DcsParser(); this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) @@ -287,9 +276,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public dispose(): void { this._executeHandlers = null; this._escHandlers = Object.create(null); - this._dcsHandlers = null; - this._activeDcsHandler = new DcsDummy(); this._oscParser.dispose(); + this._dcsParser.dispose(); } setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { @@ -373,19 +361,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._oscParser.setOscHandlerFallback(handler); } + addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable { + return this._dcsParser.addDcsHandler(collectAndFlag, handler); + } setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { - this._dcsHandlers[collectAndFlag] = handler; + this._dcsParser.setDcsHandler(collectAndFlag, handler); } clearDcsHandler(collectAndFlag: string): void { - if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag]; + this._dcsParser.clearDcsHandler(collectAndFlag); } - setDcsHandlerFallback(handler: IDcsHandler): void { - if (this._activeDcsHandler === this._dcsHandlerFb) { - this._dcsHandlerFb = handler; - this._activeDcsHandler = handler; - } else { - this._dcsHandlerFb = handler; - } + setDcsHandlerFallback(handler: DcsFallbackHandler): void { + this._dcsParser.setOscHandlerFallback(handler); } setErrorHandler(callback: (state: IParsingState) => IParsingState): void { @@ -398,10 +384,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP reset(): void { this.currentState = this.initialState; this._oscParser.reset(); + this._dcsParser.reset(); this._params.reset(); this._params.addParam(0); // ZDM this._collect = ''; - this._activeDcsHandler = this._dcsHandlerFb; + // this._activeDcsHandler = this._dcsHandlerFb; this.precedingCodepoint = 0; } @@ -424,10 +411,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let transition = 0; let currentState = this.currentState; const osc = this._oscParser; + const dcs = this._dcsParser; let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - let dcsHandler: IDcsHandler = this._activeDcsHandler; + // let dcsHandler: IDcsHandler = this._activeDcsHandler; let callback: Function | null = null; // process input string @@ -535,7 +523,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (jj < 0) { this._escHandlerFb(collect, code); } - this.precedingCodepoint = 0; break; case ParserAction.CLEAR: @@ -545,24 +532,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP collect = ''; break; case ParserAction.DCS_HOOK: - dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)]; - if (!dcsHandler) dcsHandler = this._dcsHandlerFb; - dcsHandler.hook(collect, params, code); + dcs.hook(collect, params, code); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f // unhook triggered by: 0x1b, 0x9c for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - dcsHandler.put(data, i, j); + dcs.put(data, i, j); i = j - 1; break; } } break; case ParserAction.DCS_UNHOOK: - dcsHandler.unhook(); - dcsHandler = this._dcsHandlerFb; + dcs.unhook(true); // FIXME: apply abort vs. success exit rules if (code === 0x1b) transition |= ParserState.ESCAPE; osc.reset(); params.reset(); @@ -598,10 +582,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // save non pushable buffers this._collect = collect; - this._params = params; - - // save active dcs handler reference - this._activeDcsHandler = dcsHandler; // save state this.currentState = currentState; diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 1163fd20..d6b83d64 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -42,7 +42,8 @@ export class OscParser extends Disposable { } public dispose(): void { - this._handlers = {}; + this._handlers = Object.create(null); + this._handlerFb = () => {}; } public reset(): void { diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 8c65f332..33849c4e 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -89,15 +89,16 @@ export type EscHandler = () => boolean | void; * in chunks you have to copy it, doing otherwise will lead to * data losses or corruption. * -* `unhook` marks the end of the current DCS sequence. +* `unhook` marks the end of the current DCS sequence. `success` +* indicates whether the command was aborted. */ export interface IDcsHandler { hook(collect: string, params: IParams, flag: number): void; put(data: Uint32Array, start: number, end: number): void; - unhook(): void; + unhook(success: boolean): void | boolean; } -export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; +export type DcsFallbackHandler = (collectAndFlag: string, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; export interface IOscHandler { /** @@ -120,6 +121,8 @@ export interface IOscHandler { end(success: boolean): void | boolean; } +export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; + /** * EscapeSequenceParser interface. */ @@ -166,7 +169,8 @@ export interface IEscapeSequenceParser extends IDisposable { setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; clearDcsHandler(collectAndFlag: string): void; - setDcsHandlerFallback(handler: IDcsHandler): void; + setDcsHandlerFallback(handler: DcsFallbackHandler): void; + addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; setErrorHandler(callback: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; @@ -182,3 +186,14 @@ export interface IOscParser extends IDisposable { put(data: Uint32Array, start: number, end: number): void; end(success: boolean): void; } + +export interface IDcsParser extends IDisposable { + addDcsHandler(collectAndFlag: string, handler: IDcsHandler): IDisposable; + setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; + clearDcsHandler(collectAndFlag: string): void; + setOscHandlerFallback(handler: DcsFallbackHandler): void; + reset(): void; + hook(collect: string, params: IParams, flag: number): void; + put(data: Uint32Array, start: number, end: number): void; + unhook(success: boolean): void; +}