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> _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(); } /// /// Attempt to set the cursor at the specified position. /// /// /// 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; } } }