Skip to content

Commit 3418b3b

Browse files
committed
repl: add pipe char for browsing multiline history
1 parent 2741258 commit 3418b3b

File tree

5 files changed

+220
-32
lines changed

5 files changed

+220
-32
lines changed

lib/internal/readline/interface.js

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,12 @@ class Interface extends InterfaceConstructor {
457457
const normalizedLine = StringPrototypeReplaceAll(this.line, '\n', '\r');
458458

459459
if (this.history.length === 0 || this.history[0] !== normalizedLine) {
460+
if (this.lastCommandErrored && this.historyIndex === 0) {
461+
// If the last command errored, remove it from history.
462+
// The user is issuing a new command starting from the errored command,
463+
// Hopefully with the fix
464+
ArrayPrototypeShift(this.history);
465+
}
460466
if (this.removeHistoryDuplicates) {
461467
// Remove older history line if identical to new one
462468
const dupIndex = ArrayPrototypeIndexOf(this.history, this.line);
@@ -504,8 +510,22 @@ class Interface extends InterfaceConstructor {
504510
// erase data
505511
clearScreenDown(this.output);
506512

507-
// Write the prompt and the current buffer content.
508-
this[kWriteToOutput](line);
513+
// Check if this is a multiline entry
514+
const lines = StringPrototypeSplit(this.line, '\n');
515+
const isMultiline = lines.length > 1;
516+
517+
if (!isMultiline) {
518+
// Write the prompt and the current buffer content.
519+
this[kWriteToOutput](line);
520+
} else {
521+
// Write first line with normal prompt
522+
this[kWriteToOutput](this[kPrompt] + lines[0]);
523+
524+
// For continuation lines, add the "|" prefix
525+
for (let i = 1; i < lines.length; i++) {
526+
this[kWriteToOutput]('\n| ' + lines[i]);
527+
}
528+
}
509529

510530
// Force terminal to allocate a new line
511531
if (lineCols === 0) {
@@ -945,8 +965,8 @@ class Interface extends InterfaceConstructor {
945965
if (splitLine.length > 1 && rows < splitLine.length - 1) {
946966
const currentLine = splitLine[rows];
947967
const nextLine = splitLine[rows + 1];
948-
const amountToMove = (cols > nextLine.length) ?
949-
currentLine.length - cols + nextLine.length + 1 :
968+
const amountToMove = (cols > nextLine.length + 1) ?
969+
currentLine.length - cols + nextLine.length + 3 :
950970
currentLine.length + 1;
951971
// Go to the same position on the next line, or the end of the next line
952972
// If the current position does not exist in the next line.
@@ -997,8 +1017,8 @@ class Interface extends InterfaceConstructor {
9971017
// Otherwise treat the "arrow up" as a movement to the previous row.
9981018
if (splitLine.length > 1 && rows > 0) {
9991019
const previousLine = splitLine[rows - 1];
1000-
const amountToMove = (cols > previousLine.length) ?
1001-
-cols - 1 :
1020+
const amountToMove = (cols > previousLine.length + 1) ?
1021+
-cols + 1 :
10021022
-previousLine.length - 1;
10031023
// Go to the same position on the previous line, or the end of the previous line
10041024
// If the current position does not exist in the previous line.
@@ -1075,9 +1095,19 @@ class Interface extends InterfaceConstructor {
10751095
* }}
10761096
*/
10771097
getCursorPos() {
1078-
const strBeforeCursor =
1079-
this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor);
1080-
return this[kGetDisplayPos](strBeforeCursor);
1098+
// Handle multiline input by accounting for "| " prefixes on continuation lines
1099+
const lineUptoCursor = StringPrototypeSlice(this.line, 0, this.cursor);
1100+
const lines = StringPrototypeSplit(lineUptoCursor, '\n');
1101+
1102+
// First line uses normal prompt, subsequent lines use "| " (2 chars)
1103+
let displayText = this[kPrompt] + lines[0];
1104+
1105+
// Add continuation lines with their prefixes
1106+
for (let i = 1; i < lines.length; i++) {
1107+
displayText += '\n| ' + lines[i];
1108+
}
1109+
1110+
return this[kGetDisplayPos](displayText);
10811111
}
10821112

10831113
// This function moves cursor dx places to the right

lib/internal/repl/utils.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,15 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
155155

156156
let escaped = null;
157157

158+
let justExecutedMultilineCommand = false;
159+
158160
function getPreviewPos() {
159161
const displayPos = repl._getDisplayPos(`${repl.getPrompt()}${repl.line}`);
162+
// If the line is a multi line
163+
if (StringPrototypeIndexOf(repl.line, '\n') !== -1) {
164+
// This is to consider the additional "| " that prefixes the line
165+
displayPos.cols += 2;
166+
}
160167
const cursorPos = repl.line.length !== repl.cursor ?
161168
repl.getCursorPos() :
162169
displayPos;
@@ -177,6 +184,11 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
177184
clearLine(repl.output);
178185
moveCursor(repl.output, 0, -rows);
179186
inputPreview = null;
187+
// If pressing enter on some history line,
188+
// The next preview should not be generated
189+
if ((key.name === 'return' || key.name === 'enter') && !key.meta && repl.historyIndex !== -1) {
190+
justExecutedMultilineCommand = true;
191+
}
180192
}
181193
if (completionPreview !== null) {
182194
// Prevent cursor moves if not necessary!
@@ -373,6 +385,11 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
373385
return;
374386
}
375387

388+
if (justExecutedMultilineCommand) {
389+
justExecutedMultilineCommand = false;
390+
return;
391+
}
392+
376393
hasCompletions = false;
377394

378395
// Add the autocompletion preview.
@@ -444,9 +461,14 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
444461

445462
const { cursorPos, displayPos } = getPreviewPos();
446463
const rows = displayPos.rows - cursorPos.rows;
464+
// Moves one line below all the user lines
447465
moveCursor(repl.output, 0, rows);
466+
// Writes the preview there
448467
repl.output.write(`\n${result}`);
468+
469+
// Go back to the horizontal position of the cursor
449470
cursorTo(repl.output, cursorPos.cols);
471+
// Go back to the vertical position of the cursor
450472
moveCursor(repl.output, 0, -rows - 1);
451473
};
452474

lib/repl.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,9 @@ function REPLServer(prompt,
974974

975975
if (e) {
976976
self._domain.emit('error', e.err || e);
977+
self.lastCommandErrored = true;
978+
} else {
979+
self.lastCommandErrored = false;
977980
}
978981

979982
// Clear buffer if no SyntaxErrors

test/parallel/test-repl-history-navigation.js

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -659,11 +659,19 @@ const tests = [
659659
...'which ends here`',
660660
"'I am a multiline strong\\nwhich ends here'\n",
661661
prompt,
662-
`${prompt}a = \`I am a multiline strong\nwhich ends here\``,
663-
`${prompt}a = \`I am a multiline strong\nwhich ends here\``,
664-
`${prompt}a = \`I am a multiline strng\nwhich ends here\``,
665-
`${prompt}a = \`I am a multiline string\nwhich ends here\``,
666-
`${prompt}a = \`I am a multiline string\nwhich ends here\``,
662+
`${prompt}a = \`I am a multiline strong`,
663+
`\n| which ends here\``,
664+
`${prompt}a = \`I am a multiline strong`,
665+
`\n| which ends here\``,
666+
667+
`${prompt}a = \`I am a multiline strng`,
668+
`\n| which ends here\``,
669+
670+
`${prompt}a = \`I am a multiline string`,
671+
`\n| which ends here\``,
672+
673+
`${prompt}a = \`I am a multiline string`,
674+
`\n| which ends here\``,
667675
"'I am a multiline string\\nwhich ends here'\n",
668676
prompt,
669677
],
@@ -707,14 +715,24 @@ const tests = [
707715
...'am another one`',
708716
'undefined\n',
709717
prompt,
710-
`${prompt}let c = \`I\nam another one\``,
711-
`${prompt}let c = \`I\nam another one\``,
712-
713-
`${prompt}b = \`I am a multiline strong\nwhich ends here\``,
714-
`${prompt}b = \`I am a multiline strong\nwhich ends here\``,
715-
`${prompt}b = \`I am a multiline strng\nwhich ends here\``,
716-
`${prompt}b = \`I am a multiline string\nwhich ends here\``,
717-
`${prompt}b = \`I am a multiline string\nwhich ends here\``,
718+
`${prompt}let c = \`I`,
719+
`\n| am another one\``,
720+
721+
`${prompt}let c = \`I`,
722+
`\n| am another one\``,
723+
724+
`${prompt}b = \`I am a multiline strong`,
725+
`\n| which ends here\``,
726+
`${prompt}b = \`I am a multiline strong`,
727+
`\n| which ends here\``,
728+
`${prompt}b = \`I am a multiline strng`,
729+
`\n| which ends here\``,
730+
731+
`${prompt}b = \`I am a multiline string`,
732+
`\n| which ends here\``,
733+
734+
`${prompt}b = \`I am a multiline string`,
735+
`\n| which ends here\``,
718736
"'I am a multiline string\\nwhich ends here'\n",
719737
prompt,
720738
],
@@ -758,10 +776,18 @@ const tests = [
758776
` [message]: "Unexpected identifier 'line'"\n` +
759777
'}\n',
760778
prompt,
761-
`${prompt}d = \`I am a\nsuper\nbroken\` line'`,
762-
`${prompt}d = \`I am a\nsuper\nbroken\` line`,
779+
`${prompt}d = \`I am a`,
780+
`\n| super`,
781+
`\n| broken\` line'`,
782+
783+
`${prompt}d = \`I am a`,
784+
`\n| super`,
785+
'\n| broken` line',
763786
'`',
764-
`${prompt}d = \`I am a\nsuper\nbroken line\``,
787+
788+
`${prompt}d = \`I am a`,
789+
`\n| super`,
790+
`\n| broken line\``,
765791
"'I am a\\nsuper\\nbroken line'\n",
766792
prompt,
767793
],
@@ -784,8 +810,10 @@ const tests = [
784810
...'string`',
785811
'undefined\n',
786812
prompt,
787-
`${prompt}let f = \`multiline\nstring\``,
788-
`${prompt}let f = \`multiline\nstring\``,
813+
`${prompt}let f = \`multiline`,
814+
`\n| string\``,
815+
`${prompt}let f = \`multiline`,
816+
`\n| string\``,
789817
prompt,
790818
],
791819
clean: true
@@ -837,6 +865,7 @@ function runTest() {
837865
try {
838866
assert.strictEqual(output, expected[i]);
839867
} catch (e) {
868+
console.log({ output, expected: expected[i] });
840869
console.error(`Failed test # ${numtests - tests.length}`);
841870
console.error('Last outputs: ' + inspect(lastChunks, {
842871
breakLength: 5, colors: true

test/parallel/test-repl-multiline-navigation.js

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ class ActionStream extends stream.Stream {
2828
};
2929
doAction();
3030
}
31+
write(chunk) {
32+
const chunkLines = chunk.toString('utf8').split('\n');
33+
this.lines[this.lines.length - 1] += chunkLines[0];
34+
if (chunkLines.length > 1) {
35+
this.lines.push(...chunkLines.slice(1));
36+
}
37+
this.emit('line', this.lines[this.lines.length - 1]);
38+
return true;
39+
}
3140
resume() {}
3241
pause() {}
3342
}
@@ -66,12 +75,19 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
6675
r.input.run([{ name: 'up' }]);
6776
assert.strictEqual(r.cursor, 18);
6877

69-
r.input.run([{ name: 'right' }]);
70-
r.input.run([{ name: 'right' }]);
71-
r.input.run([{ name: 'right' }]);
72-
r.input.run([{ name: 'right' }]);
73-
r.input.run([{ name: 'right' }]);
78+
for (let i = 0; i < 5; i++) {
79+
r.input.run([{ name: 'right' }]);
80+
}
81+
r.input.run([{ name: 'up' }]);
82+
assert.strictEqual(r.cursor, 15);
7483
r.input.run([{ name: 'up' }]);
84+
85+
for (let i = 0; i < 5; i++) {
86+
r.input.run([{ name: 'right' }]);
87+
}
88+
assert.strictEqual(r.cursor, 8);
89+
90+
r.input.run([{ name: 'down' }]);
7591
assert.strictEqual(r.cursor, 15);
7692

7793
r.input.run([{ name: 'down' }]);
@@ -92,3 +108,91 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
92108
checkResults
93109
);
94110
}
111+
112+
{
113+
cleanupTmpFile();
114+
// If the last command errored and the user is trying to edit it,
115+
// The errored line should be removed from history
116+
const checkResults = common.mustSucceed((r) => {
117+
r.write('let lineWithMistake = `I have some');
118+
r.input.run([{ name: 'enter' }]);
119+
r.write('problem with` my syntax\'');
120+
r.input.run([{ name: 'enter' }]);
121+
r.input.run([{ name: 'up' }]);
122+
r.input.run([{ name: 'backspace' }]);
123+
r.write('`');
124+
for (let i = 0; i < 11; i++) {
125+
r.input.run([{ name: 'left' }]);
126+
}
127+
r.input.run([{ name: 'backspace' }]);
128+
r.input.run([{ name: 'enter' }]);
129+
130+
assert.strictEqual(r.history.length, 1);
131+
assert.strictEqual(r.history[0], 'let lineWithMistake = `I have some\rproblem with my syntax`');
132+
assert.strictEqual(r.line, '');
133+
});
134+
135+
repl.createInternalRepl(
136+
{ NODE_REPL_HISTORY: defaultHistoryPath },
137+
{
138+
terminal: true,
139+
input: new ActionStream(),
140+
output: new stream.Writable({
141+
write(chunk, _, next) {
142+
next();
143+
}
144+
}),
145+
},
146+
checkResults
147+
);
148+
}
149+
150+
{
151+
cleanupTmpFile();
152+
const outputBuffer = [];
153+
154+
// Test that the REPL preview is properly shown on multiline commands
155+
// And deleted when enter is pressed
156+
const checkResults = common.mustSucceed((r) => {
157+
r.write('Array(100).fill(');
158+
r.input.run([{ name: 'enter' }]);
159+
r.write('123');
160+
r.input.run([{ name: 'enter' }]);
161+
r.write(')');
162+
r.input.run([{ name: 'enter' }]);
163+
r.input.run([{ name: 'up' }]);
164+
r.input.run([{ name: 'up' }]);
165+
166+
assert.deepStrictEqual(r.last, new Array(100).fill(123));
167+
r.input.run([{ name: 'enter' }]);
168+
assert.strictEqual(outputBuffer.includes('[\n' +
169+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
170+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
171+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
172+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
173+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
174+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
175+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
176+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
177+
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
178+
' 123\n' +
179+
']\n'), true);
180+
});
181+
182+
repl.createInternalRepl(
183+
{ NODE_REPL_HISTORY: defaultHistoryPath },
184+
{
185+
preview: true,
186+
terminal: true,
187+
input: new ActionStream(),
188+
output: new stream.Writable({
189+
write(chunk, _, next) {
190+
// Store each chunk in the buffer
191+
outputBuffer.push(chunk.toString());
192+
next();
193+
}
194+
}),
195+
},
196+
checkResults
197+
);
198+
}

0 commit comments

Comments
 (0)