mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 00:20:08 +01:00
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:
parent
5fb879c458
commit
96be7836d7
|
|
@ -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 "...".
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
37
lib/repl.js
37
lib/repl.js
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
312
test/parallel/test-repl-multiline-navigation-while-adding.js
Normal file
312
test/parallel/test-repl-multiline-navigation-while-adding.js
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user