Skip to content

Commit 7d779cf

Browse files
authored
Fix UpArrow when the cursor is at the end of a wrapped line in a multiple-line text (#1028)
1 parent 9c25008 commit 7d779cf

File tree

2 files changed

+135
-14
lines changed

2 files changed

+135
-14
lines changed

PSReadLine/Movement.cs

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,39 +76,88 @@ public static void BackwardChar(ConsoleKeyInfo? key = null, object arg = null)
7676
}
7777
}
7878

79-
private void MoveToLine(int numericArg)
79+
private void MoveToLine(int lineOffset)
8080
{
81+
// Behavior description:
82+
// - If the cursor is at the end of a logical line, then 'UpArrow' (or 'DownArrow') moves the cursor up (or down)
83+
// 'lineOffset' numbers of logical lines, and the cursor is always put at the end of the new logical line.
84+
// - If the cursor is NOT at the end of a logical line, then 'UpArrow' (or 'DownArrow') moves the cursor up (or down)
85+
// 'lineOffset' numbers of physical lines, and the cursor is always placed at the same column as is now, or at the
86+
// end of line if that physical line is shorter than the targeted column.
87+
8188
const int endOfLine = int.MaxValue;
8289

90+
Point? point = null;
8391
_moveToLineCommandCount += 1;
84-
var point = ConvertOffsetToPoint(_current);
92+
8593
if (_moveToLineCommandCount == 1)
8694
{
95+
point = ConvertOffsetToPoint(_current);
8796
_moveToLineDesiredColumn =
8897
(_current == _buffer.Length || _buffer[_current] == '\n')
8998
? endOfLine
90-
: point.X;
99+
: point.Value.X;
91100
}
92101

93-
var topLine = _initialY;
94-
95-
var newY = point.Y + numericArg;
96-
point.Y = Math.Max(newY, topLine);
97-
if (_moveToLineDesiredColumn != endOfLine)
102+
// Nothing needs to be done when:
103+
// - actually not moving the line, or
104+
// - moving the line down when it's at the end of the last line.
105+
if (lineOffset == 0 || (lineOffset > 0 && _current == _buffer.Length))
98106
{
99-
point.X = _moveToLineDesiredColumn;
107+
return;
100108
}
101109

102-
var newCurrent = ConvertLineAndColumnToOffset(point);
103-
if (newCurrent != -1)
110+
int newCurrent;
111+
if (_moveToLineDesiredColumn == endOfLine)
104112
{
105-
if (_moveToLineDesiredColumn == endOfLine)
113+
newCurrent = _current;
114+
115+
if (lineOffset > 0)
106116
{
107-
while (newCurrent < _buffer.Length && _buffer[newCurrent] != '\n')
117+
// Moving to the end of a subsequent logical line.
118+
for (int i = 0; i < lineOffset; i++)
108119
{
109-
newCurrent += 1;
120+
for (newCurrent++; newCurrent < _buffer.Length && _buffer[newCurrent] != '\n'; newCurrent++) ;
121+
122+
if (newCurrent == _buffer.Length)
123+
{
124+
break;
125+
}
110126
}
111127
}
128+
else
129+
{
130+
// Moving to the end of a previous logical line.
131+
int lastEndOfLineIndex = _current;
132+
for (int i = 0; i < -lineOffset; i++)
133+
{
134+
for (newCurrent--; newCurrent >= 0 && _buffer[newCurrent] != '\n'; newCurrent--) ;
135+
136+
if (newCurrent < 0)
137+
{
138+
newCurrent = lastEndOfLineIndex;
139+
break;
140+
}
141+
142+
lastEndOfLineIndex = newCurrent;
143+
}
144+
}
145+
}
146+
else
147+
{
148+
point = point ?? ConvertOffsetToPoint(_current);
149+
int newY = point.Value.Y + lineOffset;
150+
151+
Point newPoint = new Point() {
152+
X = _moveToLineDesiredColumn,
153+
Y = Math.Max(newY, _initialY)
154+
};
155+
156+
newCurrent = ConvertLineAndColumnToOffset(newPoint);
157+
}
158+
159+
if (newCurrent != -1)
160+
{
112161
MoveCursor(newCurrent);
113162
}
114163
}

test/MovementTest.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,78 @@ public void EndOfLine()
3131
));
3232
}
3333

34+
[SkippableFact]
35+
public void MultilineCursorMovement_WithWrappedLines()
36+
{
37+
TestSetup(KeyMode.Cmd);
38+
39+
int continutationPromptLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
40+
string line_0 = "4444";
41+
string line_1 = "33";
42+
string line_2 = "666666";
43+
string line_3 = "777";
44+
45+
int wrappedLength_1 = 9;
46+
int wrappedLength_2 = 2;
47+
string wrappedLine_1 = new string('8', _console.BufferWidth - continutationPromptLength + wrappedLength_1); // Take 2 physical lines
48+
string wrappedLine_2 = new string('6', _console.BufferWidth - continutationPromptLength + wrappedLength_2); // Take 2 physical lines
49+
50+
Test("", Keys(
51+
"", _.Shift_Enter, // physical line 0
52+
line_0, _.Shift_Enter, // physical line 1
53+
line_1, _.Shift_Enter, // physical line 2
54+
line_2, _.Shift_Enter, // physical line 3
55+
wrappedLine_1, _.Shift_Enter, // physical line 4,5
56+
wrappedLine_2, _.Shift_Enter, // physical line 6,7
57+
line_3, // physical line 8
58+
59+
// Starting at the end of the last line.
60+
// Verify that UpArrow goes to the end of the previous logical line.
61+
CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)),
62+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)),
63+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)),
64+
// Press Up/Down/Up
65+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)),
66+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)),
67+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)),
68+
// Press Up/Down/Up
69+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_1, 5)),
70+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)),
71+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_1, 5)),
72+
// Press Up/Down/Up
73+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_2.Length, 3)),
74+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_1, 5)),
75+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_2.Length, 3)),
76+
// Press Up/Up
77+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_1.Length, 2)),
78+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length, 1)),
79+
80+
// Move to left for 1 character, so the cursor now is not at the end of line.
81+
// Verify that DownArrow/UpArrow goes to the previous logical line at the same column.
82+
_.LeftArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 1)),
83+
// Press Down all the way to the end
84+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_1.Length, 2)),
85+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 3)),
86+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 4)),
87+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 5)),
88+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 6)),
89+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)),
90+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)),
91+
_.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)),
92+
// Press Up all the way to the physical line 1
93+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)),
94+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 6)),
95+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 5)),
96+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 4)),
97+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 3)),
98+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_1.Length, 2)),
99+
_.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 1)),
100+
101+
// Clear the input, we were just testing movement
102+
_.Escape
103+
));
104+
}
105+
34106
[SkippableFact]
35107
public void MultilineCursorMovement()
36108
{

0 commit comments

Comments
 (0)