Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl: prevent previews during pasting #31315

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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;
}
Expand All @@ -312,6 +314,7 @@ function REPLServer(prompt,
break;
}
}
self.isCompletionEnabled = tmpCompletionEnabled;
}

function defaultEval(code, context, file, cb) {
Expand Down Expand Up @@ -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) {
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