Skip to content

Commit

Permalink
[vi-mode] Supports the text-object command diw (#2059)
Browse files Browse the repository at this point in the history
  • Loading branch information
springcomp authored Aug 14, 2023
1 parent 4d78ce1 commit d7b9f82
Show file tree
Hide file tree
Showing 13 changed files with 709 additions and 26 deletions.
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

0 comments on commit d7b9f82

Please sign in to comment.