Skip to content

Commit 2495382

Browse files
committed
Supports di' and di" text objects
1 parent 0d9767b commit 2495382

6 files changed

+347
-33
lines changed

PSReadLine/KeyBindings.vi.cs

+2
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ private void SetDefaultViBindings()
301301

302302
_viChordTextObjectsTable = new Dictionary<PSKeyInfo, KeyHandler>
303303
{
304+
{ Keys.DQuote, MakeKeyHandler(ViHandleTextObject, "QuoteTextObject")},
305+
{ Keys.SQuote, MakeKeyHandler(ViHandleTextObject, "QuoteTextObject")},
304306
{ Keys.W, MakeKeyHandler(ViHandleTextObject, "WordTextObject")},
305307
};
306308

PSReadLine/Position.cs

+2-28
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,7 @@ public partial class PSConsoleReadLine
1010
/// </summary>
1111
/// <param name="current">The position in the current logical line.</param>
1212
private static int GetBeginningOfLinePos(int current)
13-
{
14-
int i = Math.Max(0, current);
15-
while (i > 0)
16-
{
17-
if (_singleton._buffer[--i] == '\n')
18-
{
19-
i += 1;
20-
break;
21-
}
22-
}
23-
24-
return i;
25-
}
13+
=> _singleton._buffer.GetBeginningOfLogicalLinePos(current);
2614

2715
/// <summary>
2816
/// Returns the position of the beginning of line
@@ -66,21 +54,7 @@ private static int GetBeginningOfNthLinePos(int lineIndex)
6654
/// <param name="current"></param>
6755
/// <returns></returns>
6856
private static int GetEndOfLogicalLinePos(int current)
69-
{
70-
var newCurrent = current;
71-
72-
for (var position = current; position < _singleton._buffer.Length; position++)
73-
{
74-
if (_singleton._buffer[position] == '\n')
75-
{
76-
break;
77-
}
78-
79-
newCurrent = position;
80-
}
81-
82-
return newCurrent;
83-
}
57+
=> _singleton._buffer.GetEndOfLogicalLinePos(current);
8458

8559
/// <summary>
8660
/// Returns the position of the end of the logical line

PSReadLine/StringBuilderTextObjectExtensions.cs

+157
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Management.Automation;
23
using System.Text;
34

