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 <jasnell@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
This commit is contained in:
Ruben Bridgewater 2020-01-11 13:36:21 +01:00
parent a67c5dc064
commit c3b702f9b4
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
8 changed files with 97 additions and 140 deletions

View File

@ -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;
}

View File

@ -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/);

View File

@ -296,11 +296,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;
}
@ -312,6 +314,7 @@ function REPLServer(prompt,
break;
}
}
self.isCompletionEnabled = tmpCompletionEnabled;
}
function defaultEval(code, context, file, cb) {
@ -837,7 +840,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) {

View File

@ -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}`;

View File

@ -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 ]',

View File

@ -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);

View File

@ -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']
];

View File

@ -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`] {',