Skip to content

Commit 2741258

Browse files
committed
repl: consider removeHistoryDuplicates when saving multiline history
1 parent 96fb156 commit 2741258

4 files changed

Lines changed: 135 additions & 12 deletions

File tree

lib/internal/readline/interface.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,9 @@ class Interface extends InterfaceConstructor {
454454

455455
// If the trimmed line is empty then return the line
456456
if (StringPrototypeTrim(this.line).length === 0) return this.line;
457+
const normalizedLine = StringPrototypeReplaceAll(this.line, '\n', '\r');
457458

458-
if (this.history.length === 0 || this.history[0] !== this.line) {
459+
if (this.history.length === 0 || this.history[0] !== normalizedLine) {
459460
if (this.removeHistoryDuplicates) {
460461
// Remove older history line if identical to new one
461462
const dupIndex = ArrayPrototypeIndexOf(this.history, this.line);
@@ -936,14 +937,17 @@ class Interface extends InterfaceConstructor {
936937
[kMoveDownOrHistoryNext]() {
937938
const { cols, rows } = this.getCursorPos();
938939
const splitLine = StringPrototypeSplit(this.line, '\n');
940+
if (!this.historyIndex && rows === splitLine.length) {
941+
return;
942+
}
939943
// Go to the next history only if the cursor is in the first line of the multiline input.
940944
// Otherwise treat the "arrow down" as a movement to the next row.
941945
if (splitLine.length > 1 && rows < splitLine.length - 1) {
942946
const currentLine = splitLine[rows];
943947
const nextLine = splitLine[rows + 1];
944948
const amountToMove = (cols > nextLine.length) ?
945-
+cols + nextLine.length - 1 :
946-
+currentLine.length + 1;
949+
currentLine.length - cols + nextLine.length + 1 :
950+
currentLine.length + 1;
947951
// Go to the same position on the next line, or the end of the next line
948952
// If the current position does not exist in the next line.
949953
this[kMoveCursor](amountToMove);
@@ -963,9 +967,7 @@ class Interface extends InterfaceConstructor {
963967
[kHistoryNext]() {
964968
if (this.historyIndex >= 0) {
965969
this[kBeforeEdit](this.line, this.cursor);
966-
const isLineMultiline = StringPrototypeIndexOf(this.line, '\n') !== -1;
967-
968-
const search = isLineMultiline ? '' : this[kSubstringSearch] || '';
970+
const search = this[kSubstringSearch] || '';
969971
let index = this.historyIndex - 1;
970972
while (
971973
index >= 0 &&
@@ -987,6 +989,9 @@ class Interface extends InterfaceConstructor {
987989

988990
[kMoveUpOrHistoryPrev]() {
989991
const { cols, rows } = this.getCursorPos();
992+
if (this.historyIndex === this.history.length && rows) {
993+
return;
994+
}
990995
const splitLine = StringPrototypeSplit(this.line, '\n');
991996
// Go to the previous history only if the cursor is in the first line of the multiline input.
992997
// Otherwise treat the "arrow up" as a movement to the previous row.
@@ -1007,9 +1012,7 @@ class Interface extends InterfaceConstructor {
10071012
[kHistoryPrev]() {
10081013
if (this.historyIndex < this.history.length && this.history.length) {
10091014
this[kBeforeEdit](this.line, this.cursor);
1010-
const isLineMultiline = StringPrototypeIndexOf(this.line, '\n') !== -1;
1011-
1012-
const search = isLineMultiline ? '' : this[kSubstringSearch] || '';
1015+
const search = this[kSubstringSearch] || '';
10131016
let index = this.historyIndex + 1;
10141017
while (
10151018
index < this.history.length &&
@@ -1122,7 +1125,8 @@ class Interface extends InterfaceConstructor {
11221125
!key.meta &&
11231126
!key.shift
11241127
) {
1125-
if (this[kSubstringSearch] === null) {
1128+
const isLineMultiline = StringPrototypeIndexOf(this.line, '\n') !== -1;
1129+
if (this[kSubstringSearch] === null && !isLineMultiline) {
11261130
this[kSubstringSearch] = StringPrototypeSlice(
11271131
this.line,
11281132
0,

lib/repl.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,10 @@ function REPLServer(prompt,
966966
ArrayPrototypePop(lines);
967967
// And replace them with the single command split by '\r'
968968
ArrayPrototypePush(lines, cmd);
969-
ArrayPrototypeUnshift(self.history, ArrayPrototypeJoin(lines, '\r'));
969+
const newHistoryLine = ArrayPrototypeJoin(lines, '\r');
970+
if (self.history[0] !== newHistoryLine) {
971+
ArrayPrototypeUnshift(self.history, newHistoryLine);
972+
}
970973
}
971974

972975
if (e) {

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,29 @@ const tests = [
767767
],
768768
clean: true
769769
},
770+
{
771+
// Test that multiline history is not duplicated
772+
env: { NODE_REPL_HISTORY: defaultHistoryPath },
773+
skip: !process.features.inspector,
774+
test: [
775+
'let f = `multiline',
776+
ENTER,
777+
'string`',
778+
ENTER,
779+
UP, UP, UP,
780+
],
781+
expected: [
782+
prompt, ...'let f = `multiline',
783+
'| ',
784+
...'string`',
785+
'undefined\n',
786+
prompt,
787+
`${prompt}let f = \`multiline\nstring\``,
788+
`${prompt}let f = \`multiline\nstring\``,
789+
prompt,
790+
],
791+
clean: true
792+
},
770793
];
771794
const numtests = tests.length;
772795

@@ -814,7 +837,6 @@ function runTest() {
814837
try {
815838
assert.strictEqual(output, expected[i]);
816839
} catch (e) {
817-
console.log({ output, expected: expected[i] });
818840
console.error(`Failed test # ${numtests - tests.length}`);
819841
console.error('Last outputs: ' + inspect(lastChunks, {
820842
breakLength: 5, colors: true
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
3+
// Flags: --expose-internals
4+
5+
const common = require('../common');
6+
7+
const assert = require('assert');
8+
const repl = require('internal/repl');
9+
const stream = require('stream');
10+
const fs = require('fs');
11+
12+
class ActionStream extends stream.Stream {
13+
run(data) {
14+
const _iter = data[Symbol.iterator]();
15+
const doAction = () => {
16+
const next = _iter.next();
17+
if (next.done) {
18+
// Close the repl. Note that it must have a clean prompt to do so.
19+
this.emit('keypress', '', { ctrl: true, name: 'd' });
20+
return;
21+
}
22+
const action = next.value;
23+
24+
if (typeof action === 'object') {
25+
this.emit('keypress', '', action);
26+
}
27+
setImmediate(doAction);
28+
};
29+
doAction();
30+
}
31+
resume() {}
32+
pause() {}
33+
}
34+
ActionStream.prototype.readable = true;
35+
36+
function cleanupTmpFile() {
37+
try {
38+
// Write over the file, clearing any history
39+
fs.writeFileSync(defaultHistoryPath, '');
40+
} catch (err) {
41+
if (err.code === 'ENOENT') return true;
42+
throw err;
43+
}
44+
return true;
45+
}
46+
47+
const tmpdir = require('../common/tmpdir');
48+
tmpdir.refresh();
49+
const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
50+
51+
{
52+
cleanupTmpFile();
53+
// It moves the cursor at the right places
54+
const checkResults = common.mustSucceed((r) => {
55+
r.write('let str = `');
56+
r.input.run([{ name: 'enter' }]);
57+
r.write('111');
58+
r.input.run([{ name: 'enter' }]);
59+
r.write('22222222222222');
60+
r.input.run([{ name: 'enter' }]);
61+
r.write('3`');
62+
r.input.run([{ name: 'enter' }]);
63+
r.input.run([{ name: 'up' }]);
64+
assert.strictEqual(r.cursor, 33);
65+
66+
r.input.run([{ name: 'up' }]);
67+
assert.strictEqual(r.cursor, 18);
68+
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' }]);
74+
r.input.run([{ name: 'up' }]);
75+
assert.strictEqual(r.cursor, 15);
76+
77+
r.input.run([{ name: 'down' }]);
78+
assert.strictEqual(r.cursor, 19);
79+
});
80+
81+
repl.createInternalRepl(
82+
{ NODE_REPL_HISTORY: defaultHistoryPath },
83+
{
84+
terminal: true,
85+
input: new ActionStream(),
86+
output: new stream.Writable({
87+
write(chunk, _, next) {
88+
next();
89+
}
90+
}),
91+
},
92+
checkResults
93+
);
94+
}

0 commit comments

Comments
 (0)