Skip to content

Commit 210b0da

Browse files
committed
repl: add proper vertical cursor movements
1 parent ff4639e commit 210b0da

3 files changed

Lines changed: 130 additions & 14 deletions

File tree

lib/internal/readline/interface.js

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ const kSavePreviousState = Symbol('_savePreviousState');
155155
const kRestorePreviousState = Symbol('_restorePreviousState');
156156
const kPreviousLine = Symbol('_previousLine');
157157
const kPreviousCursor = Symbol('_previousCursor');
158+
const kPreviousCursorCols = Symbol('_previousCursorCols');
159+
const kResetPreviousCursorCols = Symbol('_resetPreviousCursorCols');
158160
const kPreviousPrevRows = Symbol('_previousPrevRows');
159161
const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY');
160162

@@ -245,6 +247,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
245247
this[kRedoStack] = [];
246248
this.history = history;
247249
this.historySize = historySize;
250+
this[kResetPreviousCursorCols]();
248251

249252
// The kill ring is a global list of blocks of text that were previously
250253
// killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest
@@ -1114,6 +1117,10 @@ class Interface extends InterfaceConstructor {
11141117
this[kRefreshLine]();
11151118
}
11161119

1120+
[kResetPreviousCursorCols]() {
1121+
this[kPreviousCursorCols] = -1;
1122+
}
1123+
11171124
[kMoveDownOrHistoryNext]() {
11181125
const { cols, rows } = this.getCursorPos();
11191126
const splitLine = StringPrototypeSplit(this.line, '\n');
@@ -1125,16 +1132,34 @@ class Interface extends InterfaceConstructor {
11251132
if (this[kIsMultiline] && rows < splitLine.length - 1) {
11261133
const currentLine = splitLine[rows];
11271134
const nextLine = splitLine[rows + 1];
1135+
let amountToMove = 0;
1136+
const amountToClamp = currentLine.length - cols + // Go to the end of the current line
1137+
kMultilinePrompt.description.length + // Add the prompt length
1138+
nextLine.length + 1; // Add the length of the next line + 1 to go to the end of it
11281139
// If I am moving down and the current line is longer than the next line
1129-
const amountToMove = (cols > nextLine.length + 1) ?
1130-
currentLine.length - cols + nextLine.length +
1131-
kMultilinePrompt.description.length + 1 : // Move to the end of the current line
1132-
// + chars to account for the kMultilinePrompt prefix, + 1 to go to the first char
1133-
currentLine.length + 1; // Otherwise just move to the next line, in the same position
1140+
const shouldClampColumn = (cols > nextLine.length + 1);
1141+
if (shouldClampColumn) {
1142+
if (this[kPreviousCursorCols] === -1) {
1143+
this[kPreviousCursorCols] = cols; // Save the cursor cols position to restore it later
1144+
}
1145+
amountToMove = amountToClamp;
1146+
} else {
1147+
amountToMove = currentLine.length + 1;
1148+
if (this[kPreviousCursorCols] !== -1) {
1149+
if (this[kPreviousCursorCols] <= nextLine.length) {
1150+
// If, while moving down, the cursor passed through a line shorter than where the cursor was
1151+
// I need to move it back to the original position
1152+
amountToMove += this[kPreviousCursorCols] - cols;
1153+
this[kResetPreviousCursorCols](); // Reset it to avoid moving again
1154+
} else {
1155+
amountToMove = amountToClamp; // Go to the end of next line
1156+
}
1157+
}
1158+
}
11341159
this[kMoveCursor](amountToMove);
11351160
return;
11361161
}
1137-
1162+
this[kResetPreviousCursorCols](); // Reset it to avoid moving again
11381163
this[kHistoryNext]();
11391164
}
11401165

@@ -1178,14 +1203,38 @@ class Interface extends InterfaceConstructor {
11781203
if (this[kIsMultiline] && rows > 0) {
11791204
const splitLine = StringPrototypeSplit(this.line, '\n');
11801205
const previousLine = splitLine[rows - 1];
1206+
let amountToMove = 0;
1207+
// Move to the beginning of the current line + 1 char to go to the end of the previous line
1208+
const amountToClamp = -cols + 1;
1209+
11811210
// If I am moving up and the current line is longer than the previous line
1182-
const amountToMove = (cols > previousLine.length + 1) ?
1183-
-cols + 1 : // Move to the beginning of the current line + 1 char to go to the end of the previous line
1184-
-previousLine.length - 1; // Otherwise just move to the previous line, in the same position
1211+
const shouldClampColumn = (cols > previousLine.length + 1);
1212+
if (shouldClampColumn) {
1213+
if (this[kPreviousCursorCols] === -1) {
1214+
this[kPreviousCursorCols] = cols; // Save the cursor cols position to restore it later
1215+
}
1216+
amountToMove = amountToClamp;
1217+
} else {
1218+
// When pressing up a couple of times passing through shorter lines.
1219+
// Move to the previous line, in the same position
1220+
amountToMove = -previousLine.length - 1;
1221+
if (this[kPreviousCursorCols] !== -1) {
1222+
if (this[kPreviousCursorCols] <= previousLine.length) {
1223+
// If, while moving up, the cursor passed through a line shorter than where the cursor was
1224+
// I need to move it back to the original position
1225+
amountToMove += this[kPreviousCursorCols] - cols;
1226+
this[kResetPreviousCursorCols](); // Reset it to avoid moving again
1227+
} else {
1228+
amountToMove = amountToClamp; // Go to the end of the previous line
1229+
}
1230+
}
1231+
}
1232+
11851233
this[kMoveCursor](amountToMove);
11861234
return;
11871235
}
11881236

1237+
this[kResetPreviousCursorCols](); // Reset it to avoid moving again
11891238
this[kHistoryPrev]();
11901239
}
11911240

@@ -1296,6 +1345,7 @@ class Interface extends InterfaceConstructor {
12961345
const previousKey = this[kPreviousKey];
12971346
key ||= kEmptyObject;
12981347
this[kPreviousKey] = key;
1348+
let shouldResetPreviousCursorCols = true;
12991349

13001350
if (!key.meta || key.name !== 'y') {
13011351
// Reset yanking state unless we are doing yank pop.
@@ -1543,10 +1593,12 @@ class Interface extends InterfaceConstructor {
15431593
break;
15441594

15451595
case 'up':
1596+
shouldResetPreviousCursorCols = false;
15461597
this[kMoveUpOrHistoryPrev]();
15471598
break;
15481599

15491600
case 'down':
1601+
shouldResetPreviousCursorCols = false;
15501602
this[kMoveDownOrHistoryNext]();
15511603
break;
15521604

@@ -1582,6 +1634,9 @@ class Interface extends InterfaceConstructor {
15821634
}
15831635
}
15841636
}
1637+
if (shouldResetPreviousCursorCols) {
1638+
this[kResetPreviousCursorCols]();
1639+
}
15851640
}
15861641

15871642
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ tmpdir.refresh();
5555
r.write('22222222222222'); // The command is not complete yet. I can still edit it
5656
r.input.run([{ name: 'up' }]);
5757
r.input.run([{ name: 'up' }]); // I am on the first line
58-
for (let i = 0; i < 4; i++) {
58+
for (let i = 0; i < 3; i++) {
5959
r.input.run([{ name: 'right' }]);
6060
} // I am at the end of the first line
6161
assert.strictEqual(r.cursor, 17);
@@ -101,7 +101,7 @@ tmpdir.refresh();
101101
r.write('22222222222222'); // The command is not complete yet. I can still edit it
102102
r.input.run([{ name: 'up' }]);
103103
r.input.run([{ name: 'up' }]); // I am on the first line
104-
for (let i = 0; i < 2; i++) {
104+
for (let i = 0; i < 3; i++) {
105105
r.input.run([{ name: 'left' }]);
106106
} // I am right after the string definition
107107
assert.strictEqual(r.cursor, 11);

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

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,77 @@ tmpdir.refresh();
7171
assert.strictEqual(r.cursor, 15);
7272
r.input.run([{ name: 'up' }]);
7373

74-
for (let i = 0; i < 5; i++) {
74+
for (let i = 0; i < 4; i++) {
7575
r.input.run([{ name: 'right' }]);
7676
}
77-
assert.strictEqual(r.cursor, 8);
77+
assert.strictEqual(r.cursor, 11);
7878

7979
r.input.run([{ name: 'down' }]);
8080
assert.strictEqual(r.cursor, 15);
8181

8282
r.input.run([{ name: 'down' }]);
83-
assert.strictEqual(r.cursor, 19);
83+
assert.strictEqual(r.cursor, 27);
84+
});
85+
86+
repl.createInternalRepl(
87+
{ NODE_REPL_HISTORY: historyPath },
88+
{
89+
terminal: true,
90+
input: new ActionStream(),
91+
output: new stream.Writable({
92+
write(chunk, _, next) {
93+
next();
94+
}
95+
}),
96+
},
97+
checkResults
98+
);
99+
}
100+
101+
{
102+
const historyPath = tmpdir.resolve(`.${Math.floor(Math.random() * 10000)}`);
103+
// Make sure the cursor is at the right places.
104+
// This is testing cursor clamping and restoring when moving up and down from long lines.
105+
const checkResults = common.mustSucceed((r) => {
106+
r.write('let ddd = `000');
107+
r.input.run([{ name: 'enter' }]);
108+
r.write('1111111111111');
109+
r.input.run([{ name: 'enter' }]);
110+
r.write('22222');
111+
r.input.run([{ name: 'enter' }]);
112+
r.write('2222');
113+
r.input.run([{ name: 'enter' }]);
114+
r.write('22222');
115+
r.input.run([{ name: 'enter' }]);
116+
r.write('33333333`');
117+
r.input.run([{ name: 'up' }]);
118+
assert.strictEqual(r.cursor, 45);
119+
120+
r.input.run([{ name: 'up' }]);
121+
assert.strictEqual(r.cursor, 39);
122+
123+
r.input.run([{ name: 'up' }]);
124+
assert.strictEqual(r.cursor, 34);
125+
126+
r.input.run([{ name: 'up' }]);
127+
assert.strictEqual(r.cursor, 24);
128+
129+
r.input.run([{ name: 'right' }]);
130+
// This is to reach a cursor pos which is much higher than the line we want to go to,
131+
// So we can check that the cursor is clamped to the end of the line.
132+
r.input.run([{ name: 'right' }]);
133+
134+
r.input.run([{ name: 'down' }]);
135+
assert.strictEqual(r.cursor, 34);
136+
137+
r.input.run([{ name: 'down' }]);
138+
assert.strictEqual(r.cursor, 39);
139+
140+
r.input.run([{ name: 'down' }]);
141+
assert.strictEqual(r.cursor, 45);
142+
143+
r.input.run([{ name: 'down' }]);
144+
assert.strictEqual(r.cursor, 55);
84145
});
85146

86147
repl.createInternalRepl(

0 commit comments

Comments
 (0)