Skip to content

Commit 9ad8eb6

Browse files
springcompdaxian-dbw
authored andcommitted
Ensure the desired column number is used while moving up or down in VI mode (#1122)
1 parent f31538e commit 9ad8eb6

File tree

6 files changed

+169
-5
lines changed

6 files changed

+169
-5
lines changed

PSReadLine/Movement.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Microsoft.PowerShell
1010
{
1111
public partial class PSConsoleReadLine
1212
{
13+
private int _moveToEndOfLineCommandCount;
1314
private int _moveToLineCommandCount;
1415
private int _moveToLineDesiredColumn;
1516

@@ -125,6 +126,18 @@ private static void ViOffsetCursorPosition(int count)
125126
}
126127

127128
private void MoveToLine(int lineOffset)
129+
{
130+
if (InViCommandMode())
131+
{
132+
ViMoveToLine(lineOffset);
133+
}
134+
else
135+
{
136+
MoveToLineImpl(lineOffset);
137+
}
138+
}
139+
140+
private void MoveToLineImpl(int lineOffset)
128141
{
129142
// Behavior description:
130143
// - If the cursor is at the end of a logical line, then 'UpArrow' (or 'DownArrow') moves the cursor up (or down)

PSReadLine/Movement.vi.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public static void ViEndOfPreviousGlob(ConsoleKeyInfo? key = null, object arg =
156156
private static int ViEndOfLineFactor => InViCommandMode() ? -1 : 0;
157157

158158
/// <summary>
159-
/// Move the cursor to the end of the input.
159+
/// Move the cursor to the end of the current logical line.
160160
/// </summary>
161161
public static void MoveToEndOfLine(ConsoleKeyInfo? key = null, object arg = null)
162162
{
@@ -166,7 +166,9 @@ public static void MoveToEndOfLine(ConsoleKeyInfo? key = null, object arg = null
166166
if (eol != _singleton._current)
167167
{
168168
_singleton.MoveCursor(eol);
169-
}
169+
}
170+
_singleton._moveToEndOfLineCommandCount++;
171+
_singleton._moveToLineDesiredColumn = int.MaxValue;
170172
}
171173
else
172174
{

PSReadLine/Movement.vi.multiline.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,69 @@ public void MoveToLastLine(ConsoleKeyInfo? key = null, object arg = null)
5656

5757
_singleton.MoveCursor(position);
5858
}
59+
60+
private void ViMoveToLine(int lineOffset)
61+
{
62+
// When moving up or down in a buffer in VI mode
63+
// the cursor wants to be positioned at a desired column number, which is:
64+
// - either a specified column number, the 0-based offset from the start of the logical line.
65+
// - or the end of the line
66+
//
67+
// Only one of those desired position is available at any given time.
68+
//
69+
// If the desired column number is specified, the cursor will be positioned at
70+
// the specified offset in the target logical line, or at the end of the line as appropriate.
71+
// The fact that a logical line is shorter than the desired column number *does not*
72+
// change its value. If a subsequent move to another logical line is performed, the
73+
// desired column number will take effect.
74+
//
75+
// If the desired column number is the end of the line, the cursor will be positioned at
76+
// the end of the target logical line.
77+
78+
const int endOfLine = int.MaxValue;
79+
80+
_moveToLineCommandCount += 1;
81+
82+
// if this is the first "move to line" command
83+
// record the desired column number from the current position
84+
// on the logical line
85+
86+
if (_moveToLineCommandCount == 1 && _moveToLineDesiredColumn == -1)
87+
{
88+
var startOfLine = GetBeginningOfLinePos(_current);
89+
_moveToLineDesiredColumn = _current - startOfLine;
90+
}
91+
92+
// Nothing needs to be done when:
93+
// - actually not moving the line, or
94+
// - moving the line down when it's at the end of the last logical line.
95+
if (lineOffset == 0 || (lineOffset > 0 && _current == _buffer.Length))
96+
{
97+
return;
98+
}
99+
100+
int targetLineOffset;
101+
102+
var currentLineIndex = _singleton.GetLogicalLineNumber() - 1;
103+
104+
if (lineOffset < 0)
105+
{
106+
targetLineOffset = Math.Max(0, currentLineIndex + lineOffset);
107+
}
108+
else
109+
{
110+
var lastLineIndex = _singleton.GetLogicalLineCount() - 1;
111+
targetLineOffset = Math.Min(lastLineIndex, currentLineIndex + lineOffset);
112+
}
113+
114+
var startOfTargetLinePos = GetBeginningOfNthLinePos(targetLineOffset);
115+
var endOfTargetLinePos = GetEndOfLogicalLinePos(startOfTargetLinePos);
116+
117+
var newCurrent = _moveToLineDesiredColumn == endOfLine
118+
? endOfTargetLinePos
119+
: Math.Min(startOfTargetLinePos + _moveToLineDesiredColumn, endOfTargetLinePos);
120+
121+
MoveCursor(newCurrent);
122+
}
59123
}
60124
}

PSReadLine/Position.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,41 @@ private static int GetBeginningOfLinePos(int current)
3535
return newCurrent;
3636
}
3737

38+
/// <summary>
39+
/// Returns the position of the beginning of line
40+
/// for the 0-based specified line number.
41+
/// </summary>
42+
private static int GetBeginningOfNthLinePos(int lineIndex)
43+
{
44+
System.Diagnostics.Debug.Assert(lineIndex >= 0 || lineIndex < _singleton.GetLogicalLineCount());
45+
46+
var nth = 0;
47+
var index = 0;
48+
var result = 0;
49+
50+
for (; index < _singleton._buffer.Length; index++)
51+
{
52+
if (nth == lineIndex)
53+
{
54+
result = index;
55+
break;
56+
}
57+
58+
if (_singleton._buffer[index] == '\n')
59+
{
60+
nth++;
61+
}
62+
}
63+
64+
if (nth == lineIndex)
65+
{
66+
result = index;
67+
}
68+
69+
70+
return result;
71+
}
72+
3873
/// <summary>
3974
/// Returns the position of the end of the logical line
4075
/// as specified by the "current" position.

PSReadLine/ReadLine.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,8 @@ private string InputLoop()
476476
var recallHistoryCommandCount = _recallHistoryCommandCount;
477477
var yankLastArgCommandCount = _yankLastArgCommandCount;
478478
var visualSelectionCommandCount = _visualSelectionCommandCount;
479-
var movingAtEndOfLineCount = _moveToLineCommandCount;
479+
var moveToLineCommandCount = _moveToLineCommandCount;
480+
var moveToEndOfLineCommandCount = _moveToEndOfLineCommandCount;
480481

481482
var key = ReadKey();
482483
ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null);
@@ -537,9 +538,18 @@ private string InputLoop()
537538
_visualSelectionCommandCount = 0;
538539
Render(); // Clears the visual selection
539540
}
540-
if (movingAtEndOfLineCount == _moveToLineCommandCount)
541+
if (moveToLineCommandCount == _moveToLineCommandCount)
541542
{
542-
_moveToLineCommandCount = 0;
543+
_moveToLineCommandCount = 0;
544+
545+
if (InViCommandMode() && moveToEndOfLineCommandCount == _moveToEndOfLineCommandCount)
546+
{
547+
// the previous command was neither a "move to end of line" command
548+
// nor a "move to line" command. In that case, the desired column
549+
// number will be computed from the current position on the logical line.
550+
551+
_moveToLineDesiredColumn = -1;
552+
}
543553
}
544554
}
545555
}

