/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { /// /// Move the cursor forward to the start of the next word. /// Word boundaries are defined by a configurable set of characters. /// public static void ViNextWord(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) { return; } if (numericArg < 0) { ViBackwardWord(key, -numericArg); return; } while (numericArg-- > 0) { int i = _singleton.ViFindNextWordPoint(_singleton.Options.WordDelimiters); if (i >= _singleton._buffer.Length) { i += ViEndOfLineFactor; } _singleton.MoveCursor(Math.Max(i, 0)); } } /// /// Move the cursor back to the start of the current word, or if between words, /// the start of the previous word. Word boundaries are defined by a configurable /// set of characters. /// public static void ViBackwardWord(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) { return; } if (numericArg < 0) { ViNextWord(key, -numericArg); return; } while (numericArg-- > 0) { _singleton.MoveCursor(_singleton.ViFindPreviousWordPoint(_singleton.Options.WordDelimiters)); } } /// /// Moves the cursor back to the beginning of the previous word, using only white space as delimiters. /// public static void ViBackwardGlob(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) { return; } int i = _singleton._current; while (numericArg-- > 0) { i = _singleton.ViFindPreviousGlob(i - 1); } _singleton.MoveCursor(i); } /// /// Moves to the next word, using only white space as a word delimiter. /// public static void ViNextGlob(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) { return; } int i = _singleton._current; while (numericArg-- > 0) { i = _singleton.ViFindNextGlob(i); } int newPosition = Math.Min(i, Math.Max(0, _singleton._buffer.Length - 1)); if (newPosition != _singleton._current) { _singleton.MoveCursor(newPosition); } else { Ding(); } } /// /// Moves the cursor to the end of the word, using only white space as delimiters. /// public static void ViEndOfGlob(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) { return; } if (numericArg < 0) { ViEndOfPreviousGlob(key, -numericArg); return; } while (numericArg-- > 0) { _singleton.MoveCursor(_singleton.ViFindEndOfGlob()); } } /// /// Moves to the end of the previous word, using only white space as a word delimiter. /// public static void ViEndOfPreviousGlob(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) { return; } if (numericArg < 0) { ViEndOfGlob(key, -numericArg); return; } while (numericArg-- > 0) { _singleton.MoveCursor(_singleton.ViFindEndOfPreviousGlob()); } } /// /// Returns 0 if the cursor is allowed to go past the last character in the line, -1 otherwise. /// /// private static int ViEndOfLineFactor => InViCommandMode() ? -1 : 0; /// /// Move the cursor to the end of the current logical line. /// public static void MoveToEndOfLine(ConsoleKeyInfo? key = null, object arg = null) { var eol = GetEndOfLogicalLinePos(_singleton._current); if (eol != _singleton._current) { _singleton.MoveCursor(eol); } _singleton._moveToEndOfLineCommandCount++; _singleton._moveToLineDesiredColumn = int.MaxValue; } /// /// Move the cursor forward to the end of the current word, or if between words, /// to the end of the next word. Word boundaries are defined by a configurable /// set of characters. /// public static void NextWordEnd(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; for (; qty > 0 && _singleton._current < _singleton._buffer.Length - 1; qty--) { _singleton.MoveCursor(_singleton.ViFindNextWordEnd(_singleton.Options.WordDelimiters)); } } /// /// Move to the column indicated by arg. /// public static void GotoColumn(ConsoleKeyInfo? key = null, object arg = null) { int col = arg as int? ?? -1; if (col < 0) { Ding(); return; } if (col < _singleton._buffer.Length + ViEndOfLineFactor) { _singleton.MoveCursor(Math.Min(col, _singleton._buffer.Length) - 1); } else { _singleton.MoveCursor(_singleton._buffer.Length + ViEndOfLineFactor); Ding(); } } /// /// Move the cursor to the first non-blank character in the line. /// public static void GotoFirstNonBlankOfLine(ConsoleKeyInfo? key = null, object arg = null) { var newCurrent = GetFirstNonBlankOfLogicalLinePos(_singleton._current); if (newCurrent != _singleton._current) { _singleton.MoveCursor(newCurrent); } } /// /// Similar to , but is character based instead of token based. /// public static void ViGotoBrace(ConsoleKeyInfo? key = null, object arg = null) { int i = _singleton.ViFindBrace(_singleton._current); if (i == _singleton._current) { Ding(); return; } _singleton.MoveCursor(i); } private int ViFindBrace(int i) { if (_buffer.Length == 0) { return i; } switch (_buffer[i]) { case '{': return ViFindForward(i, '}', withoutPassing: '{'); case '[': return ViFindForward(i, ']', withoutPassing: '['); case '(': return ViFindForward(i, ')', withoutPassing: '('); case '}': return ViFindBackward(i, '{', withoutPassing: '}'); case ']': return ViFindBackward(i, '[', withoutPassing: ']'); case ')': return ViFindBackward(i, '(', withoutPassing: ')'); default: ReadOnlySpan parentheses = stackalloc char[] { '{', '}', '(', ')', '[', ']' }; int nextParen = i; // find next of any kind of paren for (; nextParen < _buffer.Length; nextParen++) for (int idx = 0; idx < parentheses.Length; idx++) if (parentheses[idx] == _buffer[nextParen]) goto Outer; // if not found, nextParen could exceed the range if (nextParen >= _buffer.Length) return i; Outer: int match = _buffer[nextParen] switch { // if next is opening, find forward '{' => ViFindForward(nextParen, '}', withoutPassing: '{'), '[' => ViFindForward(nextParen, ']', withoutPassing: '['), '(' => ViFindForward(nextParen, ')', withoutPassing: '('), // if next is closing, find backward '}' => ViFindBackward(nextParen, '{', withoutPassing: '}'), ']' => ViFindBackward(nextParen, '[', withoutPassing: ']'), ')' => ViFindBackward(nextParen, '(', withoutPassing: ')'), _ => nextParen }; return match == nextParen ? i : match; } } private int ViFindBackward(int start, char target, char withoutPassing) { if (start == 0) { return start; } int i = start - 1; int withoutPassingCount = 0; while (i != 0 && !(_buffer[i] == target && withoutPassingCount == 0)) { if (_buffer[i] == withoutPassing) { withoutPassingCount++; } if (_buffer[i] == target) { withoutPassingCount--; } i--; } if (_buffer[i] == target && withoutPassingCount == 0) { return i; } return start; } private int ViFindForward(int start, char target, char withoutPassing) { if (IsAtEndOfLine(start)) { return start; } int i = start + 1; int withoutPassingCount = 0; while (!IsAtEndOfLine(i) && !(_buffer[i] == target && withoutPassingCount == 0)) { if (_buffer[i] == withoutPassing) { withoutPassingCount++; } if (_buffer[i] == target) { withoutPassingCount--; } i++; } if (_buffer[i] == target && withoutPassingCount == 0) { return i; } return start; } } }