Skip to content

Commit 39b827c

Browse files
committed
repl: fix multiline history editing string order
1 parent f89baf2 commit 39b827c

File tree

2 files changed

+51
-18
lines changed

2 files changed

+51
-18
lines changed

lib/internal/readline/interface.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,15 @@ class Interface extends InterfaceConstructor {
467467
}
468468

469469
// Convert newlines to a consistent format for history storage
470-
[kNormalizeHistoryLineEndings](line, from, to) {
470+
[kNormalizeHistoryLineEndings](line, from, to, reverse = true) {
471471
// Multiline history entries are saved reversed
472-
if (StringPrototypeIncludes(line, '\r')) {
472+
// History is structured with the newest entries at the top
473+
// and the oldest at the bottom. Multiline histories, however, only occupy
474+
// one line in the history file. When loading multiline history with
475+
// an old node binary, the history will be saved in the old format.
476+
// This is why we need to reverse the multilines.
477+
// Reversing the multilines is necessary when adding / editing and displaying them
478+
if (reverse) {
473479
// First reverse the lines for proper order, then convert separators
474480
return ArrayPrototypeJoin(
475481
ArrayPrototypeReverse(StringPrototypeSplit(line, from)),
@@ -488,7 +494,7 @@ class Interface extends InterfaceConstructor {
488494

489495
// If the trimmed line is empty then return the line
490496
if (StringPrototypeTrim(this.line).length === 0) return this.line;
491-
const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r');
497+
const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', false);
492498

493499
if (this.history.length === 0 || this.history[0] !== normalizedLine) {
494500
if (this[kLastCommandErrored] && this.historyIndex === 0) {

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

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const assert = require('assert');
88
const repl = require('internal/repl');
99
const stream = require('stream');
1010
const fs = require('fs');
11+
const { setTimeout } = require('timers/promises');
1112

1213
class ActionStream extends stream.Stream {
1314
run(data) {
@@ -53,6 +54,23 @@ function cleanupTmpFile() {
5354
return true;
5455
}
5556

57+
// Helper function to wait for a condition with a retry mechanism
58+
// This should fix flakiness in the REPL tests, due to its nature
59+
const kMaxRetries = 3;
60+
async function pollForCondition(assertionFn) {
61+
for (let retries = 0; retries <= kMaxRetries; retries++) {
62+
try {
63+
assertionFn();
64+
return;
65+
} catch (err) {
66+
if (retries === kMaxRetries) {
67+
throw err;
68+
}
69+
await setTimeout(50);
70+
}
71+
}
72+
}
73+
5674
const tmpdir = require('../common/tmpdir');
5775
tmpdir.refresh();
5876
const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
@@ -62,7 +80,7 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
6280
// Make sure the cursor is at the right places.
6381
// If the cursor is at the end of a long line and the down key is pressed,
6482
// Move the cursor to the end of the next line, if shorter.
65-
const checkResults = common.mustSucceed((r) => {
83+
const checkResults = common.mustSucceed(async (r) => {
6684
r.write('let str = `');
6785
r.input.run([{ name: 'enter' }]);
6886
r.write('111');
@@ -72,28 +90,28 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
7290
r.write('3`');
7391
r.input.run([{ name: 'enter' }]);
7492
r.input.run([{ name: 'up' }]);
75-
assert.strictEqual(r.cursor, 33);
93+
await pollForCondition(() => assert.strictEqual(r.cursor, 33));
7694

7795
r.input.run([{ name: 'up' }]);
78-
assert.strictEqual(r.cursor, 18);
96+
await pollForCondition(() => assert.strictEqual(r.cursor, 18));
7997

8098
for (let i = 0; i < 5; i++) {
8199
r.input.run([{ name: 'right' }]);
82100
}
83101
r.input.run([{ name: 'up' }]);
84-
assert.strictEqual(r.cursor, 15);
102+
await pollForCondition(() => assert.strictEqual(r.cursor, 15));
85103
r.input.run([{ name: 'up' }]);
86104

87105
for (let i = 0; i < 5; i++) {
88106
r.input.run([{ name: 'right' }]);
89107
}
90-
assert.strictEqual(r.cursor, 8);
108+
await pollForCondition(() => assert.strictEqual(r.cursor, 8));
91109

92110
r.input.run([{ name: 'down' }]);
93-
assert.strictEqual(r.cursor, 15);
111+
await pollForCondition(() => assert.strictEqual(r.cursor, 15));
94112

95113
r.input.run([{ name: 'down' }]);
96-
assert.strictEqual(r.cursor, 19);
114+
await pollForCondition(() => assert.strictEqual(r.cursor, 19));
97115
});
98116

99117
repl.createInternalRepl(
@@ -115,7 +133,7 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
115133
cleanupTmpFile();
116134
// If the last command errored and the user is trying to edit it,
117135
// The errored line should be removed from history
118-
const checkResults = common.mustSucceed((r) => {
136+
const checkResults = common.mustSucceed(async (r) => {
119137
r.write('let lineWithMistake = `I have some');
120138
r.input.run([{ name: 'enter' }]);
121139
r.write('problem with` my syntax\'');
@@ -129,9 +147,18 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
129147
r.input.run([{ name: 'backspace' }]);
130148
r.input.run([{ name: 'enter' }]);
131149

132-
assert.strictEqual(r.history.length, 1);
133-
assert.strictEqual(r.history[0], 'let lineWithMistake = `I have some\rproblem with my syntax`');
134-
assert.strictEqual(r.line, '');
150+
await pollForCondition(() => assert.strictEqual(r.history.length, 1));
151+
// Check that the line is properly set in the history structure
152+
await pollForCondition(() => assert.strictEqual(r.history[0],
153+
'problem with my syntax`\rlet lineWithMistake = `I have some')
154+
);
155+
await pollForCondition(() => assert.strictEqual(r.line, ''));
156+
157+
r.input.run([{ name: 'up' }]);
158+
// Check that the line is properly displayed
159+
await pollForCondition(() => assert.strictEqual(r.line,
160+
'let lineWithMistake = `I have some\nproblem with my syntax`')
161+
);
135162
});
136163

137164
repl.createInternalRepl(
@@ -155,7 +182,7 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
155182

156183
// Test that the REPL preview is properly shown on multiline commands
157184
// And deleted when enter is pressed
158-
const checkResults = common.mustSucceed((r) => {
185+
const checkResults = common.mustSucceed(async (r) => {
159186
r.write('Array(100).fill(');
160187
r.input.run([{ name: 'enter' }]);
161188
r.write('123');
@@ -165,9 +192,9 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
165192
r.input.run([{ name: 'up' }]);
166193
r.input.run([{ name: 'up' }]);
167194

168-
assert.deepStrictEqual(r.last, new Array(100).fill(123));
195+
await pollForCondition(() => assert.deepStrictEqual(r.last, new Array(100).fill(123)));
169196
r.input.run([{ name: 'enter' }]);
170-
assert.strictEqual(outputBuffer.includes('[\n' +
197+
await pollForCondition(() => assert.strictEqual(outputBuffer.includes('[\n' +
171198
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
172199
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
173200
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
@@ -178,7 +205,7 @@ const defaultHistoryPath = tmpdir.resolve('.node_repl_history');
178205
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
179206
' 123, 123, 123, 123, 123, 123, 123, 123, 123, 123, 123,\n' +
180207
' 123\n' +
181-
']\n'), true);
208+
']\n'), true));
182209
});
183210

184211
repl.createInternalRepl(

0 commit comments

Comments
 (0)