Skip to content

[vi-mode] Supports 'diw' text-object command. #2059

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

Merged
merged 9 commits into from
Aug 14, 2023
Merged
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: 1 addition & 1 deletion PSReadLine/Cmdlets.cs
Original file line number Diff line number Diff line change
@@ -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?
8 changes: 8 additions & 0 deletions PSReadLine/KeyBindings.vi.cs
Original file line number Diff line number Diff line change
@@ -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;

@@ -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") },
@@ -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") },
13 changes: 2 additions & 11 deletions PSReadLine/Position.cs
Original file line number Diff line number Diff line change
@@ -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
@@ -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;
}
@@ -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] == ' ')
{
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
@@ -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
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);
}
}
}
181 changes: 181 additions & 0 deletions PSReadLine/TextObjects.Vi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;

namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
internal enum TextObjectOperation
{
None,
Change,
Delete,
}

internal enum TextObjectSpan
{
None,
Around,
Inner,
}

private TextObjectOperation _textObjectOperation = TextObjectOperation.None;
private TextObjectSpan _textObjectSpan = TextObjectSpan.None;

private readonly Dictionary<TextObjectOperation, Dictionary<TextObjectSpan, KeyHandler>> _textObjectHandlers = new()
{
[TextObjectOperation.Delete] = new() { [TextObjectSpan.Inner] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord") },
};

private void ViChordDeleteTextObject(ConsoleKeyInfo? key = null, object arg = null)
{
_textObjectOperation = TextObjectOperation.Delete;
ViChordTextObject(key, arg);
}

private void ViChordTextObject(ConsoleKeyInfo? key = null, object arg = null)
{
if (!key.HasValue)
{
ResetTextObjectState();
throw new ArgumentNullException(nameof(key));
}

_textObjectSpan = GetRequestedTextObjectSpan(key.Value);

// Handle text object
var textObjectKey = ReadKey();
if (_viChordTextObjectsTable.TryGetValue(textObjectKey, out _))
{
_singleton.ProcessOneKey(textObjectKey, _viChordTextObjectsTable, ignoreIfNoAction: true, arg: arg);
}
else
{
ResetTextObjectState();
Ding();
}
}

private TextObjectSpan GetRequestedTextObjectSpan(ConsoleKeyInfo key)
{
if (key.KeyChar == 'i')
{
return TextObjectSpan.Inner;
}
else if (key.KeyChar == 'a')
{
return TextObjectSpan.Around;
}
else
{
System.Diagnostics.Debug.Assert(false);
throw new NotSupportedException();
}
}

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))
{
ResetTextObjectState();
Ding();
return;
}

handler.Action(key, arg);
}

private static void ResetTextObjectState()
{
_singleton._textObjectOperation = TextObjectOperation.None;
_singleton._textObjectSpan = TextObjectSpan.None;
}

private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null)
{
var delimiters = _singleton.Options.WordDelimiters;

if (!TryGetArgAsInt(arg, out var numericArg, 1))
{
return;
}

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

// Unless at the end of the buffer a single delete word should not delete backwards
// so if the cursor is on an empty line, do nothing.
if (numericArg == 1 &&
_singleton._current < _singleton._buffer.Length &&
_singleton._buffer.IsLogigalLineEmpty(_singleton._current))
{
return;
}

var start = _singleton._buffer.ViFindBeginningOfWordObjectBoundary(_singleton._current, delimiters);
var end = _singleton._current;

// Attempting to find a valid position for multiple words.
// If no valid position is found, this is a no-op
{
while (numericArg-- > 0 && end < _singleton._buffer.Length)
{
end = _singleton._buffer.ViFindBeginningOfNextWordObjectBoundary(end, delimiters);
}

// Attempting to delete too many words should ding.
if (numericArg > 0)
{
Ding();
return;
}
}

if (end > 0 && _singleton._buffer.IsAtEndOfBuffer(end - 1) && _singleton._buffer.InWord(end - 1, delimiters))
{
_singleton._shouldAppend = true;
}

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

/// <summary>
/// Attempt to set the cursor at the specified position.
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
private int AdjustCursorPosition(int position)
{
// This method might prove useful in a more general case.
if (_buffer.Length == 0)
{
_current = 0;
return 0;
}

var maxPosition = _buffer[_buffer.Length - 1] == '\n'
? _buffer.Length
: _buffer.Length - 1;

var newCurrent = Math.Min(position, maxPosition);
var beginning = GetBeginningOfLinePos(newCurrent);

if (newCurrent < _buffer.Length && _buffer[newCurrent] == '\n' && (newCurrent + ViEndOfLineFactor > beginning))
{
newCurrent += ViEndOfLineFactor;
}

_current = newCurrent;
return newCurrent;
}
}
}
8 changes: 1 addition & 7 deletions PSReadLine/Words.cs
Original file line number Diff line number Diff line change
@@ -90,13 +90,7 @@ private Token FindToken(int current, FindTokenMode mode)

private bool InWord(int index, string wordDelimiters)
{
char c = _buffer[index];
return InWord(c, wordDelimiters);
}

