/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
using Microsoft.PowerShell.Internal;
namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
///
/// Insert the key
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null)
{
if (!key.HasValue)
{
return;
}
if (arg is int)
{
var count = (int)arg;
if (count <= 0)
return;
if (count > 1)
{
var toInsert = new string(key.Value.KeyChar, count);
if (_singleton._visualSelectionCommandCount > 0)
{
int start, length;
_singleton.GetRegion(out start, out length);
Replace(start, length, toInsert);
}
else
{
Insert(toInsert);
}
return;
}
}
if (_singleton._visualSelectionCommandCount > 0)
{
int start, length;
_singleton.GetRegion(out start, out length);
Replace(start, length, new string(key.Value.KeyChar, 1));
}
else
{
Insert(key.Value.KeyChar);
}
}
///
/// Reverts all of the input to the current input.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void RevertLine(ConsoleKeyInfo? key = null, object arg = null)
{
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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.ClearStatusMessage(false);
_singleton._current = _singleton._buffer.Length;
// We want to display ^C to show the line was canceled. Instead of appending ^C
// (or (char)3), we append 2 spaces so we don't affect tokenization too much, e.g.
// changing a keyword to a command.
_singleton._buffer.Append(" ");
_singleton.ReallyRender();
// Now that we've rendered with this extra spaces, go back and replace the spaces
// with ^C colored in red (so it stands out.)
var coordinates = _singleton.ConvertOffsetToCoordinates(_singleton._current);
var console = _singleton._console;
var consoleBuffer = _singleton._consoleBuffer;
int i = (coordinates.Y - _singleton._initialY) * console.BufferWidth + coordinates.X;
consoleBuffer[i].UnicodeChar = '^';
consoleBuffer[i].ForegroundColor = ConsoleColor.Red;
consoleBuffer[i].BackgroundColor = console.BackgroundColor;
consoleBuffer[i+1].UnicodeChar = 'C';
consoleBuffer[i+1].ForegroundColor = ConsoleColor.Red;
consoleBuffer[i+1].BackgroundColor = console.BackgroundColor;
console.WriteBufferLines(consoleBuffer, ref _singleton._initialY);
var y = coordinates.Y + 1;
_singleton.PlaceCursor(0, ref y);
_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 ForwardKillLine - deletes text from the point to the end of the line,
/// but does not put the deleted text in the kill ring.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void ForwardDeleteLine(ConsoleKeyInfo? key = null, object arg = null)
{
var current = _singleton._current;
var buffer = _singleton._buffer;
if (buffer.Length > 0 && current < buffer.Length)
{
int length = buffer.Length - current;
var str = buffer.ToString(current, length);
_singleton.SaveEditItem(EditItemDelete.Create(str, current));
buffer.Remove(current, length);
_singleton.Render();
}
}
///
/// Like BackwardKillLine - deletes text from the point to the start of the line,
/// but does not put the deleted text in the kill ring.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void BackwardDeleteLine(ConsoleKeyInfo? key = null, object arg = null)
{
if (_singleton._current > 0)
{
_singleton._clipboard = _singleton._buffer.ToString(0, _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, 0));
_singleton._buffer.Remove(0, _singleton._current);
_singleton._current = 0;
_singleton.Render();
}
}
///
/// Delete the character before the cursor.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void BackwardDeleteChar(ConsoleKeyInfo? key = null, object arg = null)
{
if (_singleton._visualSelectionCommandCount > 0)
{
int start, length;
_singleton.GetRegion(out start, out length);
Delete(start, length);
return;
}
if (_singleton._buffer.Length > 0 && _singleton._current > 0)
{
int qty = (arg is int) ? (int) arg : 1;
qty = Math.Min(qty, _singleton._current);
int startDeleteIndex = _singleton._current - qty;
_singleton.SaveEditItem(
EditItemDelete.Create(
_singleton._buffer.ToString(startDeleteIndex, qty),
startDeleteIndex,
BackwardDeleteChar,
arg)
);
_singleton.SaveToClipboard(startDeleteIndex, qty);
_singleton._buffer.Remove(startDeleteIndex, qty);
_singleton._current = startDeleteIndex;
_singleton.Render();
}
else
{
Ding();
}
}
private void DeleteCharImpl(int qty, bool orExit)
{
qty = Math.Min(qty, _singleton._buffer.Length + 1 + ViEndOfLineFactor - _singleton._current);
if (_visualSelectionCommandCount > 0)
{
int start, length;
GetRegion(out start, out length);
Delete(start, length);
return;
}
if (_buffer.Length > 0)
{
if (_current < _buffer.Length)
{
SaveEditItem(EditItemDelete.Create(_buffer.ToString(_current, qty), _current, DeleteChar, qty));
SaveToClipboard(_current, qty);
_buffer.Remove(_current, qty);
if (_current >= _buffer.Length)
{
_current = Math.Max(0, _buffer.Length - 1);
}
Render();
}
}
else if (orExit)
{
throw new ExitException();
}
}
///
/// Delete the character under the cursor.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void DeleteChar(ConsoleKeyInfo? key = null, object arg = null)
{
int qty = (arg is int) ? (int)arg : 1;
_singleton.DeleteCharImpl(qty, orExit: false);
}
///
/// Delete the character under the cursor, or if the line is empty, exit the process
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void DeleteCharOrExit(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.DeleteCharImpl(1, orExit: true);
}
private bool AcceptLineImpl(bool validate)
{
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;
var insertionPoint = _current;
// Make sure cursor is at the end before writing the line
_current = _buffer.Length;
if (renderNeeded)
{
ReallyRender();
}
// 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 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);
}
var coordinates = ConvertOffsetToCoordinates(_current);
var y = coordinates.Y + 1;
PlaceCursor(0, ref y);
_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;
}
static bool StaticParameterBindingSupported(CommandInfo commandInfo)
{
var aliasInfo = commandInfo as AliasInfo;
if (aliasInfo != null)
{
commandInfo = aliasInfo.ResolvedCommand;
}
return (commandInfo is ExternalScriptInfo)
|| (commandInfo is CmdletInfo)
|| (commandInfo is FunctionInfo);
}
///
/// 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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
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.
if (_singleton.LineIsMultiLine())
{
int i = Math.Max(0, _singleton._current - 1);
for (; i > 0; i--)
{
if (_singleton._buffer[i] == '\n')
{
i += 1;
break;
}
}
_singleton._current = i;
}
else
{
_singleton._current = 0;
}
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.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void InsertLineBelow(ConsoleKeyInfo? key = null, object arg = null)
{
// Move the current position to the end of the current line and only the current line.
if (_singleton.LineIsMultiLine())
{
int i = _singleton._current;
for (; i < _singleton._buffer.Length; i++)
{
if (_singleton._buffer[i] == '\n')
{
break;
}
}
_singleton._current = i;
}
else
{
_singleton._current = _singleton._buffer.Length;
}
Insert('\n');
}
}
}