repl: add possibility to edit multiline commands while adding them

PR-URL: https://github.com/nodejs/node/pull/58003
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
This commit is contained in:
Giovanni 2025-04-23 15:05:56 +02:00 committed by Node.js GitHub Bot
parent 5fb879c458
commit 96be7836d7
7 changed files with 507 additions and 54 deletions

View File

@ -680,6 +680,10 @@ A list of the names of some Node.js modules, e.g., `'http'`.
<!-- YAML
added: v0.1.91
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58003
description: Added the possibility to add/edit/remove multilines
while adding a multiline command.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/57400
description: The multi-line indicator is now "|" instead of "...".

View File

@ -64,6 +64,7 @@ const {
charLengthLeft,
commonPrefix,
kSubstringSearch,
reverseString,
} = require('internal/readline/utils');
let emitKeypressEvents;
let kFirstEventParam;
@ -98,9 +99,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
// Max length of the kill ring
const kMaxLengthOfKillRing = 32;
// TODO(puskin94): make this configurable
const kMultilinePrompt = Symbol('| ');
const kLastCommandErrored = Symbol('_lastCommandErrored');
const kAddHistory = Symbol('_addHistory');
const kBeforeEdit = Symbol('_beforeEdit');
@ -131,6 +130,7 @@ const kPrompt = Symbol('_prompt');
const kPushToKillRing = Symbol('_pushToKillRing');
const kPushToUndoStack = Symbol('_pushToUndoStack');
const kQuestionCallback = Symbol('_questionCallback');
const kLastCommandErrored = Symbol('_lastCommandErrored');
const kQuestionReject = Symbol('_questionReject');
const kRedo = Symbol('_redo');
const kRedoStack = Symbol('_redoStack');
@ -151,6 +151,12 @@ const kYank = Symbol('_yank');
const kYanking = Symbol('_yanking');
const kYankPop = Symbol('_yankPop');
const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings');
const kSavePreviousState = Symbol('_savePreviousState');
const kRestorePreviousState = Symbol('_restorePreviousState');
const kPreviousLine = Symbol('_previousLine');
const kPreviousCursor = Symbol('_previousCursor');
const kPreviousPrevRows = Symbol('_previousPrevRows');
const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY');
function InterfaceConstructor(input, output, completer, terminal) {
this[kSawReturnAt] = 0;
@ -430,7 +436,7 @@ class Interface extends InterfaceConstructor {
}
}
[kSetLine](line) {
[kSetLine](line = '') {
this.line = line;
this[kIsMultiline] = StringPrototypeIncludes(line, '\n');
}
@ -477,10 +483,7 @@ class Interface extends InterfaceConstructor {
// Reversing the multilines is necessary when adding / editing and displaying them
if (reverse) {
// First reverse the lines for proper order, then convert separators
return ArrayPrototypeJoin(
ArrayPrototypeReverse(StringPrototypeSplit(line, from)),
to,
);
return reverseString(line, from, to);
}
// For normal cases (saving to history or non-multiline entries)
return StringPrototypeReplaceAll(line, from, to);
@ -494,22 +497,28 @@ class Interface extends InterfaceConstructor {
// If the trimmed line is empty then return the line
if (StringPrototypeTrim(this.line).length === 0) return this.line;
const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', false);
// This is necessary because each line would be saved in the history while creating
// A new multiline, and we don't want that.
if (this[kIsMultiline] && this.historyIndex === -1) {
ArrayPrototypeShift(this.history);
} else if (this[kLastCommandErrored]) {
// If the last command errored and we are trying to edit the history to fix it
// Remove the broken one from the history
ArrayPrototypeShift(this.history);
}
const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', true);
if (this.history.length === 0 || this.history[0] !== normalizedLine) {
if (this[kLastCommandErrored] && this.historyIndex === 0) {
// If the last command errored, remove it from history.
// The user is issuing a new command starting from the errored command,
// Hopefully with the fix
ArrayPrototypeShift(this.history);
}
if (this.removeHistoryDuplicates) {
// Remove older history line if identical to new one
const dupIndex = ArrayPrototypeIndexOf(this.history, this.line);
if (dupIndex !== -1) ArrayPrototypeSplice(this.history, dupIndex, 1);
}
ArrayPrototypeUnshift(this.history, this.line);
// Add the new line to the history
ArrayPrototypeUnshift(this.history, normalizedLine);
// Only store so many
if (this.history.length > this.historySize)
@ -521,7 +530,7 @@ class Interface extends InterfaceConstructor {
// The listener could change the history object, possibly
// to remove the last added entry if it is sensitive and should
// not be persisted in the history, like a password
const line = this.history[0];
const line = this[kIsMultiline] ? reverseString(this.history[0]) : this.history[0];
// Emit history event to notify listeners of update
this.emit('history', this.history);
@ -938,6 +947,18 @@ class Interface extends InterfaceConstructor {
}
}
[kSavePreviousState]() {
this[kPreviousLine] = this.line;
this[kPreviousCursor] = this.cursor;
this[kPreviousPrevRows] = this.prevRows;
}
[kRestorePreviousState]() {
this[kSetLine](this[kPreviousLine]);
this.cursor = this[kPreviousCursor];
this.prevRows = this[kPreviousPrevRows];
}
clearLine() {
this[kMoveCursor](+Infinity);
this[kWriteToOutput]('\r\n');
@ -947,6 +968,7 @@ class Interface extends InterfaceConstructor {
}
[kLine]() {
this[kSavePreviousState]();
const line = this[kAddHistory]();
this[kUndoStack] = [];
this[kRedoStack] = [];
@ -954,6 +976,107 @@ class Interface extends InterfaceConstructor {
this[kOnLine](line);
}
// TODO(puskin94): edit [kTtyWrite] to make call this function on a new key combination
// to make it add a new line in the middle of a "complete" multiline.
// I tried with shift + enter but it is not detected. Find a new one.
// Make sure to call this[kSavePreviousState](); && this.clearLine();
// before calling this[kAddNewLineOnTTY] to simulate what [kLine] is doing.
// When this function is called, the actual cursor is at the very end of the whole string,
// No matter where the new line was entered.
// This function should only be used when the output is a TTY
[kAddNewLineOnTTY]() {
// Restore terminal state and store current line
this[kRestorePreviousState]();
const originalLine = this.line;
// Split the line at the current cursor position
const beforeCursor = StringPrototypeSlice(this.line, 0, this.cursor);
let afterCursor = StringPrototypeSlice(this.line, this.cursor, this.line.length);
// Add the new line where the cursor is at
this[kSetLine](`${beforeCursor}\n${afterCursor}`);
// To account for the new line
this.cursor += 1;
const hasContentAfterCursor = afterCursor.length > 0;
const cursorIsNotOnFirstLine = this.prevRows > 0;
let needsRewriteFirstLine = false;
// Handle cursor positioning based on different scenarios
if (hasContentAfterCursor) {
const splitBeg = StringPrototypeSplit(beforeCursor, '\n');
// Determine if we need to rewrite the first line
needsRewriteFirstLine = splitBeg.length < 2;
// If the cursor is not on the first line
if (cursorIsNotOnFirstLine) {
const splitEnd = StringPrototypeSplit(afterCursor, '\n');
// If the cursor when I pressed enter was at least on the second line
// I need to completely erase the line where the cursor was pressed because it is possible
// That it was pressed in the middle of the line, hence I need to write the whole line.
// To achieve that, I need to reach the line above the current line coming from the end
const dy = splitEnd.length + 1;
// Calculate how many Xs we need to move on the right to get to the end of the line
const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length + kMultilinePrompt.description.length;
moveCursor(this.output, dxEndOfLineAbove, -dy);
// This is the line that was split in the middle
// Just add it to the rest of the line that will be printed later
afterCursor = `${splitBeg[splitBeg.length - 1]}\n${afterCursor}`;
} else {
// Otherwise, go to the very beginning of the first line and erase everything
const dy = StringPrototypeSplit(originalLine, '\n').length;
moveCursor(this.output, 0, -dy);
}
// Erase from the cursor to the end of the line
clearScreenDown(this.output);
if (cursorIsNotOnFirstLine) {
this[kWriteToOutput]('\n');
}
}
if (needsRewriteFirstLine) {
this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`);
} else {
this[kWriteToOutput](kMultilinePrompt.description);
}
// Write the rest and restore the cursor to where the user left it
if (hasContentAfterCursor) {
// Save the cursor pos, we need to come back here
const oldCursor = this.getCursorPos();
// Write everything after the cursor which has been deleted by clearScreenDown
const formattedEndContent = StringPrototypeReplaceAll(
afterCursor,
'\n',
`\n${kMultilinePrompt.description}`,
);
this[kWriteToOutput](formattedEndContent);
const newCursor = this[kGetDisplayPos](this.line);
// Go back to where the cursor was, with relative movement
moveCursor(this.output, oldCursor.cols - newCursor.cols, oldCursor.rows - newCursor.rows);
// Setting how many rows we have on top of the cursor
// Necessary for kRefreshLine
this.prevRows = oldCursor.rows;
} else {
// Setting how many rows we have on top of the cursor
// Necessary for kRefreshLine
this.prevRows = StringPrototypeSplit(this.line, '\n').length - 1;
}
}
[kPushToUndoStack](text, cursor) {
if (ArrayPrototypePush(this[kUndoStack], { text, cursor }) >
kMaxUndoRedoStackSize) {
@ -1525,6 +1648,7 @@ module.exports = {
kWordRight,
kWriteToOutput,
kMultilinePrompt,
kRestorePreviousState,
kAddNewLineOnTTY,
kLastCommandErrored,
kNormalizeHistoryLineEndings,
};

View File

@ -7,6 +7,7 @@ const {
StringPrototypeCharCodeAt,
StringPrototypeCodePointAt,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeToLowerCase,
Symbol,
} = primordials;
@ -395,11 +396,26 @@ function commonPrefix(strings) {
return min;
}
function reverseString(line, from = '\r', to = '\r') {
const parts = StringPrototypeSplit(line, from);
// This implementation should be faster than
// ArrayPrototypeJoin(ArrayPrototypeReverse(StringPrototypeSplit(line, from)), to);
let result = '';
for (let i = parts.length - 1; i > 0; i--) {
result += parts[i] + to;
}
result += parts[0];
return result;
}
module.exports = {
charLengthAt,
charLengthLeft,
commonPrefix,
emitKeys,
reverseString,
kSubstringSearch,
CSI,
};

View File

@ -53,7 +53,6 @@ const {
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeReverse,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeSome,
@ -196,8 +195,8 @@ const {
} = require('internal/vm');
const {
kMultilinePrompt,
kAddNewLineOnTTY,
kLastCommandErrored,
kNormalizeHistoryLineEndings,
} = require('internal/readline/interface');
let nextREPLResourceNumber = 1;
// This prevents v8 code cache from getting confused and using a different
@ -361,6 +360,7 @@ function REPLServer(prompt,
this.editorMode = false;
// Context id for use with the inspector protocol.
this[kContextId] = undefined;
this[kLastCommandErrored] = false;
if (this.breakEvalOnSigint && eval_) {
// Allowing this would not reflect user expectations.
@ -929,8 +929,6 @@ function REPLServer(prompt,
debug('finish', e, ret);
ReflectApply(_memory, self, [cmd]);
self[kLastCommandErrored] = false;
if (e && !self[kBufferedCommandSymbol] &&
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') &&
!(e instanceof Recoverable)
@ -943,33 +941,15 @@ function REPLServer(prompt,
}
// If error was SyntaxError and not JSON.parse error
if (e) {
if (e instanceof Recoverable && !sawCtrlD) {
// Start buffering data like that:
// {
// ... x: 1
// ... }
// We can start a multiline command
if (e instanceof Recoverable && !sawCtrlD) {
if (self.terminal) {
self[kAddNewLineOnTTY]();
} else {
self[kBufferedCommandSymbol] += cmd + '\n';
self.displayPrompt();
return;
}
}
// In the next two if blocks, we do not use os.EOL instead of '\n'
// because on Windows it is '\r\n'
if (StringPrototypeIncludes(cmd, '\n')) { // If you are editing a multiline command
self.history[0] = self[kNormalizeHistoryLineEndings](cmd, '\n', '\r');
} else if (self[kBufferedCommandSymbol]) { // If a new multiline command was entered
// Remove the first N lines from the self.history array
// where N is the number of lines in the buffered command
const lines = StringPrototypeSplit(self[kBufferedCommandSymbol], '\n');
self.history = ArrayPrototypeSlice(self.history, lines.length);
lines[lines.length - 1] = cmd;
const newHistoryLine = ArrayPrototypeJoin(ArrayPrototypeReverse(lines), '\r');
if (self.history[0] !== newHistoryLine) {
ArrayPrototypeUnshift(self.history, newHistoryLine);
}
return;
}
if (e) {
@ -997,6 +977,7 @@ function REPLServer(prompt,
// Display prompt again (unless we already did by emitting the 'error'
// event on the domain instance).
if (!e) {
self[kLastCommandErrored] = false;
self.displayPrompt();
}
}

View File

@ -798,21 +798,37 @@ const tests = [
env: { NODE_REPL_HISTORY: defaultHistoryPath },
skip: !process.features.inspector,
test: [
'let f = `multiline',
"let f = ''",
ENTER,
'f = `multiline',
ENTER,
'string`',
ENTER,
UP, UP, UP,
ENTER, // Finished issuing the multiline command
UP,
ENTER, // Trying to reissue the same command
UP, UP, UP, // Going back 3 times in the history, it should show the var definition
DOWN, DOWN, // Going down 2 times should show the multiline command only once
],
expected: [
prompt, ...'let f = `multiline',
'| ',
...'string`',
prompt,
...`let f = ''`,
'undefined\n',
prompt,
`${prompt}let f = \`multiline`,
...'f = `multiline',
'| ',
...'string`',
"'multiline\\nstring'\n",
prompt,
`${prompt}f = \`multiline`,
'\n| string`',
"'multiline\\nstring'\n",
prompt,
`${prompt}f = \`multiline`,
`\n| string\``,
`${prompt}let f = \`multiline`,
`${prompt}f = \`multiline`,
`\n| string\``,
`${prompt}let f = ''`,
`${prompt}f = \`multiline`,
`\n| string\``,
prompt,
],

View File

@ -0,0 +1,312 @@
'use strict';
// Flags: --expose-internals
const common = require('../common');
const assert = require('assert');
const repl = require('internal/repl');
const stream = require('stream');
class ActionStream extends stream.Stream {
run(data) {
const _iter = data[Symbol.iterator]();
const doAction = () => {
const next = _iter.next();
if (next.done) {
// Close the repl. Note that it must have a clean prompt to do so.
this.emit('keypress', '', { ctrl: true, name: 'd' });
return;
}
const action = next.value;
if (typeof action === 'object') {
this.emit('keypress', '', action);
}
setImmediate(doAction);
};
doAction();
}
write(chunk) {
const chunkLines = chunk.toString('utf8').split('\n');
this.lines[this.lines.length - 1] += chunkLines[0];
if (chunkLines.length > 1) {
this.lines.push(...chunkLines.slice(1));
}
this.emit('line', this.lines[this.lines.length - 1]);
return true;
}
resume() {}
pause() {}
}
ActionStream.prototype.readable = true;
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
{
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
// Make sure the cursor is at the right places when pressing enter at the end of the first line.
const checkResults = common.mustSucceed((r) => {
r.write('let aaa = `I am a');
r.input.run([{ name: 'enter' }]);
r.write('1111111111111');
r.input.run([{ name: 'enter' }]);
r.write('22222222222222'); // The command is not complete yet. I can still edit it
r.input.run([{ name: 'up' }]);
r.input.run([{ name: 'up' }]); // I am on the first line
for (let i = 0; i < 4; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the first line
assert.strictEqual(r.cursor, 17);
r.input.run([{ name: 'enter' }]);
assert.strictEqual(r.cursor, 18);
assert.strictEqual(r.line, 'let aaa = `I am a\n\n1111111111111\n22222222222222');
r.write('000');
r.input.run([{ name: 'down' }]);
r.input.run([{ name: 'down' }]); // I am in the last line
for (let i = 0; i < 5; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the last line
r.write('`'); // Making the command complete
r.input.run([{ name: 'enter' }]); // Issuing it
assert.strictEqual(r.history.length, 1);
assert.strictEqual(r.history[0], '22222222`222222\r1111111111111\r000\rlet aaa = `I am a');
});
repl.createInternalRepl(
{ NODE_REPL_HISTORY: historyPath },
{
terminal: true,
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
next();
}
}),
},
checkResults
);
}
{
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
// Make sure the cursor is at the right places when pressing enter in the middle of the first line.
const checkResults = common.mustSucceed((r) => {
r.write('let bbb = `I am a');
r.input.run([{ name: 'enter' }]);
r.write('1111111111111');
r.input.run([{ name: 'enter' }]);
r.write('22222222222222'); // The command is not complete yet. I can still edit it
r.input.run([{ name: 'up' }]);
r.input.run([{ name: 'up' }]); // I am on the first line
for (let i = 0; i < 2; i++) {
r.input.run([{ name: 'left' }]);
} // I am right after the string definition
assert.strictEqual(r.cursor, 11);
r.input.run([{ name: 'enter' }]);
assert.strictEqual(r.cursor, 12);
assert.strictEqual(r.line, 'let bbb = `\nI am a\n1111111111111\n22222222222222');
r.write('000');
r.input.run([{ name: 'enter' }]);
r.input.run([{ name: 'down' }]);
r.input.run([{ name: 'down' }]); // I am in the last line
for (let i = 0; i < 14; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the last line
r.write('`'); // Making the command complete
r.input.run([{ name: 'enter' }]); // Issuing it
assert.strictEqual(r.history.length, 1);
assert.strictEqual(r.history[0], '22222222222222`\r1111111111111\rI am a\r000\rlet bbb = `');
});
repl.createInternalRepl(
{ NODE_REPL_HISTORY: historyPath },
{
terminal: true,
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
next();
}
}),
},
checkResults
);
}
{
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
// Make sure the cursor is at the right places when pressing enter at the end of the second line.
const checkResults = common.mustSucceed((r) => {
r.write('let ccc = `I am a');
r.input.run([{ name: 'enter' }]);
r.write('1111111111111');
r.input.run([{ name: 'enter' }]);
r.write('22222222222222'); // The command is not complete yet. I can still edit it
r.input.run([{ name: 'up' }]); // I am the end of second line
assert.strictEqual(r.cursor, 31);
r.input.run([{ name: 'enter' }]);
assert.strictEqual(r.cursor, 32);
assert.strictEqual(r.line, 'let ccc = `I am a\n1111111111111\n\n22222222222222');
r.write('000');
r.input.run([{ name: 'down' }]); // I am in the last line
for (let i = 0; i < 11; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the last line
r.write('`'); // Making the command complete
r.input.run([{ name: 'enter' }]); // Issuing it
assert.strictEqual(r.history.length, 1);
assert.strictEqual(r.history[0], '22222222222222`\r000\r1111111111111\rlet ccc = `I am a');
});
repl.createInternalRepl(
{ NODE_REPL_HISTORY: historyPath },
{
terminal: true,
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
next();
}
}),
},
checkResults
);
}
{
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
// Make sure the cursor is at the right places when pressing enter in the middle of the second line.
const checkResults = common.mustSucceed((r) => {
r.write('let ddd = `I am a');
r.input.run([{ name: 'enter' }]);
r.write('1111111111111');
r.input.run([{ name: 'enter' }]);
r.write('22222222222222'); // The command is not complete yet. I can still edit it
r.input.run([{ name: 'up' }]); // I am the end of second line
assert.strictEqual(r.cursor, 31);
for (let i = 0; i < 6; i++) {
r.input.run([{ name: 'left' }]);
} // I am in the middle of the second line
r.input.run([{ name: 'enter' }]);
assert.strictEqual(r.cursor, 26);
assert.strictEqual(r.line, 'let ddd = `I am a\n1111111\n111111\n22222222222222');
r.input.run([{ name: 'down' }]); // I am at the beginning of the last line
for (let i = 0; i < 14; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the last line
r.write('`'); // Making the command complete
r.input.run([{ name: 'enter' }]); // Issuing it
assert.strictEqual(r.history.length, 1);
assert.strictEqual(r.history[0], '22222222222222`\r111111\r1111111\rlet ddd = `I am a');
});
repl.createInternalRepl(
{ NODE_REPL_HISTORY: historyPath },
{
terminal: true,
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
next();
}
}),
},
checkResults
);
}
{
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
// Make sure the cursor is at the right places when pressing enter at the beginning of the third line.
const checkResults = common.mustSucceed((r) => {
r.write('let eee = `I am a');
r.input.run([{ name: 'enter' }]);
r.write('1111111111111');
r.input.run([{ name: 'enter' }]);
r.write('22222222222222'); // The command is not complete yet. I can still edit it
for (let i = 0; i < 14; i++) {
r.input.run([{ name: 'left' }]);
} // I am at the beginning of the last line
assert.strictEqual(r.cursor, 32);
r.input.run([{ name: 'enter' }]);
assert.strictEqual(r.cursor, 33);
assert.strictEqual(r.line, 'let eee = `I am a\n1111111111111\n\n22222222222222');
r.input.run([{ name: 'up' }]); // I am the beginning of the new line
r.write('000');
assert.strictEqual(r.cursor, 35);
r.input.run([{ name: 'down' }]); // I am in the last line
for (let i = 0; i < 11; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the last line
r.write('`'); // Making the command complete
r.input.run([{ name: 'enter' }]); // Issuing it
assert.strictEqual(r.history.length, 1);
assert.strictEqual(r.history[0], '22222222222222`\r000\r1111111111111\rlet eee = `I am a');
});
repl.createInternalRepl(
{ NODE_REPL_HISTORY: historyPath },
{
terminal: true,
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
next();
}
}),
},
checkResults
);
}
{
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
// Make sure the cursor is at the right places when pressing enter in the middle of the third line
// And executing the command while still in the middle of the multiline command
const checkResults = common.mustSucceed((r) => {
r.write('let fff = `I am a');
r.input.run([{ name: 'enter' }]);
r.write('1111111111111');
r.input.run([{ name: 'enter' }]);
r.write('22222222222222'); // The command is not complete yet. I can still edit it
assert.strictEqual(r.cursor, 46);
for (let i = 0; i < 6; i++) {
r.input.run([{ name: 'left' }]);
} // I am in the middle of the third line
r.input.run([{ name: 'enter' }]);
assert.strictEqual(r.cursor, 41);
assert.strictEqual(r.line, 'let fff = `I am a\n1111111111111\n22222222\n222222');
r.input.run([{ name: 'down' }]); // I am at the beginning of the last line
for (let i = 0; i < 6; i++) {
r.input.run([{ name: 'right' }]);
} // I am at the end of the last line
r.write('`'); // Making the command complete
r.input.run([{ name: 'up' }]); // I am not at the end of the last line
r.input.run([{ name: 'enter' }]); // Issuing the command
assert.strictEqual(r.history.length, 1);
assert.strictEqual(r.history[0], '222222`\r22222222\r1111111111111\rlet fff = `I am a');
});
repl.createInternalRepl(
{ NODE_REPL_HISTORY: historyPath },
{
terminal: true,
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
next();
}
}),
},
checkResults
);
}

View File

@ -11,7 +11,7 @@ const args = [ '-i' ];
const child = spawn(process.execPath, args);
const input = 'const foo = "bar\\\nbaz"';
// Match '...' as well since it marks a multi-line statement
// Match '|' as well since it marks a multi-line statement
const expectOut = /> \| undefined\n/;
child.stderr.setEncoding('utf8');