Skip to content

Commit 306ee84

Browse files
committed
repl: replace ... with | and navigate multiline history with up / down
1 parent 8b261ae commit 306ee84

9 files changed

Lines changed: 168 additions & 64 deletions

lib/internal/readline/interface.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ const {
2424
SafeStringIterator,
2525
StringPrototypeCodePointAt,
2626
StringPrototypeEndsWith,
27+
StringPrototypeIndexOf,
2728
StringPrototypeRepeat,
2829
StringPrototypeReplaceAll,
2930
StringPrototypeSlice,
31+
StringPrototypeSplit,
3032
StringPrototypeStartsWith,
3133
StringPrototypeTrim,
3234
Symbol,
@@ -100,7 +102,9 @@ const kDeleteWordLeft = Symbol('_deleteWordLeft');
100102
const kDeleteWordRight = Symbol('_deleteWordRight');
101103
const kGetDisplayPos = Symbol('_getDisplayPos');
102104
const kHistoryNext = Symbol('_historyNext');
105+
const kMoveDownOrHistoryNext = Symbol('_moveDownOrHistoryNext');
103106
const kHistoryPrev = Symbol('_historyPrev');
107+
const kMoveUpOrHistoryPrev = Symbol('_moveUpOrHistoryPrev');
104108
const kInsertString = Symbol('_insertString');
105109
const kLine = Symbol('_line');
106110
const kLine_buffer = Symbol('_line_buffer');
@@ -929,6 +933,26 @@ class Interface extends InterfaceConstructor {
929933
this[kRefreshLine]();
930934
}
931935

936+
[kMoveDownOrHistoryNext]() {
937+
const { cols, rows } = this.getCursorPos();
938+
const splitLine = StringPrototypeSplit(this.line, '\n');
939+
// Go to the next history only if the cursor is in the first line of the multiline input.
940+
// Otherwise treat the "arrow down" as a movement to the next row.
941+
if (splitLine.length > 1 && rows < splitLine.length - 1) {
942+
const currentLine = splitLine[rows];
943+
const nextLine = splitLine[rows + 1];
944+
const amountToMove = (cols > nextLine.length) ?
945+
+cols + nextLine.length - 1 :
946+
+currentLine.length + 1;
947+
// Go to the same position on the next line, or the end of the next line
948+
// If the current position does not exist in the next line.
949+
this[kMoveCursor](amountToMove);
950+
return;
951+
}
952+
953+
this[kHistoryNext]();
954+
}
955+
932956
// TODO(BridgeAR): Add underscores to the search part and a red background in
933957
// case no match is found. This should only be the visual part and not the
934958
// actual line content!
@@ -939,7 +963,9 @@ class Interface extends InterfaceConstructor {
939963
[kHistoryNext]() {
940964
if (this.historyIndex >= 0) {
941965
this[kBeforeEdit](this.line, this.cursor);
942-
const search = this[kSubstringSearch] || '';
966+
const isLineMultiline = StringPrototypeIndexOf(this.line, '\n') !== -1;
967+
968+
const search = isLineMultiline ? '' : this[kSubstringSearch] || '';
943969
let index = this.historyIndex - 1;
944970
while (
945971
index >= 0 &&
@@ -959,10 +985,31 @@ class Interface extends InterfaceConstructor {
959985
}
960986
}
961987

988+
[kMoveUpOrHistoryPrev]() {
989+
const { cols, rows } = this.getCursorPos();
990+
const splitLine = StringPrototypeSplit(this.line, '\n');
991+
// Go to the previous history only if the cursor is in the first line of the multiline input.
992+
// Otherwise treat the "arrow up" as a movement to the previous row.
993+
if (splitLine.length > 1 && rows > 0) {
994+
const previousLine = splitLine[rows - 1];
995+
const amountToMove = (cols > previousLine.length) ?
996+
-cols - 1 :
997+
-previousLine.length - 1;
998+
// Go to the same position on the previous line, or the end of the previous line
999+
// If the current position does not exist in the previous line.
1000+
this[kMoveCursor](amountToMove);
1001+
return;
1002+
}
1003+
1004+
this[kHistoryPrev]();
1005+
}
1006+
9621007
[kHistoryPrev]() {
9631008
if (this.historyIndex < this.history.length && this.history.length) {
9641009
this[kBeforeEdit](this.line, this.cursor);
965-
const search = this[kSubstringSearch] || '';
1010+
const isLineMultiline = StringPrototypeIndexOf(this.line, '\n') !== -1;
1011+
1012+
const search = isLineMultiline ? '' : this[kSubstringSearch] || '';
9661013
let index = this.historyIndex + 1;
9671014
while (
9681015
index < this.history.length &&
@@ -1309,11 +1356,11 @@ class Interface extends InterfaceConstructor {
13091356
break;
13101357

13111358
case 'up':
1312-
this[kHistoryPrev]();
1359+
this[kMoveUpOrHistoryPrev]();
13131360
break;
13141361

13151362
case 'down':
1316-
this[kHistoryNext]();
1363+
this[kMoveDownOrHistoryNext]();
13171364
break;
13181365

13191366
case 'tab':

lib/repl.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1207,7 +1207,7 @@ REPLServer.prototype.resetContext = function() {
12071207
REPLServer.prototype.displayPrompt = function(preserveCursor) {
12081208
let prompt = this._initialPrompt;
12091209
if (this[kBufferedCommandSymbol].length) {
1210-
prompt = '...';
1210+
prompt = '|';
12111211
const len = this.lines.level.length ? this.lines.level.length - 1 : 0;
12121212
const levelInd = StringPrototypeRepeat('..', len);
12131213
prompt += levelInd + ' ';

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ const tests = [
655655
prompt, ...'let a = ``',
656656
'undefined\n',
657657
prompt, ...'a = `I am a multiline strong',
658-
'... ',
658+
'| ',
659659
...'which ends here`',
660660
"'I am a multiline strong\\nwhich ends here'\n",
661661
prompt,
@@ -669,6 +669,57 @@ const tests = [
669669
],
670670
clean: true
671671
},
672+
{
673+
// Test that the previous multiline history can only be accessed going through the entirety of the current
674+
// One navigating its all lines first.
675+
env: { NODE_REPL_HISTORY: defaultHistoryPath },
676+
skip: !process.features.inspector,
677+
test: [
678+
'let b = ``',
679+
ENTER,
680+
'b = `I am a multiline strong',
681+
ENTER,
682+
'which ends here`',
683+
ENTER,
684+
'let c = `I',
685+
ENTER,
686+
'am another one`',
687+
ENTER,
688+
UP,
689+
UP,
690+
UP,
691+
UP,
692+
// press RIGHT 10 times to reach the typo
693+
...Array(10).fill(RIGHT),
694+
BACKSPACE,
695+
'i',
696+
ENTER,
697+
],
698+
expected: [
699+
prompt, ...'let b = ``',
700+
'undefined\n',
701+
prompt, ...'b = `I am a multiline strong',
702+
'| ',
703+
...'which ends here`',
704+
"'I am a multiline strong\\nwhich ends here'\n",
705+
prompt, ...'let c = `I',
706+
'| ',
707+
...'am another one`',
708+
'undefined\n',
709+
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+
"'I am a multiline string\\nwhich ends here'\n",
719+
prompt,
720+
],
721+
clean: true
722+
},
672723
];
673724
const numtests = tests.length;
674725

test/parallel/test-repl-load-multiline-from-history.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,19 @@ ActionStream.prototype.readable = true;
6363
']'
6464
);
6565

66-
r.input.run([{ name: 'up' }]);
66+
for (let i = 0; i < r.line.split('\n').length; i++) {
67+
r.input.run([{ name: 'up' }]);
68+
}
6769
assert.strictEqual(r.line, 'const c = [\n {\n a: 1,\n b: 2,\n }\n]');
6870

69-
r.input.run([{ name: 'up' }]);
71+
for (let i = 0; i < r.line.split('\n').length; i++) {
72+
r.input.run([{ name: 'up' }]);
73+
}
7074
assert.strictEqual(r.line, '`const b = [\n 1,\n 2,\n 3,\n 4,\n]`');
7175

72-
r.input.run([{ name: 'up' }]);
76+
for (let i = 0; i < r.line.split('\n').length; i++) {
77+
r.input.run([{ name: 'up' }]);
78+
}
7379
assert.strictEqual(r.line, 'a = `\nI am a multiline string\nI can be as long as I want`');
7480

7581
});

test/parallel/test-repl-multiline.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function run({ useColors }) {
2626
// Validate the output, which contains terminal escape codes.
2727
assert.strictEqual(actual.length, 6);
2828
assert.ok(actual[0].endsWith(input[0]));
29-
assert.ok(actual[1].includes('... '));
29+
assert.ok(actual[1].includes('| '));
3030
assert.ok(actual[1].endsWith(input[1]));
3131
assert.ok(actual[2].includes('undefined'));
3232
assert.ok(actual[3].endsWith(input[2]));

test/parallel/test-repl-recoverable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function customEval(code, context, file, cb) {
1818
const putIn = new ArrayStream();
1919

2020
putIn.write = function(msg) {
21-
if (msg === '... ') {
21+
if (msg === '| ') {
2222
recovered = true;
2323
}
2424

test/parallel/test-repl-top-level-await.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ async function ordinaryTests() {
9696
['const m = foo(await koo());'],
9797
['m', '4'],
9898
['const n = foo(await\nkoo());',
99-
['const n = foo(await\r', '... koo());\r', 'undefined']],
99+
['const n = foo(await\r', '| koo());\r', 'undefined']],
100100
['n', '4'],
101101
// eslint-disable-next-line no-template-curly-in-string
102102
['`status: ${(await Promise.resolve({ status: 200 })).status}`',
@@ -145,29 +145,29 @@ async function ordinaryTests() {
145145
],
146146
['for (const x of [1,2,3]) {\nawait x\n}', [
147147
'for (const x of [1,2,3]) {\r',
148-
'... await x\r',
149-
'... }\r',
148+
'| await x\r',
149+
'| }\r',
150150
'undefined',
151151
]],
152152
['for (const x of [1,2,3]) {\nawait x;\n}', [
153153
'for (const x of [1,2,3]) {\r',
154-
'... await x;\r',
155-
'... }\r',
154+
'| await x;\r',
155+
'| }\r',
156156
'undefined',
157157
]],
158158
['for await (const x of [1,2,3]) {\nconsole.log(x)\n}', [
159159
'for await (const x of [1,2,3]) {\r',
160-
'... console.log(x)\r',
161-
'... }\r',
160+
'| console.log(x)\r',
161+
'| }\r',
162162
'1',
163163
'2',
164164
'3',
165165
'undefined',
166166
]],
167167
['for await (const x of [1,2,3]) {\nconsole.log(x);\n}', [
168168
'for await (const x of [1,2,3]) {\r',
169-
'... console.log(x);\r',
170-
'... }\r',
169+
'| console.log(x);\r',
170+
'| }\r',
171171
'1',
172172
'2',
173173
'3',
@@ -192,7 +192,7 @@ async function ordinaryTests() {
192192
} else if ('line' in options) {
193193
assert.strictEqual(lines[toBeRun.length + options.line], expected);
194194
} else {
195-
const echoed = toBeRun.map((a, i) => `${i > 0 ? '... ' : ''}${a}\r`);
195+
const echoed = toBeRun.map((a, i) => `${i > 0 ? '| ' : ''}${a}\r`);
196196
assert.deepStrictEqual(lines, [...echoed, expected, PROMPT]);
197197
}
198198
}

test/parallel/test-repl-unexpected-token-recoverable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const child = spawn(process.execPath, args);
1212

1313
const input = 'const foo = "bar\\\nbaz"';
1414
// Match '...' as well since it marks a multi-line statement
15-
const expectOut = /> \.\.\. undefined\n/;
15+
const expectOut = /> \| undefined\n/;
1616

1717
child.stderr.setEncoding('utf8');
1818
child.stderr.on('data', (d) => {

0 commit comments

Comments
 (0)