Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d7b9f82

Browse files
authoredAug 14, 2023
[vi-mode] Supports the text-object command diw (#2059)
1 parent 4d78ce1 commit d7b9f82

13 files changed

+709
-26
lines changed
 

‎PSReadLine/Cmdlets.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public class PSConsoleReadLineOptions
142142
public const int DefaultCompletionQueryItems = 100;
143143

144144
// Default includes all characters PowerShell treats like a dash - em dash, en dash, horizontal bar
145-
public const string DefaultWordDelimiters = @";:,.[]{}()/\|^&*-=+'""" + "\u2013\u2014\u2015";
145+
public const string DefaultWordDelimiters = @";:,.[]{}()/\|!?^&*-=+'""" + "\u2013\u2014\u2015";
146146

147147
/// <summary>
148148
/// When ringing the bell, what should be done?

‎PSReadLine/KeyBindings.vi.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ internal static ConsoleColor AlternateBackground(ConsoleColor bg)
4545
private static Dictionary<PSKeyInfo, KeyHandler> _viChordYTable;
4646
private static Dictionary<PSKeyInfo, KeyHandler> _viChordDGTable;
4747

48+
private static Dictionary<PSKeyInfo, KeyHandler> _viChordTextObjectsTable;
49+
4850
private static Dictionary<PSKeyInfo, Dictionary<PSKeyInfo, KeyHandler>> _viCmdChordTable;
4951
private static Dictionary<PSKeyInfo, Dictionary<PSKeyInfo, KeyHandler>> _viInsChordTable;
5052

@@ -238,6 +240,7 @@ private void SetDefaultViBindings()
238240
{ Keys.ucG, MakeKeyHandler( DeleteEndOfBuffer, "DeleteEndOfBuffer") },
239241
{ Keys.ucE, MakeKeyHandler( ViDeleteEndOfGlob, "ViDeleteEndOfGlob") },
240242
{ Keys.H, MakeKeyHandler( BackwardDeleteChar, "BackwardDeleteChar") },
243+
{ Keys.I, MakeKeyHandler( ViChordDeleteTextObject, "ChordViTextObject") },
241244
{ Keys.J, MakeKeyHandler( DeleteNextLines, "DeleteNextLines") },
242245
{ Keys.K, MakeKeyHandler( DeletePreviousLines, "DeletePreviousLines") },
243246
{ Keys.L, MakeKeyHandler( DeleteChar, "DeleteChar") },
@@ -296,6 +299,11 @@ private void SetDefaultViBindings()
296299
{ Keys.Percent, MakeKeyHandler( ViYankPercent, "ViYankPercent") },
297300
};
298301

302+
_viChordTextObjectsTable = new Dictionary<PSKeyInfo, KeyHandler>
303+
{
304+
{ Keys.W, MakeKeyHandler(ViHandleTextObject, "WordTextObject")},
305+
};
306+
299307
_viChordDGTable = new Dictionary<PSKeyInfo, KeyHandler>
300308
{
301309
{ Keys.G, MakeKeyHandler( DeleteRelativeLines, "DeleteRelativeLines") },

‎PSReadLine/Position.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,23 +102,14 @@ private static int GetFirstNonBlankOfLogicalLinePos(int current)
102102
var beginningOfLine = GetBeginningOfLinePos(current);
103103

104104
var newCurrent = beginningOfLine;
105+
var buffer = _singleton._buffer;
105106

106-
while (newCurrent < _singleton._buffer.Length && IsVisibleBlank(newCurrent))
107+
while (newCurrent < buffer.Length && buffer.IsVisibleBlank(newCurrent))
107108
{
108109
newCurrent++;
109110
}
110111

111112
return newCurrent;
112113
}
113-
114-
private static bool IsVisibleBlank(int newCurrent)
115-
{
116-
var c = _singleton._buffer[newCurrent];
117-
118-
// [:blank:] of vim's pattern matching behavior
119-
// defines blanks as SPACE and TAB characters.
120-
121-
return c == ' ' || c == '\t';
122-
}
123114
}
124115
}

