Skip to content

Commit

Permalink
repl: do not preview while pasting code
Browse files Browse the repository at this point in the history
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: #31315
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
  • Loading branch information
BridgeAR authored and targos committed Apr 28, 2020
1 parent 3867f20 commit ce5c9d7
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 140 deletions.
3 changes: 1 addition & 2 deletions lib/internal/repl/utils.js
Expand Up @@ -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;
}

Expand Down
5 changes: 3 additions & 2 deletions lib/readline.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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/);
Expand Down
7 changes: 5 additions & 2 deletions lib/repl.js
Expand Up @@ -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;
}
Expand All @@ -309,6 +311,7 @@ function REPLServer(prompt,
break;
}
}
self.isCompletionEnabled = tmpCompletionEnabled;
}

function defaultEval(code, context, file, cb) {
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 1 addition & 4 deletions test/parallel/test-repl-editor.js
Expand Up @@ -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 }) {
Expand All @@ -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}`;

Expand Down
7 changes: 4 additions & 3 deletions test/parallel/test-repl-history-navigation.js
Expand Up @@ -321,7 +321,8 @@ const tests = [
showEscapeCodes: true,
skip: !process.features.inspector,
test: [
'fun',
'fu',
'n',
RIGHT,
BACKSPACE,
LEFT,
Expand Down Expand Up @@ -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 ]',
Expand Down
22 changes: 4 additions & 18 deletions test/parallel/test-repl-multiline.js
Expand Up @@ -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);
Expand Down
23 changes: 17 additions & 6 deletions 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];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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']
];
Expand Down
165 changes: 62 additions & 103 deletions test/parallel/test-repl-top-level-await.js
Expand Up @@ -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');
}
Expand All @@ -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);
Expand All @@ -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]);
Expand All @@ -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);
Expand Down Expand Up @@ -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`] {',
Expand Down

0 comments on commit ce5c9d7

Please sign in to comment.