test/MovementTest.VI.Multiline.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,46 @@ namespace Test
55
{
66
public partial class ReadLine
77
{
8+
[SkippableFact]
9+
public void ViMoveToLine_DesiredColumn()
10+
{
11+
TestSetup(KeyMode.Vi);
12+
13+
const string buffer = "\"\n12345\n1234\n123\n12\n1\n\"";
14+
15+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
16+
17+
Test(buffer, Keys(
18+
_.DQuote, _.Enter,
19+
"12345", _.Enter,
20+
"1234", _.Enter,
21+
"123", _.Enter,
22+
"12", _.Enter,
23+
"1", _.Enter,
24+
_.DQuote,
25+
_.Escape,
26+
27+
// move to second line at column 4
28+
"ggj3l", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)),
29+
// moving down on shorter lines will position the cursor at the end of each logical line
30+
_.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)),
31+
_.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 2)),
32+
// moving back up will position the cursor at the end of shorter lines or at the desired column number
33+
_.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)),
34+
_.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)),
35+
36+
// move at end of line (column 5)
37+
_.Dollar, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 4)),
38+
// moving down on shorter lines will position the cursor at the end of each logical line
39+
_.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)),
40+
_.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 2)),
41+
// moving back up will position the cursor at the end of each logical line
42+
_.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)),
43+
_.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 4))
44+
));
45+
}
46+
47+
848
[SkippableFact]
949
public void ViBackwardChar()
1050
{

0 commit comments

Comments
 (0)