Skip to content

Commit 24dbaa4

Browse files
committed
Supports 'diw' command.
1 parent 7e2b882 commit 24dbaa4

7 files changed

+586
-1
lines changed

PSReadLine/Cmdlets.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public class PSConsoleReadLineOptions
141141
public const int DefaultCompletionQueryItems = 100;
142142

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

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

PSReadLine/KeyBindings.vi.cs

+8
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

@@ -231,6 +233,7 @@ private void SetDefaultViBindings()
231233
{ Keys.ucG, MakeKeyHandler( DeleteEndOfBuffer, "DeleteEndOfBuffer") },
232234
{ Keys.ucE, MakeKeyHandler( ViDeleteEndOfGlob, "ViDeleteEndOfGlob") },
233235
{ Keys.H, MakeKeyHandler( BackwardDeleteChar, "BackwardDeleteChar") },
236+
{ Keys.I, MakeKeyHandler( ViChordDeleteTextObject, "ChordViTextObject") },
234237
{ Keys.J, MakeKeyHandler( DeleteNextLines, "DeleteNextLines") },
235238
{ Keys.K, MakeKeyHandler( DeletePreviousLines, "DeletePreviousLines") },
236239
{ Keys.L, MakeKeyHandler( DeleteChar, "DeleteChar") },
@@ -289,6 +292,11 @@ private void SetDefaultViBindings()
289292
{ Keys.Percent, MakeKeyHandler( ViYankPercent, "ViYankPercent") },
290293
};
291294

295+
_viChordTextObjectsTable = new Dictionary<PSKeyInfo, KeyHandler>
296+
{
297+
{ Keys.W, MakeKeyHandler(ViHandleTextObject, "WordTextObject")},
298+
};
299+
292300
_viChordDGTable = new Dictionary<PSKeyInfo, KeyHandler>
293301
{
294302
{ Keys.G, MakeKeyHandler( DeleteRelativeLines, "DeleteRelativeLines") },

PSReadLine/StringBuilderCharacterExtensions.cs

+12
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ public static bool InWord(this StringBuilder buffer, int i, string wordDelimiter
3434
return Character.IsInWord(buffer[i], wordDelimiters);
3535
}
3636

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

PSReadLine/TextObjects.Vi.cs

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

0 commit comments

Comments
 (0)