From ce5c9d771ce22045fc6d042599767853bf53b6e8 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 11 Jan 2020 13:36:21 +0100 Subject: [PATCH] repl: do not preview while pasting code This makes sure no previews are triggered while pasting code. The very last character is allowed to trigger the preview. The output should be completely identical to the user. PR-URL: https://github.com/nodejs/node/pull/31315 Reviewed-By: James M Snell Reviewed-By: Rich Trott Reviewed-By: Minwoo Jung --- lib/internal/repl/utils.js | 3 +- lib/readline.js | 5 +- lib/repl.js | 7 +- test/parallel/test-repl-editor.js | 5 +- test/parallel/test-repl-history-navigation.js | 7 +- test/parallel/test-repl-multiline.js | 22 +-- test/parallel/test-repl-preview.js | 23 ++- test/parallel/test-repl-top-level-await.js | 165 +++++++----------- 8 files changed, 97 insertions(+), 140 deletions(-) diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 6082fe7908ba73..3449eab785fe2c 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -298,10 +298,9 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { }, () => callback(new ERR_INSPECTOR_NOT_AVAILABLE())); } - // TODO(BridgeAR): Prevent previews while pasting code. const showPreview = () => { // Prevent duplicated previews after a refresh. - if (inputPreview !== null) { + if (inputPreview !== null || !repl.isCompletionEnabled) { return; } diff --git a/lib/readline.js b/lib/readline.js index 281a8b63fa8d7d..a87cd8f183c973 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -97,6 +97,8 @@ function Interface(input, output, completer, terminal) { } this._sawReturnAt = 0; + // TODO(BridgeAR): Document this property. The name is not ideal, so we might + // want to expose an alias and document that instead. this.isCompletionEnabled = true; this._sawKeyPress = false; this._previousKey = null; @@ -1044,8 +1046,7 @@ Interface.prototype._ttyWrite = function(s, key) { this._tabComplete(lastKeypressWasTab); break; } - // falls through - + // falls through default: if (typeof s === 'string' && s) { const lines = s.split(/\r\n|\n|\r/); diff --git a/lib/repl.js b/lib/repl.js index 89ed77be4c902c..11d88917490d19 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -293,11 +293,13 @@ function REPLServer(prompt, if (!paused) return; paused = false; let entry; + const tmpCompletionEnabled = self.isCompletionEnabled; while (entry = pausedBuffer.shift()) { - const [type, payload] = entry; + const [type, payload, isCompletionEnabled] = entry; switch (type) { case 'key': { const [d, key] = payload; + self.isCompletionEnabled = isCompletionEnabled; self._ttyWrite(d, key); break; } @@ -309,6 +311,7 @@ function REPLServer(prompt, break; } } + self.isCompletionEnabled = tmpCompletionEnabled; } function defaultEval(code, context, file, cb) { @@ -838,7 +841,7 @@ function REPLServer(prompt, self._ttyWrite = (d, key) => { key = key || {}; if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) { - pausedBuffer.push(['key', [d, key]]); + pausedBuffer.push(['key', [d, key], self.isCompletionEnabled]); return; } if (!self.editorMode || !self.terminal) { diff --git a/test/parallel/test-repl-editor.js b/test/parallel/test-repl-editor.js index b340bd66313971..6871fbdcb1a882 100644 --- a/test/parallel/test-repl-editor.js +++ b/test/parallel/test-repl-editor.js @@ -9,7 +9,6 @@ const ArrayStream = require('../common/arraystream'); // \u001b[0J - Clear screen // \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 }) { @@ -18,9 +17,7 @@ function run({ input, output, event, checkTerminalCodes = true }) { stream.write = (msg) => found += msg.replace('\r', ''); - let expected = `${terminalCode}.ed${previewCode('itor', 6)}i` + - `${previewCode('tor', 7)}t${previewCode('or', 8)}o` + - `${previewCode('r', 9)}r\n` + + let expected = `${terminalCode}.editor\n` + '// Entering editor mode (^D to finish, ^C to cancel)\n' + `${input}${output}\n${terminalCode}`; diff --git a/test/parallel/test-repl-history-navigation.js b/test/parallel/test-repl-history-navigation.js index 69ed7e062c9357..8a9f1a6dadf386 100644 --- a/test/parallel/test-repl-history-navigation.js +++ b/test/parallel/test-repl-history-navigation.js @@ -321,7 +321,8 @@ const tests = [ showEscapeCodes: true, skip: !process.features.inspector, test: [ - 'fun', + 'fu', + 'n', RIGHT, BACKSPACE, LEFT, @@ -419,8 +420,8 @@ const tests = [ '[', ' ', ']', '\n// []', '\n// []', '\n// []', '> util.inspect.replDefaults.showHidden', - '\n// false', ' ', '\n// false', - '=', ' ', 't', 'r', 'u', ' // e', 'e', + '\n// false', + ' ', '=', ' ', 't', 'r', 'u', 'e', 'true\n', '> ', '[', ' ', ']', '\n// [ [length]: 0 ]', diff --git a/test/parallel/test-repl-multiline.js b/test/parallel/test-repl-multiline.js index f99b91c84b0a85..e458555cd87f76 100644 --- a/test/parallel/test-repl-multiline.js +++ b/test/parallel/test-repl-multiline.js @@ -23,28 +23,14 @@ 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.strictEqual(actual[0], firstLine); + assert.strictEqual(actual.length, 6); + assert.ok(actual[0].endsWith(input[0])); assert.ok(actual[1].includes('... ')); assert.ok(actual[1].endsWith(input[1])); assert.ok(actual[2].includes('undefined')); - 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); - } - assert.strictEqual(actual[4 + process.features.inspector], '{}'); - // Ignore the last line, which is nothing but escape codes. + assert.ok(actual[3].endsWith(input[2])); + assert.strictEqual(actual[4], '{}'); })); inputStream.run(input); diff --git a/test/parallel/test-repl-preview.js b/test/parallel/test-repl-preview.js index 179f4f78e2e473..82f1a9e507e750 100644 --- a/test/parallel/test-repl-preview.js +++ b/test/parallel/test-repl-preview.js @@ -1,19 +1,28 @@ 'use strict'; const common = require('../common'); -const ArrayStream = require('../common/arraystream'); const assert = require('assert'); const { REPLServer } = require('repl'); +const { Stream } = require('stream'); common.skipIfInspectorDisabled(); const PROMPT = 'repl > '; -class REPLStream extends ArrayStream { +class REPLStream extends Stream { + readable = true; + writable = true; + constructor() { super(); this.lines = ['']; } + run(data) { + for (const entry of data) { + this.emit('data', entry); + } + this.emit('data', '\n'); + } write(chunk) { const chunkLines = chunk.toString('utf8').split('\n'); this.lines[this.lines.length - 1] += chunkLines[0]; @@ -41,12 +50,14 @@ class REPLStream extends ArrayStream { this.on('line', onLine); }); } + pause() {} + resume() {} } function runAndWait(cmds, repl) { const promise = repl.inputStream.wait(); for (const cmd of cmds) { - repl.inputStream.run([cmd]); + repl.inputStream.run(cmd); } return promise; } @@ -93,9 +104,9 @@ async function tests(options) { '\x1B[33mtrue\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'], [' \t { a: true};', [2, 5], '\x1B[33mtrue\x1B[39m', - ' \t { a: tru\x1B[90me\x1B[39m\x1B[26G\x1B[0Ke}', - '\x1B[90m{ a: true }\x1B[39m\x1B[28G\x1B[1A\x1B[1B\x1B[2K\x1B[1A;', - '\x1B[90mtrue\x1B[39m\x1B[29G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', + ' { a: tru\x1B[90me\x1B[39m\x1B[18G\x1B[0Ke}', + '\x1B[90m{ a: true }\x1B[39m\x1B[20G\x1B[1A\x1B[1B\x1B[2K\x1B[1A;', + '\x1B[90mtrue\x1B[39m\x1B[21G\x1B[1A\x1B[1B\x1B[2K\x1B[1A\r', '\x1B[33mtrue\x1B[39m', '\x1B[1G\x1B[0Jrepl > \x1B[8G'] ]; diff --git a/test/parallel/test-repl-top-level-await.js b/test/parallel/test-repl-top-level-await.js index 47fcb8530dee77..59f0cb1617ad4a 100644 --- a/test/parallel/test-repl-top-level-await.js +++ b/test/parallel/test-repl-top-level-await.js @@ -32,7 +32,7 @@ class REPLStream extends ArrayStream { return true; } - wait(lookFor = PROMPT) { + wait() { if (this.waitingForResponse) { throw new Error('Currently waiting for response to another command'); } @@ -43,7 +43,7 @@ class REPLStream extends ArrayStream { reject(err); }; const onLine = () => { - if (this.lines[this.lines.length - 1].includes(lookFor)) { + if (this.lines[this.lines.length - 1].includes(PROMPT)) { this.removeListener('error', onError); this.removeListener('line', onLine); resolve(this.lines); @@ -64,8 +64,8 @@ const testMe = repl.start({ breakEvalOnSigint: true }); -function runAndWait(cmds, lookFor) { - const promise = putIn.wait(lookFor); +function runAndWait(cmds) { + const promise = putIn.wait(); for (const cmd of cmds) { if (typeof cmd === 'string') { putIn.run([cmd]); @@ -84,108 +84,67 @@ async function ordinaryTests() { 'function koo() { return Promise.resolve(4); }' ]); const testCases = [ - [ '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;', - [ - '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](awaitaititt kooo())\r', '4'] ], - [ '_', ['4\r', '4'] ], - [ 'const m = foo(await koo());', - ['connst module = foo(awaitaititt kooo());\r'] ], - [ 'm', ['4\r', '4' ] ], - [ 'const n = foo(await\nkoo());', - ['connst n = foo(awaitaititt\r', '... kooo());\r', 'undefined'] ], - [ 'n', ['4\r', '4'] ], + ['await Promise.resolve(0)', '0'], + ['{ a: await Promise.resolve(1) }', '{ a: 1 }'], + ['_', '{ a: 1 }'], + ['let { aa, bb } = await Promise.resolve({ aa: 1, bb: 2 }), f = 5;'], + ['aa', '1'], + ['bb', '2'], + ['f', '5'], + ['let cc = await Promise.resolve(2)'], + ['cc', '2'], + ['let dd;'], + ['dd'], + ['let [ii, { abc: { kk } }] = [0, { abc: { kk: 1 } }];'], + ['ii', '0'], + ['kk', '1'], + ['var ll = await Promise.resolve(2);'], + ['ll', '2'], + ['foo(await koo())', '4'], + ['_', '4'], + ['const m = foo(await koo());'], + ['m', '4'], + ['const n = foo(await\nkoo());', + ['const n = foo(await\r', '... koo());\r', 'undefined']], + ['n', '4'], // eslint-disable-next-line no-template-curly-in-string - [ '`status: ${(await Promise.resolve({ status: 200 })).status}`', - [ - '`stratus: ${(awaitaititt Proroomiseisesee.resolveolvelvevee' + - '({ stratus: 200 })).stratus}`\r', - "'status: 200'" - ] - ], - [ 'for (let i = 0; i < 2; ++i) await i', - ['f', '5or (lett i = 0; i < 2; ++i) awaitaititt i\r', 'undefined'] ], - [ 'for (let i = 0; i < 2; ++i) { await i }', - ['f', '5or (lett i = 0; i < 2; ++i) { awaitaititt i }\r', 'undefined'] - ], - [ 'await 0', ['awaitaititt 0\r', '0'] ], - [ 'await 0; function foo() {}', - ['awaitaititt 0; functionnctionctiontioniononn foo() {}\r'] - ], - [ '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 (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(){}', - ['awaitaititt 0; functionnctionctiontioniononn* globalen(){}\r'] - ], - [ 'for (var i = 0; i < 10; ++i) { await i; }', - ['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 (lett j = 0; j < 5; ++j) { awaitaititt j; }\r', 'undefined'] ], - [ 'j', 'Uncaught ReferenceError: j is not defined', { line: 0 } ], - [ 'gen', - ['genn', '[GeneratorFunction: gen]\r', '[GeneratorFunction: gen]'] - ], - [ 'return 42; await 5;', 'Uncaught SyntaxError: Illegal return statement', - { line: 3 } ], - [ '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 awaitaititt (lett i of [1,2,3]) connsolelee.logogg(i)\r', - '1', - '2', - '3', - 'undefined' - ] + ['`status: ${(await Promise.resolve({ status: 200 })).status}`', + "'status: 200'"], + ['for (let i = 0; i < 2; ++i) await i'], + ['for (let i = 0; i < 2; ++i) { await i }'], + ['await 0', '0'], + ['await 0; function foo() {}'], + ['foo', '[Function: foo]'], + ['class Foo {}; await 1;', '1'], + ['Foo', '[Function: Foo]'], + ['if (await true) { function bar() {}; }'], + ['bar', '[Function: bar]'], + ['if (await true) { class Bar {}; }'], + ['Bar', 'Uncaught ReferenceError: Bar is not defined'], + ['await 0; function* gen(){}'], + ['for (var i = 0; i < 10; ++i) { await i; }'], + ['i', '10'], + ['for (let j = 0; j < 5; ++j) { await j; }'], + ['j', 'Uncaught ReferenceError: j is not defined', { line: 0 }], + ['gen', '[GeneratorFunction: gen]'], + ['return 42; await 5;', 'Uncaught SyntaxError: Illegal return statement', + { line: 3 }], + ['let o = await 1, p'], + ['p'], + ['let q = 1, s = await 2'], + ['s', '2'], + ['for await (let i of [1,2,3]) console.log(i)', + [ + 'for await (let i of [1,2,3]) console.log(i)\r', + '1', + '2', + '3', + 'undefined' + ] ] ]; - for (const [input, expected, options = {}] of testCases) { + for (const [input, expected = [`${input}\r`], options = {}] of testCases) { console.log(`Testing ${input}`); const toBeRun = input.split('\n'); const lines = await runAndWait(toBeRun); @@ -216,7 +175,7 @@ async function ctrlCTest() { 'await timeout(100000)', { ctrl: true, name: 'c' } ]), [ - 'awaitaititt timeoutmeouteoutoututt(100000)\r', + 'await timeout(100000)\r', 'Uncaught:', '[Error [ERR_SCRIPT_EXECUTION_INTERRUPTED]: ' + 'Script execution was interrupted by `SIGINT`] {',