‎PSReadLine/Prediction.Views.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,12 +1513,12 @@ internal int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimit
15131513
}
15141514

15151515
int i = currentIndex;
1516-
if (!_singleton.InWord(_suggestionText[i], wordDelimiters))
1516+
if (!Character.IsInWord(_suggestionText[i], wordDelimiters))
15171517
{
15181518
// Scan to end of current non-word region
15191519
while (++i < _suggestionText.Length)
15201520
{
1521-
if (_singleton.InWord(_suggestionText[i], wordDelimiters))
1521+
if (Character.IsInWord(_suggestionText[i], wordDelimiters))
15221522
{
15231523
break;
15241524
}
@@ -1529,7 +1529,7 @@ internal int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimit
15291529
{
15301530
while (++i < _suggestionText.Length)
15311531
{
1532-
if (!_singleton.InWord(_suggestionText[i], wordDelimiters))
1532+
if (!Character.IsInWord(_suggestionText[i], wordDelimiters))
15331533
{
15341534
if (_suggestionText[i] == ' ')
15351535
{
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Text;
2+
3+
namespace Microsoft.PowerShell
4+
{
5+
internal static class StringBuilderCharacterExtensions
6+
{
7+
/// <summary>
8+
/// Returns true if the character at the specified position is a visible whitespace character.
9+
/// A blank character is defined as a SPACE or a TAB.
10+
/// </summary>
11+
/// <param name="buffer"></param>
12+
/// <param name="i"></param>
13+
/// <returns></returns>
14+
public static bool IsVisibleBlank(this StringBuilder buffer, int i)
15+
{
16+
var c = buffer[i];
17+
18+
// [:blank:] of vim's pattern matching behavior
19+
// defines blanks as SPACE and TAB characters.
20+
21+
return c == ' ' || c == '\t';
22+
}
23+
24+
/// <summary>
25+
/// Returns true if the character at the specified position is
26+
/// not present in a list of word-delimiter characters.
27+
/// </summary>
28+
/// <param name="buffer"></param>
29+
/// <param name="i"></param>
30+
/// <param name="wordDelimiters"></param>
31+
/// <returns></returns>
32+
public static bool InWord(this StringBuilder buffer, int i, string wordDelimiters)
33+
{
34+
return Character.IsInWord(buffer[i], wordDelimiters);
35+
}
36+
37+
/// <summary>
38+
/// Returns true if the character at the specified position is
39+
/// at the end of the buffer
40+
/// </summary>
41+
/// <param name="buffer"></param>
42+
/// <param name="i"></param>
43+
/// <returns></returns>
44+
public static bool IsAtEndOfBuffer(this StringBuilder buffer, int i)
45+
{
46+
return i >= (buffer.Length - 1);
47+
}
48+
49+
/// <summary>
50+
/// Returns true if the character at the specified position is
51+
/// a unicode whitespace character.
52+
/// </summary>
53+
/// <param name="buffer"></param>
54+
/// <param name="i"></param>
55+
/// <returns></returns>
56+
public static bool IsWhiteSpace(this StringBuilder buffer, int i)
57+
{
58+
// Treat just beyond the end of buffer as whitespace because
59+
// it looks like whitespace to the user even though they haven't
60+
// entered a character yet.
61+
return i >= buffer.Length || char.IsWhiteSpace(buffer[i]);
62+
}
63+
}
64+
65+
public static class Character
66+
{
67+
/// <summary>
68+
/// Returns true if the character not present in a list of word-delimiter characters.
69+
/// </summary>
70+
/// <param name="c"></param>
71+
/// <param name="wordDelimiters"></param>
72+
/// <returns></returns>
73+
public static bool IsInWord(char c, string wordDelimiters)
74+
{
75+
return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0;
76+
}
77+
}
78+
}

‎PSReadLine/StringBuilderExtensions.cs renamed to ‎PSReadLine/StringBuilderLinewiseExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,26 @@ internal static Range GetRange(this StringBuilder buffer, int lineIndex, int lin
7272
endPosition - startPosition + 1
7373
);
7474
}
75+
76+
/// <summary>
77+
/// Returns true if the specified position is on an empty logical line.
78+
/// </summary>
79+
/// <param name="buffer"></param>
80+
/// <param name="cursor"></param>
81+
/// <returns></returns>
82+
public static bool IsLogigalLineEmpty(this StringBuilder buffer, int cursor)
83+
{
84+
// the cursor is on a logical line considered empty if...
85+
return
86+
// the entire buffer is empty (by definition),
87+
buffer.Length == 0 ||
88+
// or the cursor sits at the start of the empty last line,
89+
// meaning that it is past the end of the buffer and the
90+
// last character in the buffer is a newline character,
91+
(cursor == buffer.Length && buffer[cursor - 1] == '\n') ||
92+
// or if the cursor is on a newline character.
93+
(cursor > 0 && buffer[cursor] == '\n');
94+
}
7595
}
7696

7797
internal static class StringBuilderPredictionExtensions
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Text;
3+
4+
namespace Microsoft.PowerShell
5+
{
6+
internal static class StringBuilderTextObjectExtensions
7+
{
8+
private const string WhiteSpace = " \n\t";
9+
10+
/// <summary>
11+
/// Returns the position of the beginning of the current word as delimited by white space and delimiters
12+
/// This method differs from <see cref="ViFindPreviousWordPoint(string)"/>:
13+
/// - When the cursor location is on the first character of a word, <see cref="ViFindPreviousWordPoint(string)"/>
14+
/// returns the position of the previous word, whereas this method returns the cursor location.
15+
/// - When the cursor location is in a word, both methods return the same result.
16+
/// This method supports VI "iw" text object.
17+
/// </summary>
18+
public static int ViFindBeginningOfWordObjectBoundary(this StringBuilder buffer, int position, string wordDelimiters)
19+
{
20+
// Cursor may be past the end of the buffer when calling this method
21+
// this may happen if the cursor is at the beginning of a new line.
22+
var i = Math.Min(position, buffer.Length - 1);
23+
24+
// If starting on a word consider a text object as a sequence of characters excluding the delimiters,
25+
// otherwise, consider a word as a sequence of delimiters.
26+
var delimiters = wordDelimiters;
27+
var isInWord = buffer.InWord(i, wordDelimiters);
28+
29+
if (isInWord)
30+
{
31+
// For the purpose of this method, whitespace character is considered a delimiter.
32+
delimiters += WhiteSpace;
33+
}
34+
else
35+
{
36+
char c = buffer[i];
37+
if ((wordDelimiters + '\n').IndexOf(c) == -1 && char.IsWhiteSpace(c))
38+
{
39+
// Current position points to a whitespace that is not a newline.
40+
delimiters = WhiteSpace;
41+
}
42+
else
43+
{
44+
delimiters += '\n';
45+
}
46+
}
47+
48+
var isTextObjectChar = isInWord
49+
? (Func<char, bool>)(c => delimiters.IndexOf(c) == -1)
50+
: c => delimiters.IndexOf(c) != -1;
51+
52+
var beginning = i;
53+
while (i >= 0 && isTextObjectChar(buffer[i]))
54+
{
55+
beginning = i--;
56+
}
57+
58+
return beginning;
59+
}
60+
61+
/// <summary>
62+
/// Finds the position of the beginning of the next word object starting from the specified position.
63+
/// If positioned on the last word in the buffer, returns buffer length + 1.
64+
/// This method supports VI "iw" text-object.
65+
/// iw: "inner word", select words. White space between words is counted too.
66+
/// </summary>
67+
public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buffer, int position, string wordDelimiters)
68+
{
69+
// Cursor may be past the end of the buffer when calling this method
70+
// this may happen if the cursor is at the beginning of a new line.
71+
var i = Math.Min(position, buffer.Length - 1);
72+
73+
// Always skip the first newline character.
74+
if (buffer[i] == '\n' && i < buffer.Length - 1)
75+
{
76+
++i;
77+
}
78+
79+
// If starting on a word consider a text object as a sequence of characters excluding the delimiters,
80+
// otherwise, consider a word as a sequence of delimiters.
81+
var delimiters = wordDelimiters;
82+
var isInWord = buffer.InWord(i, wordDelimiters);
83+
84+
if (isInWord)
85+
{
86+
delimiters += WhiteSpace;
87+
}
88+
else if (char.IsWhiteSpace(buffer[i]))
89+
{
90+
delimiters = " \t";
91+
}
92+
93+
var isTextObjectChar = isInWord
94+
? (Func<char, bool>)(c => delimiters.IndexOf(c) == -1)
95+
: c => delimiters.IndexOf(c) != -1;
96+
97+
// Try to skip a second newline characters to replicate vim behaviour.
98+
if (buffer[i] == '\n' && i < buffer.Length - 1)
99+
{
100+
++i;
101+
}
102+
103+
// Skip to next non-word characters.
104+
while (i < buffer.Length && isTextObjectChar(buffer[i]))
105+
{
106+
++i;
107+
}
108+
109+
// Make sure end includes the starting position.
110+
return Math.Max(i, position);
111+
}
112+
}
113+
}

‎PSReadLine/TextObjects.Vi.cs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Microsoft.PowerShell
5+
{
6+
public partial class PSConsoleReadLine
7+
{
8+
internal enum TextObjectOperation
9+
{
10+
None,
11+
Change,
12+
Delete,
13+
}
14+
15+
internal enum TextObjectSpan
16+
{
17+
None,
18+
Around,
19+
Inner,
20+
}
21+
22+
private TextObjectOperation _textObjectOperation = TextObjectOperation.None;
23+
private TextObjectSpan _textObjectSpan = TextObjectSpan.None;
24+
25+
private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, KeyHandler>> _textObjectHandlers = new()
26+
{
27+
[TextObjectOperation.Delete] = new() { [TextObjectSpan.Inner] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord") },
28+
};
29+
30+
private void ViChordDeleteTextObject(ConsoleKeyInfo? key = null, object arg = null)
31+
{
32+
_textObjectOperation = TextObjectOperation.Delete;
33+
ViChordTextObject(key, arg);
34+
}
35+
36+
private void ViChordTextObject(ConsoleKeyInfo? key = null, object arg = null)
37+
{
38+
if (!key.HasValue)
39+
{
40+
ResetTextObjectState();
41+
throw new ArgumentNullException(nameof(key));
42+
}
43+
44+
_textObjectSpan = GetRequestedTextObjectSpan(key.Value);
45+
46+
// Handle text object
47+
var textObjectKey = ReadKey();
48+
if (_viChordTextObjectsTable.TryGetValue(textObjectKey, out _))
49+
{
50+
_singleton.ProcessOneKey(textObjectKey, _viChordTextObjectsTable, ignoreIfNoAction: true, arg: arg);
51+
}
52+
else
53+
{
54+
ResetTextObjectState();
55+
Ding();
56+
}
57+
}
58+
59+
private TextObjectSpan GetRequestedTextObjectSpan(ConsoleKeyInfo key)
60+
{
61+
if (key.KeyChar == 'i')
62+
{
63+
return TextObjectSpan.Inner;
64+
}
65+
else if (key.KeyChar == 'a')
66+
{
67+
return TextObjectSpan.Around;
68+
}
69+
else
70+
{
71+
System.Diagnostics.Debug.Assert(false);
72+
throw new NotSupportedException();
73+
}
74+
}
75+
76+
private static void ViHandleTextObject(ConsoleKeyInfo? key = null, object arg = null)
77+
{
78+
if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectHandler) ||
79+
!textObjectHandler.TryGetValue(_singleton._textObjectSpan, out var handler))
80+
{
81+
ResetTextObjectState();
82+
Ding();
83+
return;
84+
}
85+
86+
handler.Action(key, arg);
87+
}
88+
89+
private static void ResetTextObjectState()
90+
{
91+
_singleton._textObjectOperation = TextObjectOperation.None;
92+
_singleton._textObjectSpan = TextObjectSpan.None;
93+
}
94+
95+
private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null)
96+
{
97+
var delimiters = _singleton.Options.WordDelimiters;
98+
99+
if (!TryGetArgAsInt(arg, out var numericArg, 1))
100+
{
101+
return;
102+
}
103+
104+
if (_singleton._buffer.Length == 0)
105+
{
106+
if (numericArg > 1)
107+
{
108+
Ding();
109+
}
110+
return;
111+
}
112+
113+
// Unless at the end of the buffer a single delete word should not delete backwards
114+
// so if the cursor is on an empty line, do nothing.
115+
if (numericArg == 1 &&
116+
_singleton._current < _singleton._buffer.Length &&
117+
_singleton._buffer.IsLogigalLineEmpty(_singleton._current))
118+
{
119+
return;
120+
}
121+
122+
var start = _singleton._buffer.ViFindBeginningOfWordObjectBoundary(_singleton._current, delimiters);
123+
var end = _singleton._current;
124+
125+
// Attempting to find a valid position for multiple words.
126+
// If no valid position is found, this is a no-op
127+
{
128+
while (numericArg-- > 0 && end < _singleton._buffer.Length)
129+
{
130+
end = _singleton._buffer.ViFindBeginningOfNextWordObjectBoundary(end, delimiters);
131+
}
132+
133+
// Attempting to delete too many words should ding.
134+
if (numericArg > 0)
135+
{
136+
Ding();
137+
return;
138+
}
139+
}
140+
141+
if (end > 0 && _singleton._buffer.IsAtEndOfBuffer(end - 1) && _singleton._buffer.InWord(end - 1, delimiters))
142+
{
143+
_singleton._shouldAppend = true;
144+
}
145+
146+
_singleton.RemoveTextToViRegister(start, end - start);
147+
_singleton.AdjustCursorPosition(start);
148+
_singleton.Render();
149+
}
150+
151+
/// <summary>
152+
/// Attempt to set the cursor at the specified position.
153+
/// </summary>
154+
/// <param name="position"></param>
155+
/// <returns></returns>
156+
private int AdjustCursorPosition(int position)
157+
{
158+
// This method might prove useful in a more general case.
159+
if (_buffer.Length == 0)
160+
{
161+
_current = 0;
162+
return 0;
163+
}
164+
165+
var maxPosition = _buffer[_buffer.Length - 1] == '\n'
166+
? _buffer.Length
167+
: _buffer.Length - 1;
168+
169+
var newCurrent = Math.Min(position, maxPosition);
170+
var beginning = GetBeginningOfLinePos(newCurrent);
171+
172+
if (newCurrent < _buffer.Length && _buffer[newCurrent] == '\n' && (newCurrent + ViEndOfLineFactor > beginning))
173+
{
174+
newCurrent += ViEndOfLineFactor;
175+
}
176+
177+
_current = newCurrent;
178+
return newCurrent;
179+
}
180+
}
181+
}

‎PSReadLine/Words.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,7 @@ private Token FindToken(int current, FindTokenMode mode)
9090

9191
private bool InWord(int index, string wordDelimiters)
9292
{
93-
char c = _buffer[index];
94-
return InWord(c, wordDelimiters);
95-
}
96-
97-
private bool InWord(char c, string wordDelimiters)
98-
{
99-
return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0;
93+
return _buffer.InWord(index, wordDelimiters);
10094
}
10195

10296
/// <summary>

‎PSReadLine/Words.vi.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Copyright (c) Microsoft Corporation. All rights reserved.
33
--********************************************************************/
44

5+
using System;
6+
57
namespace Microsoft.PowerShell
68
{
79
public partial class PSConsoleReadLine
@@ -106,10 +108,7 @@ private int ViFindNextWordFromWord(int i, string wordDelimiters)
106108
/// </summary>
107109
private bool IsWhiteSpace(int i)
108110
{
109-
// Treat just beyond the end of buffer as whitespace because
110-
// it looks like whitespace to the user even though they haven't
111-
// entered a character yet.
112-
return i >= _buffer.Length || char.IsWhiteSpace(_buffer[i]);
111+
return _buffer.IsWhiteSpace(i);
113112
}
114113

115114
/// <summary>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Microsoft.PowerShell;
2+
using System.Text;
3+
using Xunit;
4+
5+
namespace Test
6+
{
7+
public sealed class StringBuilderCharacterExtensionsTests
8+
{
9+
[Fact]
10+
public void StringBuilderCharacterExtensions_IsVisibleBlank()
11+
{
12+
var buffer = new StringBuilder(" \tn");
13+
14+
// system under test
15+
16+
Assert.True(buffer.IsVisibleBlank(0));
17+
Assert.True(buffer.IsVisibleBlank(1));
18+
Assert.False(buffer.IsVisibleBlank(2));
19+
}
20+
21+
[Fact]
22+
public void StringBuilderCharacterExtensions_InWord()
23+
{
24+
var buffer = new StringBuilder("hello, world!");
25+
const string wordDelimiters = " ";
26+
27+
// system under test
28+
29+
Assert.True(buffer.InWord(2, wordDelimiters));
30+
Assert.True(buffer.InWord(5, wordDelimiters));
31+
}
32+
33+
[Fact]
34+
public void StringBuilderCharacterExtensions_IsWhiteSpace()
35+
{
36+
var buffer = new StringBuilder("a c");
37+
38+
39+
// system under test
40+
41+
Assert.False(buffer.IsWhiteSpace(0));
42+
Assert.True(buffer.IsWhiteSpace(1));
43+
Assert.False(buffer.IsWhiteSpace(2));
44+
}
45+
}
46+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using Microsoft.PowerShell;
2+
using System.Text;
3+
using Xunit;
4+
5+
namespace Test
6+
{
7+
public sealed class StringBuilderTextObjectExtensionsTests
8+
{
9+
[Fact]
10+
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary()
11+
{
12+
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;
13+
14+
var buffer = new StringBuilder("Hello, world!\ncruel world.\none\n\n\n\n\ntwo\n three four.");
15+
Assert.Equal(0, buffer.ViFindBeginningOfWordObjectBoundary(1, wordDelimiters));
16+
}
17+
18+
[Fact]
19+
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_whitespace()
20+
{
21+
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;
22+
23+
var buffer = new StringBuilder("Hello, world!");
24+
Assert.Equal(6, buffer.ViFindBeginningOfWordObjectBoundary(7, wordDelimiters));
25+
}
26+
27+
[Fact]
28+
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_backwards()
29+
{
30+
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;
31+
32+
var buffer = new StringBuilder("Hello!\nworld!");
33+
Assert.Equal(5, buffer.ViFindBeginningOfWordObjectBoundary(6, wordDelimiters));
34+
}
35+
36+
[Fact]
37+
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_end_of_buffer()
38+
{
39+
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;
40+
41+
var buffer = new StringBuilder("Hello, world!");
42+
Assert.Equal(12, buffer.ViFindBeginningOfWordObjectBoundary(buffer.Length, wordDelimiters));
43+
}
44+
45+
[Fact]
46+
public void StringBuilderTextObjectExtensions_ViFindBeginningOfNextWordObjectBoundary()
47+
{
48+
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;
49+
50+
var buffer = new StringBuilder("Hello, world!\ncruel world.\none\n\n\n\n\ntwo\n three four.");
51+
52+
// Words |Hello|,| |world|!|\n|cruel |world|.|\n|one\n\n|\n\n|\n|two|\n |three| |four|.|
53+
// Pos 01234 5 6 78901 2 _3 456789 01234 5 _6 789_0_1 _2_3 _4 567 _89 01234 5 6789 0
54+
// Pos 0 1 2 3 4 5
55+
56+
// system under test
57+
58+
Assert.Equal(5, buffer.ViFindBeginningOfNextWordObjectBoundary(0, wordDelimiters));
59+
Assert.Equal(6, buffer.ViFindBeginningOfNextWordObjectBoundary(5, wordDelimiters));
60+
Assert.Equal(7, buffer.ViFindBeginningOfNextWordObjectBoundary(6, wordDelimiters));
61+
Assert.Equal(12, buffer.ViFindBeginningOfNextWordObjectBoundary(7, wordDelimiters));
62+
Assert.Equal(13, buffer.ViFindBeginningOfNextWordObjectBoundary(12, wordDelimiters));
63+
Assert.Equal(19, buffer.ViFindBeginningOfNextWordObjectBoundary(13, wordDelimiters));
64+
Assert.Equal(20, buffer.ViFindBeginningOfNextWordObjectBoundary(19, wordDelimiters));
65+
Assert.Equal(25, buffer.ViFindBeginningOfNextWordObjectBoundary(20, wordDelimiters));
66+
Assert.Equal(26, buffer.ViFindBeginningOfNextWordObjectBoundary(25, wordDelimiters));
67+
Assert.Equal(30, buffer.ViFindBeginningOfNextWordObjectBoundary(26, wordDelimiters));
68+
Assert.Equal(32, buffer.ViFindBeginningOfNextWordObjectBoundary(30, wordDelimiters));
69+
Assert.Equal(34, buffer.ViFindBeginningOfNextWordObjectBoundary(32, wordDelimiters));
70+
Assert.Equal(38, buffer.ViFindBeginningOfNextWordObjectBoundary(34, wordDelimiters));
71+
Assert.Equal(40, buffer.ViFindBeginningOfNextWordObjectBoundary(38, wordDelimiters));
72+
Assert.Equal(45, buffer.ViFindBeginningOfNextWordObjectBoundary(40, wordDelimiters));
73+
Assert.Equal(46, buffer.ViFindBeginningOfNextWordObjectBoundary(45, wordDelimiters));
74+
Assert.Equal(50, buffer.ViFindBeginningOfNextWordObjectBoundary(46, wordDelimiters));
75+
}
76+
}
77+
}

‎test/TextObjects.Vi.Tests.cs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using Microsoft.PowerShell;
2+
using Xunit;
3+
4+
namespace Test
5+
{
6+
public partial class ReadLine
7+
{
8+
[SkippableFact]
9+
public void ViTextObject_diw()
10+
{
11+
TestSetup(KeyMode.Vi);
12+
13+
Test("\"hello, \ncruel world!\"", Keys(
14+
_.DQuote,
15+
"hello, world!", _.Enter,
16+
"cruel world!", _.DQuote,
17+
_.Escape,
18+
19+
// move cursor to the 'o' in 'world'
20+
"gg9l",
21+
22+
// delete text object
23+
"diw",
24+
CheckThat(() => AssertLineIs("\"hello, !\ncruel world!\"")),
25+
CheckThat(() => AssertCursorLeftIs(8)),
26+
27+
// delete
28+
"diw",
29+
CheckThat(() => AssertLineIs("\"hello, \ncruel world!\"")),
30+
CheckThat(() => AssertCursorLeftIs(7))
31+
));
32+
}
33+
34+
[SkippableFact]
35+
public void ViTextObject_diw_digit_arguments()
36+
{
37+
TestSetup(KeyMode.Vi);
38+
39+
Test("\"hello, world!\"", Keys(
40+
_.DQuote,
41+
"hello, world!", _.Enter,
42+
"cruel world!", _.DQuote,
43+
_.Escape,
44+
45+
// move cursor to the 'o' in 'world'
46+
"gg9l",
47+
48+
// delete text object
49+
"diw",
50+
CheckThat(() => AssertLineIs("\"hello, !\ncruel world!\"")),
51+
CheckThat(() => AssertCursorLeftIs(8)),
52+
53+
// delete multiple text objects (spans multiple lines)
54+
"3diw",
55+
CheckThat(() => AssertLineIs("\"hello, world!\"")),
56+
CheckThat(() => AssertCursorLeftIs(8))
57+
));
58+
}
59+
60+
61+
[SkippableFact]
62+
public void ViTextObject_diw_noop()
63+
{
64+
TestSetup(KeyMode.Vi);
65+
66+
TestMustDing("\"hello, world!\ncruel world!\"", Keys(
67+
_.DQuote,
68+
"hello, world!", _.Enter,
69+
"cruel world!", _.DQuote,
70+
_.Escape,
71+
72+
// move cursor to the 'o' in 'world'
73+
"gg9l",
74+
75+
// attempting to delete too many words must ding
76+
"1274diw"
77+
));
78+
}
79+
80+
[SkippableFact]
81+
public void ViTextObject_diw_empty_line()
82+
{
83+
TestSetup(KeyMode.Vi);
84+
85+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
86+
87+
Test("\"\nhello, world!\n\noh, bitter world!\n\"", Keys(
88+
_.DQuote, _.Enter,
89+
"hello, world!", _.Enter,
90+
_.Enter,
91+
"oh, bitter world!", _.Enter,
92+
_.DQuote, _.Escape,
93+
94+
// move cursor to the second line
95+
"ggjj",
96+
97+
// deleting single word cannot move backwards to previous line (noop)
98+
"diw",
99+
CheckThat(() => AssertLineIs("\"\nhello, world!\n\noh, bitter world!\n\""))
100+
));
101+
}
102+
103+
[SkippableFact]
104+
public void ViTextObject_diw_end_of_buffer()
105+
{
106+
TestSetup(KeyMode.Vi);
107+
108+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
109+
110+
Test("", Keys(
111+
_.DQuote,
112+
"hello, world!", _.Enter,
113+
"cruel world!", _.DQuote,
114+
_.Escape,
115+
116+
// move to end of buffer
117+
"G$",
118+
119+
// delete text object (deletes backwards)
120+
"diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel world")),
121+
"diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel ")),
122+
"diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel")),
123+
"diw", CheckThat(() => AssertLineIs("\"hello, world!\n")),
124+
"diw", CheckThat(() => AssertLineIs("\"hello, world")),
125+
"diw", CheckThat(() => AssertLineIs("\"hello, ")),
126+
"diw", CheckThat(() => AssertLineIs("\"hello,")),
127+
"diw", CheckThat(() => AssertLineIs("\"hello")),
128+
"diw", CheckThat(() => AssertLineIs("\"")),
129+
"diw", CheckThat(() => AssertLineIs(""))
130+
));
131+
}
132+
133+
[SkippableFact]
134+
public void ViTextObject_diw_empty_buffer()
135+
{
136+
TestSetup(KeyMode.Vi);
137+
Test("", Keys(_.Escape, "diw"));
138+
TestMustDing("", Keys(_.Escape, "d2iw"));
139+
}
140+
141+
[SkippableFact]
142+
public void ViTextObject_diw_new_lines()
143+
{
144+
TestSetup(KeyMode.Vi);
145+
146+
var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
147+
148+
Test("\"\ntwo\n\"", Keys(
149+
_.DQuote, _.Enter,
150+
"one", _.Enter,
151+
_.Enter, _.Enter,
152+
_.Enter, _.Enter,
153+
_.Enter,
154+
"two", _.Enter, _.DQuote,
155+
_.Escape,
156+
157+
// move to the beginning of 'one'
158+
"gg0j",
159+
160+
// delete text object
161+
"2diw",
162+
CheckThat(() => AssertLineIs("\"\n\n\n\n\ntwo\n\"")),
163+
164+
"ugg0j", // currently undo does not move the cursor to the correct position
165+
// delete multiple text objects (spans multiple lines)
166+
"3diw",
167+
CheckThat(() => AssertLineIs("\"\n\n\ntwo\n\"")),
168+
169+
"ugg0j", // currently undo does not move the cursor to the correct position
170+
// delete multiple text objects (spans multiple lines)
171+
"4diw",
172+
CheckThat(() => AssertLineIs("\"\ntwo\n\""))
173+
));
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)
Please sign in to comment.