From bf9ff164125703e5401a7698d2c26a7ba943e2b3 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 11 Dec 2019 19:33:53 +0100 Subject: [PATCH] repl: add completion preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves the already existing preview functionality by also checking for the input completion. In case there's only a single completion, it will automatically be visible to the user in grey. If colors are deactivated, it will be visible as comment. This also changes some keys by automatically accepting the preview by moving the cursor behind the current input end. PR-URL: https://github.com/nodejs/node/pull/30907 Reviewed-By: Michaƫl Zasso Reviewed-By: Rich Trott --- doc/api/repl.md | 5 +- lib/internal/repl/utils.js | 170 ++++++++++++++++++--- lib/readline.js | 8 +- lib/repl.js | 8 +- test/parallel/test-repl-editor.js | 9 +- test/parallel/test-repl-multiline.js | 13 +- test/parallel/test-repl-preview.js | 8 +- test/parallel/test-repl-top-level-await.js | 122 +++++++++------ 8 files changed, 262 insertions(+), 81 deletions(-) diff --git a/doc/api/repl.md b/doc/api/repl.md index be3c0b8c52e2ab..7c7ce9b3a303f5 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -575,8 +575,9 @@ changes: * `breakEvalOnSigint` {boolean} Stop evaluating the current piece of code when `SIGINT` is received, such as when `Ctrl+C` is pressed. This cannot be used together with a custom `eval` function. **Default:** `false`. - * `preview` {boolean} Defines if the repl prints output previews or not. - **Default:** `true`. Always `false` in case `terminal` is falsy. + * `preview` {boolean} Defines if the repl prints autocomplete and output + previews or not. **Default:** `true`. If `terminal` is falsy, then there are + no previews and the value of `preview` has no effect. * Returns: {repl.REPLServer} The `repl.start()` method creates and starts a [`repl.REPLServer`][] instance. diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index c4280c1d1fe9e2..bdc402ee08715d 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -28,6 +28,10 @@ const { moveCursor, } = require('readline'); +const { + commonPrefix +} = require('internal/readline/utils'); + const { inspect } = require('util'); const debug = require('internal/util/debuglog').debuglog('repl'); @@ -119,24 +123,103 @@ function isRecoverableError(e, code) { function setupPreview(repl, contextSymbol, bufferSymbol, active) { // Simple terminals can't handle previews. if (process.env.TERM === 'dumb' || !active) { - return { showInputPreview() {}, clearPreview() {} }; + return { showPreview() {}, clearPreview() {} }; } - let preview = null; - let lastPreview = ''; + let inputPreview = null; + let lastInputPreview = ''; + + let previewCompletionCounter = 0; + let completionPreview = null; const clearPreview = () => { - if (preview !== null) { + if (inputPreview !== null) { moveCursor(repl.output, 0, 1); clearLine(repl.output); moveCursor(repl.output, 0, -1); - lastPreview = preview; - preview = null; + lastInputPreview = inputPreview; + inputPreview = null; + } + if (completionPreview !== null) { + // Prevent cursor moves if not necessary! + const move = repl.line.length !== repl.cursor; + if (move) { + cursorTo(repl.output, repl._prompt.length + repl.line.length); + } + clearLine(repl.output, 1); + if (move) { + cursorTo(repl.output, repl._prompt.length + repl.cursor); + } + completionPreview = null; } }; + function showCompletionPreview(line, insertPreview) { + previewCompletionCounter++; + + const count = previewCompletionCounter; + + repl.completer(line, (error, data) => { + // Tab completion might be async and the result might already be outdated. + if (count !== previewCompletionCounter) { + return; + } + + if (error) { + debug('Error while generating completion preview', error); + return; + } + + // Result and the text that was completed. + const [rawCompletions, completeOn] = data; + + if (!rawCompletions || rawCompletions.length === 0) { + return; + } + + // If there is a common prefix to all matches, then apply that portion. + const completions = rawCompletions.filter((e) => e); + const prefix = commonPrefix(completions); + + // No common prefix found. + if (prefix.length <= completeOn.length) { + return; + } + + const suffix = prefix.slice(completeOn.length); + + const totalLength = repl.line.length + + repl._prompt.length + + suffix.length + + (repl.useColors ? 0 : 4); + + // TODO(BridgeAR): Fix me. This should not be necessary. See similar + // comment in `showPreview()`. + if (totalLength > repl.columns) { + return; + } + + if (insertPreview) { + repl._insertString(suffix); + return; + } + + completionPreview = suffix; + + const result = repl.useColors ? + `\u001b[90m${suffix}\u001b[39m` : + ` // ${suffix}`; + + if (repl.line.length !== repl.cursor) { + cursorTo(repl.output, repl._prompt.length + repl.line.length); + } + repl.output.write(result); + cursorTo(repl.output, repl._prompt.length + repl.cursor); + }); + } + // This returns a code preview for arbitrary input code. - function getPreviewInput(input, callback) { + function getInputPreview(input, callback) { // For similar reasons as `defaultEval`, wrap expressions starting with a // curly brace with parenthesis. if (input.startsWith('{') && !input.endsWith(';')) { @@ -184,23 +267,41 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { }, () => callback(new ERR_INSPECTOR_NOT_AVAILABLE())); } - const showInputPreview = () => { + const showPreview = () => { // Prevent duplicated previews after a refresh. - if (preview !== null) { + if (inputPreview !== null) { return; } const line = repl.line.trim(); - // Do not preview if the command is buffered or if the line is empty. - if (repl[bufferSymbol] || line === '') { + // Do not preview in case the line only contains whitespace. + if (line === '') { + return; + } + + // Add the autocompletion preview. + // TODO(BridgeAR): Trigger the input preview after the completion preview. + // That way it's possible to trigger the input prefix including the + // potential completion suffix. To do so, we also have to change the + // behavior of `enter` and `escape`: + // Enter should automatically add the suffix to the current line as long as + // escape was not pressed. We might even remove the preview in case any + // cursor movement is triggered. + if (typeof repl.completer === 'function') { + const insertPreview = false; + showCompletionPreview(repl.line, insertPreview); + } + + // Do not preview if the command is buffered. + if (repl[bufferSymbol]) { return; } - getPreviewInput(line, (error, inspected) => { + getInputPreview(line, (error, inspected) => { // Ignore the output if the value is identical to the current line and the // former preview is not identical to this preview. - if ((line === inspected && lastPreview !== inspected) || + if ((line === inspected && lastInputPreview !== inspected) || inspected === null) { return; } @@ -215,7 +316,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { return; } - preview = inspected; + inputPreview = inspected; // Limit the output to maximum 250 characters. Otherwise it becomes a) // difficult to read and b) non terminal REPLs would visualize the whole @@ -235,21 +336,50 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { repl.output.write(`\n${result}`); moveCursor(repl.output, 0, -1); - cursorTo(repl.output, repl.cursor + repl._prompt.length); + cursorTo(repl.output, repl._prompt.length + repl.cursor); }); }; + // -------------------------------------------------------------------------// + // Replace multiple interface functions. This is required to fully support // + // previews without changing readlines behavior. // + // -------------------------------------------------------------------------// + // Refresh prints the whole screen again and the preview will be removed // during that procedure. Print the preview again. This also makes sure // the preview is always correct after resizing the terminal window. - const tmpRefresh = repl._refreshLine.bind(repl); + const originalRefresh = repl._refreshLine.bind(repl); repl._refreshLine = () => { - preview = null; - tmpRefresh(); - showInputPreview(); + inputPreview = null; + originalRefresh(); + showPreview(); + }; + + let insertCompletionPreview = true; + // Insert the longest common suffix of the current input in case the user + // moves to the right while already being at the current input end. + const originalMoveCursor = repl._moveCursor.bind(repl); + repl._moveCursor = (dx) => { + const currentCursor = repl.cursor; + originalMoveCursor(dx); + if (currentCursor + dx > repl.line.length && + typeof repl.completer === 'function' && + insertCompletionPreview) { + const insertPreview = true; + showCompletionPreview(repl.line, insertPreview); + } + }; + + // This is the only function that interferes with the completion insertion. + // Monkey patch it to prevent inserting the completion when it shouldn't be. + const originalClearLine = repl.clearLine.bind(repl); + repl.clearLine = () => { + insertCompletionPreview = false; + originalClearLine(); + insertCompletionPreview = true; }; - return { showInputPreview, clearPreview }; + return { showPreview, clearPreview }; } module.exports = { diff --git a/lib/readline.js b/lib/readline.js index bd70754b5b48a2..7607d7fb03c440 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -602,8 +602,11 @@ function charLengthLeft(str, i) { } function charLengthAt(str, i) { - if (str.length <= i) - return 0; + if (str.length <= i) { + // Pretend to move to the right. This is necessary to autocomplete while + // moving to the right. + return 1; + } return str.codePointAt(i) >= kUTF16SurrogateThreshold ? 2 : 1; } @@ -958,6 +961,7 @@ Interface.prototype._ttyWrite = function(s, key) { } break; + // TODO(BridgeAR): This seems broken? case 'w': // Delete backwards to a word boundary case 'backspace': this._deleteWordLeft(); diff --git a/lib/repl.js b/lib/repl.js index 9d28234900f215..d721f1e6890603 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -821,7 +821,7 @@ function REPLServer(prompt, const { clearPreview, - showInputPreview + showPreview } = setupPreview( this, kContextId, @@ -832,7 +832,6 @@ function REPLServer(prompt, // Wrap readline tty to enable editor mode and pausing. const ttyWrite = self._ttyWrite.bind(self); self._ttyWrite = (d, key) => { - clearPreview(); key = key || {}; if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { pausedBuffer.push(['key', [d, key]]); @@ -844,14 +843,17 @@ function REPLServer(prompt, self.cursor === 0 && self.line.length === 0) { self.clearLine(); } + clearPreview(); ttyWrite(d, key); - showInputPreview(); + showPreview(); return; } // Editor mode if (key.ctrl && !key.shift) { switch (key.name) { + // TODO(BridgeAR): There should not be a special mode necessary for full + // multiline support. case 'd': // End editor mode _turnOffEditorMode(self); sawCtrlD = true; diff --git a/test/parallel/test-repl-editor.js b/test/parallel/test-repl-editor.js index 969f6172b3fb70..b340bd66313971 100644 --- a/test/parallel/test-repl-editor.js +++ b/test/parallel/test-repl-editor.js @@ -5,10 +5,11 @@ const assert = require('assert'); const repl = require('repl'); const ArrayStream = require('../common/arraystream'); -// \u001b[1G - Moves the cursor to 1st column +// \u001b[nG - Moves the cursor to n st column // \u001b[0J - Clear screen -// \u001b[3G - Moves the cursor to 3rd column +// \u001b[0K - Clear to line end const terminalCode = '\u001b[1G\u001b[0J> \u001b[3G'; +const previewCode = (str, n) => ` // ${str}\x1B[${n}G\x1B[0K`; const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g'); function run({ input, output, event, checkTerminalCodes = true }) { @@ -17,7 +18,9 @@ function run({ input, output, event, checkTerminalCodes = true }) { stream.write = (msg) => found += msg.replace('\r', ''); - let expected = `${terminalCode}.editor\n` + + let expected = `${terminalCode}.ed${previewCode('itor', 6)}i` + + `${previewCode('tor', 7)}t${previewCode('or', 8)}o` + + `${previewCode('r', 9)}r\n` + '// Entering editor mode (^D to finish, ^C to cancel)\n' + `${input}${output}\n${terminalCode}`; diff --git a/test/parallel/test-repl-multiline.js b/test/parallel/test-repl-multiline.js index 6498923b62ecfc..f99b91c84b0a85 100644 --- a/test/parallel/test-repl-multiline.js +++ b/test/parallel/test-repl-multiline.js @@ -23,14 +23,23 @@ function run({ useColors }) { r.on('exit', common.mustCall(() => { const actual = output.split('\n'); + const firstLine = useColors ? + '\x1B[1G\x1B[0J \x1B[1Gco\x1B[90mn\x1B[39m\x1B[3G\x1B[0Knst ' + + 'fo\x1B[90mr\x1B[39m\x1B[9G\x1B[0Ko = {' : + '\x1B[1G\x1B[0J \x1B[1Gco // n\x1B[3G\x1B[0Knst ' + + 'fo // r\x1B[9G\x1B[0Ko = {'; + // Validate the output, which contains terminal escape codes. assert.strictEqual(actual.length, 6 + process.features.inspector); - assert.ok(actual[0].endsWith(input[0])); + assert.strictEqual(actual[0], firstLine); assert.ok(actual[1].includes('... ')); assert.ok(actual[1].endsWith(input[1])); assert.ok(actual[2].includes('undefined')); - assert.ok(actual[3].endsWith(input[2])); if (process.features.inspector) { + assert.ok( + actual[3].endsWith(input[2]), + `"${actual[3]}" should end with "${input[2]}"` + ); assert.ok(actual[4].includes(actual[5])); assert.strictEqual(actual[4].includes('//'), !useColors); } diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 92e73dd245056f..65b56904cb03b9 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -72,13 +72,13 @@ async function tests(options) { '\x1B[36m[Function: foo]\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], ['koo', [2, 4], '[Function: koo]', - 'koo', + 'k\x1B[90moo\x1B[39m\x1B[9G\x1B[0Ko\x1B[90mo\x1B[39m\x1B[10G\x1B[0Ko', '\x1B[90m[Function: koo]\x1B[39m\x1B[1A\x1B[11G\x1B[1B\x1B[2K\x1B[1A\r', '\x1B[36m[Function: koo]\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], ['a', [1, 2], undefined], ['{ a: true }', [2, 3], '{ a: \x1B[33mtrue\x1B[39m }', - '{ a: true }\r', + '{ a: tru\x1B[90me\x1B[39m\x1B[16G\x1B[0Ke }\r', '{ a: \x1B[33mtrue\x1B[39m }', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], ['1n + 2n', [2, 5], '\x1B[33m3n\x1B[39m', @@ -88,12 +88,12 @@ async function tests(options) { '\x1B[33m3n\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], ['{ a: true };', [2, 4], '\x1B[33mtrue\x1B[39m', - '{ a: true };', + '{ a: tru\x1B[90me\x1B[39m\x1B[16G\x1B[0Ke };', '\x1B[90mtrue\x1B[39m\x1B[1A\x1B[20G\x1B[1B\x1B[2K\x1B[1A\r', '\x1B[33mtrue\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], [' \t { a: true};', [2, 5], '\x1B[33mtrue\x1B[39m', - ' \t { a: true}', + ' \t { a: tru\x1B[90me\x1B[39m\x1B[19G\x1B[0Ke}', '\x1B[90m{ a: true }\x1B[39m\x1B[1A\x1B[21G\x1B[1B\x1B[2K\x1B[1A;', '\x1B[90mtrue\x1B[39m\x1B[1A\x1B[22G\x1B[1B\x1B[2K\x1B[1A\r', '\x1B[33mtrue\x1B[39m', diff --git a/test/parallel/test-repl-top-level-await.js b/test/parallel/test-repl-top-level-await.js index cecbd3ab4563d0..47fcb8530dee77 100644 --- a/test/parallel/test-repl-top-level-await.js +++ b/test/parallel/test-repl-top-level-await.js @@ -60,7 +60,7 @@ const testMe = repl.start({ prompt: PROMPT, stream: putIn, terminal: true, - useColors: false, + useColors: true, breakEvalOnSigint: true }); @@ -84,69 +84,99 @@ async function ordinaryTests() { 'function koo() { return Promise.resolve(4); }' ]); const testCases = [ - [ 'await Promise.resolve(0)', '0' ], - [ '{ a: await Promise.resolve(1) }', '{ a: 1 }' ], - [ '_', '// { a: 1 }\r', { line: 0 } ], + [ 'await Promise.resolve(0)', + // Auto completion preview with colors stripped. + ['awaitaititt Proroomiseisesee.resolveolvelvevee(0)\r', '0'] + ], + [ '{ a: await Promise.resolve(1) }', + // Auto completion preview with colors stripped. + ['{ a: awaitaititt Proroomiseisesee.resolveolvelvevee(1) }\r', + '{ a: 1 }'] + ], + [ '_', '{ a: 1 }\r', { line: 0 } ], [ 'let { aa, bb } = await Promise.resolve({ aa: 1, bb: 2 }), f = 5;', - 'undefined' ], - [ 'aa', ['// 1\r', '1'] ], - [ 'bb', ['// 2\r', '2'] ], - [ 'f', ['// 5\r', '5'] ], - [ 'let cc = await Promise.resolve(2)', 'undefined' ], - [ 'cc', ['// 2\r', '2'] ], - [ 'let dd;', 'undefined' ], - [ 'dd', 'undefined' ], - [ 'let [ii, { abc: { kk } }] = [0, { abc: { kk: 1 } }];', 'undefined' ], - [ 'ii', ['// 0\r', '0'] ], - [ 'kk', ['// 1\r', '1'] ], - [ 'var ll = await Promise.resolve(2);', 'undefined' ], - [ 'll', ['// 2\r', '2'] ], + [ + 'letett { aa, bb } = awaitaititt Proroomiseisesee.resolveolvelvevee' + + '({ aa: 1, bb: 2 }), f = 5;\r' + ] + ], + [ 'aa', ['1\r', '1'] ], + [ 'bb', ['2\r', '2'] ], + [ 'f', ['5\r', '5'] ], + [ 'let cc = await Promise.resolve(2)', + ['letett cc = awaitaititt Proroomiseisesee.resolveolvelvevee(2)\r'] + ], + [ 'cc', ['2\r', '2'] ], + [ 'let dd;', ['letett dd;\r'] ], + [ 'dd', ['undefined\r'] ], + [ 'let [ii, { abc: { kk } }] = [0, { abc: { kk: 1 } }];', + ['letett [ii, { abc: { kook } }] = [0, { abc: { kook: 1 } }];\r'] ], + [ 'ii', ['0\r', '0'] ], + [ 'kk', ['1\r', '1'] ], + [ 'var ll = await Promise.resolve(2);', + ['var letl = awaitaititt Proroomiseisesee.resolveolvelvevee(2);\r'] + ], + [ 'll', ['2\r', '2'] ], [ 'foo(await koo())', - [ 'f', '// 5oo', '// [Function: foo](await koo())\r', '4' ] ], - [ '_', ['// 4\r', '4'] ], + ['f', '5oo', '[Function: foo](awaitaititt kooo())\r', '4'] ], + [ '_', ['4\r', '4'] ], [ 'const m = foo(await koo());', - [ 'const m = foo(await koo());\r', 'undefined' ] ], - [ 'm', ['// 4\r', '4' ] ], + ['connst module = foo(awaitaititt kooo());\r'] ], + [ 'm', ['4\r', '4' ] ], [ 'const n = foo(await\nkoo());', - [ 'const n = foo(await\r', '... koo());\r', 'undefined' ] ], - [ 'n', ['// 4\r', '4' ] ], + ['connst n = foo(awaitaititt\r', '... kooo());\r', 'undefined'] ], + [ 'n', ['4\r', '4'] ], // eslint-disable-next-line no-template-curly-in-string [ '`status: ${(await Promise.resolve({ status: 200 })).status}`', - "'status: 200'"], + [ + '`stratus: ${(awaitaititt Proroomiseisesee.resolveolvelvevee' + + '({ stratus: 200 })).stratus}`\r', + "'status: 200'" + ] + ], [ 'for (let i = 0; i < 2; ++i) await i', - ['f', '// 5or (let i = 0; i < 2; ++i) await i\r', 'undefined'] ], + ['f', '5or (lett i = 0; i < 2; ++i) awaitaititt i\r', 'undefined'] ], [ 'for (let i = 0; i < 2; ++i) { await i }', - [ 'f', '// 5or (let i = 0; i < 2; ++i) { await i }\r', 'undefined' ] ], - [ 'await 0', ['await 0\r', '0'] ], + ['f', '5or (lett i = 0; i < 2; ++i) { awaitaititt i }\r', 'undefined'] + ], + [ 'await 0', ['awaitaititt 0\r', '0'] ], [ 'await 0; function foo() {}', - ['await 0; function foo() {}\r', 'undefined'] ], + ['awaitaititt 0; functionnctionctiontioniononn foo() {}\r'] + ], [ 'foo', - ['f', '// 5oo', '// [Function: foo]\r', '[Function: foo]'] ], - [ 'class Foo {}; await 1;', ['class Foo {}; await 1;\r', '1'] ], - [ 'Foo', ['// [Function: Foo]\r', '[Function: Foo]'] ], + ['f', '5oo', '[Function: foo]\r', '[Function: foo]'] ], + [ 'class Foo {}; await 1;', ['class Foo {}; awaitaititt 1;\r', '1'] ], + [ 'Foo', ['Fooo', '[Function: Foo]\r', '[Function: Foo]'] ], [ 'if (await true) { function bar() {}; }', - ['if (await true) { function bar() {}; }\r', 'undefined'] ], - [ 'bar', ['// [Function: bar]\r', '[Function: bar]'] ], - [ 'if (await true) { class Bar {}; }', 'undefined' ], + ['if (awaitaititt truee) { functionnctionctiontioniononn bar() {}; }\r'] + ], + [ 'bar', ['barr', '[Function: bar]\r', '[Function: bar]'] ], + [ 'if (await true) { class Bar {}; }', + ['if (awaitaititt truee) { class Bar {}; }\r'] + ], [ 'Bar', 'Uncaught ReferenceError: Bar is not defined' ], - [ 'await 0; function* gen(){}', 'undefined' ], + [ 'await 0; function* gen(){}', + ['awaitaititt 0; functionnctionctiontioniononn* globalen(){}\r'] + ], [ 'for (var i = 0; i < 10; ++i) { await i; }', - ['f', '// 5or (var i = 0; i < 10; ++i) { await i; }\r', 'undefined'] ], - [ 'i', ['// 10\r', '10'] ], + ['f', '5or (var i = 0; i < 10; ++i) { awaitaititt i; }\r', 'undefined'] ], + [ 'i', ['10\r', '10'] ], [ 'for (let j = 0; j < 5; ++j) { await j; }', - ['f', '// 5or (let j = 0; j < 5; ++j) { await j; }\r', 'undefined'] ], + ['f', '5or (lett j = 0; j < 5; ++j) { awaitaititt j; }\r', 'undefined'] ], [ 'j', 'Uncaught ReferenceError: j is not defined', { line: 0 } ], - [ 'gen', ['// [GeneratorFunction: gen]\r', '[GeneratorFunction: gen]'] ], + [ 'gen', + ['genn', '[GeneratorFunction: gen]\r', '[GeneratorFunction: gen]'] + ], [ 'return 42; await 5;', 'Uncaught SyntaxError: Illegal return statement', { line: 3 } ], - [ 'let o = await 1, p', 'undefined' ], - [ 'p', 'undefined' ], - [ 'let q = 1, s = await 2', 'undefined' ], - [ 's', ['// 2\r', '2'] ], + [ 'let o = await 1, p', ['lett os = awaitaititt 1, p\r'] ], + [ 'p', ['undefined\r'] ], + [ 'let q = 1, s = await 2', ['lett que = 1, s = awaitaititt 2\r'] ], + [ 's', ['2\r', '2'] ], [ 'for await (let i of [1,2,3]) console.log(i)', [ 'f', - '// 5or await (let i of [1,2,3]) console.log(i)\r', + '5or awaitaititt (lett i of [1,2,3]) connsolelee.logogg(i)\r', '1', '2', '3', @@ -160,6 +190,8 @@ async function ordinaryTests() { const toBeRun = input.split('\n'); const lines = await runAndWait(toBeRun); if (Array.isArray(expected)) { + if (expected.length === 1) + expected.push('undefined'); if (lines[0] === input) lines.shift(); assert.deepStrictEqual(lines, [...expected, PROMPT]); @@ -184,7 +216,7 @@ async function ctrlCTest() { 'await timeout(100000)', { ctrl: true, name: 'c' } ]), [ - 'await timeout(100000)\r', + 'awaitaititt timeoutmeouteoutoututt(100000)\r', 'Uncaught:', '[Error [ERR_SCRIPT_EXECUTION_INTERRUPTED]: ' + 'Script execution was interrupted by `SIGINT`] {',