/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Management.Automation.Language; #if !CORECLR // TODO: clipboard using System.Windows.Forms; #endif namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { // Yank/Kill state private List _killRing; private int _killIndex; private int _killCommandCount; private int _yankCommandCount; private int _yankStartPoint; private int _yankLastArgCommandCount; class YankLastArgState { internal int argument; internal int historyIndex; internal int historyIncrement; internal int startPoint = -1; } private YankLastArgState _yankLastArgState; private int _visualSelectionCommandCount; /// /// Mark the current location of the cursor for use in a subsequent editing command. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SetMark(ConsoleKeyInfo? key = null, object arg = null) { _singleton._mark = _singleton._current; } /// /// The cursor is placed at the location of the mark and the mark is moved /// to the location of the cursor. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void ExchangePointAndMark(ConsoleKeyInfo? key = null, object arg = null) { var tmp = _singleton._mark; _singleton._mark = _singleton._current; _singleton._current = tmp; _singleton.PlaceCursor(); } /// /// The contents of the kill ring are cleared. /// public static void ClearKillRing() { if (_singleton._killRing != null) { _singleton._killRing.Clear(); } _singleton._killIndex = -1; // So first add indexes 0. } private void Kill(int start, int length, bool prepend) { if (length > 0) { var killText = _buffer.ToString(start, length); SaveEditItem(EditItemDelete.Create(killText, start)); _buffer.Remove(start, length); _current = start; Render(); if (_killCommandCount > 0) { if (prepend) { _killRing[_killIndex] = killText + _killRing[_killIndex]; } else { _killRing[_killIndex] += killText; } } else { if (_killRing.Count < Options.MaximumKillRingCount) { _killRing.Add(killText); _killIndex = _killRing.Count - 1; } else { _killIndex += 1; if (_killIndex == _killRing.Count) { _killIndex = 0; } _killRing[_killIndex] = killText; } } } _killCommandCount += 1; } /// /// Clear the input from the cursor to the end of the input. The cleared text is placed /// in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void KillLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.Kill(_singleton._current, _singleton._buffer.Length - _singleton._current, false); } /// /// Clear the input from the start of the input to the cursor. The cleared text is placed /// in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void BackwardKillLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.Kill(0, _singleton._current, true); } /// /// Clear the input from the cursor to the end of the current word. If the cursor /// is between words, the input is cleared from the cursor to the end of the next word. /// The cleared text is placed in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void KillWord(ConsoleKeyInfo? key = null, object arg = null) { int i = _singleton.FindForwardWordPoint(_singleton.Options.WordDelimiters); _singleton.Kill(_singleton._current, i - _singleton._current, false); } /// /// Clear the input from the cursor to the end of the current word. If the cursor /// is between words, the input is cleared from the cursor to the end of the next word. /// The cleared text is placed in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void ShellKillWord(ConsoleKeyInfo? key = null, object arg = null) { var token = _singleton.FindToken(_singleton._current, FindTokenMode.CurrentOrNext); var end = (token.Kind == TokenKind.EndOfInput) ? _singleton._buffer.Length : token.Extent.EndOffset; _singleton.Kill(_singleton._current, end - _singleton._current, false); } /// /// Clear the input from the start of the current word to the cursor. If the cursor /// is between words, the input is cleared from the start of the previous word to the /// cursor. The cleared text is placed in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void BackwardKillWord(ConsoleKeyInfo? key = null, object arg = null) { int i = _singleton.FindBackwardWordPoint(_singleton.Options.WordDelimiters); _singleton.Kill(i, _singleton._current - i, true); } /// /// Clear the input from the start of the current word to the cursor. If the cursor /// is between words, the input is cleared from the start of the previous word to the /// cursor. The cleared text is placed in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void UnixWordRubout(ConsoleKeyInfo? key = null, object arg = null) { int i = _singleton.FindBackwardWordPoint(""); _singleton.Kill(i, _singleton._current - i, true); } /// /// Clear the input from the start of the current word to the cursor. If the cursor /// is between words, the input is cleared from the start of the previous word to the /// cursor. The cleared text is placed in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void ShellBackwardKillWord(ConsoleKeyInfo? key = null, object arg = null) { var token = _singleton.FindToken(_singleton._current, FindTokenMode.Previous); var start = token == null ? 0 : token.Extent.StartOffset; _singleton.Kill(start, _singleton._current - start, true); } /// /// Kill the text between the cursor and the mark. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void KillRegion(ConsoleKeyInfo? key = null, object arg = null) { int start, length; _singleton.GetRegion(out start, out length); _singleton.Kill(start, length, true); } private void YankImpl() { if (_killRing.Count == 0) return; // Starting a yank session, yank the last thing killed and // remember where we started. _mark = _yankStartPoint = _current; Insert(_killRing[_killIndex]); _yankCommandCount += 1; } /// /// Add the most recently killed text to the input. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void Yank(ConsoleKeyInfo? key = null, object arg = null) { _singleton.YankImpl(); } private void YankPopImpl() { if (_yankCommandCount == 0) return; _killIndex -= 1; if (_killIndex < 0) { _killIndex = _killRing.Count - 1; } var yankText = _killRing[_killIndex]; Replace(_yankStartPoint, _current - _yankStartPoint, yankText); _yankCommandCount += 1; } /// /// If the previous operation was Yank or YankPop, replace the previously yanked /// text with the next killed text from the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void YankPop(ConsoleKeyInfo? key = null, object arg = null) { _singleton.YankPopImpl(); } void YankArgImpl(YankLastArgState yankLastArgState) { if (yankLastArgState.historyIndex < 0 || yankLastArgState.historyIndex >= _history.Count) { Ding(); return; } Token[] tokens; ParseError[] errors; var buffer = _history[yankLastArgState.historyIndex]; Parser.ParseInput(buffer._line, out tokens, out errors); int arg = (yankLastArgState.argument < 0) ? tokens.Length + yankLastArgState.argument - 1 : yankLastArgState.argument; if (arg < 0 || arg >= tokens.Length) { Ding(); return; } var argText = tokens[arg].Text; if (yankLastArgState.startPoint < 0) { yankLastArgState.startPoint = _current; Insert(argText); } else { Replace(yankLastArgState.startPoint, _current - yankLastArgState.startPoint, argText); } } /// /// Yank the first argument (after the command) from the previous history line. /// With an argument, yank the nth argument (starting from 0), if the argument /// is negative, start from the last argument. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void YankNthArg(ConsoleKeyInfo? key = null, object arg = null) { var yankLastArgState = new YankLastArgState { argument = (arg is int) ? (int)arg : 1, historyIndex = _singleton._currentHistoryIndex - 1, }; _singleton.YankArgImpl(yankLastArgState); } /// /// Yank the last argument from the previous history line. With an argument, /// the first time it is invoked, behaves just like YankNthArg. If invoked /// multiple times, instead it iterates through history and arg sets the direction /// (negative reverses the direction.) /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void YankLastArg(ConsoleKeyInfo? key = null, object arg = null) { if (arg != null && !(arg is int)) { Ding(); return; } _singleton._yankLastArgCommandCount += 1; if (_singleton._yankLastArgCommandCount == 1) { _singleton._yankLastArgState = new YankLastArgState { argument = (arg != null) ? (int)arg : -1, historyIncrement = -1, historyIndex = _singleton._currentHistoryIndex - 1 }; _singleton.YankArgImpl(_singleton._yankLastArgState); return; } var yankLastArgState = _singleton._yankLastArgState; if (arg != null) { if ((int)arg < 0) { yankLastArgState.historyIncrement = -yankLastArgState.historyIncrement; } } yankLastArgState.historyIndex += yankLastArgState.historyIncrement; // Don't increment more than 1 out of range so it's quick to get back to being in range. if (yankLastArgState.historyIndex < 0) { Ding(); yankLastArgState.historyIndex = 0; } else if (yankLastArgState.historyIndex >= _singleton._history.Count) { Ding(); yankLastArgState.historyIndex = _singleton._history.Count - 1; } else { _singleton.YankArgImpl(yankLastArgState); } } private void VisualSelectionCommon(Action action) { if (_singleton._visualSelectionCommandCount == 0) { SetMark(); } _singleton._visualSelectionCommandCount += 1; action(); _singleton.Render(); } /// /// Adjust the current selection to include the previous character /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectBackwardChar(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => BackwardChar(key, arg)); } /// /// Adjust the current selection to include the next character /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectForwardChar(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => ForwardChar(key, arg)); } /// /// Adjust the current selection to include the previous word /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectBackwardWord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => BackwardWord(key, arg)); } /// /// Adjust the current selection to include the next word /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectNextWord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => NextWord(key, arg)); } /// /// Adjust the current selection to include the next word using ForwardWord /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectForwardWord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => ForwardWord(key, arg)); } /// /// Adjust the current selection to include the next word using ShellForwardWord /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectShellForwardWord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => ShellForwardWord(key, arg)); } /// /// Adjust the current selection to include the next word using ShellNextWord /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectShellNextWord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => ShellNextWord(key, arg)); } /// /// Adjust the current selection to include the previous word using ShellBackwardWord /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectShellBackwardWord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => ShellBackwardWord(key, arg)); } /// /// Select the entire line /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectAll(ConsoleKeyInfo? key = null, object arg = null) { _singleton._visualSelectionCommandCount += 1; _singleton._mark = 0; _singleton._current = _singleton._buffer.Length; _singleton.Render(); } /// /// Adjust the current selection to include from the cursor to the end of the line /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => EndOfLine(key, arg)); } /// /// Adjust the current selection to include from the cursor to the start of the line /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelectBackwardsLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.VisualSelectionCommon(() => BeginningOfLine(key, arg)); } /// /// Paste text from the system clipboard. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void Paste(ConsoleKeyInfo? key = null, object arg = null) { #if !CORECLR // TODO: clipboard string textToPaste = null; ExecuteOnSTAThread(() => { if (Clipboard.ContainsText()) { textToPaste = Clipboard.GetText(); } }); if (textToPaste != null) { textToPaste = textToPaste.Replace("\r", ""); if (_singleton._visualSelectionCommandCount > 0) { int start, length; _singleton.GetRegion(out start, out length); Replace(start, length, textToPaste); } else { Insert(textToPaste); } } #endif } /// /// Copy selected region to the system clipboard. If no region is selected, copy the whole line. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void Copy(ConsoleKeyInfo? key = null, object arg = null) { #if !CORECLR // TODO: clipboard string textToSet; if (_singleton._visualSelectionCommandCount > 0) { int start, length; _singleton.GetRegion(out start, out length); textToSet = _singleton._buffer.ToString(start, length); } else { textToSet = _singleton._buffer.ToString(); } if (!string.IsNullOrEmpty(textToSet)) { ExecuteOnSTAThread(() => Clipboard.SetText(textToSet)); } #endif } /// /// If text is selected, copy to the clipboard, otherwise cancel the line. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void CopyOrCancelLine(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._visualSelectionCommandCount > 0) { Copy(key, arg); } else { CancelLine(key, arg); } } /// /// Delete selected region placing deleted text in the system clipboard. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void Cut(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._visualSelectionCommandCount > 0) { int start, length; _singleton.GetRegion(out start, out length); #if !CORECLR // TODO: clipboard ExecuteOnSTAThread(() => Clipboard.SetText(_singleton._buffer.ToString(start, length))); #endif Delete(start, length); } } } }