private bool InWord(char c, string wordDelimiters)
{
return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0;
return _buffer.InWord(index, wordDelimiters);
}

/// <summary>
7 changes: 3 additions & 4 deletions PSReadLine/Words.vi.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/

using System;

namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
@@ -106,10 +108,7 @@ private int ViFindNextWordFromWord(int i, string wordDelimiters)
/// </summary>
private bool IsWhiteSpace(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]);
return _buffer.IsWhiteSpace(i);
}

/// <summary>
46 changes: 46 additions & 0 deletions test/StringBuilderCharacterExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.PowerShell;
using System.Text;
using Xunit;

namespace Test
{
public sealed class StringBuilderCharacterExtensionsTests
{
[Fact]
public void StringBuilderCharacterExtensions_IsVisibleBlank()
{
var buffer = new StringBuilder(" \tn");

// system under test

Assert.True(buffer.IsVisibleBlank(0));
Assert.True(buffer.IsVisibleBlank(1));
Assert.False(buffer.IsVisibleBlank(2));
}

[Fact]
public void StringBuilderCharacterExtensions_InWord()
{
var buffer = new StringBuilder("hello, world!");
const string wordDelimiters = " ";

// system under test

Assert.True(buffer.InWord(2, wordDelimiters));
Assert.True(buffer.InWord(5, wordDelimiters));
}

[Fact]
public void StringBuilderCharacterExtensions_IsWhiteSpace()
{
var buffer = new StringBuilder("a c");


// system under test

Assert.False(buffer.IsWhiteSpace(0));
Assert.True(buffer.IsWhiteSpace(1));
Assert.False(buffer.IsWhiteSpace(2));
}
}
}
77 changes: 77 additions & 0 deletions test/StringBuilderTextObjectExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Microsoft.PowerShell;
using System.Text;
using Xunit;

namespace Test
{
public sealed class StringBuilderTextObjectExtensionsTests
{
[Fact]
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary()
{
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;

var buffer = new StringBuilder("Hello, world!\ncruel world.\none\n\n\n\n\ntwo\n three four.");
Assert.Equal(0, buffer.ViFindBeginningOfWordObjectBoundary(1, wordDelimiters));
}

[Fact]
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_whitespace()
{
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;

var buffer = new StringBuilder("Hello, world!");
Assert.Equal(6, buffer.ViFindBeginningOfWordObjectBoundary(7, wordDelimiters));
}

[Fact]
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_backwards()
{
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;

var buffer = new StringBuilder("Hello!\nworld!");
Assert.Equal(5, buffer.ViFindBeginningOfWordObjectBoundary(6, wordDelimiters));
}

[Fact]
public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_end_of_buffer()
{
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;

var buffer = new StringBuilder("Hello, world!");
Assert.Equal(12, buffer.ViFindBeginningOfWordObjectBoundary(buffer.Length, wordDelimiters));
}

[Fact]
public void StringBuilderTextObjectExtensions_ViFindBeginningOfNextWordObjectBoundary()
{
const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters;

var buffer = new StringBuilder("Hello, world!\ncruel world.\none\n\n\n\n\ntwo\n three four.");

// Words |Hello|,| |world|!|\n|cruel |world|.|\n|one\n\n|\n\n|\n|two|\n |three| |four|.|
// Pos 01234 5 6 78901 2 _3 456789 01234 5 _6 789_0_1 _2_3 _4 567 _89 01234 5 6789 0
// Pos 0 1 2 3 4 5

// system under test

Assert.Equal(5, buffer.ViFindBeginningOfNextWordObjectBoundary(0, wordDelimiters));
Assert.Equal(6, buffer.ViFindBeginningOfNextWordObjectBoundary(5, wordDelimiters));
Assert.Equal(7, buffer.ViFindBeginningOfNextWordObjectBoundary(6, wordDelimiters));
Assert.Equal(12, buffer.ViFindBeginningOfNextWordObjectBoundary(7, wordDelimiters));
Assert.Equal(13, buffer.ViFindBeginningOfNextWordObjectBoundary(12, wordDelimiters));
Assert.Equal(19, buffer.ViFindBeginningOfNextWordObjectBoundary(13, wordDelimiters));
Assert.Equal(20, buffer.ViFindBeginningOfNextWordObjectBoundary(19, wordDelimiters));
Assert.Equal(25, buffer.ViFindBeginningOfNextWordObjectBoundary(20, wordDelimiters));
Assert.Equal(26, buffer.ViFindBeginningOfNextWordObjectBoundary(25, wordDelimiters));
Assert.Equal(30, buffer.ViFindBeginningOfNextWordObjectBoundary(26, wordDelimiters));
Assert.Equal(32, buffer.ViFindBeginningOfNextWordObjectBoundary(30, wordDelimiters));
Assert.Equal(34, buffer.ViFindBeginningOfNextWordObjectBoundary(32, wordDelimiters));
Assert.Equal(38, buffer.ViFindBeginningOfNextWordObjectBoundary(34, wordDelimiters));
Assert.Equal(40, buffer.ViFindBeginningOfNextWordObjectBoundary(38, wordDelimiters));
Assert.Equal(45, buffer.ViFindBeginningOfNextWordObjectBoundary(40, wordDelimiters));
Assert.Equal(46, buffer.ViFindBeginningOfNextWordObjectBoundary(45, wordDelimiters));
Assert.Equal(50, buffer.ViFindBeginningOfNextWordObjectBoundary(46, wordDelimiters));
}
}
}
176 changes: 176 additions & 0 deletions test/TextObjects.Vi.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using Microsoft.PowerShell;
using Xunit;

