Skip to content

Commit 4b79ce3

Browse files
committed
readline: fix remembered column in multiline
When moving the cursor vertically past a line too short to hold the current column, the column is remembered and restored on a later, longer line. The remembered column is a visual column that includes the continuation prompt width, but it was compared against the raw target line length, so columns in the (length, length + promptLen) range wrongly clamped to the end of the line instead of being restored. Signed-off-by: Daijiro Wachi <daijiro.wachi@gmail.com>
1 parent c612f35 commit 4b79ce3

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

lib/internal/readline/interface.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,11 @@ class Interface extends InterfaceConstructor {
11151115
amountToMove = -adj.length - 1;
11161116
}
11171117
if (this[kPreviousCursorCols] !== -1) {
1118-
if (this[kPreviousCursorCols] <= adj.length) {
1118+
// kPreviousCursorCols and cols are visual columns that include the
1119+
// continuation prompt width, while adj.length is the raw length of the
1120+
// target line. The remembered column is reachable on the target line
1121+
// when prevCols - promptLen <= adj.length.
1122+
if (this[kPreviousCursorCols] <= adj.length + promptLen) {
11191123
amountToMove += this[kPreviousCursorCols] - cols;
11201124
this[kPreviousCursorCols] = -1;
11211125
} else {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
if (process.env.TERM === 'dumb') {
5+
common.skip('skipping - dumb terminal');
6+
}
7+
8+
// Regression test: when moving the cursor vertically through a line that is too
9+
// short to hold the current column, readline "remembers" the column and should
10+
// restore it once a subsequent line is long enough. The remembered column is a
11+
// visual column that includes the continuation-prompt width, so it must be
12+
// compared against the target line length plus that prompt width. Previously
13+
// the comparison omitted the prompt width, so the cursor incorrectly clamped to
14+
// the end of the line for columns in the (line length, line length + prompt]
15+
// range.
16+
17+
const { PassThrough } = require('stream');
18+
const readline = require('readline');
19+
const assert = require('assert');
20+
21+
const input = new PassThrough();
22+
const output = new PassThrough();
23+
output.columns = 80;
24+
output.isTTY = true;
25+
26+
// The history entry uses '\r' as the line separator; it is displayed as three
27+
// lines: "aaaaa" / "bb" / "cccccc".
28+
const rl = readline.createInterface({
29+
input,
30+
output,
31+
terminal: true,
32+
prompt: '> ',
33+
history: ['cccccc\rbb\raaaaa'],
34+
});
35+
36+
// Load the multiline history entry.
37+
rl.write(null, { name: 'up' });
38+
assert.strictEqual(rl.line, 'aaaaa\nbb\ncccccc');
39+
40+
// Place the cursor at visual column 6 on the bottom line ("cccccc"), which is
41+
// 4 characters into that line.
42+
rl.cursor = 13;
43+
assert.deepStrictEqual(rl.getCursorPos(), { cols: 6, rows: 2 });
44+
45+
// Move up onto "bb". It is too short for column 6, so the cursor clamps to the
46+
// end of "bb" and column 6 is remembered.
47+
rl.write(null, { name: 'up' });
48+
assert.deepStrictEqual(rl.getCursorPos(), { cols: 4, rows: 1 });
49+
50+
// Move up onto "aaaaa". Column 6 is reachable here (its columns span 2..7), so
51+
// the remembered column must be restored instead of clamping to the end.
52+
rl.write(null, { name: 'up' });
53+
assert.strictEqual(rl.cursor, 4);
54+
assert.deepStrictEqual(rl.getCursorPos(), { cols: 6, rows: 0 });
55+
56+
rl.close();

0 commit comments

Comments
 (0)