/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Collections.Generic; using System.Globalization; namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { /// /// Remembers last history search direction. /// private bool _searchHistoryBackward = true; private class ViCharacterSearcher { private char searchChar = '\0'; private bool wasBackward; private bool wasBackoff; public static readonly ViCharacterSearcher instance = new ViCharacterSearcher(); public static bool IsRepeatable => instance.searchChar != '\0'; public static char SearchChar => instance.searchChar; public static bool WasBackward => instance.wasBackward; public static bool WasBackoff => instance.wasBackoff; public static void Set(char theChar, bool isBackward, bool isBackoff) { instance.searchChar = theChar; instance.wasBackward = isBackward; instance.wasBackoff = isBackoff; } public static void Search(char keyChar, object arg, bool backoff) { int qty = arg as int? ?? 1; for (int i = _singleton._current + 1; i < _singleton._buffer.Length; i++) { if (_singleton._buffer[i] == keyChar) { qty -= 1; if (qty == 0) { _singleton.MoveCursor(backoff ? i - 1 : i); return; } } } Ding(); } public static bool SearchDelete(char keyChar, object arg, bool backoff, Action instigator) { int qty = arg as int? ?? 1; for (int i = _singleton._current + 1; i < _singleton._buffer.Length; i++) { if (_singleton._buffer[i] == keyChar) { qty -= 1; if (qty == 0) { DeleteToEndPoint(arg, backoff ? i : i + 1, instigator); return true; } } } Ding(); return false; } public static void SearchBackward(char keyChar, object arg, bool backoff) { int qty = arg as int? ?? 1; for (int i = _singleton._current - 1; i >= 0; i--) { if (_singleton._buffer[i] == keyChar) { qty -= 1; if (qty == 0) { _singleton.MoveCursor(backoff ? i + 1 : i); return; } } } Ding(); } public static bool SearchBackwardDelete(char keyChar, object arg, bool backoff, Action instigator) { Set(keyChar, isBackward: true, isBackoff: backoff); int qty = arg as int? ?? 1; for (int i = _singleton._current - 1; i >= 0; i--) { if (_singleton._buffer[i] == keyChar) { qty -= 1; if (qty == 0) { DeleteBackwardToEndPoint(arg, backoff ? i + 1 : i, instigator); return true; } } } Ding(); return false; } } /// /// Repeat the last recorded character search. /// public static void RepeatLastCharSearch(ConsoleKeyInfo? key = null, object arg = null) { if (!ViCharacterSearcher.IsRepeatable) { Ding(); return; } if (ViCharacterSearcher.WasBackward) { ViCharacterSearcher.SearchBackward(ViCharacterSearcher.SearchChar, null, ViCharacterSearcher.WasBackoff); } else { ViCharacterSearcher.Search(ViCharacterSearcher.SearchChar, null, ViCharacterSearcher.WasBackoff); } } /// /// Repeat the last recorded character search, but in the opposite direction. /// public static void RepeatLastCharSearchBackwards(ConsoleKeyInfo? key = null, object arg = null) { if (!ViCharacterSearcher.IsRepeatable) { Ding(); return; } if (ViCharacterSearcher.WasBackward) { ViCharacterSearcher.Search(ViCharacterSearcher.SearchChar, null, ViCharacterSearcher.WasBackoff); } else { ViCharacterSearcher.SearchBackward(ViCharacterSearcher.SearchChar, null, ViCharacterSearcher.WasBackoff); } } /// /// Read the next character and then find it, going forward, and then back off a character. /// This is for 't' functionality. /// public static void SearchChar(ConsoleKeyInfo? key = null, object arg = null) { char keyChar = ReadKey().KeyChar; ViCharacterSearcher.Set(keyChar, isBackward: false, isBackoff: false); ViCharacterSearcher.Search(keyChar, arg, backoff: false); } /// /// Read the next character and then find it, going backward, and then back off a character. /// This is for 'T' functionality. /// public static void SearchCharBackward(ConsoleKeyInfo? key = null, object arg = null) { char keyChar = ReadKey().KeyChar; ViCharacterSearcher.Set(keyChar, isBackward: true, isBackoff: false); ViCharacterSearcher.SearchBackward(keyChar, arg, backoff: false); } /// /// Read the next character and then find it, going forward, and then back off a character. /// This is for 't' functionality. /// public static void SearchCharWithBackoff(ConsoleKeyInfo? key = null, object arg = null) { char keyChar = ReadKey().KeyChar; ViCharacterSearcher.Set(keyChar, isBackward: false, isBackoff: true); ViCharacterSearcher.Search(keyChar, arg, backoff: true); } /// /// Read the next character and then find it, going backward, and then back off a character. /// This is for 'T' functionality. /// public static void SearchCharBackwardWithBackoff(ConsoleKeyInfo? key = null, object arg = null) { char keyChar = ReadKey().KeyChar; ViCharacterSearcher.Set(keyChar, isBackward: true, isBackoff: true); ViCharacterSearcher.SearchBackward(keyChar, arg, backoff: true); } /// /// Exits the shell. /// public static void ViExit(ConsoleKeyInfo? key = null, object arg = null) { throw new ExitException(); } /// /// Delete to the end of the line. /// public static void DeleteToEnd(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current >= _singleton._buffer.Length) { Ding(); return; } var lineCount = _singleton.GetLogicalLineCount(); var lineIndex = _singleton.GetLogicalLineNumber() - 1; if (TryGetArgAsInt(arg, out var requestedLineCount, 1)) { var targetLineIndex = lineIndex + requestedLineCount - 1; if (targetLineIndex >= lineCount) { targetLineIndex = lineCount - 1; } var startPosition = _singleton._current; var endPosition = GetEndOfNthLogicalLinePos(targetLineIndex); var length = endPosition - startPosition + 1; if (length > 0) { _singleton.RemoveTextToViRegister( startPosition, length, DeleteToEnd, arg); // the cursor will go back one character, unless at the beginning of the line var endOfLineCursorPos = GetEndOfLogicalLinePos(_singleton._current) - 1; var beginningOfLinePos = GetBeginningOfLinePos(_singleton._current); _singleton._current = Math.Max( beginningOfLinePos, Math.Min(_singleton._current, endOfLineCursorPos)); _singleton.Render(); } } } /// /// Delete the next word. /// public static void DeleteWord(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; int endPoint = _singleton._current; for (int i = 0; i < qty; i++) { endPoint = _singleton.ViFindNextWordPoint(endPoint, _singleton.Options.WordDelimiters); } if (endPoint <= _singleton._current) { Ding(); return; } DeleteToEndPoint(arg, endPoint, DeleteWord); } private static void DeleteToEndPoint(object arg, int endPoint, Action instigator) { _singleton.RemoveTextToViRegister( _singleton._current, endPoint - _singleton._current, instigator, arg); if (_singleton._current >= _singleton._buffer.Length) { _singleton._current = Math.Max(0, _singleton._buffer.Length - 1); } _singleton.Render(); } private static void DeleteBackwardToEndPoint(object arg, int endPoint, Action instigator) { int deleteLength = _singleton._current - endPoint; _singleton.RemoveTextToViRegister( endPoint, deleteLength, instigator, arg); _singleton._current = endPoint; _singleton.Render(); } /// /// Delete the next glob (white space delimited word). /// public static void ViDeleteGlob(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; int endPoint = _singleton._current; while (qty-- > 0) { endPoint = _singleton.ViFindNextGlob(endPoint); } DeleteToEndPoint(arg, endPoint, ViDeleteGlob); } /// /// Delete to the end of the word. /// public static void DeleteEndOfWord(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; int endPoint = _singleton._current; for (int i = 0; i < qty; i++) { endPoint = _singleton.ViFindNextWordEnd(endPoint, _singleton.Options.WordDelimiters); } if (endPoint <= _singleton._current) { Ding(); return; } DeleteToEndPoint(arg, 1 + endPoint, DeleteEndOfWord); } /// /// Delete to the end of the word. /// public static void ViDeleteEndOfGlob(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; int endPoint = _singleton._current; for (int i = 0; i < qty; i++) { endPoint = _singleton.ViFindGlobEnd(endPoint); } DeleteToEndPoint(arg, 1 + endPoint, ViDeleteEndOfGlob); } /// /// Deletes until given character. /// public static void ViDeleteToChar(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToChar(keyChar, key, arg); } /// /// Deletes until given character. /// public static void ViDeleteToChar(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.Set(keyChar, isBackward: false, isBackoff: false); ViCharacterSearcher.SearchDelete(keyChar, arg, backoff: false, instigator: (_key, _arg) => ViDeleteToChar(keyChar, _key, _arg)); } /// /// Deletes backwards until given character. /// public static void ViDeleteToCharBackward(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToCharBack(keyChar, key, arg); } /// /// Deletes backwards until given character. /// public static void ViDeleteToCharBack(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.SearchBackwardDelete(keyChar, arg, backoff: false, instigator: (_key, _arg) => ViDeleteToCharBack(keyChar, _key, _arg)); } /// /// Deletes until given character. /// public static void ViDeleteToBeforeChar(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToBeforeChar(keyChar, key, arg); } /// /// Deletes until given character. /// public static void ViDeleteToBeforeChar(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.Set(keyChar, isBackward: false, isBackoff: true); ViCharacterSearcher.SearchDelete(keyChar, arg, backoff: true, instigator: (_key, _arg) => ViDeleteToBeforeChar(keyChar, _key, _arg)); } /// /// Deletes until given character. /// public static void ViDeleteToBeforeCharBackward(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToBeforeCharBack(keyChar, key, arg); } private static void ViDeleteToBeforeCharBack(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.Set(keyChar, isBackward: true, isBackoff: true); ViCharacterSearcher.SearchBackwardDelete(keyChar, arg, backoff: true, instigator: (_key, _arg) => ViDeleteToBeforeCharBack(keyChar, _key, _arg)); } /// /// Ring the bell. /// private static void Ding(ConsoleKeyInfo? key = null, object arg = null) { Ding(); } /// /// Switch the current operating mode from Vi-Insert to Vi-Command. /// public static void ViCommandMode(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._editGroupStart >= 0) { _singleton._groupUndoHelper.EndGroup(); } _singleton._dispatchTable = _viCmdKeyMap; _singleton._chordDispatchTable = _viCmdChordTable; ViBackwardChar(); _singleton.ViIndicateCommandMode(); } /// /// Switch to Insert mode. /// public static void ViInsertMode(ConsoleKeyInfo? key = null, object arg = null) { _singleton._dispatchTable = _viInsKeyMap; _singleton._chordDispatchTable = _viInsChordTable; _singleton.ViIndicateInsertMode(); } /// /// Returns true if in Vi edit mode, otherwise false. /// internal static bool InViEditMode() => _singleton.Options.EditMode == EditMode.Vi; /// /// Returns true if in Vi Command mode, otherwise false. /// public static bool InViCommandMode() => _singleton._dispatchTable == _viCmdKeyMap; /// /// Returns true if in Vi Insert mode, otherwise false. /// public static bool InViInsertMode() => _singleton._dispatchTable == _viInsKeyMap; /// /// Temporarily swap in Vi-Command dispatch tables. Used for setting handlers. /// internal static IDisposable UseViCommandModeTables() { var oldDispatchTable = _singleton._dispatchTable; var oldChordDispatchTable = _singleton._chordDispatchTable; _singleton._dispatchTable = _viCmdKeyMap; _singleton._chordDispatchTable = _viCmdChordTable; return new Disposable(() => { _singleton._dispatchTable = oldDispatchTable; _singleton._chordDispatchTable = oldChordDispatchTable; }); } /// /// Temporarily swap in Vi-Insert dispatch tables. Used for setting handlers. /// internal static IDisposable UseViInsertModeTables() { var oldDispatchTable = _singleton._dispatchTable; var oldChordDispatchTable = _singleton._chordDispatchTable; _singleton._dispatchTable = _viInsKeyMap; _singleton._chordDispatchTable = _viInsChordTable; return new Disposable(() => { _singleton._dispatchTable = oldDispatchTable; _singleton._chordDispatchTable = oldChordDispatchTable; }); } private void ViIndicateCommandMode() { // Show suggestion in 'InsertMode' but not 'CommandMode'. _prediction.DisableGlobal(cursorAtEol: false); if (_options.ViModeIndicator == ViModeStyle.Cursor) { _console.CursorSize = _normalCursorSize < 50 ? 100 : 25; } else if (_options.ViModeIndicator == ViModeStyle.Prompt) { ConsoleColor savedBackground = _console.BackgroundColor; _console.BackgroundColor = AlternateBackground(_console.BackgroundColor); InvokePrompt(); _console.BackgroundColor = savedBackground; } else if (_options.ViModeIndicator == ViModeStyle.Script && _options.ViModeChangeHandler != null) { _options.ViModeChangeHandler.InvokeReturnAsIs(ViMode.Command); } } private void ViIndicateInsertMode() { // Show suggestion in 'InsertMode' but not 'CommandMode'. _prediction.EnableGlobal(); if (_options.ViModeIndicator == ViModeStyle.Cursor) { _console.CursorSize = _normalCursorSize; } else if (_options.ViModeIndicator == ViModeStyle.Prompt) { InvokePrompt(); } else if (_options.ViModeIndicator == ViModeStyle.Script && _options.ViModeChangeHandler != null) { _options.ViModeChangeHandler.InvokeReturnAsIs(ViMode.Insert); } } /// /// Switch to Insert mode and position the cursor at the beginning of the line. /// public static void ViInsertAtBegining(ConsoleKeyInfo? key = null, object arg = null) { ViInsertMode(key, arg); BeginningOfLine(key, arg); } /// /// Switch to Insert mode and position the cursor at the end of the line. /// public static void ViInsertAtEnd(ConsoleKeyInfo? key = null, object arg = null) { ViInsertMode(key, arg); EndOfLine(key, arg); } /// /// Append from the current line position. /// public static void ViInsertWithAppend(ConsoleKeyInfo? key = null, object arg = null) { _singleton._groupUndoHelper.StartGroup(ViInsertWithAppend, arg); ViInsertWithAppendImpl(key, arg); } private static void ViInsertWithAppendImpl(ConsoleKeyInfo? key = null, object arg = null) { ViInsertMode(key, arg); ForwardChar(key, arg); } /// /// Delete the current character and switch to Insert mode. /// public static void ViInsertWithDelete(ConsoleKeyInfo? key = null, object arg = null) { _singleton._groupUndoHelper.StartGroup(ViInsertWithDelete, arg); ViInsertMode(key, arg); DeleteChar(key, arg); } /// /// Accept the line and switch to Insert mode. /// public static void ViAcceptLine(ConsoleKeyInfo? key = null, object arg = null) { ViInsertMode(key, arg); AcceptLine(key, arg); } /// /// Prepend a '#' and accept the line. /// public static void PrependAndAccept(ConsoleKeyInfo? key = null, object arg = null) { BeginningOfLine(key, arg); SelfInsert(key, arg); ViAcceptLine(key, arg); } /// /// Invert the case of the current character and move to the next one. /// public static void InvertCase(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current >= _singleton._buffer.Length) { Ding(); return; } int qty = arg as int? ?? 1; for (; qty > 0 && _singleton._current < _singleton._buffer.Length; qty--) { char c = _singleton._buffer[_singleton._current]; if (Char.IsLetter(c)) { char newChar = Char.IsUpper(c) ? Char.ToLower(c, CultureInfo.CurrentCulture) : char.ToUpper(c, CultureInfo.CurrentCulture); EditItem delEditItem = EditItemDelete.Create( c.ToString(), _singleton._current, InvertCase, arg, moveCursorToEndWhenUndo: false); EditItem insEditItem = EditItemInsertChar.Create(newChar, _singleton._current); _singleton.SaveEditItem(GroupedEdit.Create(new List { delEditItem, insEditItem }, InvertCase, arg )); _singleton._buffer[_singleton._current] = newChar; } _singleton.MoveCursor(Math.Min(_singleton._current + 1, _singleton._buffer.Length)); } _singleton.Render(); } /// /// Swap the current character and the one before it. /// public static void SwapCharacters(ConsoleKeyInfo? key = null, object arg = null) { // if in vi command mode, the cursor can't go as far var bufferLength = _singleton._buffer.Length; int cursorRightLimit = bufferLength + ViEndOfLineFactor; if (_singleton._current <= 0 || bufferLength < 2 || _singleton._current > cursorRightLimit) { Ding(); return; } int cursor = _singleton._current; if (cursor == bufferLength) --cursor; // if at end of line, swap previous two chars _singleton.SaveEditItem(EditItemSwapCharacters.Create(cursor)); _singleton.SwapCharactersImpl(cursor); _singleton.MoveCursor(Math.Min(cursor + 1, cursorRightLimit)); _singleton.Render(); } private void SwapCharactersImpl(int cursor) { char current = _buffer[cursor]; char previous = _buffer[cursor - 1]; _buffer[cursor] = previous; _buffer[cursor - 1] = current; } /// /// Deletes text from the cursor to the first non-blank character of the line. /// public static void DeleteLineToFirstChar(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current > 0) { var i = GetFirstNonBlankOfLogicalLinePos(_singleton._current); _singleton.RemoveTextToViRegister( i, _singleton._current - i, DeleteLineToFirstChar, arg); _singleton._current = i; _singleton.Render(); } else { Ding(); } } /// /// Deletes the current line, enabling undo. /// public static void DeleteLine(ConsoleKeyInfo? key = null, object arg = null) { var lineCount = _singleton.GetLogicalLineCount(); var lineIndex = _singleton.GetLogicalLineNumber() - 1; TryGetArgAsInt(arg, out var requestedLineCount, 1); var deletePosition = DeleteLineImpl(lineIndex, requestedLineCount); // goto the first character of the first remaining logical line var newCurrent = deletePosition + 1; if (lineIndex + requestedLineCount >= lineCount) { // if the delete operation has removed all the remaining lines // goto the first character of the previous logical line newCurrent = GetBeginningOfLinePos(deletePosition); } _singleton._current = newCurrent; _singleton.Render(); } /// /// Deletes as many requested lines from the buffer /// starting from the specified line index, and /// return the offset to the deleted position. /// /// private static int DeleteLineImpl(int lineIndex, int lineCount) { var range = _singleton._buffer.GetRange(lineIndex, lineCount); var deleteText = _singleton._buffer.ToString(range.Offset, range.Count); _viRegister.LinewiseRecord(deleteText); var deletePosition = range.Offset; var anchor = _singleton._current; _singleton._buffer.Remove(range.Offset, range.Count); _singleton.SaveEditItem( EditItemDeleteLines.Create( deleteText, deletePosition, anchor)); return deletePosition; } /// /// Deletes from the current logical line to the end of the buffer. /// public static void DeleteEndOfBuffer(ConsoleKeyInfo? key = null, object arg = null) { var lineIndex = _singleton.GetLogicalLineNumber() - 1; var lineCount = _singleton.GetLogicalLineCount() - lineIndex; DeleteLineImpl(lineIndex, lineCount); // move the cursor to the beginning of the previous line var previousLineIndex = Math.Max(0, lineIndex - 1); var newPosition = GetBeginningOfNthLinePos(previousLineIndex); _singleton._current = newPosition; _singleton.Render(); } /// /// Deletes the current and next n logical lines. /// private static void DeleteNextLines(ConsoleKeyInfo? key = null, object arg = null) { if (TryGetArgAsInt(arg, out int requestedLineCount, 1)) { DeleteLine(key, requestedLineCount + 1); } } /// /// Deletes from the previous n logical lines to the current logical line included. /// public static void DeletePreviousLines(ConsoleKeyInfo? key = null, object arg = null) { if (TryGetArgAsInt(arg, out int requestedLineCount, 1)) { var currentLineIndex = _singleton.GetLogicalLineNumber() - 1; var startLineIndex = Math.Max(0, currentLineIndex - requestedLineCount); DeleteLineImpl(startLineIndex, currentLineIndex - startLineIndex + 1); // go the beginning of the line at index 'startLineIndex' // or at the beginning of the last line startLineIndex = Math.Min(startLineIndex, _singleton.GetLogicalLineCount() - 1); var newCurrent = GetBeginningOfNthLinePos(startLineIndex); _singleton._current = newCurrent; _singleton.Render(); } } /// /// Delete from the current logical line to the n-th requested logical line in a multiline buffer /// private static void DeleteRelativeLines(ConsoleKeyInfo? key = null, object arg = null) { if (TryGetArgAsInt(arg, out var requestedLineNumber, 1)) { var currentLineIndex = _singleton.GetLogicalLineNumber() - 1; var requestedLineIndex = requestedLineNumber - 1; if (requestedLineIndex < 0) { requestedLineIndex = 0; } var logicalLineCount = _singleton.GetLogicalLineCount(); if (requestedLineIndex >= logicalLineCount) { requestedLineIndex = logicalLineCount - 1; } var requestedLineCount = requestedLineIndex - currentLineIndex; if (requestedLineCount < 0) { DeletePreviousLines(null, -requestedLineCount); } else { DeleteNextLines(null, requestedLineCount); } } } /// /// Deletes the previous word. /// public static void BackwardDeleteWord(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; int deletePoint = _singleton._current; for (int i = 0; i < qty; i++) { deletePoint = _singleton.ViFindPreviousWordPoint(deletePoint, _singleton.Options.WordDelimiters); } if (deletePoint == _singleton._current) { Ding(); return; } _singleton.RemoveTextToViRegister( deletePoint, _singleton._current - deletePoint, BackwardDeleteWord, arg); _singleton._current = deletePoint; _singleton.Render(); } /// /// Deletes the previous word, using only white space as the word delimiter. /// public static void ViBackwardDeleteGlob(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current == 0) { Ding(); return; } int qty = arg as int? ?? 1; int deletePoint = _singleton._current; for (int i = 0; i < qty && deletePoint > 0; i++) { deletePoint = _singleton.ViFindPreviousGlob(deletePoint - 1); } if (deletePoint == _singleton._current) { Ding(); return; } _singleton.RemoveTextToViRegister( deletePoint, _singleton._current - deletePoint, BackwardDeleteWord, arg); _singleton._current = deletePoint; _singleton.Render(); } /// /// Find the matching brace, paren, or square bracket and delete all contents within, including the brace. /// public static void ViDeleteBrace(ConsoleKeyInfo? key = null, object arg = null) { int newCursor = _singleton.ViFindBrace(_singleton._current); if (_singleton._current < newCursor) { DeleteRange(_singleton._current, newCursor, ViDeleteBrace); } else if (newCursor < _singleton._current) { DeleteRange(newCursor, _singleton._current, ViDeleteBrace); } else { Ding(); } } /// /// Delete all characters included in the supplied range. /// /// Index of where to begin the delete. /// Index of where to end the delete. /// Action that generated this request, used for repeat command ('.'). private static void DeleteRange(int first, int last, Action action) { int length = last - first + 1; _singleton.RemoveTextToViRegister( first, length, action); _singleton._current = first; _singleton.Render(); } /// /// Prompts for a search string and initiates search upon AcceptLine. /// public static void ViSearchHistoryBackward(ConsoleKeyInfo? key = null, object arg = null) { _singleton.SaveCurrentLine(); _singleton.StartSearch(backward: true); } /// /// Prompts for a search string and initiates search upon AcceptLine. /// public static void SearchForward(ConsoleKeyInfo? key = null, object arg = null) { _singleton.SaveCurrentLine(); _singleton.StartSearch(backward: false); } /// /// Repeat the last search in the same direction as before. /// public static void RepeatSearch(ConsoleKeyInfo? key = null, object arg = null) { if (string.IsNullOrEmpty(_singleton._searchHistoryPrefix)) { Ding(); return; } _singleton._anyHistoryCommandCount++; _singleton.HistorySearch(); } /// /// Repeat the last search in the same direction as before. /// public static void RepeatSearchBackward(ConsoleKeyInfo? key = null, object arg = null) { _singleton._searchHistoryBackward = !_singleton._searchHistoryBackward; RepeatSearch(); _singleton._searchHistoryBackward = !_singleton._searchHistoryBackward; } /// /// Prompts for a string for history searching. /// /// True for searching backward in the history. private void StartSearch(bool backward) { _statusLinePrompt = "find: "; var argBuffer = _statusBuffer; Render(); // Render prompt while (true) { var nextKey = ReadKey(); if (nextKey == Keys.Enter || nextKey == Keys.Tab) { _searchHistoryPrefix = argBuffer.ToString(); _searchHistoryBackward = backward; HistorySearch(); break; } if (nextKey == Keys.Escape) { break; } if (nextKey == Keys.Backspace) { if (argBuffer.Length > 0) { argBuffer.Remove(argBuffer.Length - 1, 1); Render(); // Render prompt continue; } break; } argBuffer.Append(nextKey.KeyChar); Render(); // Render prompt } // Remove our status line argBuffer.Clear(); _statusLinePrompt = null; Render(); // Render prompt } /// /// Searches line history. /// private void HistorySearch() { _searchHistoryCommandCount++; int incr = _searchHistoryBackward ? -1 : +1; var moveCursor = Options.HistorySearchCursorMovesToEnd ? HistoryMoveCursor.ToEnd : HistoryMoveCursor.DontMove; for (int i = _currentHistoryIndex + incr; i >= 0 && i < _history.Count; i += incr) { if (Options.HistoryStringComparison.HasFlag(StringComparison.OrdinalIgnoreCase)) { if (_history[i].CommandLine.ToLower().Contains(_searchHistoryPrefix.ToLower())) { _currentHistoryIndex = i; UpdateFromHistory(moveCursor); return; } } else { if (_history[i].CommandLine.Contains(_searchHistoryPrefix)) { _currentHistoryIndex = i; UpdateFromHistory(moveCursor); return; } } } Ding(); } /// /// Repeat the last text modification. /// public static void RepeatLastCommand(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._undoEditIndex > 0) { EditItem editItem = _singleton._edits[_singleton._undoEditIndex - 1]; if (editItem._instigator != null) { editItem._instigator(key, editItem._instigatorArg); return; } } Ding(); } /// /// Chords in vi needs special handling because a numeric argument can be input between the 1st and 2nd key. /// private static void ViChord(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue) { throw new ArgumentNullException(nameof(key)); } if (arg != null) { Chord(key, arg); return; } if (_singleton._chordDispatchTable.TryGetValue(PSKeyInfo.FromConsoleKeyInfo(key.Value), out var secondKeyDispatchTable)) { ViChordHandler(secondKeyDispatchTable, arg); } } private static void ViChordHandler(Dictionary secondKeyDispatchTable, object arg = null) { var secondKey = ReadKey(); if (secondKeyDispatchTable.TryGetValue(secondKey, out var handler)) { _singleton.ProcessOneKey(secondKey, secondKeyDispatchTable, ignoreIfNoAction: true, arg: arg); } else if (!IsNumeric(secondKey)) { _singleton.ProcessOneKey(secondKey, secondKeyDispatchTable, ignoreIfNoAction: true, arg: arg); } else { var argBuffer = _singleton._statusBuffer; argBuffer.Clear(); _singleton._statusLinePrompt = "digit-argument: "; while (IsNumeric(secondKey)) { argBuffer.Append(secondKey.KeyChar); _singleton.Render(); secondKey = ReadKey(); } int numericArg = int.Parse(argBuffer.ToString()); if (secondKeyDispatchTable.TryGetValue(secondKey, out handler)) { _singleton.ProcessOneKey(secondKey, secondKeyDispatchTable, ignoreIfNoAction: true, arg: numericArg); } else { Ding(); } argBuffer.Clear(); _singleton.ClearStatusMessage(render: true); } } private static void ViDGChord(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue) { throw new ArgumentNullException(nameof(key)); } ViChordHandler(_viChordDGTable, arg); } private static bool IsNumeric(PSKeyInfo key) { return key.KeyChar >= '0' && key.KeyChar <= '9' && !key.Control && !key.Alt; } /// /// Start a new digit argument to pass to other functions while in one of vi's chords. /// public static void ViDigitArgumentInChord(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue || char.IsControl(key.Value.KeyChar)) { Ding(); return; } if (_singleton._options.EditMode == EditMode.Vi && key.Value.KeyChar == '0') { BeginningOfLine(); return; } bool sawDigit = false; _singleton._statusLinePrompt = "digit-argument: "; var argBuffer = _singleton._statusBuffer; argBuffer.Append(key.Value.KeyChar); if (key.Value.KeyChar == '-') { argBuffer.Append('1'); } else { sawDigit = true; } _singleton.Render(); // Render prompt while (true) { var nextKey = ReadKey(); if (_singleton._dispatchTable.TryGetValue(nextKey, out var handler) && handler.Action == DigitArgument) { if (nextKey == Keys.Minus) { if (argBuffer[0] == '-') { argBuffer.Remove(0, 1); } else { argBuffer.Insert(0, '-'); } _singleton.Render(); // Render prompt continue; } if (IsNumeric(nextKey)) { if (!sawDigit && argBuffer.Length > 0) { // Buffer is either '-1' or '1' from one or more Alt+- keys // but no digits yet. Remove the '1'. argBuffer.Length -= 1; } sawDigit = true; argBuffer.Append(nextKey.KeyChar); _singleton.Render(); // Render prompt continue; } } if (int.TryParse(argBuffer.ToString(), out var intArg)) { _singleton.ProcessOneKey(nextKey, _singleton._dispatchTable, ignoreIfNoAction: false, arg: intArg); } else { Ding(); } break; } } /// /// Like DeleteCharOrExit in Emacs mode, but accepts the line instead of deleting a character. /// public static void ViAcceptLineOrExit(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._buffer.Length > 0) { ViInsertMode(key, arg); _singleton.AcceptLineImpl(false); } else { ViExit(key, arg); } } /// /// A new line is inserted above the current line. /// public static void ViInsertLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton._groupUndoHelper.StartGroup(ViInsertLine, arg); _singleton.MoveToBeginningOfPhrase(); _singleton._buffer.Insert(_singleton._current, '\n'); //_singleton._current = Math.Max(0, _singleton._current - 1); _singleton.SaveEditItem(EditItemInsertChar.Create('\n', _singleton._current)); _singleton.Render(); ViInsertMode(); } private void MoveToBeginningOfPhrase() { while (!IsAtBeginningOfPhrase()) { _current--; } } private bool IsAtBeginningOfPhrase() { if (_current == 0) { return true; } if (_buffer[_current - 1] == '\n') { return true; } return false; } /// /// A new line is inserted below the current line. /// public static void ViAppendLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton._groupUndoHelper.StartGroup(ViInsertLine, arg); _singleton.MoveToEndOfPhrase(); int insertPoint = 0; if (_singleton.IsAtEndOfLine(_singleton._current)) { insertPoint = _singleton._buffer.Length; _singleton._buffer.Append('\n'); _singleton._current = insertPoint; } else { insertPoint = _singleton._current + 1; _singleton._buffer.Insert(insertPoint, '\n'); } _singleton.SaveEditItem(EditItemInsertChar.Create('\n', insertPoint)); _singleton.Render(); ViInsertWithAppendImpl(); } private void MoveToEndOfPhrase() { while (!IsAtEndOfPhrase()) { _current++; } } private bool IsAtEndOfPhrase() { if (_buffer.Length == 0 || _current == _buffer.Length + ViEndOfLineFactor) { return true; } if (_current == _buffer.Length && _buffer[_current - 1] == '\n') { return true; } if (_buffer[_current] == '\n') { return true; } return false; } /// /// Joins the current line and the next line. /// public static void ViJoinLines(ConsoleKeyInfo? key = null, object arg = null) { _singleton.MoveToEndOfPhrase(); if (_singleton.IsAtEndOfLine(_singleton._current)) { Ding(); } else { _singleton._buffer[_singleton._current] = ' '; _singleton._groupUndoHelper.StartGroup(ViJoinLines, arg); _singleton.SaveEditItem(EditItemDelete.Create( "\n", _singleton._current, ViJoinLines, arg, moveCursorToEndWhenUndo: false)); _singleton.SaveEditItem(EditItemInsertChar.Create(' ', _singleton._current)); _singleton._groupUndoHelper.EndGroup(); _singleton.Render(); } } } }