Skip to content

Commit

Permalink
Supports di' and di" text objects
Browse files Browse the repository at this point in the history
  • Loading branch information
springcomp committed Aug 27, 2023
1 parent 0d9767b commit 2495382
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 33 deletions.
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;
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)
{
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

0 comments on commit 2495382

Please sign in to comment.