45
namespace Microsoft.PowerShell
@@ -109,5 +110,161 @@ public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buf
109110
// Make sure end includes the starting position.
110111
return Math.Max(i, position);
111112
}
113+
114+
/// <summary>
115+
/// Returns the span of text within the quotes relative to the specified position, in the corresponding logical line.
116+
/// If the position refers to the given start delimiter, the method returns the position immediately.
117+
/// If not, it first attempts to look backwards to find the start delimiter and returns its position if found.
118+
/// Otherwise, it look forwards to find the start delimiter and returns its position if found.
119+
/// Otherwise, it returns (-1, -1).
120+
///
121+
/// If a start delimiter is found, this method then attempts to find the end delimiter within the logical line.
122+
/// Otherwise, it returns (-1, -1).
123+
///
124+
/// This method supports VI i' and i" text objects.
125+
/// </summary>
126+
public static (int Start, int End) ViFindSpanOfInnerQuotedTextObjectBoundary(this StringBuilder buffer, char delimiter, int position, int repeated = 1)
127+
{
128+
// Cursor may be past the end of the buffer when calling this method
129+
// this may happen if the cursor is at the beginning of a new line.
130+
131+
var pos = Math.Min(position, buffer.Length - 1);
132+
133+
// restrict this method to the logical line
134+
// corresponding to the given position
135+
136+
var startOfLine = buffer.GetBeginningOfLogicalLinePos(pos);
137+
var endOfLine = buffer.GetEndOfLogicalLinePos(pos);
138+
139+
var start = -1;
140+
var end = -1;
141+
142+
// if on a quote we may be on a beginning or end quote
143+
// we need to parse the line to find out
144+
145+
if (buffer[pos] == delimiter)
146+
{
147+
var count = 1;
148+
for (var offset = pos - 1; offset > startOfLine; offset--)
149+
{
150+
if (buffer[offset] == delimiter)
151+
count++;
152+
}
153+
154+
// if there are an odd number of quotes up to the current position
155+
// the position refers to the beginning a quoted text
156+
157+
if (count % 2 == 1)
158+
{
159+
start = pos;
160+
}
161+
}
162+
163+
// else look backwards
164+
165+
if (start == -1)
166+
{
167+
for (var offset = pos - 1; offset > startOfLine; offset--)
168+
{
169+
if (buffer[offset] == delimiter)
170+
{
171+
start = offset;
172+
break;
173+
}
174+
}
175+
}
176+
177+
// if not found, look forwards
178+
179+
if (start == -1)
180+
{
181+
for (var offset = pos; offset < endOfLine; offset++)
182+
{
183+
if (buffer[offset] == delimiter)
184+
{
185+
start = offset;
186+
break;
187+
}
188+
}
189+
}
190+
191+
// attempts to find the end quote
192+
193+
if (start != -1 && start < endOfLine)
194+
{
195+
for (var offset = start + 1; offset < buffer.Length; offset++)
196+
{
197+
if (buffer[offset] == delimiter)
198+
{
199+
end = offset;
200+
break;
201+
}
202+
if (buffer[offset] == '\n')
203+
{
204+
break;
205+
}
206+
}
207+
}
208+
209+
// adjust span boundaries based upon
210+
// the number of repeatitions
211+
212+
if (start != -1 && end != -1)
213+
{
214+
if (repeated > 1)
215+
{
216+
end++;
217+
}
218+
else
219+
{
220+
start++;
221+
}
222+
}
223+
224+
return (start, end);
225+
}
226+
227+
/// <summary>
228+
/// Returns the position of the beginning of line
229+
/// starting from the specified "current" position.
230+
/// </summary>
231+
/// <param name="current">The position in the current logical line.</param>
232+
internal static int GetBeginningOfLogicalLinePos(this StringBuilder buffer, int current)
233+
{
234+
int i = Math.Max(0, current);
235+
while (i > 0)
236+
{
237+
if (buffer[--i] == '\n')
238+
{
239+
i += 1;
240+
break;
241+
}
242+
}
243+
244+
return i;
245+
}
246+
247+
/// <summary>
248+
/// Returns the position of the end of the logical line
249+
/// as specified by the "current" position.
250+
/// </summary>
251+
/// <param name="current"></param>
252+
/// <returns></returns>
253+
internal static int GetEndOfLogicalLinePos(this StringBuilder buffer, int current)
254+
{
255+
var newCurrent = current;
256+
257+
for (var position = current; position < buffer.Length; position++)
258+
{
259+
if (buffer[position] == '\n')
260+
{
261+
break;
262+
}
263+
264+
newCurrent = position;
265+
}
266+
267+
return newCurrent;
268+
}
112269
}
113270
}

PSReadLine/TextObjects.Vi.cs

+51-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3-
3+
using System.Runtime.CompilerServices;
4+
45
namespace Microsoft.PowerShell
56
{
67
public partial class PSConsoleReadLine
@@ -22,9 +23,17 @@ internal enum TextObjectSpan
2223
private TextObjectOperation _textObjectOperation = TextObjectOperation.None;
2324
private TextObjectSpan _textObjectSpan = TextObjectSpan.None;
2425

25-
private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, KeyHandler>> _textObjectHandlers = new()
26+
private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, Dictionary<PSKeyInfo, KeyHandler>>> _textObjectHandlers = new()
2627
{
27-
[TextObjectOperation.Delete] = new() { [TextObjectSpan.Inner] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord") },
28+
[TextObjectOperation.Delete] = new()
29+
{
30+
[TextObjectSpan.Inner] = new()
31+
{
32+
[Keys.DQuote] = MakeKeyHandler(ViDeleteInnerDQuote, "ViDeleteInnerDQuote"),
33+
[Keys.SQuote] = MakeKeyHandler(ViDeleteInnerSQuote, "ViDeleteInnerSQuote"),
34+
[Keys.W] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord"),
35+
}
36+
},
2837
};
2938

3039
private void ViChordDeleteTextObject(ConsoleKeyInfo? key = null, object arg = null)
@@ -75,8 +84,12 @@ private TextObjectSpan GetRequestedTextObjectSpan(ConsoleKeyInfo key)
7584

7685
private static void ViHandleTextObject(ConsoleKeyInfo? key = null, object arg = null)
7786
{
78-
if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectHandler) ||
79-
!textObjectHandler.TryGetValue(_singleton._textObjectSpan, out var handler))
87+
System.Diagnostics.Debug.Assert(key != null);
88+
var keyInfo = PSKeyInfo.FromConsoleKeyInfo(key.Value);
89+
90+
if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectSpanHandlers) ||
91+
!textObjectSpanHandlers.TryGetValue(_singleton._textObjectSpan, out var textObjectKeyHandlers) ||
92+
!textObjectKeyHandlers.TryGetValue(keyInfo, out var handler))
8093
{
8194
ResetTextObjectState();
8295
Ding();
@@ -92,6 +105,39 @@ private static void ResetTextObjectState()
92105
_singleton._textObjectSpan = TextObjectSpan.None;
93106
}
94107