namespace Test
{
public partial class ReadLine
{
[SkippableFact]
public void ViTextObject_diw()
{
TestSetup(KeyMode.Vi);

Test("\"hello, \ncruel world!\"", Keys(
_.DQuote,
"hello, world!", _.Enter,
"cruel world!", _.DQuote,
_.Escape,

// move cursor to the 'o' in 'world'
"gg9l",

// delete text object
"diw",
CheckThat(() => AssertLineIs("\"hello, !\ncruel world!\"")),
CheckThat(() => AssertCursorLeftIs(8)),

// delete
"diw",
CheckThat(() => AssertLineIs("\"hello, \ncruel world!\"")),
CheckThat(() => AssertCursorLeftIs(7))
));
}

[SkippableFact]
public void ViTextObject_diw_digit_arguments()
{
TestSetup(KeyMode.Vi);

Test("\"hello, world!\"", Keys(
_.DQuote,
"hello, world!", _.Enter,
"cruel world!", _.DQuote,
_.Escape,

// move cursor to the 'o' in 'world'
"gg9l",

// delete text object
"diw",
CheckThat(() => AssertLineIs("\"hello, !\ncruel world!\"")),
CheckThat(() => AssertCursorLeftIs(8)),

// delete multiple text objects (spans multiple lines)
"3diw",
CheckThat(() => AssertLineIs("\"hello, world!\"")),
CheckThat(() => AssertCursorLeftIs(8))
));
}


[SkippableFact]
public void ViTextObject_diw_noop()
{
TestSetup(KeyMode.Vi);

TestMustDing("\"hello, world!\ncruel world!\"", Keys(
_.DQuote,
"hello, world!", _.Enter,
"cruel world!", _.DQuote,
_.Escape,

// move cursor to the 'o' in 'world'
"gg9l",

// attempting to delete too many words must ding
"1274diw"
));
}

[SkippableFact]
public void ViTextObject_diw_empty_line()
{
TestSetup(KeyMode.Vi);

var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;

Test("\"\nhello, world!\n\noh, bitter world!\n\"", Keys(
_.DQuote, _.Enter,
"hello, world!", _.Enter,
_.Enter,
"oh, bitter world!", _.Enter,
_.DQuote, _.Escape,

// move cursor to the second line
"ggjj",

// deleting single word cannot move backwards to previous line (noop)
"diw",
CheckThat(() => AssertLineIs("\"\nhello, world!\n\noh, bitter world!\n\""))
));
}

[SkippableFact]
public void ViTextObject_diw_end_of_buffer()
{
TestSetup(KeyMode.Vi);

var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;

Test("", Keys(
_.DQuote,
"hello, world!", _.Enter,
"cruel world!", _.DQuote,
_.Escape,

// move to end of buffer
"G$",

// delete text object (deletes backwards)
"diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel world")),
"diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel ")),
"diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel")),
"diw", CheckThat(() => AssertLineIs("\"hello, world!\n")),
"diw", CheckThat(() => AssertLineIs("\"hello, world")),
"diw", CheckThat(() => AssertLineIs("\"hello, ")),
"diw", CheckThat(() => AssertLineIs("\"hello,")),
"diw", CheckThat(() => AssertLineIs("\"hello")),
"diw", CheckThat(() => AssertLineIs("\"")),
"diw", CheckThat(() => AssertLineIs(""))
));
}

[SkippableFact]
public void ViTextObject_diw_empty_buffer()
{
TestSetup(KeyMode.Vi);
Test("", Keys(_.Escape, "diw"));
TestMustDing("", Keys(_.Escape, "d2iw"));
}

[SkippableFact]
public void ViTextObject_diw_new_lines()
{
TestSetup(KeyMode.Vi);

var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;

Test("\"\ntwo\n\"", Keys(
_.DQuote, _.Enter,
"one", _.Enter,
_.Enter, _.Enter,
_.Enter, _.Enter,
_.Enter,
"two", _.Enter, _.DQuote,
_.Escape,

// move to the beginning of 'one'
"gg0j",

// delete text object
"2diw",
CheckThat(() => AssertLineIs("\"\n\n\n\n\ntwo\n\"")),

"ugg0j", // currently undo does not move the cursor to the correct position
// delete multiple text objects (spans multiple lines)
"3diw",
CheckThat(() => AssertLineIs("\"\n\n\ntwo\n\"")),

"ugg0j", // currently undo does not move the cursor to the correct position
// delete multiple text objects (spans multiple lines)
"4diw",
CheckThat(() => AssertLineIs("\"\ntwo\n\""))
));
}
}
}