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 'diw' text-object command. #2059

Merged
merged 9 commits into from
Aug 14, 2023
2 changes: 1 addition & 1 deletion PSReadLine/Cmdlets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public class PSConsoleReadLineOptions
public const int DefaultCompletionQueryItems = 100;

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

/// <summary>
/// When ringing the bell, what should be done?
Expand Down
8 changes: 8 additions & 0 deletions PSReadLine/KeyBindings.vi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ internal static ConsoleColor AlternateBackground(ConsoleColor bg)
private static Dictionary<PSKeyInfo, KeyHandler> _viChordYTable;
private static Dictionary<PSKeyInfo, KeyHandler> _viChordDGTable;

private static Dictionary<PSKeyInfo, KeyHandler> _viChordTextObjectsTable;

private static Dictionary<PSKeyInfo, Dictionary<PSKeyInfo, KeyHandler>> _viCmdChordTable;
private static Dictionary<PSKeyInfo, Dictionary<PSKeyInfo, KeyHandler>> _viInsChordTable;

Expand Down Expand Up @@ -238,6 +240,7 @@ private void SetDefaultViBindings()
{ Keys.ucG, MakeKeyHandler( DeleteEndOfBuffer, "DeleteEndOfBuffer") },
{ Keys.ucE, MakeKeyHandler( ViDeleteEndOfGlob, "ViDeleteEndOfGlob") },
{ Keys.H, MakeKeyHandler( BackwardDeleteChar, "BackwardDeleteChar") },
{ Keys.I, MakeKeyHandler( ViChordDeleteTextObject, "ChordViTextObject") },
{ Keys.J, MakeKeyHandler( DeleteNextLines, "DeleteNextLines") },
{ Keys.K, MakeKeyHandler( DeletePreviousLines, "DeletePreviousLines") },
{ Keys.L, MakeKeyHandler( DeleteChar, "DeleteChar") },
Expand Down Expand Up @@ -296,6 +299,11 @@ private void SetDefaultViBindings()
{ Keys.Percent, MakeKeyHandler( ViYankPercent, "ViYankPercent") },
};

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

_viChordDGTable = new Dictionary<PSKeyInfo, KeyHandler>
{
{ Keys.G, MakeKeyHandler( DeleteRelativeLines, "DeleteRelativeLines") },
Expand Down
13 changes: 2 additions & 11 deletions PSReadLine/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,14 @@ private static int GetFirstNonBlankOfLogicalLinePos(int current)
var beginningOfLine = GetBeginningOfLinePos(current);

var newCurrent = beginningOfLine;
var buffer = _singleton._buffer;

while (newCurrent < _singleton._buffer.Length && IsVisibleBlank(newCurrent))
while (newCurrent < buffer.Length && buffer.IsVisibleBlank(newCurrent))
{
newCurrent++;
}

return newCurrent;
}

private static bool IsVisibleBlank(int newCurrent)
{
var c = _singleton._buffer[newCurrent];

// [:blank:] of vim's pattern matching behavior
// defines blanks as SPACE and TAB characters.

return c == ' ' || c == '\t';
}
}
}
6 changes: 3 additions & 3 deletions PSReadLine/Prediction.Views.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1513,12 +1513,12 @@ internal int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimit
}