108+
private static void ViDeleteInnerSQuote(ConsoleKeyInfo? key = null, object arg = null)
109+
=> ViDeleteInnerQuotes('\'', key, arg);
110+
private static void ViDeleteInnerDQuote(ConsoleKeyInfo? key = null, object arg = null)
111+
=> ViDeleteInnerQuotes('\"', key, arg);
112+
113+
private static void ViDeleteInnerQuotes(char delimiter, ConsoleKeyInfo? key = null, object arg = null)
114+
{
115+
if (!TryGetArgAsInt(arg, out var numericArg, 1))
116+
{
117+
return;
118+
}
119+
120+
if (_singleton._buffer.Length == 0)
121+
{
122+
Ding();
123+
return;
124+
}
125+
126+
var (start, end) = _singleton._buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, _singleton._current, repeated: numericArg);
127+
128+
if (start == -1 || end == -1)
129+
{
130+
Ding();
131+
return;
132+
}
133+
134+
var position = start;
135+
136+
_singleton.RemoveTextToViRegister(position, end - position);
137+
_singleton.AdjustCursorPosition(position);
138+
_singleton.Render();
139+
}
140+
95141
private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null)
96142
{
97143
var delimiters = _singleton.Options.WordDelimiters;

test/StringBuilderTextObjectExtensionsTests.cs

+46
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,51 @@ public void StringBuilderTextObjectExtensions_ViFindBeginningOfNextWordObjectBou
7373
Assert.Equal(46, buffer.ViFindBeginningOfNextWordObjectBoundary(45, wordDelimiters));
7474
Assert.Equal(50, buffer.ViFindBeginningOfNextWordObjectBoundary(46, wordDelimiters));
7575
}
76+
77+
[Theory]
78+
[InlineData('\'')]
79+
[InlineData('\"')]
80+
public void StringBuilderTextObjectExtensions_ViFindSpanOfInnerQuotedTextObjectBoundary(char delimiter)
81+
{
82+
var buffer = new StringBuilder($"_{delimiter}_{delimiter} {delimiter}_{delimiter} {delimiter}_{delimiter}");
83+
84+
// text: _"_" "_" "_"
85+
// position: 012345678901
86+
// - 1
87+
// boundary: 111135557888
88+
89+
// when invoked once, the span is within the quotes
90+
91+
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 0, repeated: 1));
92+
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 1, repeated: 1));
93+
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 2, repeated: 1));
94+
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 3, repeated: 1));
95+
Assert.Equal((4, 5), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 4, repeated: 1));
96+
Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 5, repeated: 1));
97+
Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 6, repeated: 1));
98+
Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 7, repeated: 1));
99+
Assert.Equal((8, 9), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 8, repeated: 1));
100+
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 9, repeated: 1));
101+
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 10, repeated: 1));
102+
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 11, repeated: 1));
103+
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 12, repeated: 1));
104+
105+
// when invoked more than once, the span is around the quotes
106+
107+
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 0, repeated: 42));
108+
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 1, repeated: 42));
109+
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 2, repeated: 42));
110+
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 3, repeated: 42));
111+
Assert.Equal((3, 6), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 4, repeated: 42));
112+
Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 5, repeated: 42));
113+
Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 6, repeated: 42));
114+
Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 7, repeated: 42));
115+
Assert.Equal((7, 10), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 8, repeated: 42));
116+
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 9, repeated: 42));
117+
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 10, repeated: 42));
118+
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 11, repeated: 42));
119+
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 12, repeated: 42));
120+
121+
}
76122
}
77123
}

0 commit comments

Comments
 (0)