Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Vi-Mode] Supports di' and di" commands (quoted text objects) #3791

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions PSReadLine/KeyBindings.vi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ private void SetDefaultViBindings()

_viChordTextObjectsTable = new Dictionary<PSKeyInfo, KeyHandler>
{
{ Keys.DQuote, MakeKeyHandler(ViHandleTextObject, "QuoteTextObject")},
{ Keys.SQuote, MakeKeyHandler(ViHandleTextObject, "QuoteTextObject")},
{ Keys.W, MakeKeyHandler(ViHandleTextObject, "WordTextObject")},
};

Expand Down
30 changes: 2 additions & 28 deletions PSReadLine/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,7 @@ public partial class PSConsoleReadLine
/// </summary>
/// <param name="current">The position in the current logical line.</param>
private static int GetBeginningOfLinePos(int current)
{
int i = Math.Max(0, current);
while (i > 0)
{
if (_singleton._buffer[--i] == '\n')
{
i += 1;
break;
}
}

return i;
}
=> _singleton._buffer.GetBeginningOfLogicalLinePos(current);

/// <summary>
/// Returns the position of the beginning of line
Expand Down Expand Up @@ -66,21 +54,7 @@ private static int GetBeginningOfNthLinePos(int lineIndex)
/// <param name="current"></param>
/// <returns></returns>
private static int GetEndOfLogicalLinePos(int current)
{
var newCurrent = current;

for (var position = current; position < _singleton._buffer.Length; position++)
{
if (_singleton._buffer[position] == '\n')
{
break;
}

newCurrent = position;
}

return newCurrent;
}
=> _singleton._buffer.GetEndOfLogicalLinePos(current);

/// <summary>
/// Returns the position of the end of the logical line
Expand Down
157 changes: 157 additions & 0 deletions PSReadLine/StringBuilderTextObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Management.Automation;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added by mistake? It doesn't seem to be needed by the code changes in this file.

using System.Text;

namespace Microsoft.PowerShell
Expand Down Expand Up @@ -109,5 +110,161 @@ public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buf
// Make sure end includes the starting position.
return Math.Max(i, position);
}

/// <summary>
/// Returns the span of text within the quotes relative to the specified position, in the corresponding logical line.
/// If the position refers to the given start delimiter, the method returns the position immediately.
/// If not, it first attempts to look backwards to find the start delimiter and returns its position if found.
/// Otherwise, it look forwards to find the start delimiter and returns its position if found.
/// Otherwise, it returns (-1, -1).
///
/// If a start delimiter is found, this method then attempts to find the end delimiter within the logical line.
/// Otherwise, it returns (-1, -1).
///
/// This method supports VI i' and i" text objects.
/// </summary>
public static (int Start, int End) ViFindSpanOfInnerQuotedTextObjectBoundary(this StringBuilder buffer, char delimiter, int position, int repeated = 1)
{
// Cursor may be past the end of the buffer when calling this method
// this may happen if the cursor is at the beginning of a new line.

var pos = Math.Min(position, buffer.Length - 1);

// restrict this method to the logical line
// corresponding to the given position

var startOfLine = buffer.GetBeginningOfLogicalLinePos(pos);
var endOfLine = buffer.GetEndOfLogicalLinePos(pos);

var start = -1;
var end = -1;

// if on a quote we may be on a beginning or end quote
// we need to parse the line to find out

if (buffer[pos] == delimiter)
{
var count = 1;
for (var offset = pos - 1; offset > startOfLine; offset--)
{
if (buffer[offset] == delimiter)
count++;
}

// if there are an odd number of quotes up to the current position
// the position refers to the beginning a quoted text

if (count % 2 == 1)
{
start = pos;
}
}

// else look backwards

if (start == -1)
{
for (var offset = pos - 1; offset > startOfLine; offset--)
{
if (buffer[offset] == delimiter)
{
start = offset;
break;
}
}
}

// if not found, look forwards

if (start == -1)
{
for (var offset = pos; offset < endOfLine; offset++)
{
if (buffer[offset] == delimiter)
{
start = offset;
break;
}
}
}

// attempts to find the end quote

if (start != -1 && start < endOfLine)
{
for (var offset = start + 1; offset < buffer.Length; offset++)
{
if (buffer[offset] == delimiter)
{
end = offset;
break;
}
if (buffer[offset] == '\n')
{
break;
}
}
}

// adjust span boundaries based upon
// the number of repeatitions

if (start != -1 && end != -1)
{
if (repeated > 1)
{
end++;
}
else
{
start++;
}
}

return (start, end);
}

/// <summary>
/// Returns the position of the beginning of line
/// starting from the specified "current" position.
/// </summary>
/// <param name="current">The position in the current logical line.</param>
internal static int GetBeginningOfLogicalLinePos(this StringBuilder buffer, int current)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems GetBeginningOfLogicalLinePos and GetEndOfLogicalLinePos shoudl be put in StringBuilderLinewiseExtensions instead.

{
int i = Math.Max(0, current);
while (i > 0)
{
if (buffer[--i] == '\n')
{
i += 1;
break;
}
}

return i;
}

