diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index f4ec5025..56d4c93e 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import { assert } from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; @@ -24,7 +24,7 @@ function r(a: number, b: number): string[] { } class MockOscPutParser implements IOscParser { - private _fallback: OscFallbackHandlerType = () => {}; + private _fallback: OscFallbackHandlerType = () => { }; public data = ''; public reset(): void { this.data = ''; @@ -92,6 +92,21 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public identifier(id: IFunctionIdentifier): number { return this._identifier(id); } + public get parseStack(): IParserStackState { + return this._parseStack; + } + private _trackStack = false; + public trackStackSavesOnPause(): void { + this._trackStack = true; + } + public trackedStack: IParserStackState[] = []; + public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { + const result = super.parse(data, length, promiseResult); + if (result && this._trackStack) { + this.trackedStack.push({ ...this.parseStack }); + } + return result; + } } // test object to collect parser actions and compare them with expected values @@ -1206,71 +1221,71 @@ describe('EscapeSequenceParser', () => { assert.equal(print, ''); }); it('ESC handler', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, function (): boolean { + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean { esc.push('%G'); return true; }); - parser2.registerEscHandler({final: 'E'}, function (): boolean { + parser2.registerEscHandler({ final: 'E' }, function (): boolean { esc.push('E'); return true; }); parse(parser2, INPUT); assert.deepEqual(esc, ['%G', 'E']); - parser2.clearEscHandler({intermediates: '%', final: 'G'}); - parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw + parser2.clearEscHandler({ intermediates: '%', final: 'G' }); + parser2.clearEscHandler({ intermediates: '%', final: 'G' }); // should not throw clearAccu(); parse(parser2, INPUT); assert.deepEqual(esc, ['E']); - parser2.clearEscHandler({final: 'E'}); + parser2.clearEscHandler({ final: 'E' }); clearAccu(); parse(parser2, INPUT); assert.deepEqual(esc, []); }); describe('ESC custom handlers', () => { it('prevent fallback', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); parse(parser2, INPUT); assert.deepEqual(esc, ['custom - %G']); }); it('allow fallback', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return false; }); parse(parser2, INPUT); assert.deepEqual(esc, ['custom - %G', 'default - %G']); }); it('Multiple custom handlers fallback once', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return false; }); parse(parser2, INPUT); assert.deepEqual(esc, ['custom2 - %G', 'custom - %G']); }); it('Multiple custom handlers no fallback', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return true; }); parse(parser2, INPUT); assert.deepEqual(esc, ['custom2 - %G']); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); return true; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; }); - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(1); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(2); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(3); return false; }); parse(parser2, '\x1b%G'); assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; }); - const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); parse(parser2, INPUT); assert.deepEqual(esc, ['default - %G']); }); it('Should not corrupt the parser when dispose is called twice', () => { - parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; }); - const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); dispo.dispose(); parse(parser2, INPUT); @@ -1278,14 +1293,14 @@ describe('EscapeSequenceParser', () => { }); }); it('CSI handler', () => { - parser2.registerCsiHandler({final: 'm'}, function (params: IParams): boolean { + parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean { csi.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]); - parser2.clearCsiHandler({final: 'm'}); - parser2.clearCsiHandler({final: 'm'}); // should not throw + parser2.clearCsiHandler({ final: 'm' }); + parser2.clearCsiHandler({ final: 'm' }); // should not throw clearAccu(); parse(parser2, INPUT); assert.deepEqual(csi, []); @@ -1293,16 +1308,16 @@ describe('EscapeSequenceParser', () => { describe('CSI custom handlers', () => { it('Prevent fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; }); - parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); assert.deepEqual(csi, [], 'Should not fallback to original handler'); assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]); }); it('Allow fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; }); - parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]); @@ -1310,9 +1325,9 @@ describe('EscapeSequenceParser', () => { it('Multiple custom handlers fallback once', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; }); - parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); - parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); assert.deepEqual(csi, [], 'Should not fallback to original handler'); assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]); @@ -1321,9 +1336,9 @@ describe('EscapeSequenceParser', () => { it('Multiple custom handlers no fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; }); - parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); - parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); assert.deepEqual(csi, [], 'Should not fallback to original handler'); assert.deepEqual(csiCustom, [], 'Should not fallback once'); @@ -1331,16 +1346,16 @@ describe('EscapeSequenceParser', () => { }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.registerCsiHandler({final: 'm'}, () => { order.push(1); return true; }); - parser2.registerCsiHandler({final: 'm'}, () => { order.push(2); return false; }); - parser2.registerCsiHandler({final: 'm'}, () => { order.push(3); return false; }); + parser2.registerCsiHandler({ final: 'm' }, () => { order.push(1); return true; }); + parser2.registerCsiHandler({ final: 'm' }, () => { order.push(2); return false; }); + parser2.registerCsiHandler({ final: 'm' }, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; }); - const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]); @@ -1348,8 +1363,8 @@ describe('EscapeSequenceParser', () => { }); it('Should not corrupt the parser when dispose is called twice', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; }); - const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); @@ -1455,7 +1470,7 @@ describe('EscapeSequenceParser', () => { }); }); it('DCS handler', () => { - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, { + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, { hook: function (params: IParams): void { dcs.push(['hook', '', params.toArray(), 0]); }, @@ -1478,8 +1493,8 @@ describe('EscapeSequenceParser', () => { ['put', 'abc'], ['put', ';de'], ['unhook'] ]); - parser2.clearDcsHandler({intermediates: '+', final: 'p'}); - parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw + parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); + parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); // should not throw clearAccu(); parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); @@ -1489,54 +1504,54 @@ describe('EscapeSequenceParser', () => { const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; it('Prevent fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc']]); }); it('Allow fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers fallback once', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers no fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(1); return true; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; })); - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(1); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(2); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(3); return false; })); parse(parser2, DCS_INPUT); assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); - const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); parse(parser2, DCS_INPUT); assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]); }); it('Should not corrupt the parser when dispose is called twice', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); - const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); dispo.dispose(); parse(parser2, DCS_INPUT); @@ -1570,54 +1585,54 @@ describe('EscapeSequenceParser', () => { it('prefix range 0x3c .. 0x3f, one byte', () => { for (let i = 0x3c; i <= 0x3f; ++i) { const c = String.fromCharCode(i); - assert.equal(parser.identToString(parser.identifier({prefix: c, final: 'z'})), c + 'z'); + assert.equal(parser.identToString(parser.identifier({ prefix: c, final: 'z' })), c + 'z'); } - assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); - assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); - assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported'); + assert.throws(() => { parser.identifier({ prefix: '\x3b', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f'); + assert.throws(() => { parser.identifier({ prefix: '\x40', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f'); + assert.throws(() => { parser.identifier({ prefix: '??', final: 'z' }); }, 'only one byte as prefix supported'); }); it('intermediates range 0x20 .. 0x2f, up to two bytes', () => { for (let i = 0x20; i <= 0x2f; ++i) { const c = String.fromCharCode(i); - assert.equal(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'})), c + c + 'z'); + assert.equal(parser.identToString(parser.identifier({ intermediates: c + c, final: 'z' })), c + c + 'z'); } - assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); - assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); - assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported'); + assert.throws(() => { parser.identifier({ intermediates: '\x1f', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f'); + assert.throws(() => { parser.identifier({ intermediates: '\x30', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f'); + assert.throws(() => { parser.identifier({ intermediates: '!!!', final: 'z' }); }, 'only two bytes as intermediates are supported'); }); it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => { for (let i = 0x40; i <= 0x7e; ++i) { const c = String.fromCharCode(i); - assert.equal(parser.identToString(parser.identifier({final: c})), c); + assert.equal(parser.identToString(parser.identifier({ final: c })), c); } - assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126'); - assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126'); - assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte'); + assert.throws(() => { parser.identifier({ final: '\x3f' }); }, 'final must be in range 64 .. 126'); + assert.throws(() => { parser.identifier({ final: '\x7f' }); }, 'final must be in range 64 .. 126'); + assert.throws(() => { parser.identifier({ final: 'zz' }); }, 'final must be a single byte'); }); it('final ESC range 0x30 .. 0x7e, one byte', () => { for (let i = 0x30; i <= 0x7e; ++i) { const final = String.fromCharCode(i); let handler: IDisposable | undefined; - assert.doesNotThrow(() => { handler = parser.registerEscHandler({final}, () => true); }, 'final must be in range 48 .. 126'); + assert.doesNotThrow(() => { handler = parser.registerEscHandler({ final }, () => true); }, 'final must be in range 48 .. 126'); if (handler) handler.dispose(); } - assert.throws(() => { parser.registerEscHandler({final: '\x2f'}, () => true); }, 'final must be in range 48 .. 126'); - assert.throws(() => { parser.registerEscHandler({final: '\x7f'}, () => true); }, 'final must be in range 48 .. 126'); + assert.throws(() => { parser.registerEscHandler({ final: '\x2f' }, () => true); }, 'final must be in range 48 .. 126'); + assert.throws(() => { parser.registerEscHandler({ final: '\x7f' }, () => true); }, 'final must be in range 48 .. 126'); }); it('id calculation - should stacking prefix -> intermediate -> final', () => { - assert.equal(parser.identToString(parser.identifier({final: 'z'})), 'z'); - assert.equal(parser.identToString(parser.identifier({prefix: '?', final: 'z'})), '?z'); - assert.equal(parser.identToString(parser.identifier({intermediates: '!', final: 'z'})), '!z'); - assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'})), '?!z'); - assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'})), '?!!z'); + assert.equal(parser.identToString(parser.identifier({ final: 'z' })), 'z'); + assert.equal(parser.identToString(parser.identifier({ prefix: '?', final: 'z' })), '?z'); + assert.equal(parser.identToString(parser.identifier({ intermediates: '!', final: 'z' })), '!z'); + assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!', final: 'z' })), '?!z'); + assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!!', final: 'z' })), '?!!z'); }); }); describe('identifier invocation', () => { it('ESC', () => { const callstack: string[] = []; - const h1 = parser.registerEscHandler({final: 'z'}, () => { callstack.push('z'); return true; }); - const h2 = parser.registerEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); return true; }); - const h3 = parser.registerEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); return true; }); + const h1 = parser.registerEscHandler({ final: 'z' }, () => { callstack.push('z'); return true; }); + const h2 = parser.registerEscHandler({ intermediates: '!', final: 'z' }, () => { callstack.push('!z'); return true; }); + const h3 = parser.registerEscHandler({ intermediates: '!!', final: 'z' }, () => { callstack.push('!!z'); return true; }); parse(parser, '\x1bz\x1b!z\x1b!!z'); h1.dispose(); h2.dispose(); @@ -1627,12 +1642,12 @@ describe('EscapeSequenceParser', () => { }); it('CSI', () => { const callstack: any[] = []; - const h1 = parser.registerCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); return true; }); - const h2 = parser.registerCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); return true; }); - const h3 = parser.registerCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); return true; }); - const h4 = parser.registerCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); return true; }); - const h5 = parser.registerCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); return true; }); - const h6 = parser.registerCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); return true; }); + const h1 = parser.registerCsiHandler({ final: 'z' }, params => { callstack.push(['z', params.toArray()]); return true; }); + const h2 = parser.registerCsiHandler({ intermediates: '!', final: 'z' }, params => { callstack.push(['!z', params.toArray()]); return true; }); + const h3 = parser.registerCsiHandler({ intermediates: '!!', final: 'z' }, params => { callstack.push(['!!z', params.toArray()]); return true; }); + const h4 = parser.registerCsiHandler({ prefix: '?', final: 'z' }, params => { callstack.push(['?z', params.toArray()]); return true; }); + const h5 = parser.registerCsiHandler({ prefix: '?', intermediates: '!', final: 'z' }, params => { callstack.push(['?!z', params.toArray()]); return true; }); + const h6 = parser.registerCsiHandler({ prefix: '?', intermediates: '!!', final: 'z' }, params => { callstack.push(['?!!z', params.toArray()]); return true; }); parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); h1.dispose(); h2.dispose(); @@ -1648,12 +1663,12 @@ describe('EscapeSequenceParser', () => { }); it('DCS', () => { const callstack: any[] = []; - const h1 = parser.registerDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; })); - const h2 = parser.registerDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; })); - const h3 = parser.registerDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; })); - const h4 = parser.registerDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; })); - const h5 = parser.registerDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; })); - const h6 = parser.registerDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; })); + const h1 = parser.registerDcsHandler({ final: 'z' }, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; })); + const h2 = parser.registerDcsHandler({ intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; })); + const h3 = parser.registerDcsHandler({ intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; })); + const h4 = parser.registerDcsHandler({ prefix: '?', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; })); + const h5 = parser.registerDcsHandler({ prefix: '?', intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; })); + const h6 = parser.registerDcsHandler({ prefix: '?', intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; })); parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); h1.dispose(); h2.dispose(); @@ -1678,3 +1693,365 @@ describe('EscapeSequenceParser', () => { }); // TODO: error conditions and error recovery (not implemented yet in parser) }); + + +/** + * async handler tests. + */ + +function parseSync(parser: TestEscapeSequenceParser, data: string): void | Promise { + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + return parser.parse(container, decoder.decode(data, container)); +} +async function parseP(parser: TestEscapeSequenceParser, data: string): Promise { + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + const len = decoder.decode(data, container); + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.parse(container, len, prev)) { + prev = await result; + } +} +function evalStackSaves(stackSaves: IParserStackState[], data: [number, ParserStackType, number][]): void { + assert.equal(stackSaves.length, data.length); + for (let i = 0; i < data.length; ++i) { + assert.equal(stackSaves[i].chunkPos, data[i][0]); + assert.equal(stackSaves[i].state, data[i][1]); + assert.equal(stackSaves[i].handlerPos, data[i][2]); + } +} +// helper similiar to assert.throws for async functions +async function throwsAsync(fn: () => Promise, message?: string | undefined): Promise { + let msg: string | undefined; + try { + await fn(); + } catch (e) { + if (e instanceof Error) { + msg = e.message; + } else if (typeof e === 'string') { + msg = e; + } + if (typeof message === 'string') { + assert.equal(msg, message); + } + return; + } + assert.throws(fn, message); +} + +describe('EscapeSequenceParser - async', () => { + // sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | OSC 1;foo=bar ST + // needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1 + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; + let RESULT: any[]; + let parser: TestEscapeSequenceParser; + const callstack: any[] = []; + function clearAccu(): void { + callstack.length = 0; + parser.trackedStack.length = 0; + } + beforeEach(() => { + RESULT = [ + ['SGR', [1, 31]], + ['PRINT', 'hello '], + ['ESC %G'], + ['PRINT', 'wor'], + ['ESC E'], + ['PRINT', 'ld!'], + ['SGR', [0]], + ['EXE \r'], + ['EXE \n'], + ['PRINT', '$>'], + ['OSC 1', 'foo=bar'] + ]; + parser = new TestEscapeSequenceParser(); + parser.reset(); + parser.trackStackSavesOnPause(); + clearAccu(); + }); + describe('sync handlers should behave as before', () => { + beforeEach(() => { + parser.setPrintHandler((data, start, end) => { + let result = ''; + for (let i = start; i < end; ++i) { + result += stringFromCodePoint(data[i]); + } + callstack.push(['PRINT', result]); + }); + parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['SGR', params.toArray()]); return true; }); + parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => { callstack.push(['ESC %G']); return true; }); + parser.registerEscHandler({ final: 'E' }, () => { callstack.push(['ESC E']); return true; }); + parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); + parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); + parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; })); + }); + + it('sync handlers keep parsed in sync mode', () => { + // note: if we have only sync handlers, a parse call should never return anything + assert.equal(!parseSync(parser, INPUT), true); + assert.equal(parser.parseStack.state, ParserStackType.NONE); // not paused + assert.equal(parser.trackedStack.length, 0); // never got paused + }); + it('correct result on sync parse call', () => { + parseSync(parser, INPUT); + assert.deepEqual(callstack, RESULT); + assert.equal(parser.trackedStack.length, 0); + }); + it('correct result on async parse call', async () => { + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT); + assert.equal(parser.trackedStack.length, 0); + }); + }); + describe('async handlers', () => { + beforeEach(() => { + parser.setPrintHandler((data, start, end) => { + let result = ''; + for (let i = start; i < end; ++i) { + result += stringFromCodePoint(data[i]); + } + callstack.push(['PRINT', result]); + }); + parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['SGR', params.toArray()]); return true; }); + parser.registerEscHandler({ intermediates: '%', final: 'G' }, async () => { callstack.push(['ESC %G']); return true; }); + parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['ESC E']); return true; }); + parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); + parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); + parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; })); + }); + + it('sync parse call does not work anymore', () => { + assert.notEqual(!parseSync(parser, INPUT), true); + assert.notDeepEqual(callstack, RESULT); + // due to sync calling we should save exactly one saved stack + // proper continuation is not possible anymore, as we lost the promise resolve value + assert.equal(parser.trackedStack.length, 1); + }); + it('improper continuation should throw', async () => { + /** + * Explanation: + * The first sync call will stop at the first promise returned, + * but does not await its resolve value. + * The second sync call to parse will fail due to missing `promiseResult`, + * which is needed for correct continuation. + */ + assert.notEqual(!parseSync(parser, INPUT), true); + assert.notDeepEqual(callstack, RESULT); + assert.throws(() => parseSync(parser, INPUT), 'improper continuation due to previous async handler, giving up parsing'); + // keeps being broken for further parse calls (sync and async) + assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing'); + await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing'); + // FIXME: come up with a good recovery strategy + }); + it('correct result on awaited parse call', async () => { + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 0] + ]); + }); + it('correct result on chunked awaited parse calls', async () => { + RESULT = [ + ['SGR', [1, 31]], + ['PRINT', 'h'], // due to single char input PRINT is split + ['PRINT', 'e'], + ['PRINT', 'l'], + ['PRINT', 'l'], + ['PRINT', 'o'], + ['PRINT', ' '], + ['ESC %G'], + ['PRINT', 'w'], + ['PRINT', 'o'], + ['PRINT', 'r'], + ['ESC E'], + ['PRINT', 'l'], + ['PRINT', 'd'], + ['PRINT', '!'], + ['SGR', [0]], + ['EXE \r'], + ['EXE \n'], + ['PRINT', '$'], + ['PRINT', '>'], + ['OSC 1', 'foo=bar'] + ]; + + // split to single char input + for (let i = 0; i < INPUT.length; ++i) { + // Note: a single fully awaited parse call always ends in sync mode, + // which re-enables faster sync processing in the higher up callstack + await parseP(parser, INPUT[i]); + } + assert.deepEqual(callstack, RESULT); + evalStackSaves(parser.trackedStack, [ + [0, ParserStackType.CSI, 0], + [0, ParserStackType.ESC, 0], + [0, ParserStackType.ESC, 0], + [0, ParserStackType.CSI, 0] + ]); + }); + it('multiple async SGR handlers', async () => { + // register with fallback + const SGR2 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return false; }); + await parseP(parser, INPUT); + // should contain [2# SGR, SGR] call pairs + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# SGR') assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 1], + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 1], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + SGR2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + + // register without fallback + const SGR22 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return true; }); + await parseP(parser, INPUT); + // should only contain 2# SGR + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# SGR') assert.notEqual(callstack[i + 1][0], 'SGR', 'Should not fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 1], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 1] + ]); + clearAccu(); + // after dispose we should be back to RESULT + SGR22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0] + ]); + }); + it('multiple async ESC handlers', async () => { + // register with fallback + const ESC2 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return false; }); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# ESC E') assert.equal(callstack[i + 1][0], 'ESC E', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 1], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + ESC2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + + // register without fallback + const ESC22 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return true; }); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# ESC E') assert.notEqual(callstack[i + 1][0], 'ESC E', 'Should not fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 1], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + ESC22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0] + ]); + }); + it('sync/async SGR mixed', async () => { + // sync with fallback + const SGR2 = parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['2# SGR', params.toArray()]); return false; }); + // async with fallback + const SGR3 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['3# SGR', params.toArray()]); return false; }); + await parseP(parser, INPUT); + // should contain [3# SGR, 2# SGR, SGR] call triples + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '3# SGR') { + assert.equal(callstack[i + 1][0], '2# SGR', 'Should fallback to next handler'); + assert.equal(callstack[i + 2][0], 'SGR', 'Should fallback to original handler'); + } + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 2], + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 2], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + // dispose SGR2 (sync one) + SGR2.dispose(); + await parseP(parser, INPUT); + // should contain [3# SGR, SGR] call pairs + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '3# SGR') { + assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler'); + } + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 1], + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 1], + [27, ParserStackType.CSI, 0] + ]); + clearAccu(); + // dispose SGR3 (async one) + SGR3.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0] + ]); + }); + }); +}); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index e56655ae..1ac1a22b 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -440,14 +440,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP /** * Async parse support. */ - private _parseStack: IParserStackState = { + protected _parseStack: IParserStackState = { state: ParserStackType.NONE, handlers: [], handlerPos: 0, transition: 0, chunkPos: 0 }; - private _preserveStack( + protected _preserveStack( state: ParserStackType, handlers: ResumableHandlersType, handlerPos: number, @@ -474,6 +474,39 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * - DCS_PARAM:PARAM * - OSC_STRING:OSC_PUT * - DCS_PASSTHROUGH:DCS_PUT + * + * Note on asynchronous handler support: + * Any handler returning a promise will be treated as asynchronous. + * To keep the in-band blocking working for async handlers, `parse` pauses execution, + * creates a stack save and returns the promise to the caller. + * For proper continuation of the paused state it is important + * to await the promise resolving. On resolve the parse must be repeated + * with the same chunk of data and the resolved value in `promiseResult` + * until no promise is returned. + * + * Important: With only sync handlers defined, parsing is completely synchronous as well. + * As soon as an async handler is involved, synchronous parsing is not possible anymore. + * + * FIXME: to be discussed + * While awaiting parse promises the terminal buffer state may not change. + * --> Implement lock semantics / promise chaining on buffer alterations? Waah, pandora's box ;) + * --> Maybe easier: Give up on non-mutating rule for async handlers... + * (needs explanation in docs about exact executor/thenable/worker execution contexts) + * + * Example for proper parsing of multiple chunks: + * + * ```typescript + * async function parseMultipleChunks(chunks: Uint32Array[]): Promise { + * for (const chunk of chunks) { + * let result: void | Promise; + * let prev: boolean | undefined; + * while (result = parser.parse(chunk, chunk.length, prev)) { + * prev = await result; + * } + * } + * // finished parsing all chunks... + * } + * ``` */ public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { let code = 0; @@ -483,6 +516,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // resume from async handler if (this._parseStack.state) { + if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { + /** + * Reject further parsing on improper continuation after pausing. + * This will happen with sync parse calls not awaiting a returned promise. + * It is a really bad condition with screwed up execution order, + * therefore we exit hard with an exception. + * FIXME: Do we need a method to escape from this broken parser state? (hard to achieve properly) + */ + this._parseStack.state = ParserStackType.FAIL; + throw new Error('improper continuation due to previous async handler, giving up parsing'); + } let handlerPos = this._parseStack.handlerPos - 1; // we have to resume the old handler loop if: diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 002fc8ed..f6db77fa 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -244,6 +244,7 @@ export interface IHandlerCollection { */ export const enum ParserStackType { NONE = 0, + FAIL, CSI, ESC, OSC,