mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
repl: add support for multiline history
PR-URL: https://github.com/nodejs/node/pull/57400 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Jordan Harband <ljharb@gmail.com>
This commit is contained in:
parent
964e41c5df
commit
4a4aa58fa4
17 changed files with 738 additions and 110 deletions
|
@ -625,7 +625,7 @@ The `replServer.displayPrompt()` method readies the REPL instance for input
|
|||
from the user, printing the configured `prompt` to a new line in the `output`
|
||||
and resuming the `input` to accept new input.
|
||||
|
||||
When multi-line input is being entered, an ellipsis is printed rather than the
|
||||
When multi-line input is being entered, a pipe `'|'` is printed rather than the
|
||||
'prompt'.
|
||||
|
||||
When `preserveCursor` is `true`, the cursor placement will not be reset to `0`.
|
||||
|
@ -680,6 +680,14 @@ 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/57400
|
||||
description: The multi-line indicator is now "|" instead of "...".
|
||||
Added support for multi-line history.
|
||||
It is now possible to "fix" multi-line commands with syntax errors
|
||||
by visiting the history and editing the command.
|
||||
When visiting the multiline history from an old node version,
|
||||
the multiline structure is not preserved.
|
||||
- version:
|
||||
- v13.4.0
|
||||
- v12.17.0
|
||||
|
|
|
@ -24,8 +24,11 @@ const {
|
|||
SafeStringIterator,
|
||||
StringPrototypeCodePointAt,
|
||||
StringPrototypeEndsWith,
|
||||
StringPrototypeIncludes,
|
||||
StringPrototypeRepeat,
|
||||
StringPrototypeReplaceAll,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeSplit,
|
||||
StringPrototypeStartsWith,
|
||||
StringPrototypeTrim,
|
||||
Symbol,
|
||||
|
@ -88,6 +91,10 @@ 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');
|
||||
const kDecoder = Symbol('_decoder');
|
||||
|
@ -99,7 +106,9 @@ const kDeleteWordLeft = Symbol('_deleteWordLeft');
|
|||
const kDeleteWordRight = Symbol('_deleteWordRight');
|
||||
const kGetDisplayPos = Symbol('_getDisplayPos');
|
||||
const kHistoryNext = Symbol('_historyNext');
|
||||
const kMoveDownOrHistoryNext = Symbol('_moveDownOrHistoryNext');
|
||||
const kHistoryPrev = Symbol('_historyPrev');
|
||||
const kMoveUpOrHistoryPrev = Symbol('_moveUpOrHistoryPrev');
|
||||
const kInsertString = Symbol('_insertString');
|
||||
const kLine = Symbol('_line');
|
||||
const kLine_buffer = Symbol('_line_buffer');
|
||||
|
@ -109,6 +118,7 @@ const kMoveCursor = Symbol('_moveCursor');
|
|||
const kNormalWrite = Symbol('_normalWrite');
|
||||
const kOldPrompt = Symbol('_oldPrompt');
|
||||
const kOnLine = Symbol('_onLine');
|
||||
const kSetLine = Symbol('_setLine');
|
||||
const kPreviousKey = Symbol('_previousKey');
|
||||
const kPrompt = Symbol('_prompt');
|
||||
const kPushToKillRing = Symbol('_pushToKillRing');
|
||||
|
@ -126,12 +136,14 @@ const kTabCompleter = Symbol('_tabCompleter');
|
|||
const kTtyWrite = Symbol('_ttyWrite');
|
||||
const kUndo = Symbol('_undo');
|
||||
const kUndoStack = Symbol('_undoStack');
|
||||
const kIsMultiline = Symbol('_isMultiline');
|
||||
const kWordLeft = Symbol('_wordLeft');
|
||||
const kWordRight = Symbol('_wordRight');
|
||||
const kWriteToOutput = Symbol('_writeToOutput');
|
||||
const kYank = Symbol('_yank');
|
||||
const kYanking = Symbol('_yanking');
|
||||
const kYankPop = Symbol('_yankPop');
|
||||
const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings');
|
||||
|
||||
function InterfaceConstructor(input, output, completer, terminal) {
|
||||
this[kSawReturnAt] = 0;
|
||||
|
@ -212,6 +224,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
|||
const self = this;
|
||||
|
||||
this.line = '';
|
||||
this[kIsMultiline] = false;
|
||||
this[kSubstringSearch] = null;
|
||||
this.output = output;
|
||||
this.input = input;
|
||||
|
@ -336,7 +349,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
|||
}
|
||||
|
||||
// Current line
|
||||
this.line = '';
|
||||
this[kSetLine]('');
|
||||
|
||||
input.resume();
|
||||
}
|
||||
|
@ -410,6 +423,11 @@ class Interface extends InterfaceConstructor {
|
|||
}
|
||||
}
|
||||
|
||||
[kSetLine](line) {
|
||||
this.line = line;
|
||||
this[kIsMultiline] = StringPrototypeIncludes(line, '\n');
|
||||
}
|
||||
|
||||
[kOnLine](line) {
|
||||
if (this[kQuestionCallback]) {
|
||||
const cb = this[kQuestionCallback];
|
||||
|
@ -441,6 +459,20 @@ class Interface extends InterfaceConstructor {
|
|||
}
|
||||
}
|
||||
|
||||
// Convert newlines to a consistent format for history storage
|
||||
[kNormalizeHistoryLineEndings](line, from, to) {
|
||||
// Multiline history entries are saved reversed
|
||||
if (StringPrototypeIncludes(line, '\r')) {
|
||||
// First reverse the lines for proper order, then convert separators
|
||||
return ArrayPrototypeJoin(
|
||||
ArrayPrototypeReverse(StringPrototypeSplit(line, from)),
|
||||
to,
|
||||
);
|
||||
}
|
||||
// For normal cases (saving to history or non-multiline entries)
|
||||
return StringPrototypeReplaceAll(line, from, to);
|
||||
}
|
||||
|
||||
[kAddHistory]() {
|
||||
if (this.line.length === 0) return '';
|
||||
|
||||
|
@ -449,8 +481,15 @@ 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');
|
||||
|
||||
if (this.history.length === 0 || this.history[0] !== this.line) {
|
||||
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);
|
||||
|
@ -498,8 +537,19 @@ class Interface extends InterfaceConstructor {
|
|||
// erase data
|
||||
clearScreenDown(this.output);
|
||||
|
||||
// Write the prompt and the current buffer content.
|
||||
this[kWriteToOutput](line);
|
||||
if (this[kIsMultiline]) {
|
||||
const lines = StringPrototypeSplit(this.line, '\n');
|
||||
// Write first line with normal prompt
|
||||
this[kWriteToOutput](this[kPrompt] + lines[0]);
|
||||
|
||||
// For continuation lines, add the "|" prefix
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
this[kWriteToOutput](`\n${kMultilinePrompt.description}` + lines[i]);
|
||||
}
|
||||
} else {
|
||||
// Write the prompt and the current buffer content.
|
||||
this[kWriteToOutput](line);
|
||||
}
|
||||
|
||||
// Force terminal to allocate a new line
|
||||
if (lineCols === 0) {
|
||||
|
@ -632,7 +682,7 @@ class Interface extends InterfaceConstructor {
|
|||
this.cursor,
|
||||
this.line.length,
|
||||
);
|
||||
this.line = beg + c + end;
|
||||
this[kSetLine](beg + c + end);
|
||||
this.cursor += c.length;
|
||||
this[kRefreshLine]();
|
||||
} else {
|
||||
|
@ -680,13 +730,13 @@ class Interface extends InterfaceConstructor {
|
|||
this[kInsertString](StringPrototypeSlice(prefix, completeOn.length));
|
||||
return;
|
||||
} else if (!StringPrototypeStartsWith(completeOn, prefix)) {
|
||||
this.line = StringPrototypeSlice(this.line,
|
||||
0,
|
||||
this.cursor - completeOn.length) +
|
||||
this[kSetLine](StringPrototypeSlice(this.line,
|
||||
0,
|
||||
this.cursor - completeOn.length) +
|
||||
prefix +
|
||||
StringPrototypeSlice(this.line,
|
||||
this.cursor,
|
||||
this.line.length);
|
||||
this.line.length));
|
||||
this.cursor = this.cursor - completeOn.length + prefix.length;
|
||||
this[kRefreshLine]();
|
||||
return;
|
||||
|
@ -825,7 +875,7 @@ class Interface extends InterfaceConstructor {
|
|||
[kDeleteLineLeft]() {
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
const del = StringPrototypeSlice(this.line, 0, this.cursor);
|
||||
this.line = StringPrototypeSlice(this.line, this.cursor);
|
||||
this[kSetLine](StringPrototypeSlice(this.line, this.cursor));
|
||||
this.cursor = 0;
|
||||
this[kPushToKillRing](del);
|
||||
this[kRefreshLine]();
|
||||
|
@ -834,7 +884,7 @@ class Interface extends InterfaceConstructor {
|
|||
[kDeleteLineRight]() {
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
const del = StringPrototypeSlice(this.line, this.cursor);
|
||||
this.line = StringPrototypeSlice(this.line, 0, this.cursor);
|
||||
this[kSetLine](StringPrototypeSlice(this.line, 0, this.cursor));
|
||||
this[kPushToKillRing](del);
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
|
@ -869,7 +919,7 @@ class Interface extends InterfaceConstructor {
|
|||
StringPrototypeSlice(this.line, 0, this.cursor - lastYank.length);
|
||||
const tail =
|
||||
StringPrototypeSlice(this.line, this.cursor);
|
||||
this.line = head + currentYank + tail;
|
||||
this[kSetLine](head + currentYank + tail);
|
||||
this.cursor = head.length + currentYank.length;
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
|
@ -878,7 +928,7 @@ class Interface extends InterfaceConstructor {
|
|||
clearLine() {
|
||||
this[kMoveCursor](+Infinity);
|
||||
this[kWriteToOutput]('\r\n');
|
||||
this.line = '';
|
||||
this[kSetLine]('');
|
||||
this.cursor = 0;
|
||||
this.prevRows = 0;
|
||||
}
|
||||
|
@ -907,7 +957,7 @@ class Interface extends InterfaceConstructor {
|
|||
);
|
||||
|
||||
const entry = ArrayPrototypePop(this[kUndoStack]);
|
||||
this.line = entry.text;
|
||||
this[kSetLine](entry.text);
|
||||
this.cursor = entry.cursor;
|
||||
|
||||
this[kRefreshLine]();
|
||||
|
@ -922,12 +972,36 @@ class Interface extends InterfaceConstructor {
|
|||
);
|
||||
|
||||
const entry = ArrayPrototypePop(this[kRedoStack]);
|
||||
this.line = entry.text;
|
||||
this[kSetLine](entry.text);
|
||||
this.cursor = entry.cursor;
|
||||
|
||||
this[kRefreshLine]();
|
||||
}
|
||||
|
||||
[kMoveDownOrHistoryNext]() {
|
||||
const { cols, rows } = this.getCursorPos();
|
||||
const splitLine = StringPrototypeSplit(this.line, '\n');
|
||||
if (!this.historyIndex && rows === splitLine.length) {
|
||||
return;
|
||||
}
|
||||
// Go to the next history only if the cursor is in the first line of the multiline input.
|
||||
// Otherwise treat the "arrow down" as a movement to the next row.
|
||||
if (this[kIsMultiline] && rows < splitLine.length - 1) {
|
||||
const currentLine = splitLine[rows];
|
||||
const nextLine = splitLine[rows + 1];
|
||||
// If I am moving down and the current line is longer than the next line
|
||||
const amountToMove = (cols > nextLine.length + 1) ?
|
||||
currentLine.length - cols + nextLine.length +
|
||||
kMultilinePrompt.description.length + 1 : // Move to the end of the current line
|
||||
// + chars to account for the kMultilinePrompt prefix, + 1 to go to the first char
|
||||
currentLine.length + 1; // Otherwise just move to the next line, in the same position
|
||||
this[kMoveCursor](amountToMove);
|
||||
return;
|
||||
}
|
||||
|
||||
this[kHistoryNext]();
|
||||
}
|
||||
|
||||
// TODO(BridgeAR): Add underscores to the search part and a red background in
|
||||
// case no match is found. This should only be the visual part and not the
|
||||
// actual line content!
|
||||
|
@ -948,9 +1022,9 @@ class Interface extends InterfaceConstructor {
|
|||
index--;
|
||||
}
|
||||
if (index === -1) {
|
||||
this.line = search;
|
||||
this[kSetLine](search);
|
||||
} else {
|
||||
this.line = this.history[index];
|
||||
this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
|
||||
}
|
||||
this.historyIndex = index;
|
||||
this.cursor = this.line.length; // Set cursor to end of line.
|
||||
|
@ -958,6 +1032,27 @@ class Interface extends InterfaceConstructor {
|
|||
}
|
||||
}
|
||||
|
||||
[kMoveUpOrHistoryPrev]() {
|
||||
const { cols, rows } = this.getCursorPos();
|
||||
if (this.historyIndex === this.history.length && rows) {
|
||||
return;
|
||||
}
|
||||
// Go to the previous history only if the cursor is in the first line of the multiline input.
|
||||
// Otherwise treat the "arrow up" as a movement to the previous row.
|
||||
if (this[kIsMultiline] && rows > 0) {
|
||||
const splitLine = StringPrototypeSplit(this.line, '\n');
|
||||
const previousLine = splitLine[rows - 1];
|
||||
// If I am moving up and the current line is longer than the previous line
|
||||
const amountToMove = (cols > previousLine.length + 1) ?
|
||||
-cols + 1 : // Move to the beginning of the current line + 1 char to go to the end of the previous line
|
||||
-previousLine.length - 1; // Otherwise just move to the previous line, in the same position
|
||||
this[kMoveCursor](amountToMove);
|
||||
return;
|
||||
}
|
||||
|
||||
this[kHistoryPrev]();
|
||||
}
|
||||
|
||||
[kHistoryPrev]() {
|
||||
if (this.historyIndex < this.history.length && this.history.length) {
|
||||
this[kBeforeEdit](this.line, this.cursor);
|
||||
|
@ -971,9 +1066,9 @@ class Interface extends InterfaceConstructor {
|
|||
index++;
|
||||
}
|
||||
if (index === this.history.length) {
|
||||
this.line = search;
|
||||
this[kSetLine](search);
|
||||
} else {
|
||||
this.line = this.history[index];
|
||||
this[kSetLine](this[kNormalizeHistoryLineEndings](this.history[index], '\r', '\n'));
|
||||
}
|
||||
this.historyIndex = index;
|
||||
this.cursor = this.line.length; // Set cursor to end of line.
|
||||
|
@ -987,11 +1082,13 @@ class Interface extends InterfaceConstructor {
|
|||
const col = this.columns;
|
||||
let rows = 0;
|
||||
str = stripVTControlCharacters(str);
|
||||
|
||||
for (const char of new SafeStringIterator(str)) {
|
||||
if (char === '\n') {
|
||||
// Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
|
||||
rows += MathCeil(offset / col) || 1;
|
||||
offset = 0;
|
||||
// Only add prefix offset for continuation lines in user input (not prompts)
|
||||
offset = this[kIsMultiline] ? kMultilinePrompt.description.length : 0;
|
||||
continue;
|
||||
}
|
||||
// Tabs must be aligned by an offset of the tab size.
|
||||
|
@ -1010,8 +1107,10 @@ class Interface extends InterfaceConstructor {
|
|||
offset += 2;
|
||||
}
|
||||
}
|
||||
|
||||
const cols = offset % col;
|
||||
rows += (offset - cols) / col;
|
||||
|
||||
return { cols, rows };
|
||||
}
|
||||
|
||||
|
@ -1024,8 +1123,8 @@ class Interface extends InterfaceConstructor {
|
|||
* }}
|
||||
*/
|
||||
getCursorPos() {
|
||||
const strBeforeCursor =
|
||||
this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor);
|
||||
const strBeforeCursor = this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor);
|
||||
|
||||
return this[kGetDisplayPos](strBeforeCursor);
|
||||
}
|
||||
|
||||
|
@ -1074,7 +1173,7 @@ class Interface extends InterfaceConstructor {
|
|||
!key.meta &&
|
||||
!key.shift
|
||||
) {
|
||||
if (this[kSubstringSearch] === null) {
|
||||
if (this[kSubstringSearch] === null && !this[kIsMultiline]) {
|
||||
this[kSubstringSearch] = StringPrototypeSlice(
|
||||
this.line,
|
||||
0,
|
||||
|
@ -1308,11 +1407,11 @@ class Interface extends InterfaceConstructor {
|
|||
break;
|
||||
|
||||
case 'up':
|
||||
this[kHistoryPrev]();
|
||||
this[kMoveUpOrHistoryPrev]();
|
||||
break;
|
||||
|
||||
case 'down':
|
||||
this[kHistoryNext]();
|
||||
this[kMoveDownOrHistoryNext]();
|
||||
break;
|
||||
|
||||
case 'tab':
|
||||
|
@ -1388,12 +1487,14 @@ module.exports = {
|
|||
kHistoryNext,
|
||||
kHistoryPrev,
|
||||
kInsertString,
|
||||
kIsMultiline,
|
||||
kLine,
|
||||
kLine_buffer,
|
||||
kMoveCursor,
|
||||
kNormalWrite,
|
||||
kOldPrompt,
|
||||
kOnLine,
|
||||
kSetLine,
|
||||
kPreviousKey,
|
||||
kPrompt,
|
||||
kQuestion,
|
||||
|
@ -1410,4 +1511,7 @@ module.exports = {
|
|||
kWordLeft,
|
||||
kWordRight,
|
||||
kWriteToOutput,
|
||||
kMultilinePrompt,
|
||||
kLastCommandErrored,
|
||||
kNormalizeHistoryLineEndings,
|
||||
};
|
||||
|
|
|
@ -97,7 +97,7 @@ function setupHistory(repl, historyPath, ready) {
|
|||
}
|
||||
|
||||
if (data) {
|
||||
repl.history = RegExpPrototypeSymbolSplit(/[\n\r]+/, data, repl.historySize);
|
||||
repl.history = RegExpPrototypeSymbolSplit(/\r?\n+/, data, repl.historySize);
|
||||
} else {
|
||||
repl.history = [];
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ function setupHistory(repl, historyPath, ready) {
|
|||
return;
|
||||
}
|
||||
writing = true;
|
||||
const historyData = ArrayPrototypeJoin(repl.history, os.EOL);
|
||||
const historyData = ArrayPrototypeJoin(repl.history, '\n');
|
||||
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,11 @@ const {
|
|||
moveCursor,
|
||||
} = require('internal/readline/callbacks');
|
||||
|
||||
const {
|
||||
kIsMultiline,
|
||||
kSetLine,
|
||||
} = require('internal/readline/interface');
|
||||
|
||||
const {
|
||||
commonPrefix,
|
||||
kSubstringSearch,
|
||||
|
@ -361,8 +366,11 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
|
|||
}
|
||||
|
||||
const showPreview = (showCompletion = true) => {
|
||||
// Prevent duplicated previews after a refresh.
|
||||
if (inputPreview !== null || !repl.isCompletionEnabled || !process.features.inspector) {
|
||||
// Prevent duplicated previews after a refresh or in a multiline command.
|
||||
if (inputPreview !== null ||
|
||||
repl[kIsMultiline] ||
|
||||
!repl.isCompletionEnabled ||
|
||||
!process.features.inspector) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -444,9 +452,14 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
|
|||
|
||||
const { cursorPos, displayPos } = getPreviewPos();
|
||||
const rows = displayPos.rows - cursorPos.rows;
|
||||
// Moves one line below all the user lines
|
||||
moveCursor(repl.output, 0, rows);
|
||||
// Writes the preview there
|
||||
repl.output.write(`\n${result}`);
|
||||
|
||||
// Go back to the horizontal position of the cursor
|
||||
cursorTo(repl.output, cursorPos.cols);
|
||||
// Go back to the vertical position of the cursor
|
||||
moveCursor(repl.output, 0, -rows - 1);
|
||||
};
|
||||
|
||||
|
@ -672,7 +685,7 @@ function setupReverseSearch(repl) {
|
|||
// line to the found entry.
|
||||
if (!isInReverseSearch) {
|
||||
if (lastMatch !== -1) {
|
||||
repl.line = repl.history[lastMatch];
|
||||
repl[kSetLine](repl.history[lastMatch]);
|
||||
repl.cursor = lastCursor;
|
||||
repl.historyIndex = lastMatch;
|
||||
}
|
||||
|
|
34
lib/repl.js
34
lib/repl.js
|
@ -53,6 +53,7 @@ const {
|
|||
ArrayPrototypePop,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypePushApply,
|
||||
ArrayPrototypeReverse,
|
||||
ArrayPrototypeShift,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSome,
|
||||
|
@ -193,6 +194,11 @@ const {
|
|||
const {
|
||||
makeContextifyScript,
|
||||
} = require('internal/vm');
|
||||
const {
|
||||
kMultilinePrompt,
|
||||
kLastCommandErrored,
|
||||
kNormalizeHistoryLineEndings,
|
||||
} = require('internal/readline/interface');
|
||||
let nextREPLResourceNumber = 1;
|
||||
// This prevents v8 code cache from getting confused and using a different
|
||||
// cache from a resource of the same name
|
||||
|
@ -923,6 +929,8 @@ 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)
|
||||
|
@ -945,7 +953,28 @@ function REPLServer(prompt,
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (e) {
|
||||
self._domain.emit('error', e.err || e);
|
||||
self[kLastCommandErrored] = true;
|
||||
}
|
||||
|
||||
// Clear buffer if no SyntaxErrors
|
||||
|
@ -1185,10 +1214,7 @@ REPLServer.prototype.resetContext = function() {
|
|||
REPLServer.prototype.displayPrompt = function(preserveCursor) {
|
||||
let prompt = this._initialPrompt;
|
||||
if (this[kBufferedCommandSymbol].length) {
|
||||
prompt = '...';
|
||||
const len = this.lines.level.length ? this.lines.level.length - 1 : 0;
|
||||
const levelInd = StringPrototypeRepeat('..', len);
|
||||
prompt += levelInd + ' ';
|
||||
prompt = kMultilinePrompt.description;
|
||||
}
|
||||
|
||||
// Do not overwrite `_initialPrompt` here
|
||||
|
|
4
test/fixtures/.node_repl_history_multiline
vendored
Normal file
4
test/fixtures/.node_repl_history_multiline
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
]
}
]
}
b: 4,
a: 3,
{
c: [{ a: 1, b: 2 },
b: 4,
a: 3,
{
},
b: 2,
a: 1,
{
var d = [
|
||||
]
}
b: 2,
a: 1,
{
const c = [
|
||||
]`
4,
3,
2,
1,
`const b = [
|
||||
I can be as long as I want`
I am a multiline string
a = `
|
|
@ -633,6 +633,191 @@ const tests = [
|
|||
],
|
||||
clean: false
|
||||
},
|
||||
{
|
||||
// Test that the multiline history is correctly navigated and it can be edited
|
||||
env: { NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
skip: !process.features.inspector,
|
||||
test: [
|
||||
'let a = ``',
|
||||
ENTER,
|
||||
'a = `I am a multiline strong',
|
||||
ENTER,
|
||||
'which ends here`',
|
||||
ENTER,
|
||||
UP,
|
||||
// press LEFT 19 times to reach the typo
|
||||
...Array(19).fill(LEFT),
|
||||
BACKSPACE,
|
||||
'i',
|
||||
ENTER,
|
||||
],
|
||||
expected: [
|
||||
prompt, ...'let a = ``',
|
||||
'undefined\n',
|
||||
prompt, ...'a = `I am a multiline strong', // New Line, the user pressed ENTER
|
||||
'| ',
|
||||
...'which ends here`', // New Line, the user pressed ENTER
|
||||
"'I am a multiline strong\\nwhich ends here'\n", // This is the result printed to the console
|
||||
prompt,
|
||||
`${prompt}a = \`I am a multiline strong`, // This is the history being shown and navigated
|
||||
`\n| which ends here\``,
|
||||
`${prompt}a = \`I am a multiline strong`, // This is the history being shown and navigated
|
||||
`\n| which ends here\``,
|
||||
|
||||
`${prompt}a = \`I am a multiline strng`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
|
||||
`${prompt}a = \`I am a multiline string`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
|
||||
`${prompt}a = \`I am a multiline string`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
"'I am a multiline string\\nwhich ends here'\n", // This is the result printed to the console
|
||||
prompt,
|
||||
],
|
||||
clean: true
|
||||
},
|
||||
{
|
||||
// Test that the previous multiline history can only be accessed going through the entirety of the current
|
||||
// One navigating its all lines first.
|
||||
env: { NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
skip: !process.features.inspector,
|
||||
test: [
|
||||
'let b = ``',
|
||||
ENTER,
|
||||
'b = `I am a multiline strong',
|
||||
ENTER,
|
||||
'which ends here`',
|
||||
ENTER,
|
||||
'let c = `I',
|
||||
ENTER,
|
||||
'am another one`',
|
||||
ENTER,
|
||||
UP,
|
||||
UP,
|
||||
UP,
|
||||
UP,
|
||||
// press RIGHT 10 times to reach the typo
|
||||
...Array(10).fill(RIGHT),
|
||||
BACKSPACE,
|
||||
'i',
|
||||
ENTER,
|
||||
],
|
||||
expected: [
|
||||
prompt, ...'let b = ``',
|
||||
'undefined\n',
|
||||
prompt, ...'b = `I am a multiline strong', // New Line, the user pressed ENTER
|
||||
'| ',
|
||||
...'which ends here`', // New Line, the user pressed ENTER
|
||||
"'I am a multiline strong\\nwhich ends here'\n", // This is the result printed to the console
|
||||
prompt, ...'let c = `I', // New Line, the user pressed ENTER
|
||||
'| ',
|
||||
...'am another one`', // New Line, the user pressed ENTER
|
||||
'undefined\n',
|
||||
prompt,
|
||||
`${prompt}let c = \`I`, // This is the history being shown and navigated
|
||||
`\n| am another one\``,
|
||||
|
||||
`${prompt}let c = \`I`, // This is the history being shown and navigated
|
||||
`\n| am another one\``,
|
||||
|
||||
`${prompt}b = \`I am a multiline strong`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
`${prompt}b = \`I am a multiline strong`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
`${prompt}b = \`I am a multiline strng`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
|
||||
`${prompt}b = \`I am a multiline string`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
|
||||
`${prompt}b = \`I am a multiline string`, // This is the history being shown and edited
|
||||
`\n| which ends here\``,
|
||||
"'I am a multiline string\\nwhich ends here'\n", // This is the result printed to the console
|
||||
prompt,
|
||||
],
|
||||
clean: true
|
||||
},
|
||||
{
|
||||
// Test that we can recover from a line with a syntax error
|
||||
env: { NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
skip: !process.features.inspector,
|
||||
test: [
|
||||
'let d = ``',
|
||||
ENTER,
|
||||
'd = `I am a',
|
||||
ENTER,
|
||||
'super',
|
||||
ENTER,
|
||||
'broken` line\'',
|
||||
ENTER,
|
||||
UP,
|
||||
BACKSPACE,
|
||||
'`',
|
||||
// press LEFT 6 times to reach the typo
|
||||
...Array(6).fill(LEFT),
|
||||
BACKSPACE,
|
||||
ENTER,
|
||||
],
|
||||
expected: [
|
||||
prompt, ...'let d = ``', // New Line, the user pressed ENTER
|
||||
'undefined\n',
|
||||
prompt, ...'d = `I am a', // New Line, the user pressed ENTER
|
||||
'| ',
|
||||
...'super', // New Line, the user pressed ENTER
|
||||
'| ',
|
||||
...'broken` line\'', // New Line, the user pressed ENTER
|
||||
"[broken` line'\n" +
|
||||
' ^^^^\n' +
|
||||
'\n' +
|
||||
"Uncaught SyntaxError: Unexpected identifier 'line'\n" +
|
||||
'] {\n' +
|
||||
' [stack]: [Getter/Setter],\n' +
|
||||
` [message]: "Unexpected identifier 'line'"\n` +
|
||||
'}\n',
|
||||
prompt,
|
||||
`${prompt}d = \`I am a`, // This is the history being shown and edited
|
||||
`\n| super`,
|
||||
`\n| broken\` line'`,
|
||||
|
||||
`${prompt}d = \`I am a`, // This is the history being shown and edited
|
||||
`\n| super`,
|
||||
'\n| broken` line',
|
||||
'`',
|
||||
|
||||
`${prompt}d = \`I am a`, // This is the history being shown and edited
|
||||
`\n| super`,
|
||||
`\n| broken line\``,
|
||||
"'I am a\\nsuper\\nbroken line'\n", // This is the result printed to the console
|
||||
prompt,
|
||||
],
|
||||
clean: true
|
||||
},
|
||||
{
|
||||
// Test that multiline history is not duplicated
|
||||
env: { NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
skip: !process.features.inspector,
|
||||
test: [
|
||||
'let f = `multiline',
|
||||
ENTER,
|
||||
'string`',
|
||||
ENTER,
|
||||
UP, UP, UP,
|
||||
],
|
||||
expected: [
|
||||
prompt, ...'let f = `multiline',
|
||||
'| ',
|
||||
...'string`',
|
||||
'undefined\n',
|
||||
prompt,
|
||||
`${prompt}let f = \`multiline`,
|
||||
`\n| string\``,
|
||||
`${prompt}let f = \`multiline`,
|
||||
`\n| string\``,
|
||||
prompt,
|
||||
],
|
||||
clean: true
|
||||
},
|
||||
];
|
||||
const numtests = tests.length;
|
||||
|
||||
|
|
96
test/parallel/test-repl-load-multiline-from-history.js
Normal file
96
test/parallel/test-repl-load-multiline-from-history.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
'use strict';
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
const assert = require('assert');
|
||||
const repl = require('internal/repl');
|
||||
const stream = require('stream');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
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();
|
||||
}
|
||||
resume() {}
|
||||
pause() {}
|
||||
}
|
||||
ActionStream.prototype.readable = true;
|
||||
|
||||
{
|
||||
// Testing multiline history loading
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
tmpdir.refresh();
|
||||
const replHistoryPath = fixtures.path('.node_repl_history_multiline');
|
||||
|
||||
const checkResults = common.mustSucceed((r) => {
|
||||
assert.strictEqual(r.history.length, 4);
|
||||
|
||||
r.input.run([{ name: 'up' }]);
|
||||
assert.strictEqual(r.line, 'var d = [\n' +
|
||||
' {\n' +
|
||||
' a: 1,\n' +
|
||||
' b: 2,\n' +
|
||||
' },\n' +
|
||||
' {\n' +
|
||||
' a: 3,\n' +
|
||||
' b: 4,\n' +
|
||||
' c: [{ a: 1, b: 2 },\n' +
|
||||
' {\n' +
|
||||
' a: 3,\n' +
|
||||
' b: 4,\n' +
|
||||
' }\n' +
|
||||
' ]\n' +
|
||||
' }\n' +
|
||||
']'
|
||||
);
|
||||
|
||||
// Move the cursor all lines up until the former entry is retrieved.
|
||||
for (let i = 0; i < r.line.split('\n').length; i++) {
|
||||
r.input.run([{ name: 'up' }]);
|
||||
}
|
||||
assert.strictEqual(r.line, 'const c = [\n {\n a: 1,\n b: 2,\n }\n]');
|
||||
|
||||
// Move the cursor all lines up until the former entry is retrieved.
|
||||
for (let i = 0; i < r.line.split('\n').length; i++) {
|
||||
r.input.run([{ name: 'up' }]);
|
||||
}
|
||||
assert.strictEqual(r.line, '`const b = [\n 1,\n 2,\n 3,\n 4,\n]`');
|
||||
|
||||
// Move the cursor all lines up until the former entry is retrieved.
|
||||
for (let i = 0; i < r.line.split('\n').length; i++) {
|
||||
r.input.run([{ name: 'up' }]);
|
||||
}
|
||||
assert.strictEqual(r.line, 'a = `\nI am a multiline string\nI can be as long as I want`');
|
||||
});
|
||||
|
||||
repl.createInternalRepl(
|
||||
{ NODE_REPL_HISTORY: replHistoryPath },
|
||||
{
|
||||
terminal: true,
|
||||
input: new ActionStream(),
|
||||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
next();
|
||||
}
|
||||
}),
|
||||
},
|
||||
checkResults
|
||||
);
|
||||
}
|
200
test/parallel/test-repl-multiline-navigation.js
Normal file
200
test/parallel/test-repl-multiline-navigation.js
Normal file
|
@ -0,0 +1,200 @@
|
|||
'use strict';
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
const assert = require('assert');
|
||||
const repl = require('internal/repl');
|
||||
const stream = require('stream');
|
||||
const fs = require('fs');
|
||||
|
||||
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;
|
||||
|
||||
function cleanupTmpFile() {
|
||||
try {
|
||||
// Write over the file, clearing any history
|
||||
fs.writeFileSync(defaultHistoryPath, '');
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') return true;
|
||||
throw err;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
tmpdir.refresh();
|
||||
const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
|
||||
|
||||
{
|
||||
cleanupTmpFile();
|
||||
// Make sure the cursor is at the right places.
|
||||
// If the cursor is at the end of a long line and the down key is pressed,
|
||||
// Move the cursor to the end of the next line, if shorter.
|
||||
const checkResults = common.mustSucceed((r) => {
|
||||
r.write('let str = `');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.write('111');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.write('22222222222222');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.write('3`');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.input.run([{ name: 'up' }]);
|
||||
assert.strictEqual(r.cursor, 33);
|
||||
|
||||
r.input.run([{ name: 'up' }]);
|
||||
assert.strictEqual(r.cursor, 18);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
r.input.run([{ name: 'right' }]);
|
||||
}
|
||||
r.input.run([{ name: 'up' }]);
|
||||
assert.strictEqual(r.cursor, 15);
|
||||
r.input.run([{ name: 'up' }]);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
r.input.run([{ name: 'right' }]);
|
||||
}
|
||||
assert.strictEqual(r.cursor, 8);
|
||||
|
||||
r.input.run([{ name: 'down' }]);
|
||||
assert.strictEqual(r.cursor, 15);
|
||||
|
||||
r.input.run([{ name: 'down' }]);
|
||||
assert.strictEqual(r.cursor, 19);
|
||||
});
|
||||
|
||||
repl.createInternalRepl(
|
||||
{ NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
{
|
||||
terminal: true,
|
||||
input: new ActionStream(),
|
||||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
next();
|
||||
}
|
||||
}),
|
||||
},
|
||||
checkResults
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
cleanupTmpFile();
|
||||
// If the last command errored and the user is trying to edit it,
|
||||
// The errored line should be removed from history
|
||||
const checkResults = common.mustSucceed((r) => {
|
||||
r.write('let lineWithMistake = `I have some');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.write('problem with` my syntax\'');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.input.run([{ name: 'up' }]);
|
||||
r.input.run([{ name: 'backspace' }]);
|
||||
r.write('`');
|
||||
for (let i = 0; i < 11; i++) {
|
||||
r.input.run([{ name: 'left' }]);
|
||||
}
|
||||
r.input.run([{ name: 'backspace' }]);
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
|
||||
assert.strictEqual(r.history.length, 1);
|
||||
assert.strictEqual(r.history[0], 'let lineWithMistake = `I have some\rproblem with my syntax`');
|
||||
assert.strictEqual(r.line, '');
|
||||
});
|
||||
|
||||
repl.createInternalRepl(
|
||||
{ NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
{
|
||||
terminal: true,
|
||||
input: new ActionStream(),
|
||||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
next();
|
||||
}
|
||||
}),
|
||||
},
|
||||
checkResults
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
cleanupTmpFile();
|
||||
const outputBuffer = [];
|
||||
|
||||
// Test that the REPL preview is properly shown on multiline commands
|
||||
// And deleted when enter is pressed
|
||||
const checkResults = common.mustSucceed((r) => {
|
||||
r.write('Array(100).fill(');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.write('123');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.write(')');
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
r.input.run([{ name: 'up' }]);
|
||||
r.input.run([{ name: 'up' }]);
|
||||
|
||||
assert.deepStrictEqual(r.last, new Array(100).fill(123));
|
||||
r.input.run([{ name: 'enter' }]);
|
||||
assert.strictEqual(outputBuffer.includes('[\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
|
||||
' 123\n' +
|
||||
']\n'), true);
|
||||
});
|
||||
|
||||
repl.createInternalRepl(
|
||||
{ NODE_REPL_HISTORY: defaultHistoryPath },
|
||||
{
|
||||
preview: true,
|
||||
terminal: true,
|
||||
input: new ActionStream(),
|
||||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
// Store each chunk in the buffer
|
||||
outputBuffer.push(chunk.toString());
|
||||
next();
|
||||
}
|
||||
}),
|
||||
},
|
||||
checkResults
|
||||
);
|
||||
}
|
|
@ -26,7 +26,7 @@ function run({ useColors }) {
|
|||
// Validate the output, which contains terminal escape codes.
|
||||
assert.strictEqual(actual.length, 6);
|
||||
assert.ok(actual[0].endsWith(input[0]));
|
||||
assert.ok(actual[1].includes('... '));
|
||||
assert.ok(actual[1].includes('| '));
|
||||
assert.ok(actual[1].endsWith(input[1]));
|
||||
assert.ok(actual[2].includes('undefined'));
|
||||
assert.ok(actual[3].endsWith(input[2]));
|
||||
|
|
|
@ -9,7 +9,6 @@ const REPL = require('internal/repl');
|
|||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const util = require('util');
|
||||
|
||||
if (process.env.TERM === 'dumb') {
|
||||
common.skip('skipping - dumb terminal');
|
||||
|
@ -41,11 +40,11 @@ class ActionStream extends stream.Stream {
|
|||
if (typeof action === 'object') {
|
||||
this.emit('keypress', '', action);
|
||||
} else {
|
||||
this.emit('data', `${action}\n`);
|
||||
this.emit('data', action);
|
||||
}
|
||||
setImmediate(doAction);
|
||||
};
|
||||
setImmediate(doAction);
|
||||
doAction();
|
||||
}
|
||||
resume() {}
|
||||
pause() {}
|
||||
|
@ -97,8 +96,8 @@ const tests = [
|
|||
test: [UP, '21', ENTER, "'42'", ENTER],
|
||||
expected: [
|
||||
prompt,
|
||||
'2', '1', '21\n', prompt, prompt,
|
||||
"'", '4', '2', "'", "'42'\n", prompt, prompt,
|
||||
'2', '1', '21\n', prompt,
|
||||
"'", '4', '2', "'", "'42'\n", prompt,
|
||||
],
|
||||
clean: false
|
||||
},
|
||||
|
@ -195,8 +194,6 @@ function runTest(assertCleaned) {
|
|||
const opts = tests.shift();
|
||||
if (!opts) return; // All done
|
||||
|
||||
console.log('NEW');
|
||||
|
||||
if (assertCleaned) {
|
||||
try {
|
||||
assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), '');
|
||||
|
@ -221,7 +218,6 @@ function runTest(assertCleaned) {
|
|||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
const output = chunk.toString();
|
||||
console.log('INPUT', util.inspect(output));
|
||||
|
||||
// Ignore escapes and blank lines
|
||||
if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output))
|
||||
|
|
|
@ -7,7 +7,6 @@ const REPL = require('repl');
|
|||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const util = require('util');
|
||||
|
||||
if (process.env.TERM === 'dumb') {
|
||||
common.skip('skipping - dumb terminal');
|
||||
|
@ -39,11 +38,11 @@ class ActionStream extends stream.Stream {
|
|||
if (typeof action === 'object') {
|
||||
this.emit('keypress', '', action);
|
||||
} else {
|
||||
this.emit('data', `${action}\n`);
|
||||
this.emit('data', action);
|
||||
}
|
||||
setImmediate(doAction);
|
||||
};
|
||||
setImmediate(doAction);
|
||||
doAction();
|
||||
}
|
||||
resume() {}
|
||||
pause() {}
|
||||
|
@ -95,10 +94,8 @@ const tests = [
|
|||
test: [UP, '21', ENTER, "'42'", ENTER],
|
||||
expected: [
|
||||
prompt,
|
||||
// TODO(BridgeAR): The line is refreshed too many times. The double prompt
|
||||
// is redundant and can be optimized away.
|
||||
'2', '1', '21\n', prompt, prompt,
|
||||
"'", '4', '2', "'", "'42'\n", prompt, prompt,
|
||||
'2', '1', '21\n', prompt,
|
||||
"'", '4', '2', "'", "'42'\n", prompt,
|
||||
],
|
||||
clean: false
|
||||
},
|
||||
|
@ -191,8 +188,6 @@ function runTest(assertCleaned) {
|
|||
const opts = tests.shift();
|
||||
if (!opts) return; // All done
|
||||
|
||||
console.log('NEW');
|
||||
|
||||
if (assertCleaned) {
|
||||
try {
|
||||
assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), '');
|
||||
|
@ -218,7 +213,6 @@ function runTest(assertCleaned) {
|
|||
output: new stream.Writable({
|
||||
write(chunk, _, next) {
|
||||
const output = chunk.toString();
|
||||
console.log('INPUT', util.inspect(output));
|
||||
|
||||
// Ignore escapes and blank lines
|
||||
if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output))
|
||||
|
|
|
@ -18,7 +18,7 @@ function customEval(code, context, file, cb) {
|
|||
const putIn = new ArrayStream();
|
||||
|
||||
putIn.write = function(msg) {
|
||||
if (msg === '... ') {
|
||||
if (msg === '| ') {
|
||||
recovered = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,9 @@ class ActionStream extends stream.Stream {
|
|||
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' });
|
||||
setImmediate(() => {
|
||||
this.emit('keypress', '', { ctrl: true, name: 'd' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
const action = next.value;
|
||||
|
@ -36,7 +38,7 @@ class ActionStream extends stream.Stream {
|
|||
if (typeof action === 'object') {
|
||||
this.emit('keypress', '', action);
|
||||
} else {
|
||||
this.emit('data', `${action}`);
|
||||
this.emit('data', action);
|
||||
}
|
||||
setImmediate(doAction);
|
||||
};
|
||||
|
|
|
@ -96,7 +96,7 @@ async function ordinaryTests() {
|
|||
['const m = foo(await koo());'],
|
||||
['m', '4'],
|
||||
['const n = foo(await\nkoo());',
|
||||
['const n = foo(await\r', '... koo());\r', 'undefined']],
|
||||
['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}`',
|
||||
|
@ -145,20 +145,20 @@ async function ordinaryTests() {
|
|||
],
|
||||
['for (const x of [1,2,3]) {\nawait x\n}', [
|
||||
'for (const x of [1,2,3]) {\r',
|
||||
'... await x\r',
|
||||
'... }\r',
|
||||
'| await x\r',
|
||||
'| }\r',
|
||||
'undefined',
|
||||
]],
|
||||
['for (const x of [1,2,3]) {\nawait x;\n}', [
|
||||
'for (const x of [1,2,3]) {\r',
|
||||
'... await x;\r',
|
||||
'... }\r',
|
||||
'| await x;\r',
|
||||
'| }\r',
|
||||
'undefined',
|
||||
]],
|
||||
['for await (const x of [1,2,3]) {\nconsole.log(x)\n}', [
|
||||
'for await (const x of [1,2,3]) {\r',
|
||||
'... console.log(x)\r',
|
||||
'... }\r',
|
||||
'| console.log(x)\r',
|
||||
'| }\r',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
|
@ -166,8 +166,8 @@ async function ordinaryTests() {
|
|||
]],
|
||||
['for await (const x of [1,2,3]) {\nconsole.log(x);\n}', [
|
||||
'for await (const x of [1,2,3]) {\r',
|
||||
'... console.log(x);\r',
|
||||
'... }\r',
|
||||
'| console.log(x);\r',
|
||||
'| }\r',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
|
@ -198,7 +198,7 @@ async function ordinaryTests() {
|
|||
} else if ('line' in options) {
|
||||
assert.strictEqual(lines[toBeRun.length + options.line], expected);
|
||||
} else {
|
||||
const echoed = toBeRun.map((a, i) => `${i > 0 ? '... ' : ''}${a}\r`);
|
||||
const echoed = toBeRun.map((a, i) => `${i > 0 ? '| ' : ''}${a}\r`);
|
||||
assert.deepStrictEqual(lines, [...echoed, expected, PROMPT]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const child = spawn(process.execPath, args);
|
|||
|
||||
const input = 'const foo = "bar\\\nbaz"';
|
||||
// Match '...' as well since it marks a multi-line statement
|
||||
const expectOut = /> \.\.\. undefined\n/;
|
||||
const expectOut = /> \| undefined\n/;
|
||||
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.on('data', (d) => {
|
||||
|
|
|
@ -69,7 +69,7 @@ async function runReplTests(socket, prompt, tests) {
|
|||
|
||||
// Allow to match partial text if no newline was received, because
|
||||
// sending newlines from the REPL itself would be redundant
|
||||
// (e.g. in the `... ` multiline prompt: The user already pressed
|
||||
// (e.g. in the `| ` multiline prompt: The user already pressed
|
||||
// enter for that, so the REPL shouldn't do it again!).
|
||||
if (lineBuffer === expectedLine && !expectedLine.includes('\n'))
|
||||
lineBuffer += '\n';
|
||||
|
@ -154,7 +154,7 @@ const errorTests = [
|
|||
// Common syntax error is treated as multiline command
|
||||
{
|
||||
send: 'function test_func() {',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
// You can recover with the .break command
|
||||
{
|
||||
|
@ -169,7 +169,7 @@ const errorTests = [
|
|||
// Can handle multiline template literals
|
||||
{
|
||||
send: '`io.js',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
// Special REPL commands still available
|
||||
{
|
||||
|
@ -179,7 +179,7 @@ const errorTests = [
|
|||
// Template expressions
|
||||
{
|
||||
send: '`io.js ${"1.0"',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '+ ".2"}`',
|
||||
|
@ -187,7 +187,7 @@ const errorTests = [
|
|||
},
|
||||
{
|
||||
send: '`io.js ${',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '"1.0" + ".2"}`',
|
||||
|
@ -196,7 +196,7 @@ const errorTests = [
|
|||
// Dot prefix in multiline commands aren't treated as commands
|
||||
{
|
||||
send: '("a"',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '.charAt(0))',
|
||||
|
@ -330,7 +330,7 @@ const errorTests = [
|
|||
// Multiline object
|
||||
{
|
||||
send: '{ a: ',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '1 }',
|
||||
|
@ -339,7 +339,7 @@ const errorTests = [
|
|||
// Multiline string-keyed object (e.g. JSON)
|
||||
{
|
||||
send: '{ "a": ',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '1 }',
|
||||
|
@ -348,12 +348,12 @@ const errorTests = [
|
|||
// Multiline class with private member.
|
||||
{
|
||||
send: 'class Foo { #private = true ',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
// Class field with bigint.
|
||||
{
|
||||
send: 'num = 123456789n',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
// Static class features.
|
||||
{
|
||||
|
@ -363,15 +363,15 @@ const errorTests = [
|
|||
// Multiline anonymous function with comment
|
||||
{
|
||||
send: '(function() {',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '// blah',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: 'return 1n;',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '})()',
|
||||
|
@ -380,11 +380,11 @@ const errorTests = [
|
|||
// Multiline function call
|
||||
{
|
||||
send: 'function f(){}; f(f(1,',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '2)',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: ')',
|
||||
|
@ -405,16 +405,16 @@ const errorTests = [
|
|||
...possibleTokensAfterIdentifierWithLineBreak.map((token) => (
|
||||
{
|
||||
send: `npm ${token}; undefined`,
|
||||
expect: '... undefined'
|
||||
expect: '| undefined'
|
||||
}
|
||||
)),
|
||||
{
|
||||
send: '(function() {\n\nreturn 1;\n})()',
|
||||
expect: '... ... ... 1'
|
||||
expect: '| | | 1'
|
||||
},
|
||||
{
|
||||
send: '{\n\na: 1\n}',
|
||||
expect: '... ... ... { a: 1 }'
|
||||
expect: '| | | { a: 1 }'
|
||||
},
|
||||
{
|
||||
send: 'url.format("http://google.com")',
|
||||
|
@ -449,7 +449,7 @@ const errorTests = [
|
|||
// Do not fail when a String is created with line continuation
|
||||
{
|
||||
send: '\'the\\\nfourth\\\neye\'',
|
||||
expect: ['... ... \'thefourtheye\'']
|
||||
expect: ['| | \'thefourtheye\'']
|
||||
},
|
||||
// Don't fail when a partial String is created and line continuation is used
|
||||
// with whitespace characters at the end of the string. We are to ignore it.
|
||||
|
@ -462,17 +462,17 @@ const errorTests = [
|
|||
// Multiline strings preserve whitespace characters in them
|
||||
{
|
||||
send: '\'the \\\n fourth\t\t\\\n eye \'',
|
||||
expect: '... ... \'the fourth\\t\\t eye \''
|
||||
expect: '| | \'the fourth\\t\\t eye \''
|
||||
},
|
||||
// More than one multiline strings also should preserve whitespace chars
|
||||
{
|
||||
send: '\'the \\\n fourth\' + \'\t\t\\\n eye \'',
|
||||
expect: '... ... \'the fourth\\t\\t eye \''
|
||||
expect: '| | \'the fourth\\t\\t eye \''
|
||||
},
|
||||
// using REPL commands within a string literal should still work
|
||||
{
|
||||
send: '\'\\\n.break',
|
||||
expect: '... ' + prompt_unix
|
||||
expect: '| ' + prompt_unix
|
||||
},
|
||||
// Using REPL command "help" within a string literal should still work
|
||||
{
|
||||
|
@ -519,7 +519,7 @@ const errorTests = [
|
|||
// Empty lines in the string literals should not affect the string
|
||||
{
|
||||
send: '\'the\\\n\\\nfourtheye\'\n',
|
||||
expect: '... ... \'thefourtheye\''
|
||||
expect: '| | \'thefourtheye\''
|
||||
},
|
||||
// Regression test for https://github.com/nodejs/node/issues/597
|
||||
{
|
||||
|
@ -536,32 +536,32 @@ const errorTests = [
|
|||
// Regression tests for https://github.com/nodejs/node/issues/2749
|
||||
{
|
||||
send: 'function x() {\nreturn \'\\n\';\n }',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x() {\nreturn \'\\\\\';\n }',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
// Regression tests for https://github.com/nodejs/node/issues/3421
|
||||
{
|
||||
send: 'function x() {\n//\'\n }',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x() {\n//"\n }',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x() {//\'\n }',
|
||||
expect: '... undefined'
|
||||
expect: '| undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x() {//"\n }',
|
||||
expect: '... undefined'
|
||||
expect: '| undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x() {\nvar i = "\'";\n }',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x(/*optional*/) {}',
|
||||
|
@ -589,7 +589,7 @@ const errorTests = [
|
|||
},
|
||||
{
|
||||
send: '/* \'\n"\n\'"\'\n*/',
|
||||
expect: '... ... ... undefined'
|
||||
expect: '| | | undefined'
|
||||
},
|
||||
// REPL should get a normal require() function, not one that allows
|
||||
// access to internal modules without the --expose-internals flag.
|
||||
|
@ -614,19 +614,19 @@ const errorTests = [
|
|||
// REPL should handle quotes within regexp literal in multiline mode
|
||||
{
|
||||
send: "function x(s) {\nreturn s.replace(/'/,'');\n}",
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: "function x(s) {\nreturn s.replace(/'/,'');\n}",
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x(s) {\nreturn s.replace(/"/,"");\n}',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: 'function x(s) {\nreturn s.replace(/.*/,"");\n}',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
{
|
||||
send: '{ var x = 4; }',
|
||||
|
@ -697,17 +697,17 @@ const errorTests = [
|
|||
// https://github.com/nodejs/node/issues/9300
|
||||
{
|
||||
send: 'function foo() {\nvar bar = 1 / 1; // "/"\n}',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
|
||||
{
|
||||
send: '(function() {\nreturn /foo/ / /bar/;\n}())',
|
||||
expect: '... ... NaN'
|
||||
expect: '| | NaN'
|
||||
},
|
||||
|
||||
{
|
||||
send: '(function() {\nif (false) {} /bar"/;\n}())',
|
||||
expect: '... ... undefined'
|
||||
expect: '| | undefined'
|
||||
},
|
||||
|
||||
// https://github.com/nodejs/node/issues/16483
|
||||
|
@ -723,7 +723,7 @@ const errorTests = [
|
|||
// Newline within template string maintains whitespace.
|
||||
{
|
||||
send: '`foo \n`',
|
||||
expect: '... \'foo \\n\''
|
||||
expect: '| \'foo \\n\''
|
||||
},
|
||||
// Whitespace is not evaluated.
|
||||
{
|
||||
|
@ -757,7 +757,7 @@ const errorTests = [
|
|||
{
|
||||
send: 'x = {\nfield\n{',
|
||||
expect: [
|
||||
'... ... {',
|
||||
'| | {',
|
||||
kArrow,
|
||||
'',
|
||||
/^Uncaught SyntaxError: /,
|
||||
|
@ -774,11 +774,11 @@ const errorTests = [
|
|||
},
|
||||
{
|
||||
send: 'if (typeof process === "object"); {',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: 'console.log("process is defined");',
|
||||
expect: '... '
|
||||
expect: '| '
|
||||
},
|
||||
{
|
||||
send: '} else {',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue