From bc1048062dda8d64020309c425a5bb8094c37490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 20 Jan 2021 20:05:14 +0100 Subject: [PATCH 01/22] early version of async support for CSI and ESC handlers --- src/common/CoreTerminal.ts | 2 +- src/common/InputHandler.ts | 66 +++++++++++--- src/common/input/WriteBuffer.ts | 65 +++++++++++-- src/common/parser/EscapeSequenceParser.ts | 106 +++++++++++++++++++++- src/common/parser/Types.d.ts | 2 +- test/benchmark/Terminal.benchmark.ts | 36 ++++++-- 6 files changed, 245 insertions(+), 32 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 2f636349..11e71bad 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -109,7 +109,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); // Setup WriteBuffer - this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); + this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); } public dispose(): void { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 626e1fe2..32428c77 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -333,7 +333,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params)); this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params)); this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params)); - this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params)); + this._parser.setCsiHandler({final: 'm'}, (params => this.charAttributes(params)) as (p: any) => void); this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params)); this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params)); this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params)); @@ -454,10 +454,43 @@ export class InputHandler extends Disposable implements IInputHandler { super.dispose(); } - public parse(data: string | Uint8Array): void { + // FIXME: cleanup async handling + private _parseStack = { + paused: false, + cursorStartX: 0, + cursorStartY: 0, + decodedLength: 0, + position: 0 + }; + + private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void { + this._parseStack.paused = true; + this._parseStack.cursorStartX = cursorStartX; + this._parseStack.cursorStartY = cursorStartY; + this._parseStack.decodedLength = decodedLength; + this._parseStack.position = position; + } + + public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise { + let result: void | Promise; let buffer = this._bufferService.buffer; - const cursorStartX = buffer.x; - const cursorStartY = buffer.y; + let cursorStartX = buffer.x; + let cursorStartY = buffer.y; + let start = 0; + const wasPaused = this._parseStack.paused; + + if (wasPaused) { + // assumption: _parseBuffer never mutates between async calls + if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) { + return result; + } + cursorStartX = this._parseStack.cursorStartX; + cursorStartY = this._parseStack.cursorStartY; + this._parseStack.paused = false; + if (data.length > MAX_PARSEBUFFER_LENGTH) { + start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH; + } + } this._logService.debug('parsing data', data); @@ -469,22 +502,33 @@ export class InputHandler extends Disposable implements IInputHandler { } // Clear the dirty row service so we know which lines changed as a result of parsing - this._dirtyRowService.clearRange(); + // Important: do not clear between async calls, otherwise we lost pending update information. + if (!wasPaused) { + this._dirtyRowService.clearRange(); + } // process big data in smaller chunks if (data.length > MAX_PARSEBUFFER_LENGTH) { - for (let i = 0; i < data.length; i += MAX_PARSEBUFFER_LENGTH) { + for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) { const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length; const len = (typeof data === 'string') ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer) : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer); - this._parser.parse(this._parseBuffer, len); + if (result = this._parser.parse(this._parseBuffer, len)) { + this._preserveStack(cursorStartX, cursorStartY, len, i); + return result; + } } } else { - const len = (typeof data === 'string') - ? this._stringDecoder.decode(data, this._parseBuffer) - : this._utf8Decoder.decode(data, this._parseBuffer); - this._parser.parse(this._parseBuffer, len); + if (!wasPaused) { + const len = (typeof data === 'string') + ? this._stringDecoder.decode(data, this._parseBuffer) + : this._utf8Decoder.decode(data, this._parseBuffer); + if (result = this._parser.parse(this._parseBuffer, len)) { + this._preserveStack(cursorStartX, cursorStartY, len, 0); + return result; + } + } } buffer = this._bufferService.buffer; diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index c7d82458..c6178024 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -37,8 +37,9 @@ export class WriteBuffer { private _pendingData = 0; private _bufferOffset = 0; - constructor(private _action: (data: string | Uint8Array) => void) { } + constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } + // FIXME: does not work that way anymore with async handlers!!! public writeSync(data: string | Uint8Array): void { // force sync processing on pending data chunks to avoid in-band data scrambling // does the same as innerWrite but without event loop @@ -76,16 +77,64 @@ export class WriteBuffer { this._callbacks.push(callback); } - protected _innerWrite(): void { - const startTime = Date.now(); + protected _innerWrite(d: number = 0, promiseResult: boolean = true): void { + let result: void | Promise; + const startTime = d || Date.now(); while (this._writeBuffer.length > this._bufferOffset) { const data = this._writeBuffer[this._bufferOffset]; - const cb = this._callbacks[this._bufferOffset]; - this._bufferOffset++; - this._action(data); - this._pendingData -= data.length; + if (result = this._action(data, promiseResult)) { + /** + * If we get a promise as return value, we re-schedule the continuation + * as thenable on the promise and exit right away. + * + * The exit here means, that we block input processing at the current active chunk, + * the exact execution position within the chunk is preserved by the saved + * stack content in InputHandler and EscapeSequenceParser. + * + * Resuming happens automatically from that saved stack state. + * Also the resolved promise value is passed along the callstack to + * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop. + * + * Exceptions on async handlers will be logged to console async, but do not interrupt + * the input processing (continues with next handler at the current input position). + * FIXME: No clear exception handling rules for sync handlers yet (will exit whole processing?). + */ + + /** + * If a promise takes long to resolve, we should schedule continuation behind setTimeout. + * This might already be too late, if our .then enters really late (executor + prev thens took very long). + * This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work), + * but we can at least schedule a screen update as we gain control. + */ + const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS + ? setTimeout(() => this._innerWrite(0, r)) + : this._innerWrite(startTime, r); + + /** + * Optimization considerations: + * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve. + * This might schedule too many screen updates with bad throughput drops (in case a slow + * resolving handler sliced its work properly behind setTimeout calls). We cannot spot + * this condition here, also the renderer has no way to spot nonsense updates either. + * FIXME: A proper fix for this would track the FPS at the renderer entry level separately. + * + * If favoring of FPS shows bad throughtput impact, use the following instead. It favors + * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the + * current microtask queue (executed before setTimeout). + */ + // const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS + // ? r => setTimeout(() => this._innerWrite(0, r)) + // : r => this._innerWrite(startTime, r); + + result.then(continuation, err => { setTimeout(() => { throw err; }); continuation(true); }); + return; + } + + const cb = this._callbacks[this._bufferOffset]; if (cb) cb(); + this._bufferOffset++; + this._pendingData -= data.length; if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { break; @@ -99,7 +148,7 @@ export class WriteBuffer { this._callbacks = this._callbacks.slice(this._bufferOffset); this._bufferOffset = 0; } - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite()); } else { this._writeBuffer = []; this._callbacks = []; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 96d206b9..daf9e6eb 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -449,7 +449,45 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.precedingCodepoint = 0; } + // FIXME: cleanup async handling + private _parseStack: { + paused: boolean; + type: 'ESC' | 'CSI'; // FIXME: support for DCS and OSC + handlers: CsiHandlerType[] | EscHandlerType[]; + handlerPos: number; + transition: number; + currentState: ParserState; + collect: number; + pos: number; + } = { + paused: false, + type: 'ESC', + handlers: [], + handlerPos: 0, + transition: 0, + currentState: 0, + collect: 0, + pos: 0 + }; + private _preserveStack( + type: 'ESC' | 'CSI', + handlers: CsiHandlerType[] | EscHandlerType[], + handlerPos: number, + transition: number, + currentState: ParserState, + collect: number, + pos: number + ): void { + this._parseStack.paused = true; + this._parseStack.type = type; + this._parseStack.handlers = handlers; + this._parseStack.handlerPos = handlerPos; + this._parseStack.transition = transition; + this._parseStack.currentState = currentState; + this._parseStack.collect = collect; + this._parseStack.pos = pos; + } /** * Parse UTF32 codepoints in `data` up to `length`. @@ -465,7 +503,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * - OSC_STRING:OSC_PUT * - DCS_PASSTHROUGH:DCS_PUT */ - public parse(data: Uint32Array, length: number): void { + public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { let code = 0; let transition = 0; let currentState = this.currentState; @@ -475,8 +513,58 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP const params = this._params; const table: Uint8Array = this._transitions.table; + let res: any; + let start = 0; + if (this._parseStack.paused) { + const handlers = this._parseStack.handlers; + let handlerPos = this._parseStack.handlerPos - 1; + transition = this._parseStack.transition; + currentState = this._parseStack.currentState; + collect = this._parseStack.collect; + start = this._parseStack.pos; + + // we have to resume the old handler loop if: + // - return value of the promise was `false` + // - handlers are not exhausted yet + // FIXME: removing handlers from within a handler of the same sequence + // is not supported atm (also true for sync handlers)!! + if (promiseResult === false && handlerPos > -1) { + switch (this._parseStack.type) { + case 'CSI': + for (; handlerPos >= 0; handlerPos--) { + if ((res = (handlers as CsiHandlerType[])[handlerPos](params)) !== false) { + if (res instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return res; + } + break; + } + } + break; + case 'ESC': + for (; handlerPos >= 0; handlerPos--) { + if ((res = (handlers as EscHandlerType[])[handlerPos]()) !== false) { + if (res instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return res; + } + break; + } + } + break; + } + } + // cleanup before continuing with the main loop + this.precedingCodepoint = 0; + this._parseStack.paused = false; + start++; + currentState = transition & TableAccess.TRANSITION_STATE_MASK; + } + + // console.log('startPos', start, length); + // process input string - for (let i = 0; i < length; ++i) { + for (let i = start; i < length; ++i) { code = data[i]; // normal transition & action lookup @@ -534,7 +622,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { // undefined or true means success and to stop bubbling - if (handlers[j](params) !== false) { + // FIXME: remove setHandler interface, always use addHandler with proper return value in true|false + // background - result of undefined leads to nonsense instanceof Promise test below + if ((res = handlers[j](params)) !== false) { + if (res && res instanceof Promise) { + this._preserveStack('CSI', handlers, j, transition, currentState, collect, i); + return res; + } break; } } @@ -568,7 +662,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let jj = handlersEsc ? handlersEsc.length - 1 : -1; for (; jj >= 0; jj--) { // undefined or true means success and to stop bubbling - if (handlersEsc[jj]() !== false) { + if ((res = handlersEsc[jj]()) !== false) { + if (res && res instanceof Promise) { + this._preserveStack('ESC', handlersEsc, jj, transition, currentState, collect, i); + return res; + } break; } } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index e2fac89f..8d55c51a 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -160,7 +160,7 @@ export interface IEscapeSequenceParser extends IDisposable { * Parse UTF32 codepoints in `data` up to `length`. * @param data The data to parse. */ - parse(data: Uint32Array, length: number): void; + parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise; /** * Get string from numercial function identifier `ident`. diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 76139cb6..7d0f5d84 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -29,7 +29,7 @@ perfContext('Terminal: ls -lR /usr/lib', () => { chunks.push(data as unknown as Buffer); length += data.length; }); - await new Promise(resolve => p.on('exit', () => resolve())); + await new Promise(resolve => p.on('exit', () => resolve())); contentUtf8 = Buffer.concat(chunks, length); // translate to content string const buffer = new Uint32Array(contentUtf8.length); @@ -44,24 +44,46 @@ perfContext('Terminal: ls -lR /usr/lib', () => { } }); - perfContext('write', () => { + // perfContext('write/string/sync', () => { + // let terminal: Terminal; + // before(() => { + // terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); + // }); + // new ThroughputRuntimeCase('', () => { + // terminal.writeSync(content); + // return {payloadSize: contentUtf8.length}; + // }, {fork: false}).showAverageThroughput(); + // }); + // + // perfContext('write/Utf8/sync', () => { + // let terminal: Terminal; + // before(() => { + // terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); + // }); + // new ThroughputRuntimeCase('', () => { + // terminal.writeSync(content); + // return {payloadSize: contentUtf8.length}; + // }, {fork: false}).showAverageThroughput(); + // }); + + perfContext('write/string/async', () => { let terminal: Terminal; before(() => { terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); }); - new ThroughputRuntimeCase('', () => { - terminal.writeSync(content); + new ThroughputRuntimeCase('', async () => { + await new Promise(res => terminal.write(content, res)); return {payloadSize: contentUtf8.length}; }, {fork: false}).showAverageThroughput(); }); - perfContext('writeUtf8', () => { + perfContext('write/Utf8/async', () => { let terminal: Terminal; before(() => { terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); }); - new ThroughputRuntimeCase('', () => { - terminal.writeSync(content); + new ThroughputRuntimeCase('', async () => { + await new Promise(res => terminal.write(content, res)); return {payloadSize: contentUtf8.length}; }, {fork: false}).showAverageThroughput(); }); From 4a1bdad7590e87e03653db15c8743a48be3cc6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 22 Jan 2021 19:48:50 +0100 Subject: [PATCH 02/22] fix tests for async handlers in inputhandler --- src/common/InputHandler.test.ts | 1038 ++++++++++++++++--------------- 1 file changed, 526 insertions(+), 512 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 4c0d8559..ebc0df6a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -41,6 +41,18 @@ class TestInputHandler extends InputHandler { public get windowTitleStack(): string[] { return this._windowTitleStack; } public get iconNameStack(): string[] { return this._iconNameStack; } public parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { return this._parseAnsiColorChange(data); } + + /** + * Promise based parse call to await the full resolve of given input data. + * This is useful to test async handlers in inputhandler directly. + */ + public async parseP(data: string | Uint8Array): Promise { + let result: Promise | void; + let prev: boolean | undefined; + while (result = this.parse(data, prev)) { + prev = await result; + } + } } describe('InputHandler', () => { @@ -118,7 +130,7 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle bracketedPasteMode', () => { const coreService = new MockCoreService(); - const inputHandler = new InputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); @@ -134,15 +146,15 @@ describe('InputHandler', () => { return result; } - it('insertChars', function(): void { + it('insertChars', async () => { const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); // insert some data in first and second line - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); const line1: IBufferLine = bufferService.buffer.lines.get(0)!; expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); @@ -171,15 +183,15 @@ describe('InputHandler', () => { expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); }); - it('deleteChars', function(): void { + it('deleteChars', async () => { const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); // insert some data in first and second line - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); const line1: IBufferLine = bufferService.buffer.lines.get(0)!; expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); @@ -211,14 +223,14 @@ describe('InputHandler', () => { expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); }); - it('eraseInLine', function(): void { + it('eraseInLine', async () => { const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); // fill 6 lines to test 3 different states - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - inputHandler.parse(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params[0] - right erase bufferService.buffer.y = 0; @@ -239,12 +251,12 @@ describe('InputHandler', () => { expect(bufferService.buffer.lines.get(2)!.translateToString(false)).equals(Array(bufferService.cols + 1).join(' ')); }); - it('eraseInDisplay', function(): void { + it('eraseInDisplay', async () => { const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); // fill display with a's - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params [0] - right and below erase bufferService.buffer.y = 5; @@ -272,7 +284,7 @@ describe('InputHandler', () => { // reset bufferService.buffer.y = 0; bufferService.buffer.x = 0; - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params [1] - left and above bufferService.buffer.y = 5; @@ -300,7 +312,7 @@ describe('InputHandler', () => { // reset bufferService.buffer.y = 0; bufferService.buffer.x = 0; - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params [2] - whole screen bufferService.buffer.y = 5; @@ -328,9 +340,9 @@ describe('InputHandler', () => { // reset and add a wrapped line bufferService.buffer.y = 0; bufferService.buffer.x = 0; - inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 - inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 - for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // line 0 + await inputHandler.parseP(Array(bufferService.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params[1] left and above with wrap // confirm precondition that line 2 is wrapped @@ -343,9 +355,9 @@ describe('InputHandler', () => { // reset and add a wrapped line bufferService.buffer.y = 0; bufferService.buffer.x = 0; - inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 - inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 - for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // line 0 + await inputHandler.parseP(Array(bufferService.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params[1] left and above with wrap // confirm precondition that line 2 is wrapped @@ -358,45 +370,45 @@ describe('InputHandler', () => { }); describe('print', () => { it('should not cause an infinite loop (regression test)', () => { - const inputHandler = new InputHandler(new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); }); - it('should clear cells to the right on early wrap-around', () => { + it('should clear cells to the right on early wrap-around', async () => { bufferService.resize(5, 5); optionsService.options.scrollback = 1; - inputHandler.parse('12345'); + await inputHandler.parseP('12345'); bufferService.buffer.x = 0; - inputHandler.parse('¥¥¥'); + await inputHandler.parseP('¥¥¥'); assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']); }); }); describe('alt screen', () => { let bufferService: IBufferService; - let handler: InputHandler; + let handler: TestInputHandler; beforeEach(() => { bufferService = new MockBufferService(80, 30); - handler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); }); - it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { - handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); + it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => { + await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red expect((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor())).to.equal(1); }); - it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { - handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); + it('should handle DECSET/DECRST 1047 (alt screen buffer)', async () => { + await handler.parseP('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red expect((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor())).to.equal(1); }); - it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { - handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); + it('should handle DECSET/DECRST 1048 (alt screen cursor)', async () => { + await handler.parseP('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); // Text color of 'TEST' should be default @@ -404,166 +416,166 @@ describe('InputHandler', () => { // Text color of 'JUNK' should be red expect((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor())).to.equal(1); }); - it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { - handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); + it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', async () => { + await handler.parseP('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(''); // Text color of 'TEST' should be default expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); }); - it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { - handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); + it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', async () => { + await handler.parseP('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); // Text color of 'TEST' should be default expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); - handler.parse('\x1b[?1049h\x1b[uTEST'); + await handler.parseP('\x1b[?1049h\x1b[uTEST'); expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red expect((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor())).to.equal(1); }); - it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { - handler.parse('\x1b[42m\x1b[?1049h'); + it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', async () => { + await handler.parseP('\x1b[42m\x1b[?1049h'); // Buffer should be filled with green background expect(bufferService.buffer.lines.get(20)!.loadCell(10, new CellData()).getBgColor()).to.equal(2); }); }); describe('text attributes', () => { - it('bold', () => { - inputHandler.parse('\x1b[1m'); + it('bold', async () => { + await inputHandler.parseP('\x1b[1m'); assert.equal(!!inputHandler.curAttrData.isBold(), true); - inputHandler.parse('\x1b[22m'); + await inputHandler.parseP('\x1b[22m'); assert.equal(!!inputHandler.curAttrData.isBold(), false); }); - it('dim', () => { - inputHandler.parse('\x1b[2m'); + it('dim', async () => { + await inputHandler.parseP('\x1b[2m'); assert.equal(!!inputHandler.curAttrData.isDim(), true); - inputHandler.parse('\x1b[22m'); + await inputHandler.parseP('\x1b[22m'); assert.equal(!!inputHandler.curAttrData.isDim(), false); }); - it('italic', () => { - inputHandler.parse('\x1b[3m'); + it('italic', async () => { + await inputHandler.parseP('\x1b[3m'); assert.equal(!!inputHandler.curAttrData.isItalic(), true); - inputHandler.parse('\x1b[23m'); + await inputHandler.parseP('\x1b[23m'); assert.equal(!!inputHandler.curAttrData.isItalic(), false); }); - it('underline', () => { - inputHandler.parse('\x1b[4m'); + it('underline', async () => { + await inputHandler.parseP('\x1b[4m'); assert.equal(!!inputHandler.curAttrData.isUnderline(), true); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(!!inputHandler.curAttrData.isUnderline(), false); }); - it('blink', () => { - inputHandler.parse('\x1b[5m'); + it('blink', async () => { + await inputHandler.parseP('\x1b[5m'); assert.equal(!!inputHandler.curAttrData.isBlink(), true); - inputHandler.parse('\x1b[25m'); + await inputHandler.parseP('\x1b[25m'); assert.equal(!!inputHandler.curAttrData.isBlink(), false); }); - it('inverse', () => { - inputHandler.parse('\x1b[7m'); + it('inverse', async () => { + await inputHandler.parseP('\x1b[7m'); assert.equal(!!inputHandler.curAttrData.isInverse(), true); - inputHandler.parse('\x1b[27m'); + await inputHandler.parseP('\x1b[27m'); assert.equal(!!inputHandler.curAttrData.isInverse(), false); }); - it('invisible', () => { - inputHandler.parse('\x1b[8m'); + it('invisible', async () => { + await inputHandler.parseP('\x1b[8m'); assert.equal(!!inputHandler.curAttrData.isInvisible(), true); - inputHandler.parse('\x1b[28m'); + await inputHandler.parseP('\x1b[28m'); assert.equal(!!inputHandler.curAttrData.isInvisible(), false); }); - it('colormode palette 16', () => { + it('colormode palette 16', async () => { assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 8; ++i) { - inputHandler.parse(`\x1b[${i + 30};${i + 40}m`); + await inputHandler.parseP(`\x1b[${i + 30};${i + 40}m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getFgColor(), i); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getBgColor(), i); } // reset to DEFAULT - inputHandler.parse(`\x1b[39;49m`); + await inputHandler.parseP(`\x1b[39;49m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); }); - it('colormode palette 256', () => { + it('colormode palette 256', async () => { assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 256; ++i) { - inputHandler.parse(`\x1b[38;5;${i};48;5;${i}m`); + await inputHandler.parseP(`\x1b[38;5;${i};48;5;${i}m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getFgColor(), i); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getBgColor(), i); } // reset to DEFAULT - inputHandler.parse(`\x1b[39;49m`); + await inputHandler.parseP(`\x1b[39;49m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); assert.equal(inputHandler.curAttrData.getFgColor(), -1); assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); assert.equal(inputHandler.curAttrData.getBgColor(), -1); }); - it('colormode RGB', () => { + it('colormode RGB', async () => { assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT - inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_RGB); assert.equal(inputHandler.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [1, 2, 3]); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_RGB); assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getBgColor()), [4, 5, 6]); // reset to DEFAULT - inputHandler.parse(`\x1b[39;49m`); + await inputHandler.parseP(`\x1b[39;49m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); assert.equal(inputHandler.curAttrData.getFgColor(), -1); assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); assert.equal(inputHandler.curAttrData.getBgColor(), -1); }); - it('colormode transition RGB to 256', () => { + it('colormode transition RGB to 256', async () => { // enter RGB for FG and BG - inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`); // enter 256 for FG and BG - inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getFgColor(), 255); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getBgColor(), 255); }); - it('colormode transition RGB to 16', () => { + it('colormode transition RGB to 16', async () => { // enter RGB for FG and BG - inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`); // enter 16 for FG and BG - inputHandler.parse(`\x1b[37;47m`); + await inputHandler.parseP(`\x1b[37;47m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getFgColor(), 7); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getBgColor(), 7); }); - it('colormode transition 16 to 256', () => { + it('colormode transition 16 to 256', async () => { // enter 16 for FG and BG - inputHandler.parse(`\x1b[37;47m`); + await inputHandler.parseP(`\x1b[37;47m`); // enter 256 for FG and BG - inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getFgColor(), 255); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getBgColor(), 255); }); - it('colormode transition 256 to 16', () => { + it('colormode transition 256 to 16', async () => { // enter 256 for FG and BG - inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`); // enter 16 for FG and BG - inputHandler.parse(`\x1b[37;47m`); + await inputHandler.parseP(`\x1b[37;47m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getFgColor(), 7); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getBgColor(), 7); }); - it('should zero missing RGB values', () => { - inputHandler.parse(`\x1b[38;2;1;2;3m`); - inputHandler.parse(`\x1b[38;2;5m`); + it('should zero missing RGB values', async () => { + await inputHandler.parseP(`\x1b[38;2;1;2;3m`); + await inputHandler.parseP(`\x1b[38;2;5m`); assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [5, 0, 0]); }); }); @@ -573,141 +585,141 @@ describe('InputHandler', () => { inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); }); describe('should equal to semicolon', () => { - it('CSI 38:2::50:100:150 m', () => { + it('CSI 38:2::50:100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38:2::50:100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38:2::50:100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:2::50:100: m', () => { + it('CSI 38:2::50:100: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;m'); - inputHandler.parse('\x1b[38:2::50:100:m'); + await inputHandler2.parseP('\x1b[38;2;50;100;m'); + await inputHandler.parseP('\x1b[38:2::50:100:m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:2::50:: m', () => { + it('CSI 38:2::50:: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;;m'); - inputHandler.parse('\x1b[38:2::50::m'); + await inputHandler2.parseP('\x1b[38;2;50;;m'); + await inputHandler.parseP('\x1b[38:2::50::m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:2:::: m', () => { + it('CSI 38:2:::: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;;;m'); - inputHandler.parse('\x1b[38:2::::m'); + await inputHandler2.parseP('\x1b[38;2;;;m'); + await inputHandler.parseP('\x1b[38:2::::m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;2::50:100:150 m', () => { + it('CSI 38;2::50:100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38;2::50:100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38;2::50:100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;2;50:100:150 m', () => { + it('CSI 38;2;50:100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38;2;50:100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38;2;50:100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;2;50;100:150 m', () => { + it('CSI 38;2;50;100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38;2;50;100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38;2;50;100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:5:50 m', () => { + it('CSI 38:5:50 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5;50m'); - inputHandler.parse('\x1b[38:5:50m'); + await inputHandler2.parseP('\x1b[38;5;50m'); + await inputHandler.parseP('\x1b[38:5:50m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:5: m', () => { + it('CSI 38:5: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5;m'); - inputHandler.parse('\x1b[38:5:m'); + await inputHandler2.parseP('\x1b[38;5;m'); + await inputHandler.parseP('\x1b[38:5:m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;5:50 m', () => { + it('CSI 38;5:50 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5;50m'); - inputHandler.parse('\x1b[38;5:50m'); + await inputHandler2.parseP('\x1b[38;5;50m'); + await inputHandler.parseP('\x1b[38;5:50m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); }); describe('should fill early sequence end with default of 0', () => { - it('CSI 38:2 m', () => { + it('CSI 38:2 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2m'); - inputHandler.parse('\x1b[38:2m'); + await inputHandler2.parseP('\x1b[38;2m'); + await inputHandler.parseP('\x1b[38:2m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:5 m', () => { + it('CSI 38:5 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5m'); - inputHandler.parse('\x1b[38:5m'); + await inputHandler2.parseP('\x1b[38;5m'); + await inputHandler.parseP('\x1b[38:5m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); }); describe('should not interfere with leading/following SGR attrs', () => { - it('CSI 1 ; 38:2::50:100:150 ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;50;100;150;4m'); - inputHandler.parse('\x1b[1;38:2::50:100:150;4m'); + it('CSI 1 ; 38:2::50:100:150 ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;50;100;150;4m'); + await inputHandler.parseP('\x1b[1;38:2::50:100:150;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38:2::50:100: ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;50;100;;4m'); - inputHandler.parse('\x1b[1;38:2::50:100:;4m'); + it('CSI 1 ; 38:2::50:100: ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;50;100;;4m'); + await inputHandler.parseP('\x1b[1;38:2::50:100:;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38:2::50:100 ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;50;100;;4m'); - inputHandler.parse('\x1b[1;38:2::50:100;4m'); + it('CSI 1 ; 38:2::50:100 ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;50;100;;4m'); + await inputHandler.parseP('\x1b[1;38:2::50:100;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38:2:: ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;;;;4m'); - inputHandler.parse('\x1b[1;38:2::;4m'); + it('CSI 1 ; 38:2:: ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;;;;4m'); + await inputHandler.parseP('\x1b[1;38:2::;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38;2:: ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;;;;4m'); - inputHandler.parse('\x1b[1;38;2::;4m'); + it('CSI 1 ; 38;2:: ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;;;;4m'); + await inputHandler.parseP('\x1b[1;38;2::;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0); @@ -719,372 +731,372 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 10); }); - it('cursor forward (CUF)', () => { - inputHandler.parse('\x1b[C'); + it('cursor forward (CUF)', async () => { + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[1C'); + await inputHandler.parseP('\x1b[1C'); assert.deepEqual(getCursor(bufferService), [2, 0]); - inputHandler.parse('\x1b[4C'); + await inputHandler.parseP('\x1b[4C'); assert.deepEqual(getCursor(bufferService), [6, 0]); - inputHandler.parse('\x1b[100C'); + await inputHandler.parseP('\x1b[100C'); assert.deepEqual(getCursor(bufferService), [9, 0]); // should not change y bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[C'); + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [9, 4]); }); - it('cursor backward (CUB)', () => { - inputHandler.parse('\x1b[D'); + it('cursor backward (CUB)', async () => { + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1D'); + await inputHandler.parseP('\x1b[1D'); assert.deepEqual(getCursor(bufferService), [0, 0]); // place cursor at end of first line - inputHandler.parse('\x1b[100C'); - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[100C'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [8, 0]); - inputHandler.parse('\x1b[1D'); + await inputHandler.parseP('\x1b[1D'); assert.deepEqual(getCursor(bufferService), [7, 0]); - inputHandler.parse('\x1b[4D'); + await inputHandler.parseP('\x1b[4D'); assert.deepEqual(getCursor(bufferService), [3, 0]); - inputHandler.parse('\x1b[100D'); + await inputHandler.parseP('\x1b[100D'); assert.deepEqual(getCursor(bufferService), [0, 0]); // should not change y bufferService.buffer.x = 4; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [3, 4]); }); - it('cursor down (CUD)', () => { - inputHandler.parse('\x1b[B'); + it('cursor down (CUD)', async () => { + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[1B'); + await inputHandler.parseP('\x1b[1B'); assert.deepEqual(getCursor(bufferService), [0, 2]); - inputHandler.parse('\x1b[4B'); + await inputHandler.parseP('\x1b[4B'); assert.deepEqual(getCursor(bufferService), [0, 6]); - inputHandler.parse('\x1b[100B'); + await inputHandler.parseP('\x1b[100B'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 0; - inputHandler.parse('\x1b[B'); + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [8, 1]); }); - it('cursor up (CUU)', () => { - inputHandler.parse('\x1b[A'); + it('cursor up (CUU)', async () => { + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1A'); + await inputHandler.parseP('\x1b[1A'); assert.deepEqual(getCursor(bufferService), [0, 0]); // place cursor at beginning of last row - inputHandler.parse('\x1b[100B'); - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[100B'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [0, 8]); - inputHandler.parse('\x1b[1A'); + await inputHandler.parseP('\x1b[1A'); assert.deepEqual(getCursor(bufferService), [0, 7]); - inputHandler.parse('\x1b[4A'); + await inputHandler.parseP('\x1b[4A'); assert.deepEqual(getCursor(bufferService), [0, 3]); - inputHandler.parse('\x1b[100A'); + await inputHandler.parseP('\x1b[100A'); assert.deepEqual(getCursor(bufferService), [0, 0]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 9; - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [8, 8]); }); - it('cursor next line (CNL)', () => { - inputHandler.parse('\x1b[E'); + it('cursor next line (CNL)', async () => { + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[1E'); + await inputHandler.parseP('\x1b[1E'); assert.deepEqual(getCursor(bufferService), [0, 2]); - inputHandler.parse('\x1b[4E'); + await inputHandler.parseP('\x1b[4E'); assert.deepEqual(getCursor(bufferService), [0, 6]); - inputHandler.parse('\x1b[100E'); + await inputHandler.parseP('\x1b[100E'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should reset x to zero bufferService.buffer.x = 8; bufferService.buffer.y = 0; - inputHandler.parse('\x1b[E'); + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('cursor previous line (CPL)', () => { - inputHandler.parse('\x1b[F'); + it('cursor previous line (CPL)', async () => { + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1F'); + await inputHandler.parseP('\x1b[1F'); assert.deepEqual(getCursor(bufferService), [0, 0]); // place cursor at beginning of last row - inputHandler.parse('\x1b[100E'); - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[100E'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 8]); - inputHandler.parse('\x1b[1F'); + await inputHandler.parseP('\x1b[1F'); assert.deepEqual(getCursor(bufferService), [0, 7]); - inputHandler.parse('\x1b[4F'); + await inputHandler.parseP('\x1b[4F'); assert.deepEqual(getCursor(bufferService), [0, 3]); - inputHandler.parse('\x1b[100F'); + await inputHandler.parseP('\x1b[100F'); assert.deepEqual(getCursor(bufferService), [0, 0]); // should reset x to zero bufferService.buffer.x = 8; bufferService.buffer.y = 9; - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 8]); }); - it('cursor character absolute (CHA)', () => { - inputHandler.parse('\x1b[G'); + it('cursor character absolute (CHA)', async () => { + await inputHandler.parseP('\x1b[G'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1G'); + await inputHandler.parseP('\x1b[1G'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[2G'); + await inputHandler.parseP('\x1b[2G'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[5G'); + await inputHandler.parseP('\x1b[5G'); assert.deepEqual(getCursor(bufferService), [4, 0]); - inputHandler.parse('\x1b[100G'); + await inputHandler.parseP('\x1b[100G'); assert.deepEqual(getCursor(bufferService), [9, 0]); }); - it('cursor position (CUP)', () => { + it('cursor position (CUP)', async () => { bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[H'); + await inputHandler.parseP('\x1b[H'); assert.deepEqual(getCursor(bufferService), [0, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[1H'); + await inputHandler.parseP('\x1b[1H'); assert.deepEqual(getCursor(bufferService), [0, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[1;1H'); + await inputHandler.parseP('\x1b[1;1H'); assert.deepEqual(getCursor(bufferService), [0, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[8H'); + await inputHandler.parseP('\x1b[8H'); assert.deepEqual(getCursor(bufferService), [0, 7]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[;8H'); + await inputHandler.parseP('\x1b[;8H'); assert.deepEqual(getCursor(bufferService), [7, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[100;100H'); + await inputHandler.parseP('\x1b[100;100H'); assert.deepEqual(getCursor(bufferService), [9, 9]); }); - it('horizontal position absolute (HPA)', () => { - inputHandler.parse('\x1b[`'); + it('horizontal position absolute (HPA)', async () => { + await inputHandler.parseP('\x1b[`'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1`'); + await inputHandler.parseP('\x1b[1`'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[2`'); + await inputHandler.parseP('\x1b[2`'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[5`'); + await inputHandler.parseP('\x1b[5`'); assert.deepEqual(getCursor(bufferService), [4, 0]); - inputHandler.parse('\x1b[100`'); + await inputHandler.parseP('\x1b[100`'); assert.deepEqual(getCursor(bufferService), [9, 0]); }); - it('horizontal position relative (HPR)', () => { - inputHandler.parse('\x1b[a'); + it('horizontal position relative (HPR)', async () => { + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[1a'); + await inputHandler.parseP('\x1b[1a'); assert.deepEqual(getCursor(bufferService), [2, 0]); - inputHandler.parse('\x1b[4a'); + await inputHandler.parseP('\x1b[4a'); assert.deepEqual(getCursor(bufferService), [6, 0]); - inputHandler.parse('\x1b[100a'); + await inputHandler.parseP('\x1b[100a'); assert.deepEqual(getCursor(bufferService), [9, 0]); // should not change y bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[a'); + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [9, 4]); }); - it('vertical position absolute (VPA)', () => { - inputHandler.parse('\x1b[d'); + it('vertical position absolute (VPA)', async () => { + await inputHandler.parseP('\x1b[d'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1d'); + await inputHandler.parseP('\x1b[1d'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[2d'); + await inputHandler.parseP('\x1b[2d'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[5d'); + await inputHandler.parseP('\x1b[5d'); assert.deepEqual(getCursor(bufferService), [0, 4]); - inputHandler.parse('\x1b[100d'); + await inputHandler.parseP('\x1b[100d'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[d'); + await inputHandler.parseP('\x1b[d'); assert.deepEqual(getCursor(bufferService), [8, 0]); }); - it('vertical position relative (VPR)', () => { - inputHandler.parse('\x1b[e'); + it('vertical position relative (VPR)', async () => { + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[1e'); + await inputHandler.parseP('\x1b[1e'); assert.deepEqual(getCursor(bufferService), [0, 2]); - inputHandler.parse('\x1b[4e'); + await inputHandler.parseP('\x1b[4e'); assert.deepEqual(getCursor(bufferService), [0, 6]); - inputHandler.parse('\x1b[100e'); + await inputHandler.parseP('\x1b[100e'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[e'); + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [8, 5]); }); describe('should clamp cursor into addressible range', () => { - it('CUF', () => { + it('CUF', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[C'); + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[C'); + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [1, 0]); }); - it('CUB', () => { + it('CUB', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [8, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('CUD', () => { + it('CUD', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[B'); + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[B'); + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('CUU', () => { + it('CUU', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [9, 8]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('CNL', () => { + it('CNL', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[E'); + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[E'); + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('CPL', () => { + it('CPL', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 8]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('CHA', () => { + it('CHA', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5G'); + await inputHandler.parseP('\x1b[5G'); assert.deepEqual(getCursor(bufferService), [4, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5G'); + await inputHandler.parseP('\x1b[5G'); assert.deepEqual(getCursor(bufferService), [4, 0]); }); - it('CUP', () => { + it('CUP', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5;5H'); + await inputHandler.parseP('\x1b[5;5H'); assert.deepEqual(getCursor(bufferService), [4, 4]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5;5H'); + await inputHandler.parseP('\x1b[5;5H'); assert.deepEqual(getCursor(bufferService), [4, 4]); }); - it('HPA', () => { + it('HPA', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5`'); + await inputHandler.parseP('\x1b[5`'); assert.deepEqual(getCursor(bufferService), [4, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5`'); + await inputHandler.parseP('\x1b[5`'); assert.deepEqual(getCursor(bufferService), [4, 0]); }); - it('HPR', () => { + it('HPR', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[a'); + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[a'); + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [1, 0]); }); - it('VPA', () => { + it('VPA', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5d'); + await inputHandler.parseP('\x1b[5d'); assert.deepEqual(getCursor(bufferService), [9, 4]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5d'); + await inputHandler.parseP('\x1b[5d'); assert.deepEqual(getCursor(bufferService), [0, 4]); }); - it('VPR', () => { + it('VPR', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[e'); + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[e'); + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('DCH', () => { + it('DCH', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[P'); + await inputHandler.parseP('\x1b[P'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[P'); + await inputHandler.parseP('\x1b[P'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('DCH - should delete last cell', () => { - inputHandler.parse('0123456789\x1b[P'); + it('DCH - should delete last cell', async () => { + await inputHandler.parseP('0123456789\x1b[P'); assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); }); - it('ECH', () => { + it('ECH', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[X'); + await inputHandler.parseP('\x1b[X'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[X'); + await inputHandler.parseP('\x1b[X'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('ECH - should delete last cell', () => { - inputHandler.parse('0123456789\x1b[X'); + it('ECH - should delete last cell', async () => { + await inputHandler.parseP('0123456789\x1b[X'); assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); }); - it('ICH', () => { + it('ICH', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[@'); + await inputHandler.parseP('\x1b[@'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[@'); + await inputHandler.parseP('\x1b[@'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('ICH - should delete last cell', () => { - inputHandler.parse('0123456789\x1b[@'); + it('ICH - should delete last cell', async () => { + await inputHandler.parseP('0123456789\x1b[@'); assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); }); }); @@ -1093,31 +1105,31 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 10); }); - it('should default to whole viewport', () => { - inputHandler.parse('\x1b[r'); + it('should default to whole viewport', async () => { + await inputHandler.parseP('\x1b[r'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, 9); - inputHandler.parse('\x1b[3;7r'); + await inputHandler.parseP('\x1b[3;7r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 6); - inputHandler.parse('\x1b[0;0r'); + await inputHandler.parseP('\x1b[0;0r'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, 9); }); - it('should clamp bottom', () => { - inputHandler.parse('\x1b[3;1000r'); + it('should clamp bottom', async () => { + await inputHandler.parseP('\x1b[3;1000r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 9); }); - it('should only apply for top < bottom', () => { - inputHandler.parse('\x1b[7;2r'); + it('should only apply for top < bottom', async () => { + await inputHandler.parseP('\x1b[7;2r'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, 9); }); - it('should home cursor', () => { + it('should home cursor', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[2;7r'); + await inputHandler.parseP('\x1b[2;7r'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); }); @@ -1125,76 +1137,76 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 10); }); - it('scrollUp', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); + it('scrollUp', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); assert.deepEqual(getLines(bufferService), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']); }); - it('scrollDown', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm'); + it('scrollDown', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm'); assert.deepEqual(getLines(bufferService), ['m', '', '', '1', '4', '5', '6', '7', '8', '9']); }); - it('insertLines - out of margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('insertLines - out of margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[2Lm'); + await inputHandler.parseP('\x1b[2Lm'); assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - inputHandler.parse('\x1b[2H\x1b[2Ln'); + await inputHandler.parseP('\x1b[2H\x1b[2Ln'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); // skip below scrollbottom - inputHandler.parse('\x1b[7H\x1b[2Lo'); + await inputHandler.parseP('\x1b[7H\x1b[2Lo'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); - inputHandler.parse('\x1b[8H\x1b[2Lp'); + await inputHandler.parseP('\x1b[8H\x1b[2Lp'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); - inputHandler.parse('\x1b[100H\x1b[2Lq'); + await inputHandler.parseP('\x1b[100H\x1b[2Lq'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); }); - it('insertLines - within margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('insertLines - within margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[3H\x1b[2Lm'); + await inputHandler.parseP('\x1b[3H\x1b[2Lm'); assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', '3', '6', '7', '8', '9']); - inputHandler.parse('\x1b[6H\x1b[2Ln'); + await inputHandler.parseP('\x1b[6H\x1b[2Ln'); assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', 'n', '6', '7', '8', '9']); }); - it('deleteLines - out of margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('deleteLines - out of margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[2Mm'); + await inputHandler.parseP('\x1b[2Mm'); assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - inputHandler.parse('\x1b[2H\x1b[2Mn'); + await inputHandler.parseP('\x1b[2H\x1b[2Mn'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); // skip below scrollbottom - inputHandler.parse('\x1b[7H\x1b[2Mo'); + await inputHandler.parseP('\x1b[7H\x1b[2Mo'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); - inputHandler.parse('\x1b[8H\x1b[2Mp'); + await inputHandler.parseP('\x1b[8H\x1b[2Mp'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); - inputHandler.parse('\x1b[100H\x1b[2Mq'); + await inputHandler.parseP('\x1b[100H\x1b[2Mq'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); }); - it('deleteLines - within margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('deleteLines - within margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[6H\x1b[2Mm'); + await inputHandler.parseP('\x1b[6H\x1b[2Mm'); assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']); - inputHandler.parse('\x1b[3H\x1b[2Mn'); + await inputHandler.parseP('\x1b[3H\x1b[2Mn'); assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); }); }); - it('should parse big chunks in smaller subchunks', () => { + it('should parse big chunks in smaller subchunks', async () => { // max single chunk size is hardcoded as 131072 const calls: any[] = []; bufferService.resize(10, 10); (inputHandler as any)._parser.parse = (data: Uint32Array, length: number) => { calls.push([data.length, length]); }; - inputHandler.parse('12345'); - inputHandler.parse('a'.repeat(10000)); - inputHandler.parse('a'.repeat(200000)); - inputHandler.parse('a'.repeat(300000)); + await inputHandler.parseP('12345'); + await inputHandler.parseP('a'.repeat(10000)); + await inputHandler.parseP('a'.repeat(200000)); + await inputHandler.parseP('a'.repeat(300000)); assert.deepEqual(calls, [ [4096, 5], [10000, 10000], @@ -1203,187 +1215,187 @@ describe('InputHandler', () => { ]); }); describe('windowOptions', () => { - it('all should be disabled by default and not report', () => { + it('all should be disabled by default and not report', async () => { bufferService.resize(10, 10); const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[14t'); - inputHandler.parse('\x1b[16t'); - inputHandler.parse('\x1b[18t'); - inputHandler.parse('\x1b[20t'); - inputHandler.parse('\x1b[21t'); + await inputHandler.parseP('\x1b[14t'); + await inputHandler.parseP('\x1b[16t'); + await inputHandler.parseP('\x1b[18t'); + await inputHandler.parseP('\x1b[20t'); + await inputHandler.parseP('\x1b[21t'); assert.deepEqual(stack, []); }); - it('14 - GetWinSizePixels', () => { + it('14 - GetWinSizePixels', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.getWinSizePixels = true; const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[14t'); + await inputHandler.parseP('\x1b[14t'); // does not report in test terminal due to missing renderer assert.deepEqual(stack, []); }); - it('16 - GetCellSizePixels', () => { + it('16 - GetCellSizePixels', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.getCellSizePixels = true; const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[16t'); + await inputHandler.parseP('\x1b[16t'); // does not report in test terminal due to missing renderer assert.deepEqual(stack, []); }); - it('18 - GetWinSizeChars', () => { + it('18 - GetWinSizeChars', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.getWinSizeChars = true; const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[18t'); + await inputHandler.parseP('\x1b[18t'); assert.deepEqual(stack, ['\x1b[8;10;10t']); bufferService.resize(50, 20); - inputHandler.parse('\x1b[18t'); + await inputHandler.parseP('\x1b[18t'); assert.deepEqual(stack, ['\x1b[8;10;10t', '\x1b[8;20;50t']); }); - it('22/23 - PushTitle/PopTitle', () => { + it('22/23 - PushTitle/PopTitle', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.pushTitle = true; optionsService.options.windowOptions.popTitle = true; const stack: string[] = []; inputHandler.onTitleChange(data => stack.push(data)); - inputHandler.parse('\x1b]0;1\x07'); - inputHandler.parse('\x1b[22t'); - inputHandler.parse('\x1b]0;2\x07'); - inputHandler.parse('\x1b[22t'); - inputHandler.parse('\x1b]0;3\x07'); - inputHandler.parse('\x1b[22t'); + await inputHandler.parseP('\x1b]0;1\x07'); + await inputHandler.parseP('\x1b[22t'); + await inputHandler.parseP('\x1b]0;2\x07'); + await inputHandler.parseP('\x1b[22t'); + await inputHandler.parseP('\x1b]0;3\x07'); + await inputHandler.parseP('\x1b[22t'); assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']); assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']); assert.deepEqual(stack, ['1', '2', '3']); - inputHandler.parse('\x1b[23t'); - inputHandler.parse('\x1b[23t'); - inputHandler.parse('\x1b[23t'); - inputHandler.parse('\x1b[23t'); // one more to test "overflow" + await inputHandler.parseP('\x1b[23t'); + await inputHandler.parseP('\x1b[23t'); + await inputHandler.parseP('\x1b[23t'); + await inputHandler.parseP('\x1b[23t'); // one more to test "overflow" assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); }); - it('22/23 - PushTitle/PopTitle with ;1', () => { + it('22/23 - PushTitle/PopTitle with ;1', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.pushTitle = true; optionsService.options.windowOptions.popTitle = true; const stack: string[] = []; inputHandler.onTitleChange(data => stack.push(data)); - inputHandler.parse('\x1b]0;1\x07'); - inputHandler.parse('\x1b[22;1t'); - inputHandler.parse('\x1b]0;2\x07'); - inputHandler.parse('\x1b[22;1t'); - inputHandler.parse('\x1b]0;3\x07'); - inputHandler.parse('\x1b[22;1t'); + await inputHandler.parseP('\x1b]0;1\x07'); + await inputHandler.parseP('\x1b[22;1t'); + await inputHandler.parseP('\x1b]0;2\x07'); + await inputHandler.parseP('\x1b[22;1t'); + await inputHandler.parseP('\x1b]0;3\x07'); + await inputHandler.parseP('\x1b[22;1t'); assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']); assert.deepEqual(stack, ['1', '2', '3']); - inputHandler.parse('\x1b[23;1t'); - inputHandler.parse('\x1b[23;1t'); - inputHandler.parse('\x1b[23;1t'); - inputHandler.parse('\x1b[23;1t'); // one more to test "overflow" + await inputHandler.parseP('\x1b[23;1t'); + await inputHandler.parseP('\x1b[23;1t'); + await inputHandler.parseP('\x1b[23;1t'); + await inputHandler.parseP('\x1b[23;1t'); // one more to test "overflow" assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3']); }); - it('22/23 - PushTitle/PopTitle with ;2', () => { + it('22/23 - PushTitle/PopTitle with ;2', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.pushTitle = true; optionsService.options.windowOptions.popTitle = true; const stack: string[] = []; inputHandler.onTitleChange(data => stack.push(data)); - inputHandler.parse('\x1b]0;1\x07'); - inputHandler.parse('\x1b[22;2t'); - inputHandler.parse('\x1b]0;2\x07'); - inputHandler.parse('\x1b[22;2t'); - inputHandler.parse('\x1b]0;3\x07'); - inputHandler.parse('\x1b[22;2t'); + await inputHandler.parseP('\x1b]0;1\x07'); + await inputHandler.parseP('\x1b[22;2t'); + await inputHandler.parseP('\x1b]0;2\x07'); + await inputHandler.parseP('\x1b[22;2t'); + await inputHandler.parseP('\x1b]0;3\x07'); + await inputHandler.parseP('\x1b[22;2t'); assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3']); - inputHandler.parse('\x1b[23;2t'); - inputHandler.parse('\x1b[23;2t'); - inputHandler.parse('\x1b[23;2t'); - inputHandler.parse('\x1b[23;2t'); // one more to test "overflow" + await inputHandler.parseP('\x1b[23;2t'); + await inputHandler.parseP('\x1b[23;2t'); + await inputHandler.parseP('\x1b[23;2t'); + await inputHandler.parseP('\x1b[23;2t'); // one more to test "overflow" assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); }); - it('DECCOLM - should only work with "SetWinLines" (24) enabled', () => { + it('DECCOLM - should only work with "SetWinLines" (24) enabled', async () => { // disabled bufferService.resize(10, 10); - inputHandler.parse('\x1b[?3l'); + await inputHandler.parseP('\x1b[?3l'); assert.equal(bufferService.cols, 10); - inputHandler.parse('\x1b[?3h'); + await inputHandler.parseP('\x1b[?3h'); assert.equal(bufferService.cols, 10); // enabled inputHandler.reset(); optionsService.options.windowOptions.setWinLines = true; - inputHandler.parse('\x1b[?3l'); + await inputHandler.parseP('\x1b[?3l'); assert.equal(bufferService.cols, 80); - inputHandler.parse('\x1b[?3h'); + await inputHandler.parseP('\x1b[?3h'); assert.equal(bufferService.cols, 132); }); }); describe('should correctly reset cells taken by wide chars', () => { - beforeEach(() => { + beforeEach(async () => { bufferService.resize(10, 5); optionsService.options.scrollback = 1; - inputHandler.parse('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); + await inputHandler.parseP('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); }); - it('print', () => { - inputHandler.parse('\x1b[H#'); + it('print', async () => { + await inputHandler.parseP('\x1b[H#'); assert.deepEqual(getLines(bufferService), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[1;6H######'); + await inputHandler.parseP('\x1b[1;6H######'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;9H#'); + await inputHandler.parseP('\x1b[3;9H#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']); - inputHandler.parse('\x1b[4;10H#'); + await inputHandler.parseP('\x1b[4;10H#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']); }); - it('EL', () => { - inputHandler.parse('\x1b[1;6H\x1b[K#'); + it('EL', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[K#'); assert.deepEqual(getLines(bufferService), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;5H\x1b[1K'); + await inputHandler.parseP('\x1b[2;5H\x1b[1K'); assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;6H\x1b[1K'); + await inputHandler.parseP('\x1b[3;6H\x1b[1K'); assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']); }); - it('ICH', () => { - inputHandler.parse('\x1b[1;6H\x1b[@'); + it('ICH', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;4H\x1b[2@'); + await inputHandler.parseP('\x1b[2;4H\x1b[2@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;4H\x1b[3@'); + await inputHandler.parseP('\x1b[3;4H\x1b[3@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[4;4H\x1b[4@'); + await inputHandler.parseP('\x1b[4;4H\x1b[4@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']); }); - it('DCH', () => { - inputHandler.parse('\x1b[1;6H\x1b[P'); + it('DCH', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[P'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;6H\x1b[2P'); + await inputHandler.parseP('\x1b[2;6H\x1b[2P'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;6H\x1b[3P'); + await inputHandler.parseP('\x1b[3;6H\x1b[3P'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); }); - it('ECH', () => { - inputHandler.parse('\x1b[1;6H\x1b[X'); + it('ECH', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[X'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;6H\x1b[2X'); + await inputHandler.parseP('\x1b[2;6H\x1b[2X'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;6H\x1b[3X'); + await inputHandler.parseP('\x1b[3;6H\x1b[3X'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); }); }); @@ -1395,74 +1407,74 @@ describe('InputHandler', () => { optionsService.options.scrollback = 1; }); describe('reverseWraparound unset (default)', () => { - it('cannot delete last cell', () => { - inputHandler.parse('12345'); - inputHandler.parse(ttyBS); + it('cannot delete last cell', async () => { + await inputHandler.parseP('12345'); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 1), ['123 5']); - inputHandler.parse(ttyBS.repeat(10)); + await inputHandler.parseP(ttyBS.repeat(10)); assert.deepEqual(getLines(bufferService, 1), [' 5']); }); - it('cannot access prev line', () => { - inputHandler.parse('12345'.repeat(2)); - inputHandler.parse(ttyBS); + it('cannot access prev line', async () => { + await inputHandler.parseP('12345'.repeat(2)); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['12345', '123 5']); - inputHandler.parse(ttyBS.repeat(10)); + await inputHandler.parseP(ttyBS.repeat(10)); assert.deepEqual(getLines(bufferService, 2), ['12345', ' 5']); }); }); describe('reverseWraparound set', () => { - it('can delete last cell', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345'); - inputHandler.parse(ttyBS); + it('can delete last cell', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345'); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 1), ['1234 ']); - inputHandler.parse(ttyBS.repeat(7)); + await inputHandler.parseP(ttyBS.repeat(7)); assert.deepEqual(getLines(bufferService, 1), [' ']); }); - it('can access prev line if wrapped', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345'.repeat(2)); - inputHandler.parse(ttyBS); + it('can access prev line if wrapped', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345'.repeat(2)); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['12345', '1234 ']); - inputHandler.parse(ttyBS.repeat(7)); + await inputHandler.parseP(ttyBS.repeat(7)); assert.deepEqual(getLines(bufferService, 2), ['12 ', ' ']); }); - it('should lift isWrapped', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345'.repeat(2)); + it('should lift isWrapped', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345'.repeat(2)); assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, true); - inputHandler.parse(ttyBS.repeat(7)); + await inputHandler.parseP(ttyBS.repeat(7)); assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, false); }); - it('stops at hard NLs', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345\r\n'); - inputHandler.parse('12345'.repeat(2)); - inputHandler.parse(ttyBS.repeat(50)); + it('stops at hard NLs', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345\r\n'); + await inputHandler.parseP('12345'.repeat(2)); + await inputHandler.parseP(ttyBS.repeat(50)); assert.deepEqual(getLines(bufferService, 3), ['12345', ' ', ' ']); assert.equal(bufferService.buffer.x, 0); assert.equal(bufferService.buffer.y, 1); }); - it('handles wide chars correctly', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('¥¥¥'); + it('handles wide chars correctly', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('¥¥¥'); assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']); assert.equal(bufferService.buffer.x, 1); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']); assert.equal(bufferService.buffer.x, 0); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']); assert.equal(bufferService.buffer.x, 3); // x=4 skipped due to early wrap-around - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']); assert.equal(bufferService.buffer.x, 2); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), [' ', ' ']); assert.equal(bufferService.buffer.x, 1); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), [' ', ' ']); assert.equal(bufferService.buffer.x, 0); }); @@ -1473,72 +1485,72 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 5); }); - it('4 | 24', () => { - inputHandler.parse('\x1b[4m'); + it('4 | 24', async () => { + await inputHandler.parseP('\x1b[4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('21 | 24', () => { - inputHandler.parse('\x1b[21m'); + it('21 | 24', async () => { + await inputHandler.parseP('\x1b[21m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:1 | 4:0', () => { - inputHandler.parse('\x1b[4:1m'); + it('4:1 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:1m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:1m'); + await inputHandler.parseP('\x1b[4:1m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:2 | 4:0', () => { - inputHandler.parse('\x1b[4:2m'); + it('4:2 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:2m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:2m'); + await inputHandler.parseP('\x1b[4:2m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:3 | 4:0', () => { - inputHandler.parse('\x1b[4:3m'); + it('4:3 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:3m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:3m'); + await inputHandler.parseP('\x1b[4:3m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:4 | 4:0', () => { - inputHandler.parse('\x1b[4:4m'); + it('4:4 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:4m'); + await inputHandler.parseP('\x1b[4:4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:5 | 4:0', () => { - inputHandler.parse('\x1b[4:5m'); + it('4:5 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:5m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:5m'); + await inputHandler.parseP('\x1b[4:5m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:x --> 4 should revert to single underline', () => { - inputHandler.parse('\x1b[4:5m'); + it('4:x --> 4 should revert to single underline', async () => { + await inputHandler.parseP('\x1b[4:5m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); - inputHandler.parse('\x1b[4m'); + await inputHandler.parseP('\x1b[4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); }); }); @@ -1546,9 +1558,9 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 5); }); - it('defaults to FG color', () => { + it('defaults to FG color', async () => { for (const s of ['', '\x1b[30m', '\x1b[38;510m', '\x1b[38;2;1;2;3m']) { - inputHandler.parse(s); + await inputHandler.parseP(s); assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); @@ -1556,31 +1568,31 @@ describe('InputHandler', () => { assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); } }); - it('correctly sets P256/RGB colors', () => { - inputHandler.parse('\x1b[4m'); - inputHandler.parse('\x1b[58;5;123m'); + it('correctly sets P256/RGB colors', async () => { + await inputHandler.parseP('\x1b[4m'); + await inputHandler.parseP('\x1b[58;5;123m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); - inputHandler.parse('\x1b[58;2::1:2:3m'); + await inputHandler.parseP('\x1b[58;2::1:2:3m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); }); - it('P256/RGB persistence', () => { + it('P256/RGB persistence', async () => { const cell = new CellData(); - inputHandler.parse('\x1b[4m'); - inputHandler.parse('\x1b[58;5;123m'); + await inputHandler.parseP('\x1b[4m'); + await inputHandler.parseP('\x1b[58;5;123m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); - inputHandler.parse('ab'); + await inputHandler.parseP('ab'); bufferService.buffer!.lines.get(0)!.loadCell(1, cell); assert.equal(cell.getUnderlineColor(), 123); assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); @@ -1588,13 +1600,13 @@ describe('InputHandler', () => { assert.equal(cell.isUnderlineColorPalette(), true); assert.equal(cell.isUnderlineColorDefault(), false); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); - inputHandler.parse('a'); + await inputHandler.parseP('a'); bufferService.buffer!.lines.get(0)!.loadCell(1, cell); assert.equal(cell.getUnderlineColor(), 123); assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); @@ -1608,15 +1620,15 @@ describe('InputHandler', () => { assert.equal(cell.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); assert.equal(cell.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); - inputHandler.parse('\x1b[4m'); - inputHandler.parse('\x1b[58;2::1:2:3m'); + await inputHandler.parseP('\x1b[4m'); + await inputHandler.parseP('\x1b[58;2::1:2:3m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); - inputHandler.parse('a'); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('a'); + await inputHandler.parseP('\x1b[24m'); bufferService.buffer!.lines.get(0)!.loadCell(1, cell); assert.equal(cell.getUnderlineColor(), 123); assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); @@ -1645,51 +1657,51 @@ describe('InputHandler', () => { }); }); describe('DECSTR', () => { - beforeEach(() => { + beforeEach(async () => { bufferService.resize(10, 5); optionsService.options.scrollback = 1; - inputHandler.parse('01234567890123'); + await inputHandler.parseP('01234567890123'); }); - it('should reset IRM', () => { - inputHandler.parse('\x1b[4h'); + it('should reset IRM', async () => { + await inputHandler.parseP('\x1b[4h'); assert.equal(coreService.modes.insertMode, true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(coreService.modes.insertMode, false); }); - it('should reset cursor visibility', () => { - inputHandler.parse('\x1b[?25l'); + it('should reset cursor visibility', async () => { + await inputHandler.parseP('\x1b[?25l'); assert.equal(coreService.isCursorHidden, true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(coreService.isCursorHidden, false); }); - it('should reset scroll margins', () => { - inputHandler.parse('\x1b[2;4r'); + it('should reset scroll margins', async () => { + await inputHandler.parseP('\x1b[2;4r'); assert.equal(bufferService.buffer.scrollTop, 1); assert.equal(bufferService.buffer.scrollBottom, 3); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, bufferService.rows - 1); }); - it('should reset text attributes', () => { - inputHandler.parse('\x1b[1;2;32;43m'); + it('should reset text attributes', async () => { + await inputHandler.parseP('\x1b[1;2;32;43m'); assert.equal(!!inputHandler.curAttrData.isBold(), true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(!!inputHandler.curAttrData.isBold(), false); assert.equal(inputHandler.curAttrData.fg, 0); assert.equal(inputHandler.curAttrData.bg, 0); }); - it('should reset DECSC data', () => { - inputHandler.parse('\x1b7'); + it('should reset DECSC data', async () => { + await inputHandler.parseP('\x1b7'); assert.equal(bufferService.buffer.savedX, 4); assert.equal(bufferService.buffer.savedY, 1); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(bufferService.buffer.savedX, 0); assert.equal(bufferService.buffer.savedY, 0); }); - it('should reset DECOM', () => { - inputHandler.parse('\x1b[?6h'); + it('should reset DECOM', async () => { + await inputHandler.parseP('\x1b[?6h'); assert.equal(coreService.decPrivateModes.origin, true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(coreService.decPrivateModes.origin, false); }); }); @@ -1733,15 +1745,17 @@ describe('InputHandler', () => { assert.equal(event!.colors.length, 1); assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); }); - it('4: should fire event on Ansi color change', (done) => { - inputHandler.onAnsiColorChange(e => { - assert.isNotNull(e); - assert.isNotNull(e!.colors); - assert.deepEqual(e!.colors[0], { colorIndex: 17, red: 0x1a, green: 0x2b, blue: 0x3c }); - assert.deepEqual(e!.colors[1], { colorIndex: 12, red: 0x11, green: 0x22, blue: 0x33 }); - done(); + it('4: should fire event on Ansi color change', async () => { + return new Promise(async r => { + inputHandler.onAnsiColorChange(e => { + assert.isNotNull(e); + assert.isNotNull(e!.colors); + assert.deepEqual(e!.colors[0], { colorIndex: 17, red: 0x1a, green: 0x2b, blue: 0x3c }); + assert.deepEqual(e!.colors[1], { colorIndex: 12, red: 0x11, green: 0x22, blue: 0x33 }); + r(); + }); + await inputHandler.parseP('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\'); }); - inputHandler.parse('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\'); }); }); }); From adfeffec764caffe4bfcca15014350f6ea650573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 22 Jan 2021 21:17:34 +0100 Subject: [PATCH 03/22] deprecate writeSync --- src/browser/Terminal.test.ts | 471 +++++++++++++++++----------------- src/browser/TestUtils.test.ts | 3 + src/common/CoreTerminal.ts | 10 + 3 files changed, 254 insertions(+), 230 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 8626be09..2e09f719 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -55,22 +55,30 @@ describe('Terminal', () => { // term.onData(() => done()); // term.handler('fake'); // }); - it('should fire the onCursorMove event', (done) => { - term.onCursorMove(() => done()); - term.writeSync('foo'); + it('should fire the onCursorMove event', () => { + return new Promise(async r => { + term.onCursorMove(() => r()); + await term.writeP('foo'); + }); }); - it('should fire the onLineFeed event', (done) => { - term.onLineFeed(() => done()); - term.writeSync('\n'); + it('should fire the onLineFeed event', () => { + return new Promise(async r => { + term.onLineFeed(() => r()); + await term.writeP('\n'); + }); }); - it('should fire a scroll event when scrollback is created', (done) => { - term.onScroll(() => done()); - term.writeSync('\n'.repeat(INIT_ROWS)); + it('should fire a scroll event when scrollback is created', () => { + return new Promise(async r => { + term.onScroll(() => r()); + await term.writeP('\n'.repeat(INIT_ROWS)); + }); }); - it('should fire a scroll event when scrollback is cleared', (done) => { - term.writeSync('\n'.repeat(INIT_ROWS)); - term.onScroll(() => done()); - term.clear(); + it('should fire a scroll event when scrollback is cleared', () => { + return new Promise(async r => { + await term.writeP('\n'.repeat(INIT_ROWS)); + term.onScroll(() => r()); + term.clear(); + }); }); it('should fire a key event after a keypress DOM event', (done) => { term.onKey(e => { @@ -178,10 +186,10 @@ describe('Terminal', () => { assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); - it('should clear a buffer larger than rows', () => { + it('should clear a buffer larger than rows', async () => { // Fill the buffer with dummy rows for (let i = 0; i < term.rows * 2; i++) { - term.writeSync('test\n'); + await term.writeP('test\n'); } const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); @@ -225,22 +233,24 @@ describe('Terminal', () => { }); term.paste('\r\nfoo\nbar\r'); }); - it('should respect bracketed paste mode', done => { - term.onData(e => { - assert.equal(e, '\x1b[200~foo\x1b[201~'); - done(); + it('should respect bracketed paste mode', () => { + return new Promise(async r => { + term.onData(e => { + assert.equal(e, '\x1b[200~foo\x1b[201~'); + r(); + }); + await term.writeP('\x1b[?2004h'); + term.paste('foo'); }); - term.writeSync('\x1b[?2004h'); - term.paste('foo'); }); }); describe('scroll', () => { describe('scrollLines', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < INIT_ROWS * 2; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = INIT_ROWS + 1; }); @@ -273,9 +283,9 @@ describe('Terminal', () => { describe('scrollPages', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -296,9 +306,9 @@ describe('Terminal', () => { }); describe('scrollToTop', () => { - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } }); it('should scroll to the top', () => { @@ -310,9 +320,9 @@ describe('Terminal', () => { describe('scrollToBottom', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -331,9 +341,9 @@ describe('Terminal', () => { describe('scrollToLine', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -375,10 +385,10 @@ describe('Terminal', () => { assert.equal(term.buffer.ydisp, term.buffer.ybase); }); - it('should not scroll down, when a custom keydown handler prevents the event', () => { + it('should not scroll down, when a custom keydown handler prevents the event', async () => { // Add some output to the terminal for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } const startYDisp = (term.rows * 2) + 1; term.attachCustomKeyEventHandler(() => { @@ -717,12 +727,12 @@ describe('Terminal', () => { }); describe('unicode - surrogates', () => { - it('2 characters per cell', function (): void { + it('2 characters per cell', async function (): Promise { this.timeout(10000); // This is needed because istanbul patches code and slows it down const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.writeSync(high + String.fromCharCode(i)); + await term.writeP(high + String.fromCharCode(i)); const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars().length).eql(2); @@ -731,25 +741,25 @@ describe('Terminal', () => { term.reset(); } }); - it('2 characters at last cell', () => { + it('2 characters at last cell', async () => { const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.writeSync(high + String.fromCharCode(i)); + await term.writeP(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars()).eql(''); term.reset(); } }); - it('2 characters per cell over line end with autowrap', () => { + it('2 characters per cell over line end with autowrap', async () => { const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.writeSync('a' + high + String.fromCharCode(i)); + await term.writeP('a' + high + String.fromCharCode(i)); expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars()).eql('a'); expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length).eql(2); @@ -757,17 +767,17 @@ describe('Terminal', () => { term.reset(); } }); - it('2 characters per cell over line end without autowrap', () => { + it('2 characters per cell over line end without autowrap', async () => { const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.writeSync('\x1b[?7l'); // Disable wraparound mode + await term.writeP('\x1b[?7l'); // Disable wraparound mode const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); if (width !== 1) { continue; } - term.writeSync('a' + high + String.fromCharCode(i)); + await term.writeP('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length).eql(2); @@ -775,12 +785,11 @@ describe('Terminal', () => { term.reset(); } }); - it('splitted surrogates', () => { + it('splitted surrogates', async () => { const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.writeSync(high); - term.writeSync(String.fromCharCode(i)); + await term.writeP(high + String.fromCharCode(i)); const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars().length).eql(2); @@ -793,16 +802,16 @@ describe('Terminal', () => { describe('unicode - combining characters', () => { const cell = new CellData(); - it('café', () => { - term.writeSync('cafe\u0301'); + it('café', async () => { + await term.writeP('cafe\u0301'); term.buffer.lines.get(0)!.loadCell(3, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(1); }); - it('café - end of line', () => { + it('café - end of line', async () => { term.buffer.x = term.cols - 1 - 3; - term.writeSync('cafe\u0301'); + await term.writeP('cafe\u0301'); term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); @@ -812,8 +821,8 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(0); expect(cell.getWidth()).eql(1); }); - it('multiple combined é', () => { - term.writeSync(Array(100).join('e\u0301')); + it('multiple combined é', async () => { + await term.writeP(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); expect(cell.getChars()).eql('e\u0301'); @@ -825,8 +834,8 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(1); }); - it('multiple surrogate with combined', () => { - term.writeSync(Array(100).join('\uD800\uDC00\u0301')); + it('multiple surrogate with combined', async () => { + await term.writeP(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); expect(cell.getChars()).eql('\uD800\uDC00\u0301'); @@ -842,19 +851,19 @@ describe('Terminal', () => { describe('unicode - fullwidth characters', () => { const cell = new CellData(); - it('cursor movement even', () => { + it('cursor movement even', async () => { expect(term.buffer.x).eql(0); - term.writeSync('¥'); + await term.writeP('¥'); expect(term.buffer.x).eql(2); }); - it('cursor movement odd', () => { + it('cursor movement odd', async () => { term.buffer.x = 1; expect(term.buffer.x).eql(1); - term.writeSync('¥'); + await term.writeP('¥'); expect(term.buffer.x).eql(3); }); - it('line of ¥ even', () => { - term.writeSync(Array(50).join('¥')); + it('line of ¥ even', async () => { + await term.writeP(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { @@ -872,9 +881,9 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(1); expect(cell.getWidth()).eql(2); }); - it('line of ¥ odd', () => { + it('line of ¥ odd', async () => { term.buffer.x = 1; - term.writeSync(Array(50).join('¥')); + await term.writeP(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { @@ -896,9 +905,9 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(1); expect(cell.getWidth()).eql(2); }); - it('line of ¥ with combining odd', () => { + it('line of ¥ with combining odd', async () => { term.buffer.x = 1; - term.writeSync(Array(50).join('¥\u0301')); + await term.writeP(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { @@ -920,8 +929,8 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(2); }); - it('line of ¥ with combining even', () => { - term.writeSync(Array(50).join('¥\u0301')); + it('line of ¥ with combining even', async () => { + await term.writeP(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { @@ -939,9 +948,9 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(2); }); - it('line of surrogate fullwidth with combining odd', () => { + it('line of surrogate fullwidth with combining odd', async () => { term.buffer.x = 1; - term.writeSync(Array(50).join('\ud843\ude6d\u0301')); + await term.writeP(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { @@ -963,8 +972,8 @@ describe('Terminal', () => { expect(cell.getChars().length).eql(3); expect(cell.getWidth()).eql(2); }); - it('line of surrogate fullwidth with combining even', () => { - term.writeSync(Array(50).join('\ud843\ude6d\u0301')); + it('line of surrogate fullwidth with combining even', async () => { + await term.writeP(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { @@ -986,24 +995,24 @@ describe('Terminal', () => { describe('insert mode', () => { const cell = new CellData(); - it('halfwidth - all', () => { - term.writeSync(Array(9).join('0123456789').slice(-80)); + it('halfwidth - all', async () => { + await term.writeP(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; term.write('\x1b[4h'); - term.writeSync('abcde'); + await term.writeP('abcde'); expect(term.buffer.lines.get(0)!.length).eql(term.cols); expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0)!.loadCell(14, cell).getChars()).eql('e'); expect(term.buffer.lines.get(0)!.loadCell(15, cell).getChars()).eql('0'); expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql('4'); }); - it('fullwidth - insert', () => { - term.writeSync(Array(9).join('0123456789').slice(-80)); + it('fullwidth - insert', async () => { + await term.writeP(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; term.write('\x1b[4h'); - term.writeSync('¥¥¥'); + await term.writeP('¥¥¥'); expect(term.buffer.lines.get(0)!.length).eql(term.cols); expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql(''); @@ -1011,17 +1020,17 @@ describe('Terminal', () => { expect(term.buffer.lines.get(0)!.loadCell(15, cell).getChars()).eql(''); expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql('3'); }); - it('fullwidth - right border', () => { - term.writeSync(Array(41).join('¥')); + it('fullwidth - right border', async () => { + await term.writeP(Array(41).join('¥')); term.buffer.x = 10; term.buffer.y = 0; term.write('\x1b[4h'); - term.writeSync('a'); + await term.writeP('a'); expect(term.buffer.lines.get(0)!.length).eql(term.cols); expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced - term.writeSync('b'); + await term.writeP('b'); expect(term.buffer.lines.get(0)!.length).eql(term.cols); expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql('b'); expect(term.buffer.lines.get(0)!.loadCell(12, cell).getChars()).eql('¥'); @@ -1043,85 +1052,87 @@ describe('Terminal', () => { linkifier.attachToDom({} as any, mouseZoneManager); }); - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: Mocha.Done): void { - terminal.writeSync(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRows(); - // Allow linkify to happen - setTimeout(() => { - assert.equal(mouseZoneManager.zones.length, links.length); - links.forEach((l, i) => { - assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); - assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); - assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); - assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); - }); - done(); - }, 0); + function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[]): Promise { + return new Promise(async r => { + await terminal.writeP(rowText); + linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); + linkifier.linkifyRows(); + // Allow linkify to happen + setTimeout(() => { + assert.equal(mouseZoneManager.zones.length, links.length); + links.forEach((l, i) => { + assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); + assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); + assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); + assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); + }); + r(); + }, 0); + }); } describe('unicode before the match', () => { - it('combining - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + it('combining - match within one line', () => { + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); }); - it('combining - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('combining - match over two lines', () => { + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); }); - it('surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + it('surrogate - match within one line', () => { + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); }); - it('surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); }); - it('combining surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + it('combining surrogate - match within one line', () => { + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); }); - it('combining surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('combining surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); }); - it('fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); }); - it('fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); }); - it('combining fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('combining fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); }); - it('combining fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('combining fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]); }); }); describe('unicode within the match', () => { - it('combining - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + it('combining - match within one line', () => { + return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); }); - it('combining - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + it('combining - match over two lines', () => { + return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); }); - it('surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('surrogate - match within one line', () => { + return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); }); - it('surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); + it('surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]); }); - it('combining surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('combining surrogate - match within one line', () => { + return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); }); - it('combining surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); + it('combining surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]); }); - it('fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + it('fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); }); - it('fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + it('fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); }); - it('combining fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + it('combining fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); }); - it('combining fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + it('combining fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); }); }); }); @@ -1133,9 +1144,9 @@ describe('Terminal', () => { terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); }); - it('multiline ascii', () => { + it('multiline ascii', async () => { const input = 'This is ASCII text spanning multiple lines.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { @@ -1144,9 +1155,9 @@ describe('Terminal', () => { } }); - it('combining e\u0301 in a sentence', () => { + it('combining e\u0301 in a sentence', async () => { const input = 'Sitting in the cafe\u0301 drinking coffee.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < 19; ++i) { @@ -1164,9 +1175,9 @@ describe('Terminal', () => { } }); - it('multiline combining e\u0301', () => { + it('multiline combining e\u0301', async () => { const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // every buffer cell index contains 2 string indices @@ -1176,9 +1187,9 @@ describe('Terminal', () => { } }); - it('surrogate char in a sentence', () => { + it('surrogate char in a sentence', async () => { const input = 'The 𝄞 is a clef widely used in modern notation.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < 5; ++i) { @@ -1196,9 +1207,9 @@ describe('Terminal', () => { } }); - it('multiline surrogate char', () => { + it('multiline surrogate char', async () => { const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // every buffer cell index contains 2 string indices @@ -1208,10 +1219,10 @@ describe('Terminal', () => { } }); - it('surrogate char with combining', () => { + it('surrogate char with combining', async () => { // eye of Ra with acute accent - string length of 3 const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // index 0..2 should map to 0 @@ -1223,9 +1234,9 @@ describe('Terminal', () => { } }); - it('multiline surrogate with combining', () => { + it('multiline surrogate with combining', async () => { const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // every buffer cell index contains 3 string indices @@ -1235,9 +1246,9 @@ describe('Terminal', () => { } }); - it('fullwidth chars', () => { + it('fullwidth chars', async () => { const input = 'These 123 are some fat numbers.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < 6; ++i) { @@ -1254,9 +1265,9 @@ describe('Terminal', () => { } }); - it('multiline fullwidth chars', () => { + it('multiline fullwidth chars', async () => { const input = '12345678901234567890'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 9; i < input.length; ++i) { @@ -1265,9 +1276,9 @@ describe('Terminal', () => { } }); - it('fullwidth combining with emoji - match emoji cell', () => { + it('fullwidth combining with emoji - match emoji cell', async () => { const input = 'Lots of ¥\u0301 make me 😃.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); const stringIndex = s.match(/😃/)!.index!; @@ -1275,14 +1286,14 @@ describe('Terminal', () => { assert(terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); }); - it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { + it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', async () => { const input = 'a12345678901234567890'; // the 'a' at the beginning moves all fullwidth chars one to the right // now the end of the line contains a dangling empty cell since // the next fullwidth char has to wrap early // the dangling last cell is wrongly added in the string // --> fixable after resolving #1685 - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 10; i < input.length; ++i) { @@ -1292,9 +1303,9 @@ describe('Terminal', () => { } }); - it('test fully wrapped buffer up to last char', () => { + it('test fully wrapped buffer up to last char', async () => { const input = Array(6).join('1234567890'); - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { @@ -1303,10 +1314,10 @@ describe('Terminal', () => { } }); - it('test fully wrapped buffer up to last char with full width odd', () => { + it('test fully wrapped buffer up to last char with full width odd', async () => { const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { @@ -1321,16 +1332,16 @@ describe('Terminal', () => { } }); - it('should handle \t in lines correctly', () => { + it('should handle \t in lines correctly', async () => { const input = '\thttps://google.de'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); }); }); describe('BufferStringIterator', function(): void { - it('iterator does not overflow buffer limits', function(): void { + it('iterator does not overflow buffer limits', async () => { const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); const data = [ 'aaaaaaaaaa', @@ -1344,7 +1355,7 @@ describe('Terminal', () => { 'aaaaaaaaaa', 'aaaaaaaaaa' ]; - terminal.writeSync(data.join('')); + await terminal.writeP(data.join('')); // brute force test with insane values expect(() => { for (let overscan = 0; overscan < 20; ++overscan) { @@ -1362,7 +1373,7 @@ describe('Terminal', () => { }); describe('Windows Mode', () => { - it('should mark lines as wrapped when the line ends in a non-null character after a LF', () => { + it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => { const data = [ 'aaaaaaaaaa\n\r', // cannot wrap as it's the first 'aaaaaaaaa\n\r', // wrapped (windows mode only) @@ -1370,19 +1381,19 @@ describe('Terminal', () => { ]; const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); - normalTerminal.writeSync(data.join('')); + await normalTerminal.writeP(data.join('')); assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); - windowsModeTerminal.writeSync(data.join('')); + await windowsModeTerminal.writeP(data.join('')); assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false); }); - it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => { + it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => { const data = [ 'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first 'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only) @@ -1390,22 +1401,22 @@ describe('Terminal', () => { ]; const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); - normalTerminal.writeSync(data.join('')); + await normalTerminal.writeP(data.join('')); assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); - windowsModeTerminal.writeSync(data.join('')); + await windowsModeTerminal.writeP(data.join('')); assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false); }); }); - it('convertEol setting', function(): void { + it('convertEol setting', async () => { // not converting const termNotConverting = new TestTerminal({cols: 15, rows: 10}); - termNotConverting.writeSync('Hello\nWorld'); + await termNotConverting.writeP('Hello\nWorld'); expect(termNotConverting.buffer.lines.get(0)!.translateToString(false)).equals('Hello '); expect(termNotConverting.buffer.lines.get(1)!.translateToString(false)).equals(' World '); expect(termNotConverting.buffer.lines.get(0)!.translateToString(true)).equals('Hello'); @@ -1413,7 +1424,7 @@ describe('Terminal', () => { // converting const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true}); - termConverting.writeSync('Hello\nWorld'); + await termConverting.writeP('Hello\nWorld'); expect(termConverting.buffer.lines.get(0)!.translateToString(false)).equals('Hello '); expect(termConverting.buffer.lines.get(1)!.translateToString(false)).equals('World '); expect(termConverting.buffer.lines.get(0)!.translateToString(true)).equals('Hello'); @@ -1434,54 +1445,54 @@ describe('Terminal', () => { beforeEach(() => { term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); }); - it('SL (scrollLeft)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[ @'); + it('SL (scrollLeft)', async () => { + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[ @'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']); - term.writeSync('\x1b[0 @'); + await term.writeP('\x1b[0 @'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']); - term.writeSync('\x1b[2 @'); + await term.writeP('\x1b[2 @'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']); }); - it('SR (scrollRight)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[ A'); + it('SR (scrollRight)', async () => { + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[ A'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); - term.writeSync('\x1b[0 A'); + await term.writeP('\x1b[0 A'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - term.writeSync('\x1b[2 A'); + await term.writeP('\x1b[2 A'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); }); - it('insertColumns (DECIC)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[\'}'); + it('insertColumns (DECIC)', async () => { + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[3;3H'); + await term.writeP('\x1b[\'}'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[1\'}'); + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[3;3H'); + await term.writeP('\x1b[1\'}'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[2\'}'); + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[3;3H'); + await term.writeP('\x1b[2\'}'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); }); - it('deleteColumns (DECDC)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[\'~'); + it('deleteColumns (DECDC)', async () => { + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[3;3H'); + await term.writeP('\x1b[\'~'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[1\'~'); + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[3;3H'); + await term.writeP('\x1b[1\'~'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[2\'~'); + await term.writeP('12345'.repeat(6)); + await term.writeP('\x1b[3;3H'); + await term.writeP('\x1b[2\'~'); assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']); }); }); @@ -1494,41 +1505,41 @@ describe('Terminal', () => { }); describe('reverseWraparound set', () => { - it('should not reverse outside of scroll margins', () => { + it('should not reverse outside of scroll margins', async () => { // prepare buffer content - term.writeSync('#####abcdefghijklmnopqrstuvwxy'); + await term.writeP('#####abcdefghijklmnopqrstuvwxy'); assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); assert.equal(term.buffer.ydisp, 1); assert.equal(term.buffer.x, 5); assert.equal(term.buffer.y, 4); - term.writeSync(ttyBS.repeat(100)); + await term.writeP(ttyBS.repeat(100)); assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); - term.writeSync('\x1b[?45h'); - term.writeSync('uvwxy'); + await term.writeP('\x1b[?45h'); + await term.writeP('uvwxy'); // set top/bottom to 1/3 (0-based) - term.writeSync('\x1b[2;4r'); + await term.writeP('\x1b[2;4r'); // place cursor below scroll bottom term.buffer.x = 5; term.buffer.y = 4; - term.writeSync(ttyBS.repeat(100)); + await term.writeP(ttyBS.repeat(100)); assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); - term.writeSync('uvwxy'); + await term.writeP('uvwxy'); // place cursor within scroll margins term.buffer.x = 5; term.buffer.y = 3; - term.writeSync(ttyBS.repeat(100)); + await term.writeP(ttyBS.repeat(100)); assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); assert.equal(term.buffer.x, 0); assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop - term.writeSync('fghijklmnopqrst'); + await term.writeP('fghijklmnopqrst'); // place cursor above scroll top term.buffer.x = 5; term.buffer.y = 0; - term.writeSync(ttyBS.repeat(100)); + await term.writeP(ttyBS.repeat(100)); assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); }); }); @@ -1542,22 +1553,22 @@ describe('Terminal', () => { let markers: IMarker[]; let disposeStack: IMarker[]; let term: TestTerminal; - beforeEach(() => { + beforeEach(async () => { term = new TestTerminal({}); markers = []; disposeStack = []; term.optionsService.setOption('scrollback', 1); term.resize(10, 5); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('\x1b[r0\r\n'); + await term.writeP('\x1b[r0\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('1\r\n'); + await term.writeP('1\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('2\r\n'); + await term.writeP('2\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('3\r\n'); + await term.writeP('3\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('4'); + await term.writeP('4'); for (let i = 0; i < markers.length; ++i) { const marker = markers[i]; marker.onDispose(() => disposeStack.push(marker)); @@ -1566,15 +1577,15 @@ describe('Terminal', () => { it('initial', () => { assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]); }); - it('should dispose on normal trim off the top', () => { + it('should dispose on normal trim off the top', async () => { // moves top line into scrollback - term.writeSync('\n'); + await term.writeP('\n'); assert.deepEqual(disposeStack, []); // trims first marker - term.writeSync('\n'); + await term.writeP('\n'); assert.deepEqual(disposeStack, [markers[0]]); // trims second marker - term.writeSync('\n'); + await term.writeP('\n'); assert.deepEqual(disposeStack, [markers[0], markers[1]]); // trimmed marker objs should be disposed assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]); @@ -1582,14 +1593,14 @@ describe('Terminal', () => { // trimmed markers should contain line -1 assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); }); - it('should dispose on DL', () => { - term.writeSync('\x1b[3;1H'); // move cursor to 0, 2 - term.writeSync('\x1b[2M'); // delete 2 lines + it('should dispose on DL', async () => { + await term.writeP('\x1b[3;1H'); // move cursor to 0, 2 + await term.writeP('\x1b[2M'); // delete 2 lines assert.deepEqual(disposeStack, [markers[2], markers[3]]); }); - it('should dispose on IL', () => { - term.writeSync('\x1b[3;1H'); // move cursor to 0, 2 - term.writeSync('\x1b[2L'); // insert 2 lines + it('should dispose on IL', async () => { + await term.writeP('\x1b[3;1H'); // move cursor to 0, 2 + await term.writeP('\x1b[2L'); // insert 2 lines assert.deepEqual(disposeStack, [markers[4], markers[3]]); assert.deepEqual(markers.map(el => el.line), [0, 1, 4, -1, -1]); }); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 6a4b4a0d..38f9c8f1 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -21,6 +21,9 @@ export class TestTerminal extends Terminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); } public keyPress(ev: any): boolean { return this._keyPress(ev); } + public writeP(data: string | Uint8Array): Promise { + return new Promise(r => this.write(data, r)); + } } export class MockTerminal implements ITerminal { diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 11e71bad..e67a1f26 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -125,7 +125,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._writeBuffer.write(data, callback); } + /** + * Write data to terminal synchonously. + * + * This method is unreliable with async parser handlers, thus should not + * be used anymore. If you need blocking semantics on data input consider + * `write` with a callback instead. + * + * @deprecated Unreliable, will be removed soon. + */ public writeSync(data: string | Uint8Array): void { + console.error('writeSync is unreliable and will be removed soon.'); this._writeBuffer.writeSync(data); } From 22d777c9f96c263377003cce82244a19a21f7209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 22 Jan 2021 21:24:54 +0100 Subject: [PATCH 04/22] make linter happy --- src/common/CoreTerminal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index e67a1f26..7b45a172 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -127,11 +127,11 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { /** * Write data to terminal synchonously. - * + * * This method is unreliable with async parser handlers, thus should not * be used anymore. If you need blocking semantics on data input consider * `write` with a callback instead. - * + * * @deprecated Unreliable, will be removed soon. */ public writeSync(data: string | Uint8Array): void { From b07ca8bb2422d6ea5d4fb45527e5f715626bdf4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 22 Jan 2021 21:34:12 +0100 Subject: [PATCH 05/22] fix timeout issue on macos --- src/browser/Terminal.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 2e09f719..fe5b0bd5 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -753,7 +753,8 @@ describe('Terminal', () => { term.reset(); } }); - it('2 characters per cell over line end with autowrap', async () => { + it('2 characters per cell over line end with autowrap', async function (): Promise { + this.timeout(10000); const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { @@ -767,7 +768,8 @@ describe('Terminal', () => { term.reset(); } }); - it('2 characters per cell over line end without autowrap', async () => { + it('2 characters per cell over line end without autowrap', async function (): Promise { + this.timeout(10000); const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { @@ -785,7 +787,8 @@ describe('Terminal', () => { term.reset(); } }); - it('splitted surrogates', async () => { + it('splitted surrogates', async function (): Promise { + this.timeout(10000); const high = String.fromCharCode(0xD800); const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { From 2f07ed997605d9c289c8a6cc5c3a2a8bd86e11c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 23 Jan 2021 13:05:57 +0100 Subject: [PATCH 06/22] simplify stack save in parser --- src/common/InputHandler.ts | 18 ++- src/common/parser/EscapeSequenceParser.ts | 173 +++++++++------------- src/common/parser/Types.d.ts | 20 +++ 3 files changed, 108 insertions(+), 103 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 001c9995..099cdff7 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -460,7 +460,9 @@ export class InputHandler extends Disposable implements IInputHandler { super.dispose(); } - // FIXME: cleanup async handling + /** + * Async parse support. + */ private _parseStack = { paused: false, cursorStartX: 0, @@ -468,7 +470,6 @@ export class InputHandler extends Disposable implements IInputHandler { decodedLength: 0, position: 0 }; - private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void { this._parseStack.paused = true; this._parseStack.cursorStartX = cursorStartX; @@ -477,6 +478,19 @@ export class InputHandler extends Disposable implements IInputHandler { this._parseStack.position = position; } + /** + * Parse call with async handler support. + * + * Whether the stack state got preserved for the next call, is indicated by the return value: + * - undefined (void): + * all handlers were sync, no stack save, continue normally with next chunk + * - Promise\: + * execution stopped at async handler, stack saved, continue with + * same chunk and the promise resolve value as `promiseResult` until the method returns `undefined` + * + * Note: Never call this directly for a running terminal instance in production. + * Always use `Terminal.write`, which provides in-band blocking and correct exection order. + */ public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise { let result: void | Promise; let buffer = this._bufferService.buffer; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 3635ca78..e56655ae 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -437,44 +437,28 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.precedingCodepoint = 0; } - // FIXME: cleanup async handling - private _parseStack: { - paused: boolean; - type: 'ESC' | 'CSI'; // FIXME: support for DCS and OSC - handlers: CsiHandlerType[] | EscHandlerType[]; - handlerPos: number; - transition: number; - currentState: ParserState; - collect: number; - pos: number; - } = { - paused: false, - type: 'ESC', + /** + * Async parse support. + */ + private _parseStack: IParserStackState = { + state: ParserStackType.NONE, handlers: [], handlerPos: 0, transition: 0, - currentState: 0, - collect: 0, - pos: 0 + chunkPos: 0 }; - private _preserveStack( - type: 'ESC' | 'CSI', - handlers: CsiHandlerType[] | EscHandlerType[], + state: ParserStackType, + handlers: ResumableHandlersType, handlerPos: number, transition: number, - currentState: ParserState, - collect: number, - pos: number - ): void { - this._parseStack.paused = true; - this._parseStack.type = type; + chunkPos: number): void + { + this._parseStack.state = state; this._parseStack.handlers = handlers; this._parseStack.handlerPos = handlerPos; this._parseStack.transition = transition; - this._parseStack.currentState = currentState; - this._parseStack.collect = collect; - this._parseStack.pos = pos; + this._parseStack.chunkPos = chunkPos; } /** @@ -494,22 +478,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { let code = 0; 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 res: any; let start = 0; - if (this._parseStack.paused) { - const handlers = this._parseStack.handlers; + let handlerResult: any; + + // resume from async handler + if (this._parseStack.state) { let handlerPos = this._parseStack.handlerPos - 1; - transition = this._parseStack.transition; - currentState = this._parseStack.currentState; - collect = this._parseStack.collect; - start = this._parseStack.pos; // we have to resume the old handler loop if: // - return value of the promise was `false` @@ -517,24 +491,25 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // FIXME: removing handlers from within a handler of the same sequence // is not supported atm (also true for sync handlers)!! if (promiseResult === false && handlerPos > -1) { - switch (this._parseStack.type) { - case 'CSI': + const handlers = this._parseStack.handlers; + switch (this._parseStack.state) { + case ParserStackType.CSI: for (; handlerPos >= 0; handlerPos--) { - if ((res = (handlers as CsiHandlerType[])[handlerPos](params)) !== false) { - if (res instanceof Promise) { + if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { + if (handlerResult instanceof Promise) { this._parseStack.handlerPos = handlerPos; - return res; + return handlerResult; } break; } } break; - case 'ESC': + case ParserStackType.ESC: for (; handlerPos >= 0; handlerPos--) { - if ((res = (handlers as EscHandlerType[])[handlerPos]()) !== false) { - if (res instanceof Promise) { + if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { + if (handlerResult instanceof Promise) { this._parseStack.handlerPos = handlerPos; - return res; + return handlerResult; } break; } @@ -543,20 +518,20 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } // cleanup before continuing with the main loop + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; this.precedingCodepoint = 0; - this._parseStack.paused = false; - start++; - currentState = transition & TableAccess.TRANSITION_STATE_MASK; + this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK; } - // console.log('startPos', start, length); + // continue with main sync loop // process input string for (let i = start; i < length; ++i) { code = data[i]; // normal transition & action lookup - transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; + transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) { case ParserAction.PRINT: // read ahead with loop unrolling @@ -596,9 +571,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP { position: i, code, - currentState, - collect, - params, + currentState: this.currentState, + collect: this._collect, + params: this._params, abort: false }); if (inject.abort) return; @@ -606,19 +581,20 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.CSI_DISPATCH: // Trigger CSI Handler - const handlers = this._csiHandlers[collect << 8 | code]; + const handlers = this._csiHandlers[this._collect << 8 | code]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { - // undefined or true means success and to stop bubbling - if ((res = handlers[j](params)) === true) { + // true means success and to stop bubbling + // a promise indicates an async handler that needs to finish before progressing + if ((handlerResult = handlers[j](this._params)) === true) { break; - } else if (res instanceof Promise) { - this._preserveStack('CSI', handlers, j, transition, currentState, collect, i); - return res; + } else if (handlerResult instanceof Promise) { + this._preserveStack(ParserStackType.CSI, handlers, j, transition, i); + return handlerResult; } } if (j < 0) { - this._csiHandlerFb(collect << 8 | code, params); + this._csiHandlerFb(this._collect << 8 | code, this._params); } this.precedingCodepoint = 0; break; @@ -627,94 +603,89 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP do { switch (code) { case 0x3b: - params.addParam(0); // ZDM + this._params.addParam(0); // ZDM break; case 0x3a: - params.addSubParam(-1); + this._params.addSubParam(-1); break; default: // 0x30 - 0x39 - params.addDigit(code - 48); + this._params.addDigit(code - 48); } } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c); i--; break; case ParserAction.COLLECT: - collect <<= 8; - collect |= code; + this._collect <<= 8; + this._collect |= code; break; case ParserAction.ESC_DISPATCH: - const handlersEsc = this._escHandlers[collect << 8 | code]; + const handlersEsc = this._escHandlers[this._collect << 8 | code]; let jj = handlersEsc ? handlersEsc.length - 1 : -1; for (; jj >= 0; jj--) { - // undefined or true means success and to stop bubbling - if ((res = handlersEsc[jj]()) === true) { + // true means success and to stop bubbling + // a promise indicates an async handler that needs to finish before progressing + if ((handlerResult = handlersEsc[jj]()) === true) { break; - } else if (res instanceof Promise) { - this._preserveStack('ESC', handlersEsc, jj, transition, currentState, collect, i); - return res; + } else if (handlerResult instanceof Promise) { + this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i); + return handlerResult; } } if (jj < 0) { - this._escHandlerFb(collect << 8 | code); + this._escHandlerFb(this._collect << 8 | code); } this.precedingCodepoint = 0; break; case ParserAction.CLEAR: - params.reset(); - params.addParam(0); // ZDM - collect = 0; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; break; case ParserAction.DCS_HOOK: - dcs.hook(collect << 8 | code, params); + this._dcsParser.hook(this._collect << 8 | code, this._params); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort) for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - dcs.put(data, i, j); + this._dcsParser.put(data, i, j); i = j - 1; break; } } break; case ParserAction.DCS_UNHOOK: - dcs.unhook(code !== 0x18 && code !== 0x1a); + this._dcsParser.unhook(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - params.reset(); - params.addParam(0); // ZDM - collect = 0; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; this.precedingCodepoint = 0; break; case ParserAction.OSC_START: - osc.start(); + this._oscParser.start(); break; case ParserAction.OSC_PUT: // inner loop: 0x20 (SP) included, 0x7F (DEL) included for (let j = i + 1; ; j++) { if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - osc.put(data, i, j); + this._oscParser.put(data, i, j); i = j - 1; break; } } break; case ParserAction.OSC_END: - osc.end(code !== 0x18 && code !== 0x1a); + this._oscParser.end(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - params.reset(); - params.addParam(0); // ZDM - collect = 0; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; this.precedingCodepoint = 0; break; } - currentState = transition & TableAccess.TRANSITION_STATE_MASK; + this.currentState = transition & TableAccess.TRANSITION_STATE_MASK; } - - // save collected intermediates - this._collect = collect; - - // save state - this.currentState = currentState; } } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 9db38fbd..002fc8ed 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -5,6 +5,7 @@ import { IDisposable } from 'common/Types'; import { ParserState } from 'common/parser/Constants'; +import { OscParser } from 'common/parser/OscParser'; /** sequence params serialized to js arrays */ export type ParamsArray = (number | number[])[]; @@ -237,3 +238,22 @@ export interface IFunctionIdentifier { export interface IHandlerCollection { [key: string]: T[]; } + +/** + * Types for async parser support. + */ +export const enum ParserStackType { + NONE = 0, + CSI, + ESC, + OSC, + DCS +} +export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[]; +export interface IParserStackState { + state: ParserStackType; + handlers: ResumableHandlersType; + handlerPos: number; + transition: number; + chunkPos: number; +} From 68b7f885235b22d20de40c4085d9582070264b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 24 Jan 2021 17:09:16 +0100 Subject: [PATCH 07/22] async parser tests for CSI and ESC --- .../parser/EscapeSequenceParser.test.ts | 575 +++++++++++++++--- src/common/parser/EscapeSequenceParser.ts | 48 +- src/common/parser/Types.d.ts | 1 + 3 files changed, 523 insertions(+), 101 deletions(-) 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, From d59f4d2f6f6da9fe862e52e0152b8d87583d4b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 8 Feb 2021 01:53:59 +0100 Subject: [PATCH 08/22] async impl for DCS, DcsParser tests --- src/common/parser/DcsParser.test.ts | 205 ++++++++++++++++++++++ src/common/parser/DcsParser.ts | 63 +++++-- src/common/parser/EscapeSequenceParser.ts | 45 +++-- src/common/parser/Types.d.ts | 6 +- 4 files changed, 289 insertions(+), 30 deletions(-) diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index cf174139..096ebb58 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -252,3 +252,208 @@ describe('DcsParser', () => { }); }); }); + + +class TestHandlerAsync implements IDcsHandler { + constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} + public hook(params: IParams): void { + this.output.push([this.msg, 'HOOK', params.toArray()]); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); + } + public async unhook(success: boolean): Promise { + // simple sleep to check in tests whether ordering gets messed up + await new Promise(res => setTimeout(res, 20)); + this.output.push([this.msg, 'UNHOOK', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} +async function unhookP(parser: DcsParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.unhook(success, prev)) { + prev = await result; + } +} + + +describe('DcsParser - async tests', () => { + let parser: DcsParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new DcsParser(); + parser.setHandlerFallback((id, action, data) => { + if (action === 'HOOK') { + data = data.toArray(); + } + reports.push([id, action, data]); + }); + }); + describe('sync and async mixed', () => { + describe('sync | async | sync', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['s2', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['s2', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['s2', 'UNHOOK', true], + ['a1', 'UNHOOK', false], // important: a1 before s1 + ['s1', 'UNHOOK', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['s2', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['s2', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['s2', 'UNHOOK', true], + ['a1', 'UNHOOK', true], // important: a1 before s1 + ['s1', 'UNHOOK', true] + ]); + }); + }); + describe('async | sync | async', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['a2', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['a2', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['a2', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['a2', 'UNHOOK', true], + ['s1', 'UNHOOK', false], // important: s1 between a2 .. a1 + ['a1', 'UNHOOK', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['a2', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['a2', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['a2', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['a2', 'UNHOOK', true], + ['s1', 'UNHOOK', true], // important: s1 between a2 .. a1 + ['a1', 'UNHOOK', true] + ]); + }); + }); + describe('DcsHandlerFactory', () => { + it('should be called once on end(true)', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, false); + assert.deepEqual(reports, []); + }); + it('should be disposable', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); + dispo.dispose(); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + data = toUtf32('some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); + }); + it('should respect return false', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]); + }); + }); + }); +}); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 2fa28992..9f326099 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -15,11 +15,11 @@ export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; - private _handlerFb: DcsFallbackHandlerType = () => {}; + private _handlerFb: DcsFallbackHandlerType = () => { }; public dispose(): void { this._handlers = Object.create(null); - this._handlerFb = () => {}; + this._handlerFb = () => { }; this._active = EMPTY_HANDLERS; } @@ -79,20 +79,46 @@ export class DcsParser implements IDcsParser { } } - public unhook(success: boolean): void { + private _stack = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + public unhook(success: boolean, promiseResult?: boolean): void | Promise { if (!this._active.length) { this._handlerFb(this._ident, 'UNHOOK', success); } else { + let handlerResult: any = false; let j = this._active.length - 1; - for (; j >= 0; j--) { - if (this._active[j].unhook(success)) { - break; - } + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; } - j--; - // cleanup left over handlers + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + if ((handlerResult = this._active[j].unhook(success)) !== false) { + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + break; + } + } + j--; + } + // cleanup left over handlers (fallThrough for async) for (; j >= 0; j--) { - this._active[j].unhook(false); + if ((handlerResult = this._active[j].unhook(false)) instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } } } this._active = EMPTY_HANDLERS; @@ -113,7 +139,7 @@ export class DcsHandler implements IDcsHandler { private _params: IParams = EMPTY_PARAMS; private _hitLimit: boolean = false; - constructor(private _handler: (data: string, params: IParams) => boolean) {} + constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { } public hook(params: IParams): void { // since we need to preserve params until `unhook`, we have to clone it @@ -136,12 +162,21 @@ export class DcsHandler implements IDcsHandler { } } - public unhook(success: boolean): boolean { - let ret = false; + public unhook(success: boolean): boolean | Promise { + let ret: boolean | Promise = false; if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data, this._params); + if ((ret = this._handler(this._data, this._params)) instanceof Promise) { + // FIXME: should this be behind a catch rule? + return ret.then(res => { + // cleanup handler state late + this._params = EMPTY_PARAMS; + this._data = ''; + this._hitLimit = false; + return res; + }); + } } this._params = EMPTY_PARAMS; this._data = ''; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 1ac1a22b..842a99b8 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: PrintHandlerType; - protected _executeHandlers: {[flag: number]: ExecuteHandlerType}; + protected _executeHandlers: { [flag: number]: ExecuteHandlerType }; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; @@ -280,7 +280,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) - this.registerEscHandler({final: '\\'}, () => true); + this.registerEscHandler({ final: '\\' }, () => true); } protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { @@ -452,8 +452,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP handlers: ResumableHandlersType, handlerPos: number, transition: number, - chunkPos: number): void - { + chunkPos: number): void { this._parseStack.state = state; this._parseStack.handlers = handlers; this._parseStack.handlerPos = handlerPos; @@ -534,10 +533,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // - handlers are not exhausted yet // FIXME: removing handlers from within a handler of the same sequence // is not supported atm (also true for sync handlers)!! - if (promiseResult === false && handlerPos > -1) { - const handlers = this._parseStack.handlers; - switch (this._parseStack.state) { - case ParserStackType.CSI: + let handlers: ResumableHandlersType; + switch (this._parseStack.state) { + case ParserStackType.CSI: + if (promiseResult === false && handlerPos > -1) { + handlers = this._parseStack.handlers; for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { if (handlerResult instanceof Promise) { @@ -547,8 +547,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; } } - break; - case ParserStackType.ESC: + } + break; + case ParserStackType.ESC: + if (promiseResult === false && handlerPos > -1) { + handlers = this._parseStack.handlers; for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { if (handlerResult instanceof Promise) { @@ -558,8 +561,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; } } - break; - } + } + break; + case ParserStackType.DCS: + code = data[this._parseStack.chunkPos]; + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + case ParserStackType.OSC: + // TODO + break; } // cleanup before continuing with the main loop this._parseStack.state = ParserStackType.NONE; @@ -700,7 +716,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.DCS_UNHOOK: - this._dcsParser.unhook(code !== 0x18 && code !== 0x1a); + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + this._parseStack.state = ParserStackType.DCS; + return handlerResult; + } if (code === 0x1b) transition |= ParserState.ESCAPE; this._params.reset(); this._params.addParam(0); // ZDM diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index f6db77fa..30217cc6 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -5,7 +5,7 @@ import { IDisposable } from 'common/Types'; import { ParserState } from 'common/parser/Constants'; -import { OscParser } from 'common/parser/OscParser'; + /** sequence params serialized to js arrays */ export type ParamsArray = (number | number[])[]; @@ -94,7 +94,7 @@ export interface IDcsHandler { * execution of the command should depend on `success`. * To save memory also cleanup data structures here. */ - unhook(success: boolean): boolean; + unhook(success: boolean): boolean | Promise; } export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; @@ -219,7 +219,7 @@ export interface IOscParser extends ISubParser { hook(ident: number, params: IParams): void; - unhook(success: boolean): void; + unhook(success: boolean, promiseResult?: boolean): void | Promise; } /** From 041e259c11e065f8451052a300dd2e0c8b9fd59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 8 Feb 2021 03:03:28 +0100 Subject: [PATCH 09/22] OSC impl, OscParser tests --- src/common/parser/EscapeSequenceParser.ts | 20 ++- src/common/parser/OscParser.test.ts | 201 ++++++++++++++++++++++ src/common/parser/OscParser.ts | 86 ++++++--- src/common/parser/Types.d.ts | 4 +- 4 files changed, 277 insertions(+), 34 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 842a99b8..d2da17c6 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -548,6 +548,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } } + this._parseStack.handlers = []; break; case ParserStackType.ESC: if (promiseResult === false && handlerPos > -1) { @@ -562,10 +563,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } } + this._parseStack.handlers = []; break; case ParserStackType.DCS: code = data[this._parseStack.chunkPos]; - if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) { return handlerResult; } if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; @@ -574,7 +576,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._collect = 0; break; case ParserStackType.OSC: - // TODO + code = data[this._parseStack.chunkPos]; + if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { + return handlerResult; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; break; } // cleanup before continuing with the main loop @@ -717,7 +726,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.DCS_UNHOOK: if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { - this._parseStack.state = ParserStackType.DCS; + this._preserveStack(ParserStackType.DCS, [], 0, transition, i); return handlerResult; } if (code === 0x1b) transition |= ParserState.ESCAPE; @@ -740,7 +749,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.OSC_END: - this._oscParser.end(code !== 0x18 && code !== 0x1a); + if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a)) { + this._preserveStack(ParserStackType.OSC, [], 0, transition, i); + return handlerResult; + } if (code === 0x1b) transition |= ParserState.ESCAPE; this._params.reset(); this._params.addParam(0); // ZDM diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index e2c8641c..5c7f5777 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -250,3 +250,204 @@ describe('OscParser', () => { }); }); }); + + +class TestHandlerAsync implements IOscHandler { + constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {} + public start(): void { + this.output.push([this.msg, this.id, 'START']); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); + } + public async end(success: boolean): Promise { + await new Promise(res => setTimeout(res, 20)); + this.output.push([this.msg, this.id, 'END', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} +async function endP(parser: OscParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.end(success, prev)) { + prev = await result; + } +} + +describe('OscParser - async tests', () => { + let parser: OscParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new OscParser(); + parser.setHandlerFallback((id, action, data) => { + reports.push([id, action, data]); + }); + }); + describe('sync and async mixed', () => { + describe('sync | async | sync', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(1234, new TestHandler(1234, reports, 's1')); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 's2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', false], + ['s1', 1234, 'END', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true)); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', true], + ['s1', 1234, 'END', true] + ]); + }); + }); + describe('async | sync | async', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'a1')); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', false], + ['s1', 1234, 'END', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true)); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', true], + ['s1', 1234, 'END', true] + ]); + }); + }); + describe('OscHandlerFactory', () => { + it('should be called once on end(true)', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + await endP(parser, true); + assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, false); + assert.deepEqual(reports, []); + }); + it('should be disposable', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; })); + const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!']]); + dispo.dispose(); + parser.start(); + data = toUtf32('1234;some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); + }); + it('should respect return false', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; })); + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]); + }); + }); + }); +}); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index e7edac66..b0b51e23 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -41,7 +41,7 @@ export class OscParser implements IOscParser { public dispose(): void { this._handlers = Object.create(null); - this._handlerFb = () => {}; + this._handlerFb = () => { }; this._active = EMPTY_HANDLERS; } @@ -76,27 +76,6 @@ export class OscParser implements IOscParser { } } - private _end(success: boolean): void { - // other than the old code we always have to call .end - // to keep the bubbling we use `success` to indicate - // whether a handler should execute - if (!this._active.length) { - this._handlerFb(this._id, 'END', success); - } else { - let j = this._active.length - 1; - for (; j >= 0; j--) { - if (this._active[j].end(success)) { - break; - } - } - j--; - // cleanup left over handlers - for (; j >= 0; j--) { - this._active[j].end(false); - } - } - } - public start(): void { // always reset leftover handlers this.reset(); @@ -137,12 +116,18 @@ export class OscParser implements IOscParser { } } + private _stack = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + /** * Indicates end of an OSC command. * Whether the OSC got aborted or finished normally * is indicated by `success`. */ - public end(success: boolean): void { + public end(success: boolean, promiseResult?: boolean): void | Promise { if (this._state === OscState.START) { return; } @@ -154,7 +139,46 @@ export class OscParser implements IOscParser { if (this._state === OscState.ID) { this._start(); } - this._end(success); + + if (!this._active.length) { + this._handlerFb(this._id, 'END', success); + } else { + let handlerResult: any = false; + let j = this._active.length - 1; + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; + } + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + if ((handlerResult = this._active[j].end(success)) !== false) { + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + break; + } + } + j--; + } + // cleanup left over handlers + // we always have to call .end for proper cleanup, + // here we use `success` to indicate whether a handler should execute + for (; j >= 0; j--) { + if ((handlerResult = this._active[j].end(false)) instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } + } + } + } this._active = EMPTY_HANDLERS; this._id = -1; @@ -170,7 +194,7 @@ export class OscHandler implements IOscHandler { private _data = ''; private _hitLimit: boolean = false; - constructor(private _handler: (data: string) => boolean) {} + constructor(private _handler: (data: string) => boolean | Promise) { } public start(): void { this._data = ''; @@ -188,12 +212,18 @@ export class OscHandler implements IOscHandler { } } - public end(success: boolean): boolean { - let ret = false; + public end(success: boolean): boolean | Promise { + let ret: boolean | Promise = false; if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data); + if ((ret = this._handler(this._data)) instanceof Promise) { + return ret.then(res => { + this._data = ''; + this._hitLimit = false; + return res; + }); + } } this._data = ''; this._hitLimit = false; diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 30217cc6..dda798cc 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -130,7 +130,7 @@ export interface IOscHandler { * execution of the command should depend on `success`. * To save memory also cleanup data structures here. */ - end(success: boolean): boolean; + end(success: boolean): boolean | Promise; } export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; @@ -214,7 +214,7 @@ export interface ISubParser extends IDisposable { export interface IOscParser extends ISubParser { start(): void; - end(success: boolean): void; + end(success: boolean, promiseResult?: boolean): void | Promise; } export interface IDcsParser extends ISubParser { From d0b555687fc3f80d54f777060f30b7155b2498fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 8 Feb 2021 03:28:49 +0100 Subject: [PATCH 10/22] update parser hooks api and tests --- src/browser/Terminal2.test.ts | 2 +- src/browser/TestUtils.test.ts | 8 ++++---- src/browser/Types.d.ts | 8 ++++---- src/browser/public/Terminal.ts | 24 ++++++++++++------------ src/common/CoreTerminal.ts | 18 +++++++++--------- src/common/InputHandler.ts | 16 ++++++++-------- src/common/Types.d.ts | 6 +++++- test/api/Parser.api.ts | 28 ++++++++++++++-------------- 8 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/browser/Terminal2.test.ts b/src/browser/Terminal2.test.ts index 7d3fe851..1ad0e905 100644 --- a/src/browser/Terminal2.test.ts +++ b/src/browser/Terminal2.test.ts @@ -74,7 +74,7 @@ describe('Escape Sequence Files', function(): void { let content = ''; const OSC_CODE = 12345; await new Promise(resolve => { - customHandler = term.addOscHandler(OSC_CODE, () => { + customHandler = term.registerOscHandler(OSC_CODE, () => { // grab terminal viewport content content = terminalToString(term); resolve(); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 38f9c8f1..a2140aa5 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -78,16 +78,16 @@ export class MockTerminal implements ITerminal { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index e8e0cabd..1262117f 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -52,10 +52,10 @@ export interface IPublicTerminal extends IDisposable { resize(columns: number, rows: number): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; - addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 70247f88..bd0b78f9 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -292,28 +292,28 @@ class BufferLineApiView implements IBufferLineApi { class ParserApi implements IParser { constructor(private _core: ITerminal) { } - public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { - return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable { + return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray())); } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable { return this.registerCsiHandler(id, callback); } - public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable { + return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable { return this.registerDcsHandler(id, callback); } - public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { - return this._core.addEscHandler(id, handler); + public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable { + return this._core.registerEscHandler(id, handler); } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable { return this.registerEscHandler(id, handler); } - public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._core.addOscHandler(ident, callback); + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._core.registerOscHandler(ident, callback); } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { return this.registerOscHandler(ident, callback); } } diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 7b45a172..50be0d7c 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -278,23 +278,23 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ - public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { - return this._inputHandler.addEscHandler(id, callback); + public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable { + return this._inputHandler.registerEscHandler(id, callback); } /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - return this._inputHandler.addDcsHandler(id, callback); + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { + return this._inputHandler.registerDcsHandler(id, callback); } /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { - return this._inputHandler.addCsiHandler(id, callback); + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { + return this._inputHandler.registerCsiHandler(id, 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); + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._inputHandler.registerOscHandler(ident, callback); } protected _setup(): void { @@ -332,7 +332,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { if (!this._windowsMode) { const disposables: IDisposable[] = []; disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); - disposables.push(this.addCsiHandler({ final: 'H' }, () => { + disposables.push(this.registerCsiHandler({ final: 'H' }, () => { updateWindowsModeWrappedState(this._bufferService); return false; })); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 099cdff7..08f04d9e 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -701,9 +701,9 @@ export class InputHandler extends Disposable implements IInputHandler { } /** - * Forward addCsiHandler from parser. + * Forward registerCsiHandler from parser. */ - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { if (id.final === 't' && !id.prefix && !id.intermediates) { // security: always check whether window option is allowed return this._parser.registerCsiHandler(id, params => { @@ -717,23 +717,23 @@ export class InputHandler extends Disposable implements IInputHandler { } /** - * Forward addDcsHandler from parser. + * Forward registerDcsHandler from parser. */ - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { return this._parser.registerDcsHandler(id, new DcsHandler(callback)); } /** - * Forward addEscHandler from parser. + * Forward registerEscHandler from parser. */ - public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable { return this._parser.registerEscHandler(id, callback); } /** - * Forward addOscHandler from parser. + * Forward registerOscHandler from parser. */ - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { return this._parser.registerOscHandler(ident, new OscHandler(callback)); } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 1ea26c2a..e345e11d 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminalOptions as IPublicTerminalOptions } from 'xterm'; +import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm'; import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; import { IParams } from 'common/parser/Types'; @@ -351,6 +351,10 @@ export interface IInputHandler { parse(data: string | Uint8Array): void; print(data: Uint32Array, start: number, end: number): void; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index 77101e23..ff82895d 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -28,12 +28,12 @@ describe('Parser Integration Tests', function(): void { after(async () => browser.close()); - describe('addCsiHandler', () => { + describe('registerCsiHandler', () => { it('should call custom CSI handler with js array params', async () => { await page.evaluate(` window.term.reset(); window._customCsiHandlerParams = []; - const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => { + const _customCsiHandler = window.term.parser.registerCsiHandler({final: 'm'}, (params, collect) => { window._customCsiHandlerParams.push(params); return false; }, ''); @@ -42,20 +42,20 @@ describe('Parser Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`(() => window._customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); }); }); - describe('addDcsHandler', () => { + describe('registerDcsHandler', () => { it('should respects return value', async () => { await page.evaluate(` window.term.reset(); window._customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { window._customDcsHandlerCallStack.push(['A', params, data]); return false; }); - const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { window._customDcsHandlerCallStack.push(['B', params, data]); return true; }); - const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { window._customDcsHandlerCallStack.push(['C', params, data]); return false; }); @@ -64,20 +64,20 @@ describe('Parser Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`(() => window._customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); }); }); - describe('addEscHandler', () => { + describe('registerEscHandler', () => { it('should respects return value', async () => { await page.evaluate(` window.term.reset(); window._customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { window._customEscHandlerCallStack.push('A'); return false; }); - const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { window._customEscHandlerCallStack.push('B'); return true; }); - const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { window._customEscHandlerCallStack.push('C'); return false; }); @@ -86,20 +86,20 @@ describe('Parser Integration Tests', function(): void { assert.deepEqual(await page.evaluate(`(() => window._customEscHandlerCallStack)();`), ['C', 'B']); }); }); - describe('addOscHandler', () => { + describe('registerOscHandler', () => { it('should respects return value', async () => { await page.evaluate(` window.term.reset(); window._customOscHandlerCallStack = []; - const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => { + const _customOscHandlerA = window.term.parser.registerOscHandler(1234, data => { window._customOscHandlerCallStack.push(['A', data]); return false; }); - const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => { + const _customOscHandlerB = window.term.parser.registerOscHandler(1234, data => { window._customOscHandlerCallStack.push(['B', data]); return true; }); - const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => { + const _customOscHandlerC = window.term.parser.registerOscHandler(1234, data => { window._customOscHandlerCallStack.push(['C', data]); return false; }); From 5db479c25d706c22e0ba14a80bdfe95b83fc81ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 8 Feb 2021 03:35:42 +0100 Subject: [PATCH 11/22] update public API --- typings/xterm.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d860ddfa..a842a82b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1515,7 +1515,7 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable; /** * Adds a handler for DCS escape sequences. @@ -1534,7 +1534,7 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable; /** * Adds a handler for ESC escape sequences. @@ -1547,7 +1547,7 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable; /** * Adds a handler for OSC escape sequences. @@ -1565,7 +1565,7 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; } /** From 26502114d42134f7b67bb6602759f20b7f0710b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 15 Feb 2021 16:24:29 +0100 Subject: [PATCH 12/22] minor tweaks, API docs update --- src/common/parser/EscapeSequenceParser.ts | 6 ++-- typings/xterm.d.ts | 37 +++++++++++++++-------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index d2da17c6..79f8b844 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -526,18 +526,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP 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: // - return value of the promise was `false` // - handlers are not exhausted yet // FIXME: removing handlers from within a handler of the same sequence // is not supported atm (also true for sync handlers)!! - let handlers: ResumableHandlersType; + let handlers = this._parseStack.handlers; + let handlerPos = this._parseStack.handlerPos - 1; switch (this._parseStack.state) { case ParserStackType.CSI: if (promiseResult === false && handlerPos > -1) { - handlers = this._parseStack.handlers; for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { if (handlerResult instanceof Promise) { @@ -552,7 +551,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserStackType.ESC: if (promiseResult === false && handlerPos > -1) { - handlers = this._parseStack.handlers; for (; handlerPos >= 0; handlerPos--) { if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { if (handlerResult instanceof Promise) { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a842a82b..cab9bdcc 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1501,6 +1501,23 @@ declare module 'xterm' { /** * Allows hooking into the parser for custom handling of escape sequences. + * + * Note on sync vs. async handlers: + * xterm.js implements all parser actions with synchronous handlers. + * In general custom handlers should also operate in sync mode wherever + * possible to keep the parser fast. + * Still the exposed interfaces allow to register async handlers by returning + * a `Promise`. Here the parser will pause input processing until + * the promise got resolved or rejected (in-band blocking). This "full stop" + * on the input chain allows to implement backpressure from a certain async + * action while the terminal state will not progress any further from input. + * It does not mean that the terminal state will not change at all in between, + * as user actions like resize or reset are still processed immediately. + * It is an error to assume a stable terminal state while giving back control + * in between, e.g. by multiple chained `then` calls. + * Downside of an async handler is a rather bad throughput performance, + * thus use async handlers only as a last resort or for actions that have + * to rely on async interfaces itself. */ export interface IParser { /** @@ -1510,9 +1527,8 @@ declare module 'xterm' { * @param callback The function to handle the sequence. The callback is * called with the numerical params. If the sequence has subparams the * array will contain subarrays with their numercial values. - * 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 `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable; @@ -1529,9 +1545,8 @@ declare module 'xterm' { * big payloads. Currently xterm.js limits DCS payload to 10 MB * which should give enough room for most use cases. * The function gets the payload and numerical parameters as arguments. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addDcsHandler or setDcsHandler). - * The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable; @@ -1542,9 +1557,8 @@ declare module 'xterm' { * gets registered, e.g. {intermediates: '%' final: 'G'} for * default charset selection. * @param callback The function to handle the sequence. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addEscHandler or setEscHandler). - * The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable; @@ -1560,9 +1574,8 @@ declare module 'xterm' { * big payloads. Currently xterm.js limits OSC payload to 10 MB * which should give enough room for most use cases. * 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 `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; From bce3269e445376f3dc7c5b3faac65855c30209f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Feb 2021 15:41:10 +0100 Subject: [PATCH 13/22] async OSC/DCS registering/dispose tests on parser instance --- .../parser/EscapeSequenceParser.test.ts | 201 ++++++++++++++++-- src/common/parser/EscapeSequenceParser.ts | 18 +- 2 files changed, 192 insertions(+), 27 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 56d4c93e..6d00adda 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -102,7 +102,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public trackedStack: IParserStackState[] = []; public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { const result = super.parse(data, length, promiseResult); - if (result && this._trackStack) { + if (result instanceof Promise && this._trackStack) { this.trackedStack.push({ ...this.parseStack }); } return result; @@ -1742,9 +1742,9 @@ async function throwsAsync(fn: () => Promise, message?: string | undefined) } 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 + // sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | FIN // 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\\'; + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\FIN'; let RESULT: any[]; let parser: TestEscapeSequenceParser; const callstack: any[] = []; @@ -1764,7 +1764,9 @@ describe('EscapeSequenceParser - async', () => { ['EXE \r'], ['EXE \n'], ['PRINT', '$>'], - ['OSC 1', 'foo=bar'] + ['DCS a', ['xyz', [1, 2]]], + ['OSC 1', 'foo=bar'], + ['PRINT', 'FIN'] ]; parser = new TestEscapeSequenceParser(); parser.reset(); @@ -1786,9 +1788,10 @@ describe('EscapeSequenceParser - async', () => { 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; })); + parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); }); - it('sync handlers keep parsed in sync mode', () => { + it('sync handlers keep being 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 @@ -1819,7 +1822,8 @@ describe('EscapeSequenceParser - async', () => { 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; })); + parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; })); + parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); }); it('sync parse call does not work anymore', () => { @@ -1849,7 +1853,12 @@ describe('EscapeSequenceParser - 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] + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); it('correct result on chunked awaited parse calls', async () => { @@ -1874,7 +1883,11 @@ describe('EscapeSequenceParser - async', () => { ['EXE \n'], ['PRINT', '$'], ['PRINT', '>'], - ['OSC 1', 'foo=bar'] + ['DCS a', ['xyz', [1, 2]]], + ['OSC 1', 'foo=bar'], + ['PRINT', 'F'], + ['PRINT', 'I'], + ['PRINT', 'N'] ]; // split to single char input @@ -1888,7 +1901,9 @@ describe('EscapeSequenceParser - async', () => { [0, ParserStackType.CSI, 0], [0, ParserStackType.ESC, 0], [0, ParserStackType.ESC, 0], - [0, ParserStackType.CSI, 0] + [0, ParserStackType.CSI, 0], + [0, ParserStackType.DCS, 0], + [0, ParserStackType.OSC, 0] ]); }); it('multiple async SGR handlers', async () => { @@ -1906,7 +1921,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 1], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1917,7 +1934,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); @@ -1933,7 +1952,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 1], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 1] + [27, ParserStackType.CSI, 1], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1944,7 +1965,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); it('multiple async ESC handlers', async () => { @@ -1960,7 +1983,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 1], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1971,7 +1996,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); @@ -1986,7 +2013,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 1], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // after dispose we should be back to RESULT @@ -1997,7 +2026,9 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); it('sync/async SGR mixed', async () => { @@ -2020,7 +2051,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 2], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // dispose SGR2 (sync one) @@ -2039,7 +2072,9 @@ describe('EscapeSequenceParser - async', () => { [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 1], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); clearAccu(); // dispose SGR3 (async one) @@ -2050,8 +2085,134 @@ describe('EscapeSequenceParser - async', () => { [6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], - [27, ParserStackType.CSI, 0] + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] ]); }); + it('multiple async OSC handlers', async () => { + // register with fallback + const OSC2 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return false; })); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# OSC 1') assert.equal(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + OSC2.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], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const OSC22 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return true; })); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# OSC 1') assert.notEqual(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + OSC22.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], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + }); + it('multiple async DCS handlers', async () => { + // register with fallback + const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;})); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# DCS a') assert.equal(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + DCS2.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], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;})); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# DCS a') assert.notEqual(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + DCS22.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], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + }); }); }); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 79f8b844..3b5c1d05 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -492,7 +492,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * --> 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: + * Boilerplate for proper parsing of multiple chunks with async handlers: * * ```typescript * async function parseMultipleChunks(chunks: Uint32Array[]): Promise { @@ -518,10 +518,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP 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 is a really bad condition with screwed up execution order and messed up terminal state, + * therefore we exit hard with an exception and reject any further parsing. + * + * Note: With `Terminal.write` usage this exception should never occur, as the top level + * calls are guaranteed to handle async conditions properly. If you ever encounter this + * exception in your terminal integration it indicates, that you injected data chunks to + * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for + * continuation of a running async handler. */ this._parseStack.state = ParserStackType.FAIL; throw new Error('improper continuation due to previous async handler, giving up parsing'); @@ -578,13 +582,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { return handlerResult; } - if (code === 0x1b) transition |= ParserState.ESCAPE; + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; break; } - // cleanup before continuing with the main loop + // cleanup before continuing with the main sync loop this._parseStack.state = ParserStackType.NONE; start = this._parseStack.chunkPos + 1; this.precedingCodepoint = 0; From 9985ae2c38de82968a7bb7e4c5e496caf753698b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Feb 2021 15:44:39 +0100 Subject: [PATCH 14/22] fix linter warning --- src/common/parser/EscapeSequenceParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 3b5c1d05..f9a14553 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -536,7 +536,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // - handlers are not exhausted yet // FIXME: removing handlers from within a handler of the same sequence // is not supported atm (also true for sync handlers)!! - let handlers = this._parseStack.handlers; + const handlers = this._parseStack.handlers; let handlerPos = this._parseStack.handlerPos - 1; switch (this._parseStack.state) { case ParserStackType.CSI: From d500b0f204ffad672139f63f78293a8e3ac11ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Feb 2021 17:06:10 +0100 Subject: [PATCH 15/22] inputhandler tests --- src/common/InputHandler.test.ts | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index d127f673..0402f262 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -17,6 +17,7 @@ import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; import { BufferService } from 'common/services/BufferService'; import { CoreService } from 'common/services/CoreService'; +import { OscHandler } from 'common/parser/OscParser'; function getCursor(bufferService: IBufferService): number[] { return [ @@ -1805,3 +1806,61 @@ describe('InputHandler', () => { }); }); }); + + +describe('InputHandler - async handlers', () => { + let bufferService: IBufferService; + let coreService: ICoreService; + let optionsService: MockOptionsService; + let inputHandler: TestInputHandler; + + beforeEach(() => { + optionsService = new MockOptionsService(); + bufferService = new BufferService(optionsService); + bufferService.resize(80, 30); + coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService); + coreService.onData(data => { console.log(data); }); + + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + }); + + it('async CUP with CPR check', async () => { + const cup: number[][] = []; + const cpr: number[][] = []; + inputHandler.registerCsiHandler({final: 'H'}, async params => { + cup.push(params.toArray() as number[]); + await new Promise(res => setTimeout(res, 50)); + // late call of real repositioning + return inputHandler.cursorPosition(params); + }); + coreService.onData(data => { + const m = data.match(/\x1b\[(.*?);(.*?)R/); + if (m) { + cpr.push([parseInt(m[1]), parseInt(m[2])]); + } + }); + await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n'); + assert.deepEqual(cup, cpr); + }); + it('async OSC between', async () => { + inputHandler.registerOscHandler(1000, async data => { + await new Promise(res => setTimeout(res, 50)); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); + assert.equal(data, 'some data'); + return true; + }); + await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line'); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); + }); + it('async DCS between', async () => { + inputHandler.registerDcsHandler({final: 'a'}, async (data, params) => { + await new Promise(res => setTimeout(res, 50)); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); + assert.equal(data, 'some data'); + assert.deepEqual(params.toArray(), [1, 2]); + return true; + }); + await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line'); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); + }); +}); From 6b579215674808166b0f62c0ba5ba228967ef0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Feb 2021 17:41:16 +0100 Subject: [PATCH 16/22] async handler API tests --- test/api/Parser.api.ts | 117 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index ff82895d..7bd55544 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -14,8 +14,8 @@ let page: Page; const width = 800; const height = 600; -describe('Parser Integration Tests', function(): void { - before(async function(): Promise { +describe('Parser Integration Tests', function (): void { + before(async function (): Promise { const browserType = getBrowserType(); browser = await browserType.launch({ headless: process.argv.indexOf('--headless') !== -1 @@ -39,7 +39,35 @@ describe('Parser Integration Tests', function(): void { }, ''); `); await writeSync(page, '\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - assert.deepEqual(await page.evaluate(`(() => window._customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); + assert.deepEqual(await page.evaluate(`window._customCsiHandlerParams`), [ + [38, 5, 123], + [38, [2, -1, 50, 100, 150]] + ]); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customCsiHandlerCallStack = []; + const _customCsiHandlerA = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + window._customCsiHandlerCallStack.push(['A', params]); + return false; + }); + const _customCsiHandlerB = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, async params => { + await new Promise(res => setTimeout(res, 50)); + window._customCsiHandlerCallStack.push(['B', params]); + return false; + }); + const _customCsiHandlerC = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + window._customCsiHandlerCallStack.push(['C', params]); + return false; + }); + `); + await writeSync(page, '\x1b[1;2+Z'); + assert.deepEqual(await page.evaluate(`window._customCsiHandlerCallStack`), [ + ['C', [1, 2]], + ['B', [1, 2]], + ['A', [1, 2]] + ]); }); }); describe('registerDcsHandler', () => { @@ -61,7 +89,35 @@ describe('Parser Integration Tests', function(): void { }); `); await writeSync(page, '\x1bP1;2+psome data\x1b\\\\'); - assert.deepEqual(await page.evaluate(`(() => window._customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); + assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [ + ['C', [1, 2], 'some data'], + ['B', [1, 2], 'some data'] + ]); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customDcsHandlerCallStack = []; + const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + window._customDcsHandlerCallStack.push(['A', params, data]); + return false; + }); + const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, async (data, params) => { + await new Promise(res => setTimeout(res, 50)); + window._customDcsHandlerCallStack.push(['B', params, data]); + return false; + }); + const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + window._customDcsHandlerCallStack.push(['C', params, data]); + return false; + }); + `); + await writeSync(page, '\x1bP1;2+qsome data\x1b\\\\'); + assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [ + ['C', [1, 2], 'some data'], + ['B', [1, 2], 'some data'], + ['A', [1, 2], 'some data'] + ]); }); }); describe('registerEscHandler', () => { @@ -83,7 +139,28 @@ describe('Parser Integration Tests', function(): void { }); `); await writeSync(page, '\x1b(B'); - assert.deepEqual(await page.evaluate(`(() => window._customEscHandlerCallStack)();`), ['C', 'B']); + assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B']); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customEscHandlerCallStack = []; + const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + window._customEscHandlerCallStack.push('A'); + return false; + }); + const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, async () => { + await new Promise(res => setTimeout(res, 50)); + window._customEscHandlerCallStack.push('B'); + return false; + }); + const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + window._customEscHandlerCallStack.push('C'); + return false; + }); + `); + await writeSync(page, '\x1b(Z'); + assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B', 'A']); }); }); describe('registerOscHandler', () => { @@ -105,7 +182,35 @@ describe('Parser Integration Tests', function(): void { }); `); await writeSync(page, '\x1b]1234;some data\x07'); - assert.deepEqual(await page.evaluate(`(() => window._customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); + assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [ + ['C', 'some data'], + ['B', 'some data'] + ]); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customOscHandlerCallStack = []; + const _customOscHandlerA = window.term.parser.registerOscHandler(666, data => { + window._customOscHandlerCallStack.push(['A', data]); + return false; + }); + const _customOscHandlerB = window.term.parser.registerOscHandler(666, async data => { + await new Promise(res => setTimeout(res, 50)); + window._customOscHandlerCallStack.push(['B', data]); + return false; + }); + const _customOscHandlerC = window.term.parser.registerOscHandler(666, data => { + window._customOscHandlerCallStack.push(['C', data]); + return false; + }); + `); + await writeSync(page, '\x1b]666;some data\x07'); + assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [ + ['C', 'some data'], + ['B', 'some data'], + ['A', 'some data'] + ]); }); }); }); From b4635eddf3ec6c206c32bc81e2aa22ef84d916e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 25 Feb 2021 19:04:37 +0100 Subject: [PATCH 17/22] make reset async aware --- src/common/parser/DcsParser.ts | 6 +- .../parser/EscapeSequenceParser.test.ts | 4 +- src/common/parser/EscapeSequenceParser.ts | 173 ++++++++++-------- src/common/parser/OscParser.ts | 7 +- src/common/parser/Types.d.ts | 1 + 5 files changed, 111 insertions(+), 80 deletions(-) diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 9f326099..5389b828 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -48,9 +48,13 @@ export class DcsParser implements IDcsParser { } public reset(): void { + // force cleanup leftover handlers if (this._active.length) { - this.unhook(false); + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].unhook(false); + } } + this._stack.paused = false; this._active = EMPTY_HANDLERS; this._ident = 0; } diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 6d00adda..4b23fa00 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -1847,7 +1847,9 @@ describe('EscapeSequenceParser - async', () => { // 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 + // reset should lift the error condition + parser.reset(); + await parseP(parser, INPUT); // does not throw anymore }); it('correct result on awaited parse call', async () => { await parseP(parser, INPUT); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index f9a14553..9ed3e462 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -427,6 +427,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; } + /** + * Reset parser to initial values. + * + * This can also be used to lift the improper continuation error condition + * when dealing with async handlers. Use this only as a last resort to silence + * that error when the terminal has no pending data to be processed. Note that + * the interrupted async handler might continue its work in the future messing + * up the terminal state even further. + */ public reset(): void { this.currentState = this.initialState; this._oscParser.reset(); @@ -435,6 +444,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.addParam(0); // ZDM this._collect = 0; this.precedingCodepoint = 0; + // abort pending continuation from async handler + // Here the RESET type indicates, that the next parse call will + // ignore any saved stack, instead continues sync with next codepoint from GROUND + if (this._parseStack.state !== ParserStackType.NONE) { + this._parseStack.state = ParserStackType.RESET; + this._parseStack.handlers = []; // also release handlers ref + } } /** @@ -486,12 +502,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * 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) - * * Boilerplate for proper parsing of multiple chunks with async handlers: * * ```typescript @@ -515,84 +525,95 @@ 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 is a really bad condition with screwed up execution order and messed up terminal state, - * therefore we exit hard with an exception and reject any further parsing. - * - * Note: With `Terminal.write` usage this exception should never occur, as the top level - * calls are guaranteed to handle async conditions properly. If you ever encounter this - * exception in your terminal integration it indicates, that you injected data chunks to - * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for - * continuation of a running async handler. - */ - this._parseStack.state = ParserStackType.FAIL; - throw new Error('improper continuation due to previous async handler, giving up parsing'); - } + // allow sync parser reset even in continuation mode + // Note: can be used to recover parser from improper continuation error above + if (this._parseStack.state === ParserStackType.RESET) { + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND + } else { + if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { + /** + * Reject further parsing on improper continuation after pausing. + * This is a really bad condition with screwed up execution order and messed up terminal state, + * therefore we exit hard with an exception and reject any further parsing. + * + * Note: With `Terminal.write` usage this exception should never occur, as the top level + * calls are guaranteed to handle async conditions properly. If you ever encounter this + * exception in your terminal integration it indicates, that you injected data chunks to + * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for + * continuation of a running async handler. + * + * Its possible to get rid of this error condition by calling `reset`, but dont rely on that, + * as the pending async handler might mess up the terminal even further. Instead fix the faulty + * async handling, so this error will not be thrown anymore. + */ + this._parseStack.state = ParserStackType.FAIL; + throw new Error('improper continuation due to previous async handler, giving up parsing'); + } - // we have to resume the old handler loop if: - // - return value of the promise was `false` - // - handlers are not exhausted yet - // FIXME: removing handlers from within a handler of the same sequence - // is not supported atm (also true for sync handlers)!! - const handlers = this._parseStack.handlers; - let handlerPos = this._parseStack.handlerPos - 1; - switch (this._parseStack.state) { - case ParserStackType.CSI: - if (promiseResult === false && handlerPos > -1) { - for (; handlerPos >= 0; handlerPos--) { - if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { - if (handlerResult instanceof Promise) { - this._parseStack.handlerPos = handlerPos; - return handlerResult; + // we have to resume the old handler loop if: + // - return value of the promise was `false` + // - handlers are not exhausted yet + // FIXME: removing handlers from within a handler of the same sequence + // is not supported atm (also true for sync handlers)!! + const handlers = this._parseStack.handlers; + let handlerPos = this._parseStack.handlerPos - 1; + switch (this._parseStack.state) { + case ParserStackType.CSI: + if (promiseResult === false && handlerPos > -1) { + for (; handlerPos >= 0; handlerPos--) { + if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { + if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + break; } - break; } } - } - this._parseStack.handlers = []; - break; - case ParserStackType.ESC: - if (promiseResult === false && handlerPos > -1) { - for (; handlerPos >= 0; handlerPos--) { - if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { - if (handlerResult instanceof Promise) { - this._parseStack.handlerPos = handlerPos; - return handlerResult; + this._parseStack.handlers = []; + break; + case ParserStackType.ESC: + if (promiseResult === false && handlerPos > -1) { + for (; handlerPos >= 0; handlerPos--) { + if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { + if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + break; } - break; } } - } - this._parseStack.handlers = []; - break; - case ParserStackType.DCS: - code = data[this._parseStack.chunkPos]; - if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) { - return handlerResult; - } - if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; - this._params.reset(); - this._params.addParam(0); // ZDM - this._collect = 0; - break; - case ParserStackType.OSC: - code = data[this._parseStack.chunkPos]; - if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { - return handlerResult; - } - if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; - this._params.reset(); - this._params.addParam(0); // ZDM - this._collect = 0; - break; + this._parseStack.handlers = []; + break; + case ParserStackType.DCS: + code = data[this._parseStack.chunkPos]; + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + case ParserStackType.OSC: + code = data[this._parseStack.chunkPos]; + if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + } + // cleanup before continuing with the main sync loop + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; + this.precedingCodepoint = 0; + this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK; } - // cleanup before continuing with the main sync loop - this._parseStack.state = ParserStackType.NONE; - start = this._parseStack.chunkPos + 1; - this.precedingCodepoint = 0; - this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK; } // continue with main sync loop diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index b0b51e23..1516308c 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -46,10 +46,13 @@ export class OscParser implements IOscParser { } public reset(): void { - // cleanup handlers if payload was already sent + // force cleanup handlers if payload was already sent if (this._state === OscState.PAYLOAD) { - this.end(false); + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].end(false); + } } + this._stack.paused = false; this._active = EMPTY_HANDLERS; this._id = -1; this._state = OscState.START; diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index dda798cc..26a02e83 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -245,6 +245,7 @@ export interface IHandlerCollection { export const enum ParserStackType { NONE = 0, FAIL, + RESET, CSI, ESC, OSC, From cc005a274dc440c40cf985fe1ebfa184e2f0f517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 27 Feb 2021 20:18:41 +0100 Subject: [PATCH 18/22] multiple changes: - reorder exception throwing to be sync to band position - document WriteBuffer._innerWrite - remove any declarations - remove conditional assigments - use faster return value branching in all parsers - remove dead code in benchmark --- src/common/InputHandler.ts | 4 +- src/common/input/WriteBuffer.ts | 57 +++++++++++++++++++---- src/common/parser/DcsParser.ts | 28 +++++------ src/common/parser/EscapeSequenceParser.ts | 55 ++++++++++++---------- src/common/parser/OscParser.ts | 26 ++++++----- test/benchmark/Terminal.benchmark.ts | 22 --------- 6 files changed, 111 insertions(+), 81 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 08f04d9e..f663ded7 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -488,8 +488,8 @@ export class InputHandler extends Disposable implements IInputHandler { * execution stopped at async handler, stack saved, continue with * same chunk and the promise resolve value as `promiseResult` until the method returns `undefined` * - * Note: Never call this directly for a running terminal instance in production. - * Always use `Terminal.write`, which provides in-band blocking and correct exection order. + * Note: This method should only be called by `Terminal.write` to ensure correct execution order and + * proper continuation of async parser handlers. */ public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise { let result: void | Promise; diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index c6178024..a2c70004 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -31,6 +31,12 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; +// queueMicrotask polyfill for nodejs < v11 +const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined') + ? (cb: () => void) => { Promise.resolve().then(cb); } + : queueMicrotask; + + export class WriteBuffer { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; @@ -39,7 +45,9 @@ export class WriteBuffer { constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } - // FIXME: does not work that way anymore with async handlers!!! + /** + * @deprecated Unreliable, to be removed soon. + */ public writeSync(data: string | Uint8Array): void { // force sync processing on pending data chunks to avoid in-band data scrambling // does the same as innerWrite but without event loop @@ -77,13 +85,40 @@ export class WriteBuffer { this._callbacks.push(callback); } - protected _innerWrite(d: number = 0, promiseResult: boolean = true): void { - let result: void | Promise; - const startTime = d || Date.now(); + /** + * Inner write call, that enters the sliced chunk processing by timing. + * + * `lastTime` indicates, when the last _innerWrite call had started. + * It is used to aggregate async handler execution under a timeout constraint + * effectively lowering the redrawing needs, schematically: + * + * macroTask _innerWrite: + * if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS): + * schedule microTask _innerWrite(lastTime) + * else: + * schedule macroTask _innerWrite(0) + * + * overall execution order on task queues: + * + * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...] + * m t: | + * i a: [...] + * c s: | + * r k: while < timeout: + * o s: _innerWrite(timeout) + * + * `promiseResult` depicts the promise resolve value of an async handler. + * This value gets carried forward through all saved stack states of the + * paused parser for proper continuation. + * + * Note, for pure sync code `lastTime` and `promiseResult` have no meaning. + */ + protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void { + const startTime = lastTime || Date.now(); while (this._writeBuffer.length > this._bufferOffset) { const data = this._writeBuffer[this._bufferOffset]; - - if (result = this._action(data, promiseResult)) { + const result = this._action(data, promiseResult); + if (result) { /** * If we get a promise as return value, we re-schedule the continuation * as thenable on the promise and exit right away. @@ -98,7 +133,6 @@ export class WriteBuffer { * * Exceptions on async handlers will be logged to console async, but do not interrupt * the input processing (continues with next handler at the current input position). - * FIXME: No clear exception handling rules for sync handlers yet (will exit whole processing?). */ /** @@ -127,7 +161,14 @@ export class WriteBuffer { // ? r => setTimeout(() => this._innerWrite(0, r)) // : r => this._innerWrite(startTime, r); - result.then(continuation, err => { setTimeout(() => { throw err; }); continuation(true); }); + // Handle exceptions synchronously to current band position, idea: + // 1. spawn a single microtask which we allow to throw hard + // 2. spawn a promise immediately resolving to `true` + // (executed on the same queue, thus properly aligned before continuation happens) + result.catch(err => { + qmt(() => {throw err;}); + return Promise.resolve(true); + }).then(continuation); return; } diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 5389b828..6b5ddd6e 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -88,11 +88,11 @@ export class DcsParser implements IDcsParser { loopPosition: 0, fallThrough: false }; - public unhook(success: boolean, promiseResult?: boolean): void | Promise { + public unhook(success: boolean, promiseResult: boolean = true): void | Promise { if (!this._active.length) { this._handlerFb(this._ident, 'UNHOOK', success); } else { - let handlerResult: any = false; + let handlerResult: boolean | Promise = false; let j = this._active.length - 1; let fallThrough = false; if (this._stack.paused) { @@ -103,21 +103,22 @@ export class DcsParser implements IDcsParser { } if (!fallThrough && handlerResult === false) { for (; j >= 0; j--) { - if ((handlerResult = this._active[j].unhook(success)) !== false) { - if (handlerResult instanceof Promise) { - this._stack.paused = true; - this._stack.loopPosition = j; - this._stack.fallThrough = false; - return handlerResult; - } + handlerResult = this._active[j].unhook(success); + if (handlerResult === true) { break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; } } j--; } // cleanup left over handlers (fallThrough for async) for (; j >= 0; j--) { - if ((handlerResult = this._active[j].unhook(false)) instanceof Promise) { + handlerResult = this._active[j].unhook(false); + if (handlerResult instanceof Promise) { this._stack.paused = true; this._stack.loopPosition = j; this._stack.fallThrough = true; @@ -171,10 +172,11 @@ export class DcsHandler implements IDcsHandler { if (this._hitLimit) { ret = false; } else if (success) { - if ((ret = this._handler(this._data, this._params)) instanceof Promise) { - // FIXME: should this be behind a catch rule? + ret = this._handler(this._data, this._params); + if (ret instanceof Promise) { + // need to hold data and params until `ret` got resolved + // dont care for errors, data will be freed anyway on next start return ret.then(res => { - // cleanup handler state late this._params = EMPTY_PARAMS; this._data = ''; this._hitLimit = false; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 9ed3e462..c1d80c5c 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -468,7 +468,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP handlers: ResumableHandlersType, handlerPos: number, transition: number, - chunkPos: number): void { + chunkPos: number + ): void { this._parseStack.state = state; this._parseStack.handlers = handlers; this._parseStack.handlerPos = handlerPos; @@ -521,12 +522,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let code = 0; let transition = 0; let start = 0; - let handlerResult: any; + let handlerResult: void | boolean | Promise; // resume from async handler if (this._parseStack.state) { // allow sync parser reset even in continuation mode - // Note: can be used to recover parser from improper continuation error above + // Note: can be used to recover parser from improper continuation error below if (this._parseStack.state === ParserStackType.RESET) { this._parseStack.state = ParserStackType.NONE; start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND @@ -534,8 +535,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { /** * Reject further parsing on improper continuation after pausing. - * This is a really bad condition with screwed up execution order and messed up terminal state, - * therefore we exit hard with an exception and reject any further parsing. + * This is a really bad condition with screwed up execution order and prolly messed up + * terminal state, therefore we exit hard with an exception and reject any further parsing. * * Note: With `Terminal.write` usage this exception should never occur, as the top level * calls are guaranteed to handle async conditions properly. If you ever encounter this @@ -543,8 +544,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for * continuation of a running async handler. * - * Its possible to get rid of this error condition by calling `reset`, but dont rely on that, - * as the pending async handler might mess up the terminal even further. Instead fix the faulty + * It is possible to get rid of this error by calling `reset`. But dont rely on that, + * as the pending async handler still might mess up the terminal later. Instead fix the faulty * async handling, so this error will not be thrown anymore. */ this._parseStack.state = ParserStackType.FAIL; @@ -554,20 +555,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // we have to resume the old handler loop if: // - return value of the promise was `false` // - handlers are not exhausted yet - // FIXME: removing handlers from within a handler of the same sequence - // is not supported atm (also true for sync handlers)!! const handlers = this._parseStack.handlers; let handlerPos = this._parseStack.handlerPos - 1; switch (this._parseStack.state) { case ParserStackType.CSI: if (promiseResult === false && handlerPos > -1) { for (; handlerPos >= 0; handlerPos--) { - if ((handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params)) !== false) { - if (handlerResult instanceof Promise) { - this._parseStack.handlerPos = handlerPos; - return handlerResult; - } + handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params); + if (handlerResult === true) { break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; } } } @@ -576,12 +575,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserStackType.ESC: if (promiseResult === false && handlerPos > -1) { for (; handlerPos >= 0; handlerPos--) { - if ((handlerResult = (handlers as EscHandlerType[])[handlerPos]()) !== false) { - if (handlerResult instanceof Promise) { - this._parseStack.handlerPos = handlerPos; - return handlerResult; - } + handlerResult = (handlers as EscHandlerType[])[handlerPos](); + if (handlerResult === true) { break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; } } } @@ -589,7 +588,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserStackType.DCS: code = data[this._parseStack.chunkPos]; - if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) { + handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { return handlerResult; } if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; @@ -599,7 +599,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserStackType.OSC: code = data[this._parseStack.chunkPos]; - if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { + handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { return handlerResult; } if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; @@ -678,7 +679,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP for (; j >= 0; j--) { // true means success and to stop bubbling // a promise indicates an async handler that needs to finish before progressing - if ((handlerResult = handlers[j](this._params)) === true) { + handlerResult = handlers[j](this._params); + if (handlerResult === true) { break; } else if (handlerResult instanceof Promise) { this._preserveStack(ParserStackType.CSI, handlers, j, transition, i); @@ -716,7 +718,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP for (; jj >= 0; jj--) { // true means success and to stop bubbling // a promise indicates an async handler that needs to finish before progressing - if ((handlerResult = handlersEsc[jj]()) === true) { + handlerResult = handlersEsc[jj](); + if (handlerResult === true) { break; } else if (handlerResult instanceof Promise) { this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i); @@ -748,7 +751,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.DCS_UNHOOK: - if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a); + if (handlerResult) { this._preserveStack(ParserStackType.DCS, [], 0, transition, i); return handlerResult; } @@ -772,7 +776,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.OSC_END: - if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a)) { + handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a); + if (handlerResult) { this._preserveStack(ParserStackType.OSC, [], 0, transition, i); return handlerResult; } diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index 1516308c..f043bd82 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -130,7 +130,7 @@ export class OscParser implements IOscParser { * Whether the OSC got aborted or finished normally * is indicated by `success`. */ - public end(success: boolean, promiseResult?: boolean): void | Promise { + public end(success: boolean, promiseResult: boolean = true): void | Promise { if (this._state === OscState.START) { return; } @@ -146,7 +146,7 @@ export class OscParser implements IOscParser { if (!this._active.length) { this._handlerFb(this._id, 'END', success); } else { - let handlerResult: any = false; + let handlerResult: boolean | Promise = false; let j = this._active.length - 1; let fallThrough = false; if (this._stack.paused) { @@ -157,14 +157,14 @@ export class OscParser implements IOscParser { } if (!fallThrough && handlerResult === false) { for (; j >= 0; j--) { - if ((handlerResult = this._active[j].end(success)) !== false) { - if (handlerResult instanceof Promise) { - this._stack.paused = true; - this._stack.loopPosition = j; - this._stack.fallThrough = false; - return handlerResult; - } + handlerResult = this._active[j].end(success); + if (handlerResult === true) { break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; } } j--; @@ -173,7 +173,8 @@ export class OscParser implements IOscParser { // we always have to call .end for proper cleanup, // here we use `success` to indicate whether a handler should execute for (; j >= 0; j--) { - if ((handlerResult = this._active[j].end(false)) instanceof Promise) { + handlerResult = this._active[j].end(false); + if (handlerResult instanceof Promise) { this._stack.paused = true; this._stack.loopPosition = j; this._stack.fallThrough = true; @@ -220,7 +221,10 @@ export class OscHandler implements IOscHandler { if (this._hitLimit) { ret = false; } else if (success) { - if ((ret = this._handler(this._data)) instanceof Promise) { + ret = this._handler(this._data); + if (ret instanceof Promise) { + // need to hold data until `ret` got resolved + // dont care for errors, data will be freed anyway on next start return ret.then(res => { this._data = ''; this._hitLimit = false; diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 7d0f5d84..40a3c208 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -44,28 +44,6 @@ perfContext('Terminal: ls -lR /usr/lib', () => { } }); - // perfContext('write/string/sync', () => { - // let terminal: Terminal; - // before(() => { - // terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); - // }); - // new ThroughputRuntimeCase('', () => { - // terminal.writeSync(content); - // return {payloadSize: contentUtf8.length}; - // }, {fork: false}).showAverageThroughput(); - // }); - // - // perfContext('write/Utf8/sync', () => { - // let terminal: Terminal; - // before(() => { - // terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); - // }); - // new ThroughputRuntimeCase('', () => { - // terminal.writeSync(content); - // return {payloadSize: contentUtf8.length}; - // }, {fork: false}).showAverageThroughput(); - // }); - perfContext('write/string/async', () => { let terminal: Terminal; before(() => { From 48a93c379fb2aa04a02317ae9c6768d1c7cc9465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 27 Feb 2021 22:24:55 +0100 Subject: [PATCH 19/22] log a warning if an async handler takes too long --- src/common/InputHandler.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index f663ded7..228df3a3 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -20,6 +20,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; +import { LogLevel } from 'common/services/LogService'; /** * Map collect to glevel. Used in `selectCharset`. @@ -97,6 +98,9 @@ export enum WindowsOptionsReportType { GET_CELL_SIZE_PIXELS = 1 } +// create a warning log if an async handler takes longer than the limit (in ms) +const SLOW_ASYNC_LIMIT = 5000; + /** * DCS subparser implementations */ @@ -477,6 +481,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._parseStack.decodedLength = decodedLength; this._parseStack.position = position; } + private _logSlowResolvingAsync(p: Promise): void { + // log a limited warning about an async taking too long + if ((this._logService as any)._logLevel <= LogLevel.WARN) { + Promise.race([p, new Promise((res, rej) => setTimeout(rej, SLOW_ASYNC_LIMIT))]) + .catch(() => console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`)); + } + } /** * Parse call with async handler support. @@ -502,6 +513,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (wasPaused) { // assumption: _parseBuffer never mutates between async calls if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) { + this._logSlowResolvingAsync(result); return result; } cursorStartX = this._parseStack.cursorStartX; @@ -536,6 +548,7 @@ export class InputHandler extends Disposable implements IInputHandler { : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer); if (result = this._parser.parse(this._parseBuffer, len)) { this._preserveStack(cursorStartX, cursorStartY, len, i); + this._logSlowResolvingAsync(result); return result; } } @@ -546,6 +559,7 @@ export class InputHandler extends Disposable implements IInputHandler { : this._utf8Decoder.decode(data, this._parseBuffer); if (result = this._parser.parse(this._parseBuffer, len)) { this._preserveStack(cursorStartX, cursorStartY, len, 0); + this._logSlowResolvingAsync(result); return result; } } From cdd238d75e72b9ffe3b484242caa87be2cccd58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 28 Feb 2021 13:53:06 +0100 Subject: [PATCH 20/22] do not timeout on faulty async handlers, continue with false to give default handlers chance to run --- src/common/InputHandler.ts | 9 +++++++-- src/common/input/WriteBuffer.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 228df3a3..7ab2a186 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -484,8 +484,13 @@ export class InputHandler extends Disposable implements IInputHandler { private _logSlowResolvingAsync(p: Promise): void { // log a limited warning about an async taking too long if ((this._logService as any)._logLevel <= LogLevel.WARN) { - Promise.race([p, new Promise((res, rej) => setTimeout(rej, SLOW_ASYNC_LIMIT))]) - .catch(() => console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`)); + Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))]) + .catch(err => { + if (err !== '#SLOW_TIMEOUT') { + throw err; + } + console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`); + }); } } diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index a2c70004..d22de912 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -167,7 +167,7 @@ export class WriteBuffer { // (executed on the same queue, thus properly aligned before continuation happens) result.catch(err => { qmt(() => {throw err;}); - return Promise.resolve(true); + return Promise.resolve(false); }).then(continuation); return; } From 8e7cdd25874c2792497888313c46e6dcd59c1242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 17 Mar 2021 22:29:01 +0100 Subject: [PATCH 21/22] interface for inputhandler parse stack, proper typing of LogService.logLevel --- src/common/InputHandler.ts | 25 +++++++++++----------- src/common/TestUtils.test.ts | 3 ++- src/common/Types.d.ts | 10 ++++++++- src/common/services/LogService.ts | 35 ++++++++++++------------------- src/common/services/Services.ts | 9 ++++++++ 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 7ab2a186..a35f7981 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent } from 'common/Types'; +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; @@ -17,10 +17,9 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; -import { LogLevel } from 'common/services/LogService'; /** * Map collect to glevel. Used in `selectCharset`. @@ -263,6 +262,14 @@ export class InputHandler extends Disposable implements IInputHandler { private _onAnsiColorChange = new EventEmitter(); public get onAnsiColorChange(): IEvent { return this._onAnsiColorChange.event; } + private _parseStack: IParseStack = { + paused: false, + cursorStartX: 0, + cursorStartY: 0, + decodedLength: 0, + position: 0 + }; + constructor( private readonly _bufferService: IBufferService, private readonly _charsetService: ICharsetService, @@ -467,13 +474,6 @@ export class InputHandler extends Disposable implements IInputHandler { /** * Async parse support. */ - private _parseStack = { - paused: false, - cursorStartX: 0, - cursorStartY: 0, - decodedLength: 0, - position: 0 - }; private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void { this._parseStack.paused = true; this._parseStack.cursorStartX = cursorStartX; @@ -481,9 +481,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._parseStack.decodedLength = decodedLength; this._parseStack.position = position; } + private _logSlowResolvingAsync(p: Promise): void { - // log a limited warning about an async taking too long - if ((this._logService as any)._logLevel <= LogLevel.WARN) { + // log a limited warning about an async handler taking too long + if (this._logService.logLevel <= LogLevelEnum.WARN) { Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))]) .catch(err => { if (err !== '#SLOW_TIMEOUT') { diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 35357744..01ceacbd 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -92,6 +92,7 @@ export class MockDirtyRowService implements IDirtyRowService { export class MockLogService implements ILogService { public serviceBrand: any; + public logLevel = LogLevelEnum.DEBUG; public debug(message: any, ...optionalParams: any[]): void {} public info(message: any, ...optionalParams: any[]): void {} public warn(message: any, ...optionalParams: any[]): void {} diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index e345e11d..108d81ac 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -349,7 +349,7 @@ export interface IInputHandler { onTitleChange: IEvent; onRequestScroll: IEvent; - parse(data: string | Uint8Array): void; + parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise; print(data: Uint32Array, start: number, end: number): void; registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable; registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; @@ -431,3 +431,11 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; /** ESC # 8 */ screenAlignmentPattern(): void; } + +interface IParseStack { + paused: boolean; + cursorStartX: number; + cursorStartY: number; + decodedLength: number; + position: number; +} diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index 3c0ccaf1..de4e18f8 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ILogService, IOptionsService } from 'common/services/Services'; +import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services'; type LogType = (message?: any, ...optionalParams: any[]) => void; @@ -19,21 +19,12 @@ interface IConsole { // module doesn't depend on them so we need to explicitly declare it. declare const console: IConsole; - -export enum LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - OFF = 4 -} - -const optionsKeyToLogLevel: { [key: string]: LogLevel } = { - debug: LogLevel.DEBUG, - info: LogLevel.INFO, - warn: LogLevel.WARN, - error: LogLevel.ERROR, - off: LogLevel.OFF +const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = { + debug: LogLevelEnum.DEBUG, + info: LogLevelEnum.INFO, + warn: LogLevelEnum.WARN, + error: LogLevelEnum.ERROR, + off: LogLevelEnum.OFF }; const LOG_PREFIX = 'xterm.js: '; @@ -41,7 +32,7 @@ const LOG_PREFIX = 'xterm.js: '; export class LogService implements ILogService { public serviceBrand: any; - private _logLevel!: LogLevel; + public logLevel: LogLevelEnum = LogLevelEnum.OFF; constructor( @IOptionsService private readonly _optionsService: IOptionsService @@ -55,7 +46,7 @@ export class LogService implements ILogService { } private _updateLogLevel(): void { - this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel]; + this.logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel]; } private _evalLazyOptionalParams(optionalParams: any[]): void { @@ -72,25 +63,25 @@ export class LogService implements ILogService { } public debug(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.DEBUG) { + if (this.logLevel <= LogLevelEnum.DEBUG) { this._log(console.log, message, optionalParams); } } public info(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.INFO) { + if (this.logLevel <= LogLevelEnum.INFO) { this._log(console.info, message, optionalParams); } } public warn(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.WARN) { + if (this.logLevel <= LogLevelEnum.WARN) { this._log(console.warn, message, optionalParams); } } public error(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.ERROR) { + if (this.logLevel <= LogLevelEnum.ERROR) { this._log(console.error, message, optionalParams); } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index c733e73e..1fbf57fb 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -161,6 +161,8 @@ export const ILogService = createDecorator('LogService'); export interface ILogService { serviceBrand: undefined; + logLevel: LogLevelEnum; + debug(message: any, ...optionalParams: any[]): void; info(message: any, ...optionalParams: any[]): void; warn(message: any, ...optionalParams: any[]): void; @@ -181,6 +183,13 @@ export interface IOptionsService { export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; +export enum LogLevelEnum { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + OFF = 4 +} export type RendererType = 'dom' | 'canvas'; export interface IPartialTerminalOptions { From 869c11f1d8ce7edee3a262d904f8e4a4768ad746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 17 Mar 2021 22:39:09 +0100 Subject: [PATCH 22/22] interface for subparser stack saves --- src/common/parser/DcsParser.ts | 12 ++++++------ src/common/parser/EscapeSequenceParser.ts | 16 +++++++++------- src/common/parser/OscParser.ts | 13 ++++++------- src/common/parser/Types.d.ts | 13 +++++++++++++ 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index 6b5ddd6e..b66524ba 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -16,6 +16,11 @@ export class DcsParser implements IDcsParser { private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; private _handlerFb: DcsFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; public dispose(): void { this._handlers = Object.create(null); @@ -83,11 +88,6 @@ export class DcsParser implements IDcsParser { } } - private _stack = { - paused: false, - loopPosition: 0, - fallThrough: false - }; public unhook(success: boolean, promiseResult: boolean = true): void | Promise { if (!this._active.length) { this._handlerFb(this._ident, 'UNHOOK', success); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index c1d80c5c..f20a7e91 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -253,6 +253,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _escHandlerFb: EscFallbackHandlerType; protected _errorHandlerFb: (state: IParsingState) => IParsingState; + // parser stack save for async handler support + protected _parseStack: IParserStackState = { + state: ParserStackType.NONE, + handlers: [], + handlerPos: 0, + transition: 0, + chunkPos: 0 + }; + constructor( protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE ) { @@ -456,13 +465,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP /** * Async parse support. */ - protected _parseStack: IParserStackState = { - state: ParserStackType.NONE, - handlers: [], - handlerPos: 0, - transition: 0, - chunkPos: 0 - }; protected _preserveStack( state: ParserStackType, handlers: ResumableHandlersType, diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index f043bd82..32710aed 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types'; +import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types'; import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; @@ -16,6 +16,11 @@ export class OscParser implements IOscParser { private _id = -1; private _handlers: IHandlerCollection = Object.create(null); private _handlerFb: OscFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; public registerHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { @@ -119,12 +124,6 @@ export class OscParser implements IOscParser { } } - private _stack = { - paused: false, - loopPosition: 0, - fallThrough: false - }; - /** * Indicates end of an OSC command. * Whether the OSC got aborted or finished normally diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 26a02e83..3a621eab 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -242,6 +242,8 @@ export interface IHandlerCollection { /** * Types for async parser support. */ + +// type of saved stack state in parser export const enum ParserStackType { NONE = 0, FAIL, @@ -251,7 +253,11 @@ export const enum ParserStackType { OSC, DCS } + +// aggregate of resumable handler lists export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[]; + +// saved stack state of the parser export interface IParserStackState { state: ParserStackType; handlers: ResumableHandlersType; @@ -259,3 +265,10 @@ export interface IParserStackState { transition: number; chunkPos: number; } + +// saved stack state of subparser (OSC and DCS) +export interface ISubParserStackState { + paused: boolean; + loopPosition: number; + fallThrough: boolean; +}