/// <summary>
/// Returns the position of the end of the logical line
/// as specified by the "current" position.
/// </summary>
/// <param name="current"></param>
/// <returns></returns>
internal static int GetEndOfLogicalLinePos(this StringBuilder buffer, int current)
{
var newCurrent = current;

for (var position = current; position < buffer.Length; position++)
{
if (buffer[position] == '\n')
{
break;
}

newCurrent = position;
}

return newCurrent;
}
}
}
56 changes: 51 additions & 5 deletions PSReadLine/TextObjects.Vi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;

using System.Runtime.CompilerServices;

namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
Expand All @@ -22,9 +23,17 @@ internal enum TextObjectSpan
private TextObjectOperation _textObjectOperation = TextObjectOperation.None;
private TextObjectSpan _textObjectSpan = TextObjectSpan.None;

private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, KeyHandler>> _textObjectHandlers = new()
private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, Dictionary<PSKeyInfo, KeyHandler>>> _textObjectHandlers = new()
{
[TextObjectOperation.Delete] = new() { [TextObjectSpan.Inner] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord") },
[TextObjectOperation.Delete] = new()
{
[TextObjectSpan.Inner] = new()
{
[Keys.DQuote] = MakeKeyHandler(ViDeleteInnerDQuote, "ViDeleteInnerDQuote"),
[Keys.SQuote] = MakeKeyHandler(ViDeleteInnerSQuote, "ViDeleteInnerSQuote"),
[Keys.W] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord"),
}
},
};

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

private static void ViHandleTextObject(ConsoleKeyInfo? key = null, object arg = null)
{
if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectHandler) ||
!textObjectHandler.TryGetValue(_singleton._textObjectSpan, out var handler))
System.Diagnostics.Debug.Assert(key != null);
var keyInfo = PSKeyInfo.FromConsoleKeyInfo(key.Value);

if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectSpanHandlers) ||
!textObjectSpanHandlers.TryGetValue(_singleton._textObjectSpan, out var textObjectKeyHandlers) ||
!textObjectKeyHandlers.TryGetValue(keyInfo, out var handler))
{
ResetTextObjectState();
Ding();
Expand All @@ -92,6 +105,39 @@ private static void ResetTextObjectState()
_singleton._textObjectSpan = TextObjectSpan.None;
}

private static void ViDeleteInnerSQuote(ConsoleKeyInfo? key = null, object arg = null)
=> ViDeleteInnerQuotes('\'', key, arg);
private static void ViDeleteInnerDQuote(ConsoleKeyInfo? key = null, object arg = null)
=> ViDeleteInnerQuotes('\"', key, arg);

private static void ViDeleteInnerQuotes(char delimiter, ConsoleKeyInfo? key = null, object arg = null)
{
if (!TryGetArgAsInt(arg, out var numericArg, 1))
{
return;
}

if (_singleton._buffer.Length == 0)
{
Ding();
return;
}

var (start, end) = _singleton._buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, _singleton._current, repeated: numericArg);

if (start == -1 || end == -1)
{
Ding();
return;
}

var position = start;

_singleton.RemoveTextToViRegister(position, end - position);
_singleton.AdjustCursorPosition(position);
_singleton.Render();
}

private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null)
{
var delimiters = _singleton.Options.WordDelimiters;
Expand Down
46 changes: 46 additions & 0 deletions test/StringBuilderTextObjectExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,51 @@ public void StringBuilderTextObjectExtensions_ViFindBeginningOfNextWordObjectBou
Assert.Equal(46, buffer.ViFindBeginningOfNextWordObjectBoundary(45, wordDelimiters));
Assert.Equal(50, buffer.ViFindBeginningOfNextWordObjectBoundary(46, wordDelimiters));
}

[Theory]
[InlineData('\'')]
[InlineData('\"')]
public void StringBuilderTextObjectExtensions_ViFindSpanOfInnerQuotedTextObjectBoundary(char delimiter)
{
var buffer = new StringBuilder($"_{delimiter}_{delimiter} {delimiter}_{delimiter} {delimiter}_{delimiter}");

// text: _"_" "_" "_"
// position: 012345678901
// - 1
// boundary: 111135557888

// when invoked once, the span is within the quotes

Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 0, repeated: 1));
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 1, repeated: 1));
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 2, repeated: 1));
Assert.Equal((2, 3), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 3, repeated: 1));
Assert.Equal((4, 5), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 4, repeated: 1));
Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 5, repeated: 1));
Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 6, repeated: 1));
Assert.Equal((6, 7), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 7, repeated: 1));
Assert.Equal((8, 9), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 8, repeated: 1));
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 9, repeated: 1));
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 10, repeated: 1));
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 11, repeated: 1));
Assert.Equal((10, 11), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 12, repeated: 1));

// when invoked more than once, the span is around the quotes

Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 0, repeated: 42));
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 1, repeated: 42));
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 2, repeated: 42));
Assert.Equal((1, 4), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 3, repeated: 42));
Assert.Equal((3, 6), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 4, repeated: 42));
Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 5, repeated: 42));
Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 6, repeated: 42));
Assert.Equal((5, 8), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 7, repeated: 42));
Assert.Equal((7, 10), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 8, repeated: 42));
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 9, repeated: 42));
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 10, repeated: 42));
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 11, repeated: 42));
Assert.Equal((9, 12), buffer.ViFindSpanOfInnerQuotedTextObjectBoundary(delimiter, 12, repeated: 42));

}
}
}
Loading