int i = currentIndex;
if (!_singleton.InWord(_suggestionText[i], wordDelimiters))
if (!Character.IsInWord(_suggestionText[i], wordDelimiters))
{
// Scan to end of current non-word region
while (++i < _suggestionText.Length)
{
if (_singleton.InWord(_suggestionText[i], wordDelimiters))
if (Character.IsInWord(_suggestionText[i], wordDelimiters))
{
break;
}
Expand All @@ -1529,7 +1529,7 @@ internal int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimit
{
while (++i < _suggestionText.Length)
{
if (!_singleton.InWord(_suggestionText[i], wordDelimiters))
if (!Character.IsInWord(_suggestionText[i], wordDelimiters))
{
if (_suggestionText[i] == ' ')
{
Expand Down
78 changes: 78 additions & 0 deletions PSReadLine/StringBuilderCharacterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Text;

namespace Microsoft.PowerShell
{
internal static class StringBuilderCharacterExtensions
{
/// <summary>
/// Returns true if the character at the specified position is a visible whitespace character.
/// A blank character is defined as a SPACE or a TAB.
/// </summary>
/// <param name="buffer"></param>
/// <param name="i"></param>
/// <returns></returns>
public static bool IsVisibleBlank(this StringBuilder buffer, int i)
{
var c = buffer[i];

// [:blank:] of vim's pattern matching behavior
// defines blanks as SPACE and TAB characters.

return c == ' ' || c == '\t';
}

/// <summary>
/// Returns true if the character at the specified position is
/// not present in a list of word-delimiter characters.
/// </summary>
/// <param name="buffer"></param>
/// <param name="i"></param>
/// <param name="wordDelimiters"></param>
/// <returns></returns>
public static bool InWord(this StringBuilder buffer, int i, string wordDelimiters)
{
return Character.IsInWord(buffer[i], wordDelimiters);
}

/// <summary>
/// Returns true if the character at the specified position is
/// at the end of the buffer
/// </summary>
/// <param name="buffer"></param>
/// <param name="i"></param>
/// <returns></returns>
public static bool IsAtEndOfBuffer(this StringBuilder buffer, int i)
{
return i >= (buffer.Length - 1);
}

/// <summary>
/// Returns true if the character at the specified position is
/// a unicode whitespace character.
/// </summary>
/// <param name="buffer"></param>
/// <param name="i"></param>
/// <returns></returns>
public static bool IsWhiteSpace(this StringBuilder buffer, int i)
{
// Treat just beyond the end of buffer as whitespace because
// it looks like whitespace to the user even though they haven't
// entered a character yet.
return i >= buffer.Length || char.IsWhiteSpace(buffer[i]);
}
}

public static class Character
{
/// <summary>
/// Returns true if the character not present in a list of word-delimiter characters.
/// </summary>
/// <param name="c"></param>
/// <param name="wordDelimiters"></param>
/// <returns></returns>
public static bool IsInWord(char c, string wordDelimiters)
{
return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ internal static Range GetRange(this StringBuilder buffer, int lineIndex, int lin
endPosition - startPosition + 1
);
}

/// <summary>
/// Returns true if the specified position is on an empty logical line.
/// </summary>
/// <param name="buffer"></param>
/// <param name="cursor"></param>
/// <returns></returns>
public static bool IsLogigalLineEmpty(this StringBuilder buffer, int cursor)
{
// the cursor is on a logical line considered empty if...
return
// the entire buffer is empty (by definition),
buffer.Length == 0 ||
// or the cursor sits at the start of the empty last line,
// meaning that it is past the end of the buffer and the
// last character in the buffer is a newline character,
(cursor == buffer.Length && buffer[cursor - 1] == '\n') ||
// or if the cursor is on a newline character.
(cursor > 0 && buffer[cursor] == '\n');
}
}

internal static class StringBuilderPredictionExtensions
Expand Down
113 changes: 113 additions & 0 deletions PSReadLine/StringBuilderTextObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Text;

namespace Microsoft.PowerShell
{
internal static class StringBuilderTextObjectExtensions
{
private const string WhiteSpace = " \n\t";

/// <summary>
/// Returns the position of the beginning of the current word as delimited by white space and delimiters
/// This method differs from <see cref="ViFindPreviousWordPoint(string)"/>:
/// - When the cursor location is on the first character of a word, <see cref="ViFindPreviousWordPoint(string)"/>
/// returns the position of the previous word, whereas this method returns the cursor location.
/// - When the cursor location is in a word, both methods return the same result.
/// This method supports VI "iw" text object.
/// </summary>
public static int ViFindBeginningOfWordObjectBoundary(this StringBuilder buffer, int position, string wordDelimiters)
{
// 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 i = Math.Min(position, buffer.Length - 1);

// If starting on a word consider a text object as a sequence of characters excluding the delimiters,
// otherwise, consider a word as a sequence of delimiters.
var delimiters = wordDelimiters;
var isInWord = buffer.InWord(i, wordDelimiters);

if (isInWord)
{
// For the purpose of this method, whitespace character is considered a delimiter.
delimiters += WhiteSpace;
}
else
{
char c = buffer[i];
if ((wordDelimiters + '\n').IndexOf(c) == -1 && char.IsWhiteSpace(c))
{
// Current position points to a whitespace that is not a newline.
delimiters = WhiteSpace;
}
else
{
delimiters += '\n';
}
}

var isTextObjectChar = isInWord
? (Func<char, bool>)(c => delimiters.IndexOf(c) == -1)
: c => delimiters.IndexOf(c) != -1;

var beginning = i;
while (i >= 0 && isTextObjectChar(buffer[i]))
{
beginning = i--;
}

return beginning;
}

/// <summary>
/// Finds the position of the beginning of the next word object starting from the specified position.
/// If positioned on the last word in the buffer, returns buffer length + 1.
/// This method supports VI "iw" text-object.
/// iw: "inner word", select words. White space between words is counted too.
/// </summary>
public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buffer, int position, string wordDelimiters)
{
// 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 i = Math.Min(position, buffer.Length - 1);

// Always skip the first newline character.
if (buffer[i] == '\n' && i < buffer.Length - 1)
{
++i;
}

// If starting on a word consider a text object as a sequence of characters excluding the delimiters,
// otherwise, consider a word as a sequence of delimiters.
var delimiters = wordDelimiters;
var isInWord = buffer.InWord(i, wordDelimiters);

if (isInWord)
{
delimiters += WhiteSpace;
}
else if (char.IsWhiteSpace(buffer[i]))
{
delimiters = " \t";
}

var isTextObjectChar = isInWord
? (Func<char, bool>)(c => delimiters.IndexOf(c) == -1)
: c => delimiters.IndexOf(c) != -1;

// Try to skip a second newline characters to replicate vim behaviour.
if (buffer[i] == '\n' && i < buffer.Length - 1)
{
++i;
}

// Skip to next non-word characters.
while (i < buffer.Length && isTextObjectChar(buffer[i]))
{
++i;
}

// Make sure end includes the starting position.
return Math.Max(i, position);
}
}
}
Loading