Skip to content

Commit a93af44

Browse files
committed
Supports di' and di" text objects
1 parent 24a4c86 commit a93af44

6 files changed

+310
-33
lines changed

PSReadLine/KeyBindings.vi.cs

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 28 deletions
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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,105 @@ public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buf
109109
// Make sure end includes the starting position.
110110
return Math.Max(i, position);
111111
}
112+
113+
/// <summary>
114+
/// Returns the position of the first delimiter in the buffer, started at the specified position.
115+
/// This method attempts to find the position of a delimiter within the logical line at the specified position.
116+
/// If the position refers to the given delimiter, the method returns the position immediately.
117+
/// If not, it first attempts to look backwards to find the first delimiter and returns its position if found.
118+
/// Otherwise, it look forwards to find the first delimiter and returns its position if found.
119+
/// Otherwise, it return -1.
120+
/// This method supports VI i' and i" text objects.
121+
/// </summary>
122+
public static int ViFindBeginningOfQuotedTextObjectBoundary(this StringBuilder buffer, char delimiter, int position)
123+
{
124+
// Cursor may be past the end of the buffer when calling this method
125+
// this may happen if the cursor is at the beginning of a new line.
126+
var pos = Math.Min(position, buffer.Length - 1);
127+
128+
var beginning = buffer.GetBeginningOfLogicalLinePos(pos);
129+
var end = buffer.GetEndOfLogicalLinePos(pos);
130+
131+
// if on a quote we may be on a beginning or end quote
132+
// we need to parse the line to find out
133+
134+
if (buffer[pos] == delimiter) {
135+
var count = 1;
136+
for (var offset = pos - 1; offset > beginning; offset--)
137+
{
138+
if (buffer[offset] == delimiter)
139+
count++;
140+
}
141+
142+
// if there are an odd number of quotes up to the current position
143+
// the position refers to the beginning a quoted text
144+
145+
if (count % 2 == 1)
146+
{
147+
return pos;
148+
}
149+
}
150+
151+
// look backwards
152+
153+
for (var offset = pos - 1; offset > beginning; offset--)
154+
{
155+
if (buffer[offset] == delimiter)
156+
return offset;
157+
}
158+
159+
// if not found, look forwards
160+
161+
for (var offset = pos; offset < end; offset++)
162+
{
163+
if (buffer[offset] == delimiter)
164+
return offset;
165+
}
166+
167+
return -1;
168+
}
169+
170+
/// <summary>
171+
/// Returns the position of the beginning of line
172+
/// starting from the specified "current" position.
173+
/// </summary>
174+
/// <param name="current">The position in the current logical line.</param>
175+
internal static int GetBeginningOfLogicalLinePos(this StringBuilder buffer, int current)
176+
{
177+
int i = Math.Max(0, current);
178+
while (i > 0)
179+
{
180+
if (buffer[--i] == '\n')
181+
{
182+
i += 1;
183+
break;
184+
}
185+
}
186+
187+
return i;
188+
}
189+
190+
/// <summary>
191+
/// Returns the position of the end of the logical line
192+
/// as specified by the "current" position.
193+
/// </summary>
194+
/// <param name="current"></param>
195+
/// <returns></returns>
196+
internal static int GetEndOfLogicalLinePos(this StringBuilder buffer, int current)
197+
{
198+
var newCurrent = current;
199+
200+
for (var position = current; position < buffer.Length; position++)
201+
{
202+
if (buffer[position] == '\n')
203+
{
204+
break;
205+
}
206+
207+
newCurrent = position;
208+
}
209+
210+
return newCurrent;
211+
}
112212
}
113213
}

PSReadLine/TextObjects.Vi.cs

Lines changed: 89 additions & 5 deletions
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,77 @@ 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 = _singleton._buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, _singleton._current);
127+
if (start == -1)
128+
{
129+
Ding();
130+
return;
131+
}
132+
133+
// find the position of the ending delimiter by walking forward
134+
if (start + 1 >= _singleton._buffer.Length)
135+
{
136+
Ding();
137+
return;
138+
}
139+
140+
var end = -1;
141+
142+
for (var offset = start + 1; offset < _singleton._buffer.Length; offset++)
143+
{
144+
if (_singleton._buffer[offset] == delimiter)
145+
{
146+
end = offset;
147+
break;
148+
}
149+
if (_singleton._buffer[offset] == '\n')
150+
{
151+
break;
152+
}
153+
}
154+
155+
if (end == -1)
156+
{
157+
Ding();
158+
return;
159+
}
160+
161+
var position = start + 1;
162+
163+
// deleting multiple times
164+
// removes the surrounding quotes
165+
166+
if (numericArg > 1)
167+
{
168+
position = start;
169+
// TODO:
170+
// make sure we can do that
171+
end = end + 1;
172+
}
173+
174+
_singleton.RemoveTextToViRegister(position, end - position);
175+
_singleton.AdjustCursorPosition(position);
176+
_singleton.Render();
177+
}
178+
95179
private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null)
96180
{
97181
var delimiters = _singleton.Options.WordDelimiters;

test/StringBuilderTextObjectExtensionsTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,33 @@ 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_ViFindBeginningOfQuotedTextObjectBoundary(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+
Assert.Equal(1, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 0));
90+
Assert.Equal(1, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 1));
91+
Assert.Equal(1, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 2));
92+
Assert.Equal(1, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 3));
93+
Assert.Equal(3, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 4));
94+
Assert.Equal(5, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 5));
95+
Assert.Equal(5, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 6));
96+
Assert.Equal(5, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 7));
97+
Assert.Equal(7, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 8));
98+
Assert.Equal(9, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 9));
99+
Assert.Equal(9, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 10));
100+
Assert.Equal(9, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 11));
101+
102+
Assert.Equal(9, buffer.ViFindBeginningOfQuotedTextObjectBoundary(delimiter, 12));
103+
}
76104
}
77105
}

0 commit comments

Comments
 (0)