/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Globalization; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { /// /// Insert the key. /// public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue) { return; } var keyChar = key.Value.KeyChar; if (keyChar == '\0') return; if (arg is int count) { if (count <= 0) return; } else { count = 1; } if (_singleton._visualSelectionCommandCount > 0) { _singleton.GetRegion(out var start, out var length); Replace(start, length, new string(keyChar, count)); } else if (count > 1) { Insert(new string(keyChar, count)); } else { Insert(keyChar); } } /// /// Reverts all of the input to the current input. /// public static void RevertLine(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._prediction.RevertSuggestion()) { return; } if (_singleton._statusIsErrorMessage) { // After an edit, clear the error message _singleton.ClearStatusMessage(render: false); } while (_singleton._undoEditIndex > 0) { _singleton._edits[_singleton._undoEditIndex - 1].Undo(); _singleton._undoEditIndex--; } _singleton.Render(); } /// /// Cancel the current input, leaving the input on the screen, /// but returns back to the host so the prompt is evaluated again. /// public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.ClearStatusMessage(false); _singleton._current = _singleton._buffer.Length; using var _ = _singleton._prediction.DisableScoped(); _singleton.ForceRender(); _singleton._console.Write("\x1b[91m^C\x1b[0m"); _singleton._buffer.Clear(); // Clear so we don't actually run the input _singleton._current = 0; // If Render is called, _current must be correct. _singleton._currentHistoryIndex = _singleton._history.Count; _singleton._inputAccepted = true; } /// /// Like KillLine - deletes text from the point to the end of the input, /// but does not put the deleted text in the kill ring. /// public static void ForwardDeleteInput(ConsoleKeyInfo? key = null, object arg = null) { ForwardDeleteImpl(_singleton._buffer.Length, ForwardDeleteInput); } /// /// Deletes text from the point to the end of the current logical line, /// but does not put the deleted text in the kill ring. /// public static void ForwardDeleteLine(ConsoleKeyInfo? key = null, object arg = null) { ForwardDeleteImpl(GetEndOfLogicalLinePos(_singleton._current) + 1, ForwardDeleteLine); } /// /// Deletes text from the cursor position to the specified end position /// but does not put the deleted text in the kill ring. /// /// 0-based offset to one character past the end of the text. private static void ForwardDeleteImpl(int endPosition, Action instigator) { var current = _singleton._current; var buffer = _singleton._buffer; if (buffer.Length > 0 && current < endPosition) { int length = endPosition - current; var str = buffer.ToString(current, length); _singleton.SaveEditItem( EditItemDelete.Create( str, current, instigator, instigatorArg: null, !InViEditMode())); buffer.Remove(current, length); _singleton.Render(); } } /// /// Like BackwardKillInput - deletes text from the point to the start of the input, /// but does not put the deleted text in the kill ring. public static void BackwardDeleteInput(ConsoleKeyInfo? key = null, object arg = null) { BackwardDeleteSubstring(0, BackwardDeleteInput); } /// /// Like BackwardKillLine - deletes text from the point to the start of the logical line, /// but does not put the deleted text in the kill ring. /// public static void BackwardDeleteLine(ConsoleKeyInfo? key = null, object arg = null) { var position = GetBeginningOfLinePos(_singleton._current); BackwardDeleteSubstring(position, BackwardDeleteLine); } private static void BackwardDeleteSubstring(int position, Action instigator) { if (_singleton._current > position) { var count = _singleton._current - position; _singleton.RemoveTextToViRegister(position, count, instigator, arg: null, !InViEditMode()); _singleton._current = position; _singleton.Render(); } } /// /// Delete the character before the cursor. /// public static void BackwardDeleteChar(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._visualSelectionCommandCount > 0) { _singleton.GetRegion(out var start, out var length); Delete(start, length); return; } if (_singleton._buffer.Length > 0 && _singleton._current > 0) { int qty = arg as int? ?? 1; if (qty < 1) return; // Ignore useless counts qty = Math.Min(qty, _singleton._current); int startDeleteIndex = _singleton._current - qty; _singleton.RemoveTextToViRegister(startDeleteIndex, qty, BackwardDeleteChar, arg, !InViEditMode()); _singleton._current = startDeleteIndex; _singleton.Render(); } } private void DeleteCharImpl(int qty, bool orExit) { if (_visualSelectionCommandCount > 0) { GetRegion(out var start, out var length); Delete(start, length); return; } if (_buffer.Length > 0) { if (_current < _buffer.Length) { qty = Math.Min(qty, _singleton._buffer.Length - _singleton._current); RemoveTextToViRegister(_current, qty, DeleteChar, qty, !InViEditMode()); if (_current >= _buffer.Length) { _current = Math.Max(0, _buffer.Length + ViEndOfLineFactor); } Render(); } } else if (orExit) { throw new ExitException(); } } /// /// Delete the character under the cursor. /// public static void DeleteChar(ConsoleKeyInfo? key = null, object arg = null) { int qty = arg as int? ?? 1; if (qty < 1) return; // Ignore useless counts _singleton.DeleteCharImpl(qty, orExit: false); } /// /// Delete the character under the cursor, or if the line is empty, exit the process. /// public static void DeleteCharOrExit(ConsoleKeyInfo? key = null, object arg = null) { _singleton.DeleteCharImpl(1, orExit: true); } /// /// A helper function to change the case of the current word. /// private static void UpdateWordCase(bool toUpper) { if (_singleton._current >= _singleton._buffer.Length) { Ding(); return; } int endOfWord = _singleton.FindForwardWordPoint(_singleton.Options.WordDelimiters); int wordlen = endOfWord - _singleton._current; string word = _singleton._buffer.ToString(_singleton._current, wordlen); word = toUpper ? word.ToUpper() : word.ToLower(); Replace(_singleton._current, wordlen, word); _singleton.MoveCursor(endOfWord); _singleton.Render(); } /// /// Upcase the current word and move to the next one. /// public static void UpcaseWord(ConsoleKeyInfo? key = null, object arg = null) { UpdateWordCase(toUpper: true); } /// /// Downcase the current word and move to the next one. /// public static void DowncaseWord(ConsoleKeyInfo? key = null, object arg = null) { UpdateWordCase(toUpper: false); } /// /// Capitalize the current word and move to the next one. /// public static void CapitalizeWord(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current >= _singleton._buffer.Length) { Ding(); return; } int endOfWord = _singleton.FindForwardWordPoint(_singleton.Options.WordDelimiters); int wordlen = endOfWord - _singleton._current; char[] word = _singleton._buffer.ToString(_singleton._current, wordlen).ToLower().ToCharArray(); int firstLetterIdx = Array.FindIndex(word, static x => char.IsLetter(x)); if (firstLetterIdx >= 0) { word[firstLetterIdx] = char.ToUpper(word[firstLetterIdx]); Replace(_singleton._current, wordlen, new string(word)); } _singleton.MoveCursor(endOfWord); _singleton.Render(); } private bool AcceptLineImpl(bool validate) { using var _ = _prediction.DisableScoped(); ParseInput(); if (_parseErrors.Any(e => e.IncompleteInput)) { Insert('\n'); return false; } // If text was pasted, for performance reasons we skip rendering for some time, // but if input is accepted, we won't have another chance to render. // // Also - if there was an emphasis, we want to clear that before accepting // and that requires rendering. bool renderNeeded = _emphasisStart >= 0 || _queuedKeys.Count > 0; _emphasisStart = -1; _emphasisLength = 0; if (renderNeeded) { ForceRender(); } // Only run validation if we haven't before. If we have and status line shows an error, // treat that as a -Force and accept the input so it is added to history, and PowerShell // can report an error as it normally does. if (validate && !_statusIsErrorMessage) { var insertionPoint = _current; var errorMessage = Validate(_ast); if (!string.IsNullOrWhiteSpace(errorMessage)) { // If there are more keys, assume the user pasted with a right click and // we should insert a newline even though validation failed. if (_queuedKeys.Count > 0) { // Validation may have moved the cursor. Because there are queued // keys, we need to move the cursor back to the correct place, and // ignore where validation put the cursor because the queued keys // will be inserted in the wrong place. SetCursorPosition(insertionPoint); Insert('\n'); } _statusLinePrompt = ""; _statusBuffer.Append(errorMessage); _statusIsErrorMessage = true; Render(); return false; } } if (_statusIsErrorMessage) { ClearStatusMessage(render: true); } // Make sure cursor is at the end before writing the line. if (_current != _buffer.Length) { // Let public API set cursor to end of line incase end of line is end of buffer. _current = _buffer.Length; SetCursorPosition(_current); } if (_prediction.ActiveView is PredictionListView listView) { // Send feedback to prediction plugin if a list item is accepted as the final command line. listView.OnSuggestionAccepted(); } // Clear the prediction view if there is one. _prediction.ActiveView.Clear(cursorAtEol: true); _console.Write("\n"); _inputAccepted = true; return true; } class CommandValidationVisitor : AstVisitor { private readonly Ast _rootAst; internal string detectedError; internal CommandValidationVisitor(Ast rootAst) { _rootAst = rootAst; } public override AstVisitAction VisitCommand(CommandAst commandAst) { var commandName = commandAst.GetCommandName(); if (commandName != null) { if (_singleton._engineIntrinsics != null) { var commandInfo = _singleton._engineIntrinsics.InvokeCommand.GetCommand(commandName, CommandTypes.All); if (commandInfo == null && !_singleton.UnresolvedCommandCouldSucceed(commandName, _rootAst)) { _singleton._current = commandAst.CommandElements[0].Extent.EndOffset; detectedError = string.Format(CultureInfo.CurrentCulture, PSReadLineResources.CommandNotFoundError, commandName); return AstVisitAction.StopVisit; } } if (commandAst.CommandElements.Any(e => e is ScriptBlockExpressionAst)) { if (_singleton._options.CommandsToValidateScriptBlockArguments == null || !_singleton._options.CommandsToValidateScriptBlockArguments.Contains(commandName)) { return AstVisitAction.SkipChildren; } } } if (_singleton._options.CommandValidationHandler != null) { try { _singleton._options.CommandValidationHandler(commandAst); } catch (Exception e) { detectedError = e.Message; } } return !string.IsNullOrWhiteSpace(detectedError) ? AstVisitAction.StopVisit : AstVisitAction.Continue; } } private string Validate(Ast rootAst) { if (_parseErrors != null && _parseErrors.Length > 0) { // Move the cursor to the point of error _current = _parseErrors[0].Extent.EndOffset; return _parseErrors[0].Message; } var validationVisitor = new CommandValidationVisitor(rootAst); rootAst.Visit(validationVisitor); if (!string.IsNullOrWhiteSpace(validationVisitor.detectedError)) { return validationVisitor.detectedError; } return null; } private bool UnresolvedCommandCouldSucceed(string commandName, Ast rootAst) { // This is a little hacky, but we check for a few things where part of the current // command defines/imports new commands that PowerShell might not yet know about. // There is little reason to go to great lengths at being correct here, validation // is just a small usability tweak to avoid cluttering up history - PowerShell // will report errors for stuff we actually let through. // Do we define a function matching the command name? var fnDefns = rootAst.FindAll(ast => ast is FunctionDefinitionAst, true).OfType(); if (fnDefns.Any(fnDefnAst => fnDefnAst.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase))) { return true; } var cmdAsts = rootAst.FindAll(ast => ast is CommandAst, true).OfType(); foreach (var cmdAst in cmdAsts) { // If we dot source something, we can't in general know what is being // dot sourced so just assume the unresolved command will work. // If we use the invocation operator, allow that because an expression // is being invoked and it's reasonable to just allow it. if (cmdAst.InvocationOperator != TokenKind.Unknown) { return true; } // Are we importing a module or being tricky with Invoke-Expression? Let those through. var candidateCommand = cmdAst.GetCommandName(); if (candidateCommand.Equals("Import-Module", StringComparison.OrdinalIgnoreCase) || candidateCommand.Equals("ipmo", StringComparison.OrdinalIgnoreCase) || candidateCommand.Equals("Invoke-Expression", StringComparison.OrdinalIgnoreCase) || candidateCommand.Equals("iex", StringComparison.OrdinalIgnoreCase)) { return true; } } if (commandName.Length == 1) { switch (commandName[0]) { // The following are debugger commands that should be accepted if we're debugging // because the console host will interpret these commands directly. case 's': case 'v': case 'o': case 'c': case 'q': case 'k': case 'l': case 'S': case 'V': case 'O': case 'C': case 'Q': case 'K': case 'L': case '?': case 'h': case 'H': // Ideally we would check $PSDebugContext, but it is set at function // scope, and because we're in a module, we can't find that variable // (arguably a PowerShell issue.) // NestedPromptLevel is good enough though - it's rare to be in a nested. var nestedPromptLevel = _engineIntrinsics.SessionState.PSVariable.GetValue("NestedPromptLevel"); if (nestedPromptLevel is int) { return ((int)nestedPromptLevel) > 0; } break; } } return false; } /// /// Attempt to execute the current input. If the current input is incomplete (for /// example there is a missing closing parenthesis, bracket, or quote, then the /// continuation prompt is displayed on the next line and PSReadLine waits for /// keys to edit the current input. /// public static void AcceptLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.AcceptLineImpl(false); } /// /// Attempt to execute the current input. If the current input is incomplete (for /// example there is a missing closing parenthesis, bracket, or quote, then the /// continuation prompt is displayed on the next line and PSReadLine waits for /// keys to edit the current input. /// public static void ValidateAndAcceptLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.AcceptLineImpl(true); } /// /// Attempt to execute the current input. If it can be executed (like AcceptLine), /// then recall the next item from history the next time ReadLine is called. /// public static void AcceptAndGetNext(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton.AcceptLineImpl(false)) { if (_singleton._currentHistoryIndex < (_singleton._history.Count - 1)) { _singleton._getNextHistoryIndex = _singleton._currentHistoryIndex + 1; } else { Ding(); } } } /// /// The continuation prompt is displayed on the next line and PSReadLine waits for /// keys to edit the current input. This is useful to enter multi-line input as /// a single command even when a single line is complete input by itself. /// public static void AddLine(ConsoleKeyInfo? key = null, object arg = null) { Insert('\n'); } /// /// A new empty line is created above the current line regardless of where the cursor /// is on the current line. The cursor moves to the beginning of the new line. /// public static void InsertLineAbove(ConsoleKeyInfo? key = null, object arg = null) { // Move the current position to the beginning of the current line and only the current line. _singleton._current = GetBeginningOfLinePos(_singleton._current); Insert('\n'); PreviousLine(); } /// /// A new empty line is created below the current line regardless of where the cursor /// is on the current line. The cursor moves to the beginning of the new line. /// public static void InsertLineBelow(ConsoleKeyInfo? key = null, object arg = null) { int i = _singleton._current; for (; i < _singleton._buffer.Length; i++) { if (_singleton._buffer[i] == '\n') { break; } } _singleton._current = i; Insert('\n'); } } }