Skip to content

Commit 929f4f9

Browse files
springcomplzybkr
authored andcommitted
Supports '_' and '$' to move to the beginning and end of the logical line in Vi mode. (#812)
Fix #795
1 parent a7af25a commit 929f4f9

File tree

4 files changed

+125
-13
lines changed

4 files changed

+125
-13
lines changed

PSReadLine/KeyBindings.vi.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ private void SetDefaultViBindings()
196196
{ Keys.Pound, MakeKeyHandler(PrependAndAccept, "PrependAndAccept") },
197197
{ Keys.Pipe, MakeKeyHandler(GotoColumn, "GotoColumn") },
198198
{ Keys.Uphat, MakeKeyHandler(GotoFirstNonBlankOfLine, "GotoFirstNonBlankOfLine") },
199+
{ Keys.Underbar, MakeKeyHandler(GotoFirstNonBlankOfLine, "GotoFirstNonBlankOfLine") },
199200
{ Keys.Tilde, MakeKeyHandler(InvertCase, "InvertCase") },
200201
{ Keys.Slash, MakeKeyHandler(ViSearchHistoryBackward, "ViSearchHistoryBackward") },
201202
{ Keys.CtrlR, MakeKeyHandler(ViSearchHistoryBackward, "ViSearchHistoryBackward") },

PSReadLine/Movement.vi.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,18 @@ public static void ViEndOfPreviousGlob(ConsoleKeyInfo? key = null, object arg =
152152
/// </summary>
153153
public static void MoveToEndOfLine(ConsoleKeyInfo? key = null, object arg = null)
154154
{
155-
_singleton.MoveCursor(Math.Max(0, _singleton._buffer.Length + ViEndOfLineFactor));
155+
if (_singleton.LineIsMultiLine())
156+
{
157+
var eol = GetEndOfLogicalLinePos(_singleton._current);
158+
if (eol != _singleton._current)
159+
{
160+
_singleton.MoveCursor(eol);
161+
}
162+
}
163+
else
164+
{
165+
_singleton.MoveCursor(Math.Max(0, _singleton._buffer.Length + ViEndOfLineFactor));
166+
}
156167
}
157168

158169
/// <summary>
@@ -175,7 +186,8 @@ public static void NextWordEnd(ConsoleKeyInfo? key = null, object arg = null)
175186
public static void GotoColumn(ConsoleKeyInfo? key = null, object arg = null)
176187
{
177188
int col = arg as int? ?? -1;
178-
if (col < 0 ) {
189+
if (col < 0)
190+
{
179191
Ding();
180192
return;
181193
}
@@ -195,14 +207,11 @@ public static void GotoColumn(ConsoleKeyInfo? key = null, object arg = null)
195207
/// Move the cursor to the first non-blank character in the line.
196208
/// </summary>
197209
public static void GotoFirstNonBlankOfLine(ConsoleKeyInfo? key = null, object arg = null)
198-
{
199-
for (int i = 0; i < _singleton._buffer.Length; i++)
200-
{
201-
if (!Char.IsWhiteSpace(_singleton._buffer[i]))
202-
{
203-
_singleton.MoveCursor(i);
204-
return;
205-
}
210+
{
211+
var newCurrent = GetFirstNonBlankOfLogicalLinePos(_singleton._current);
212+
if (newCurrent != _singleton._current)
213+
{
214+
_singleton.MoveCursor(newCurrent);
206215
}
207216
}
208217

PSReadLine/Position.cs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ public partial class PSConsoleReadLine
88
/// Returns the position of the beginning of line
99
/// starting from the specified "current" position.
1010
/// </summary>
11-
/// <param name="current"></param>
12-
/// <returns></returns>
11+
/// <param name="current">The position in the current logical line.</param>
1312
private static int GetBeginningOfLinePos(int current)
1413
{
1514
var newCurrent = current;
@@ -35,5 +34,57 @@ private static int GetBeginningOfLinePos(int current)
3534

3635
return newCurrent;
3736
}
37+
38+
/// <summary>
39+
/// Returns the position of the end of the logical line
40+
/// as specified by the "current" position.
41+
/// </summary>
42+
/// <param name="current"></param>
43+
/// <returns></returns>
44+
private static int GetEndOfLogicalLinePos(int current)
45+
{
46+
var newCurrent = current;
47+
48+
for (var position = newCurrent; position < _singleton._buffer.Length; position++)
49+
{
50+
if (_singleton._buffer[position] == '\n')
51+
{
52+
break;
53+
}
54+
55+
newCurrent = position;
56+
}
57+
58+
return newCurrent;
59+
}
60+
61+
/// <summary>
62+
/// Returns the position of the first non whitespace character in
63+
/// the current logical line as specified by the "current" position.
64+
/// </summary>
65+
/// <param name="current">The position in the current logical line.</param>
66+
private static int GetFirstNonBlankOfLogicalLinePos(int current)
67+
{
68+
var beginningOfLine = GetBeginningOfLinePos(current);
69+
70+
var newCurrent = beginningOfLine;
71+
72+
while (IsVisibleBlank(newCurrent))
73+
{
74+
newCurrent++;
75+
}
76+
77+
return newCurrent;
78+
}
79+
80+
private static bool IsVisibleBlank(int newCurrent)
81+
{
82+
var c = _singleton._buffer[newCurrent];
83+
84+
// [:blank:] of vim's pattern matching behavior
85+
// defines blanks as SPACE and TAB characters.
86+
87+
return c == ' ' || c == '\t';
88+
}
3889
}
39-
}
90+
}

test/MovementTest.VI.Multiline.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,57 @@ public void ViMoveToFirstLogicalLine_MustDing_ForSingleLine()
5858
ViJumpMustDing(buffer, keys);
5959
}
6060

61+
[Fact]
62+
public void ViMoveToFirstNonBlankOfLogicalLineThenJumpToEndOfLogicalLine()
63+
{
64+
TestSetup(KeyMode.Vi);
65+
66+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
67+
68+
const string buffer = "\"\n line\"";
69+
70+
Test(buffer, Keys(
71+
_.DQuote, _.Enter, " line", _.DQuote, _.Escape, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 6)),
72+
_.Underbar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 2, 1)),
73+
_.Dollar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 6, 1)),
74+
// also works forward
75+
'0', CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength, 1)),
76+
_.Underbar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 2, 1))
77+
));
78+
}
79+
80+
[Fact]
81+
public void ViMoveToFirstNonBlankOfLogicalLine_NoOp_OnEmptyLine()
82+
{
83+
TestSetup(KeyMode.Vi);
84+
85+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
86+
87+
const string buffer = "\"\n\n\"";
88+
89+
Test(buffer, Keys(
90+
_.DQuote, _.Enter, _.Enter, _.DQuote, _.Escape, _.K,
91+
CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1)),
92+
_.Underbar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1))
93+
));
94+
}
95+
96+
[Fact]
97+
public void ViMoveToEndOfLine_NoOp_OnEmptyLine()
98+
{
99+
TestSetup(KeyMode.Vi);
100+
101+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
102+
103+
const string buffer = "\"\n\n\"";
104+
105+
Test(buffer, Keys(
106+
_.DQuote, _.Enter, _.Enter, _.DQuote, _.Escape, _.K,
107+
CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1)),
108+
_.Dollar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1))
109+
));
110+
}
111+
61112
private void ViJumpMustDing(string expectedResult, params object[] keys)
62113
{
63114
TestSetup(KeyMode.Vi);

0 commit comments

Comments
 (0)