diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleReadLine.cs deleted file mode 100644 index 55174610c..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleReadLine.cs +++ /dev/null @@ -1,631 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Text; -using System.Threading; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using System.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Security; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console -{ - using System; - - internal class ConsoleReadLine : IReadLine - { - private readonly PSReadLineProxy _psrlProxy; - - private readonly PsesInternalHost _psesHost; - - private readonly EngineIntrinsics _engineIntrinsics; - - #region Constructors - - public ConsoleReadLine( - PSReadLineProxy psrlProxy, - PsesInternalHost psesHost, - EngineIntrinsics engineIntrinsics) - { - _psrlProxy = psrlProxy; - _psesHost = psesHost; - _engineIntrinsics = engineIntrinsics; - } - - #endregion - - #region Public Methods - - public string ReadLine(CancellationToken cancellationToken) - { - return _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); - } - - public bool TryOverrideReadKey(Func readKeyFunc) - { - _psrlProxy.OverrideReadKey(readKeyFunc); - return true; - } - - public bool TryOverrideIdleHandler(Action idleHandler) - { - _psrlProxy.OverrideIdleHandler(idleHandler); - return true; - } - - public SecureString ReadSecureLine(CancellationToken cancellationToken) - { - SecureString secureString = new SecureString(); - - // TODO: Are these values used? - int initialPromptRow = ConsoleProxy.GetCursorTop(cancellationToken); - int initialPromptCol = ConsoleProxy.GetCursorLeft(cancellationToken); - int previousInputLength = 0; - - Console.TreatControlCAsInput = true; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); - - if ((int)keyInfo.Key == 3 || - keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - throw new PipelineStoppedException(); - } - if (keyInfo.Key == ConsoleKey.Enter) - { - // Break to return the completed string - break; - } - if (keyInfo.Key == ConsoleKey.Tab) - { - continue; - } - if (keyInfo.Key == ConsoleKey.Backspace) - { - if (secureString.Length > 0) - { - secureString.RemoveAt(secureString.Length - 1); - } - } - else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) - { - secureString.AppendChar(keyInfo.KeyChar); - } - - // Re-render the secure string characters - int currentInputLength = secureString.Length; - int consoleWidth = Console.WindowWidth; - - if (currentInputLength > previousInputLength) - { - Console.Write('*'); - } - else if (previousInputLength > 0 && currentInputLength < previousInputLength) - { - int row = ConsoleProxy.GetCursorTop(cancellationToken); - int col = ConsoleProxy.GetCursorLeft(cancellationToken); - - // Back up the cursor before clearing the character - col--; - if (col < 0) - { - col = consoleWidth - 1; - row--; - } - - Console.SetCursorPosition(col, row); - Console.Write(' '); - Console.SetCursorPosition(col, row); - } - - previousInputLength = currentInputLength; - } - } - finally - { - Console.TreatControlCAsInput = false; - } - - return secureString; - } - - #endregion - - #region Private Methods - - private static ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) - { - return ConsoleProxy.ReadKey(intercept: true, cancellationToken); - } - - private string InvokePSReadLine(CancellationToken cancellationToken) - { - EngineIntrinsics engineIntrinsics = _psesHost.IsRunspacePushed ? null : _engineIntrinsics; - return _psrlProxy.ReadLine(_psesHost.Runspace, engineIntrinsics, cancellationToken, /* lastExecutionStatus */ null); - } - - /// - /// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine. - /// This method should be used when PSReadLine is disabled, either by user settings or - /// unsupported PowerShell versions. - /// - /// - /// Indicates whether ReadLine should act like a command line. - /// - /// - /// The cancellation token that will be checked prior to completing the returned task. - /// - /// - /// A task object representing the asynchronus operation. The Result property on - /// the task object returns the user input string. - /// - internal string InvokeLegacyReadLine(bool isCommandLine, CancellationToken cancellationToken) - { - string inputAfterCompletion = null; - CommandCompletion currentCompletion = null; - - int historyIndex = -1; - IReadOnlyList currentHistory = null; - - StringBuilder inputLine = new StringBuilder(); - - int initialCursorCol = ConsoleProxy.GetCursorLeft(cancellationToken); - int initialCursorRow = ConsoleProxy.GetCursorTop(cancellationToken); - - // TODO: Are these used? - int initialWindowLeft = Console.WindowLeft; - int initialWindowTop = Console.WindowTop; - - int currentCursorIndex = 0; - - Console.TreatControlCAsInput = true; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); - - // Do final position calculation after the key has been pressed - // because the window could have been resized before then - int promptStartCol = initialCursorCol; - int promptStartRow = initialCursorRow; - int consoleWidth = Console.WindowWidth; - - if ((int)keyInfo.Key == 3 || - keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - throw new PipelineStoppedException(); - } - else if (keyInfo.Key == ConsoleKey.Tab && isCommandLine) - { - /* - if (currentCompletion == null) - { - inputBeforeCompletion = inputLine.ToString(); - inputAfterCompletion = null; - - // TODO: This logic should be moved to AstOperations or similar! - - if (this.powerShellContext.IsDebuggerStopped) - { - PSCommand command = new PSCommand(); - command.AddCommand("TabExpansion2"); - command.AddParameter("InputScript", inputBeforeCompletion); - command.AddParameter("CursorColumn", currentCursorIndex); - command.AddParameter("Options", null); - - var results = await this.powerShellContext - .ExecuteCommandAsync(command, sendOutputToHost: false, sendErrorToHost: false) - .ConfigureAwait(false); - - currentCompletion = results.FirstOrDefault(); - } - else - { - */ - /* - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = _ - currentCompletion = - CommandCompletion.CompleteInput( - inputBeforeCompletion, - currentCursorIndex, - null, - powerShell); - - if (currentCompletion.CompletionMatches.Count > 0) - { - int replacementEndIndex = - currentCompletion.ReplacementIndex + - currentCompletion.ReplacementLength; - - inputAfterCompletion = - inputLine.ToString( - replacementEndIndex, - inputLine.Length - replacementEndIndex); - } - else - { - currentCompletion = null; - } - } - } - */ - - CompletionResult completion = - currentCompletion?.GetNextResult( - !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); - - if (completion != null) - { - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - $"{completion.CompletionText}{inputAfterCompletion}", - currentCursorIndex, - insertIndex: currentCompletion.ReplacementIndex, - replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, - finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); - } - } - else if (keyInfo.Key == ConsoleKey.LeftArrow) - { - currentCompletion = null; - - if (currentCursorIndex > 0) - { - currentCursorIndex = - MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - currentCursorIndex - 1); - } - } - else if (keyInfo.Key == ConsoleKey.Home) - { - currentCompletion = null; - - currentCursorIndex = - MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - 0); - } - else if (keyInfo.Key == ConsoleKey.RightArrow) - { - currentCompletion = null; - - if (currentCursorIndex < inputLine.Length) - { - currentCursorIndex = - MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - currentCursorIndex + 1); - } - } - else if (keyInfo.Key == ConsoleKey.End) - { - currentCompletion = null; - - currentCursorIndex = - MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - inputLine.Length); - } - else if (keyInfo.Key == ConsoleKey.UpArrow && isCommandLine) - { - currentCompletion = null; - - // TODO: Ctrl+Up should allow navigation in multi-line input - - if (currentHistory == null) - { - historyIndex = -1; - - PSCommand command = new PSCommand().AddCommand("Get-History"); - - currentHistory = _psesHost.InvokePSCommand( - command, - PowerShellExecutionOptions.Default, - cancellationToken); - - if (currentHistory != null) - { - historyIndex = currentHistory.Count; - } - } - - if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) - { - historyIndex--; - - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - (string)currentHistory[historyIndex].Properties["CommandLine"].Value, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - } - else if (keyInfo.Key == ConsoleKey.DownArrow && isCommandLine) - { - currentCompletion = null; - - // The down arrow shouldn't cause history to be loaded, - // it's only for navigating an active history array - - if (historyIndex > -1 && historyIndex < currentHistory.Count && - currentHistory != null && currentHistory.Count > 0) - { - historyIndex++; - - if (historyIndex < currentHistory.Count) - { - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - (string)currentHistory[historyIndex].Properties["CommandLine"].Value, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - else if (historyIndex == currentHistory.Count) - { - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - } - } - else if (keyInfo.Key == ConsoleKey.Escape) - { - currentCompletion = null; - historyIndex = currentHistory != null ? currentHistory.Count : -1; - - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: 0, - replaceLength: inputLine.Length); - } - else if (keyInfo.Key == ConsoleKey.Backspace) - { - currentCompletion = null; - - if (currentCursorIndex > 0) - { - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - insertIndex: currentCursorIndex - 1, - replaceLength: 1, - finalCursorIndex: currentCursorIndex - 1); - } - } - else if (keyInfo.Key == ConsoleKey.Delete) - { - currentCompletion = null; - - if (currentCursorIndex < inputLine.Length) - { - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - string.Empty, - currentCursorIndex, - replaceLength: 1, - finalCursorIndex: currentCursorIndex); - } - } - else if (keyInfo.Key == ConsoleKey.Enter) - { - string completedInput = inputLine.ToString(); - currentCompletion = null; - currentHistory = null; - - //if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) - //{ - // // TODO: Start a new line! - // continue; - //} - - Parser.ParseInput( - completedInput, - out Token[] tokens, - out ParseError[] parseErrors); - - //if (parseErrors.Any(e => e.IncompleteInput)) - //{ - // // TODO: Start a new line! - // continue; - //} - - return completedInput; - } - else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) - { - // Normal character input - currentCompletion = null; - - currentCursorIndex = - InsertInput( - inputLine, - promptStartCol, - promptStartRow, - keyInfo.KeyChar.ToString(), // TODO: Determine whether this should take culture into account - currentCursorIndex, - finalCursorIndex: currentCursorIndex + 1); - } - } - } - finally - { - Console.TreatControlCAsInput = false; - } - - return null; - } - - // TODO: Is this used? - private static int CalculateIndexFromCursor( - int promptStartCol, - int promptStartRow, - int consoleWidth) - { - return - ((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) + - ConsoleProxy.GetCursorLeft() - promptStartCol; - } - - private static void CalculateCursorFromIndex( - int promptStartCol, - int promptStartRow, - int consoleWidth, - int inputIndex, - out int cursorCol, - out int cursorRow) - { - cursorCol = promptStartCol + inputIndex; - cursorRow = promptStartRow + cursorCol / consoleWidth; - cursorCol = cursorCol % consoleWidth; - } - - private static int InsertInput( - StringBuilder inputLine, - int promptStartCol, - int promptStartRow, - string insertedInput, - int cursorIndex, - int insertIndex = -1, - int replaceLength = 0, - int finalCursorIndex = -1) - { - int consoleWidth = Console.WindowWidth; - int previousInputLength = inputLine.Length; - - if (insertIndex == -1) - { - insertIndex = cursorIndex; - } - - // Move the cursor to the new insertion point - MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - insertIndex); - - // Edit the input string based on the insertion - if (insertIndex < inputLine.Length) - { - if (replaceLength > 0) - { - inputLine.Remove(insertIndex, replaceLength); - } - - inputLine.Insert(insertIndex, insertedInput); - } - else - { - inputLine.Append(insertedInput); - } - - // Re-render affected section - Console.Write( - inputLine.ToString( - insertIndex, - inputLine.Length - insertIndex)); - - if (inputLine.Length < previousInputLength) - { - Console.Write( - new string( - ' ', - previousInputLength - inputLine.Length)); - } - - // Automatically set the final cursor position to the end - // of the new input string. This is needed if the previous - // input string is longer than the new one and needed to have - // its old contents overwritten. This will position the cursor - // back at the end of the new text - if (finalCursorIndex == -1 && inputLine.Length < previousInputLength) - { - finalCursorIndex = inputLine.Length; - } - - if (finalCursorIndex > -1) - { - // Move the cursor to the final position - return - MoveCursorToIndex( - promptStartCol, - promptStartRow, - consoleWidth, - finalCursorIndex); - } - else - { - return inputLine.Length; - } - } - - private static int MoveCursorToIndex( - int promptStartCol, - int promptStartRow, - int consoleWidth, - int newCursorIndex) - { - CalculateCursorFromIndex( - promptStartCol, - promptStartRow, - consoleWidth, - newCursorIndex, - out int newCursorCol, - out int newCursorRow); - - Console.SetCursorPosition(newCursorCol, newCursorRow); - - return newCursorIndex; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs index 3bb0bede8..1233df7b0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/IReadLine.cs @@ -12,9 +12,5 @@ internal interface IReadLine string ReadLine(CancellationToken cancellationToken); SecureString ReadSecureLine(CancellationToken cancellationToken); - - bool TryOverrideReadKey(Func readKeyOverride); - - bool TryOverrideIdleHandler(Action idleHandler); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs new file mode 100644 index 000000000..a29fe17f2 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -0,0 +1,539 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal class LegacyReadLine : TerminalReadLine + { + private readonly PsesInternalHost _psesHost; + + private readonly Task[] _readKeyTasks; + + private readonly Func _readKeyFunc; + + private readonly Action _onIdleAction; + + public LegacyReadLine( + PsesInternalHost psesHost, + Func readKeyFunc, + Action onIdleAction) + { + _psesHost = psesHost; + _readKeyTasks = new Task[2]; + _readKeyFunc = readKeyFunc; + _onIdleAction = onIdleAction; + } + + public override string ReadLine(CancellationToken cancellationToken) + { + string inputBeforeCompletion = null; + string inputAfterCompletion = null; + CommandCompletion currentCompletion = null; + + int historyIndex = -1; + IReadOnlyList currentHistory = null; + + StringBuilder inputLine = new StringBuilder(); + + int initialCursorCol = ConsoleProxy.GetCursorLeft(cancellationToken); + int initialCursorRow = ConsoleProxy.GetCursorTop(cancellationToken); + + int currentCursorIndex = 0; + + Console.TreatControlCAsInput = true; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); + + // Do final position calculation after the key has been pressed + // because the window could have been resized before then + int promptStartCol = initialCursorCol; + int promptStartRow = initialCursorRow; + int consoleWidth = Console.WindowWidth; + + switch (keyInfo.Key) + { + case ConsoleKey.Tab: + if (currentCompletion == null) + { + inputBeforeCompletion = inputLine.ToString(); + inputAfterCompletion = null; + + // TODO: This logic should be moved to AstOperations or similar! + + if (_psesHost.DebugContext.IsStopped) + { + PSCommand command = new PSCommand() + .AddCommand("TabExpansion2") + .AddParameter("InputScript", inputBeforeCompletion) + .AddParameter("CursorColumn", currentCursorIndex) + .AddParameter("Options", null); + + currentCompletion = _psesHost.InvokePSCommand(command, PowerShellExecutionOptions.Default, cancellationToken).FirstOrDefault(); + } + else + { + currentCompletion = _psesHost.InvokePSDelegate( + "Legacy readline inline command completion", + ExecutionOptions.Default, + (pwsh, cancellationToken) => CommandCompletion.CompleteInput(inputAfterCompletion, currentCursorIndex, options: null, pwsh), + cancellationToken); + + if (currentCompletion.CompletionMatches.Count > 0) + { + int replacementEndIndex = + currentCompletion.ReplacementIndex + + currentCompletion.ReplacementLength; + + inputAfterCompletion = + inputLine.ToString( + replacementEndIndex, + inputLine.Length - replacementEndIndex); + } + else + { + currentCompletion = null; + } + } + } + + CompletionResult completion = + currentCompletion?.GetNextResult( + !keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)); + + if (completion != null) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + $"{completion.CompletionText}{inputAfterCompletion}", + currentCursorIndex, + insertIndex: currentCompletion.ReplacementIndex, + replaceLength: inputLine.Length - currentCompletion.ReplacementIndex, + finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length); + } + + continue; + + case ConsoleKey.LeftArrow: + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex - 1); + } + + continue; + + case ConsoleKey.Home: + currentCompletion = null; + + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + 0); + + continue; + + case ConsoleKey.RightArrow: + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + currentCursorIndex + 1); + } + + continue; + + case ConsoleKey.End: + currentCompletion = null; + + currentCursorIndex = + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + inputLine.Length); + + continue; + + case ConsoleKey.UpArrow: + currentCompletion = null; + + // TODO: Ctrl+Up should allow navigation in multi-line input + + if (currentHistory == null) + { + historyIndex = -1; + + PSCommand command = new PSCommand() + .AddCommand("Get-History"); + + currentHistory = _psesHost.InvokePSCommand(command, PowerShellExecutionOptions.Default, cancellationToken); + + if (currentHistory != null) + { + historyIndex = currentHistory.Count; + } + } + + if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0) + { + historyIndex--; + + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + + continue; + + case ConsoleKey.DownArrow: + currentCompletion = null; + + // The down arrow shouldn't cause history to be loaded, + // it's only for navigating an active history array + + if (historyIndex > -1 && historyIndex < currentHistory.Count && + currentHistory != null && currentHistory.Count > 0) + { + historyIndex++; + + if (historyIndex < currentHistory.Count) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + (string)currentHistory[historyIndex].Properties["CommandLine"].Value, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + else if (historyIndex == currentHistory.Count) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + } + } + + continue; + + case ConsoleKey.Escape: + currentCompletion = null; + historyIndex = currentHistory != null ? currentHistory.Count : -1; + + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: 0, + replaceLength: inputLine.Length); + + continue; + + case ConsoleKey.Backspace: + currentCompletion = null; + + if (currentCursorIndex > 0) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + insertIndex: currentCursorIndex - 1, + replaceLength: 1, + finalCursorIndex: currentCursorIndex - 1); + } + + continue; + + case ConsoleKey.Delete: + currentCompletion = null; + + if (currentCursorIndex < inputLine.Length) + { + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + string.Empty, + currentCursorIndex, + replaceLength: 1, + finalCursorIndex: currentCursorIndex); + } + + continue; + + case ConsoleKey.Enter: + string completedInput = inputLine.ToString(); + currentCompletion = null; + currentHistory = null; + + // TODO: Add line continuation support: + // - When shift+enter is pressed, or + // - When the parse indicates incomplete input + + //if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) + //{ + // // TODO: Start a new line! + // continue; + //} + //Parser.ParseInput( + // completedInput, + // out Token[] tokens, + // out ParseError[] parseErrors); + //if (parseErrors.Any(e => e.IncompleteInput)) + //{ + // // TODO: Start a new line! + // continue; + //} + + return completedInput; + + default: + if (keyInfo.IsCtrlC()) + { + throw new PipelineStoppedException(); + } + + // Normal character input + if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + currentCompletion = null; + + currentCursorIndex = + InsertInput( + inputLine, + promptStartCol, + promptStartRow, + keyInfo.KeyChar.ToString(), // TODO: Determine whether this should take culture into account + currentCursorIndex, + finalCursorIndex: currentCursorIndex + 1); + } + + continue; + } + } + } + catch (OperationCanceledException) + { + // We've broken out of the loop + } + finally + { + Console.TreatControlCAsInput = false; + } + + // If we break out of the loop without returning (because of the Enter key) + // then the readline has been aborted in some way and we should return nothing + return null; + } + + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return _onIdleAction is null + ? InvokeReadKeyFunc() + : ReadKeyWithIdleSupport(cancellationToken); + } + finally + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private ConsoleKeyInfo ReadKeyWithIdleSupport(CancellationToken cancellationToken) + { + // We run the readkey function on another thread so we can run an idle handler + Task readKeyTask = Task.Run(InvokeReadKeyFunc); + + _readKeyTasks[0] = readKeyTask; + _readKeyTasks[1] = Task.Delay(millisecondsDelay: 300, cancellationToken); + + while (true) + { + switch (Task.WaitAny(_readKeyTasks, cancellationToken)) + { + // ReadKey returned + case 0: + return readKeyTask.Result; + + // The idle timed out + case 1: + _onIdleAction(cancellationToken); + _readKeyTasks[1] = Task.Delay(millisecondsDelay: 300, cancellationToken); + continue; + } + } + } + + private ConsoleKeyInfo InvokeReadKeyFunc() + { + // intercept = false means we display the key in the console + return _readKeyFunc(/* intercept */ false); + } + + private static int InsertInput( + StringBuilder inputLine, + int promptStartCol, + int promptStartRow, + string insertedInput, + int cursorIndex, + int insertIndex = -1, + int replaceLength = 0, + int finalCursorIndex = -1) + { + int consoleWidth = Console.WindowWidth; + int previousInputLength = inputLine.Length; + + if (insertIndex == -1) + { + insertIndex = cursorIndex; + } + + // Move the cursor to the new insertion point + MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + insertIndex); + + // Edit the input string based on the insertion + if (insertIndex < inputLine.Length) + { + if (replaceLength > 0) + { + inputLine.Remove(insertIndex, replaceLength); + } + + inputLine.Insert(insertIndex, insertedInput); + } + else + { + inputLine.Append(insertedInput); + } + + // Re-render affected section + Console.Write( + inputLine.ToString( + insertIndex, + inputLine.Length - insertIndex)); + + if (inputLine.Length < previousInputLength) + { + Console.Write( + new string( + ' ', + previousInputLength - inputLine.Length)); + } + + // Automatically set the final cursor position to the end + // of the new input string. This is needed if the previous + // input string is longer than the new one and needed to have + // its old contents overwritten. This will position the cursor + // back at the end of the new text + if (finalCursorIndex == -1 && inputLine.Length < previousInputLength) + { + finalCursorIndex = inputLine.Length; + } + + if (finalCursorIndex > -1) + { + // Move the cursor to the final position + return MoveCursorToIndex( + promptStartCol, + promptStartRow, + consoleWidth, + finalCursorIndex); + } + else + { + return inputLine.Length; + } + } + + private static int MoveCursorToIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int newCursorIndex) + { + CalculateCursorFromIndex( + promptStartCol, + promptStartRow, + consoleWidth, + newCursorIndex, + out int newCursorCol, + out int newCursorRow); + + Console.SetCursorPosition(newCursorCol, newCursorRow); + + return newCursorIndex; + } + private static void CalculateCursorFromIndex( + int promptStartCol, + int promptStartRow, + int consoleWidth, + int inputIndex, + out int cursorCol, + out int cursorRow) + { + cursorCol = promptStartCol + inputIndex; + cursorRow = promptStartRow + cursorCol / consoleWidth; + cursorCol = cursorCol % consoleWidth; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs new file mode 100644 index 000000000..1bee46f76 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using System.Management.Automation; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal class PsrlReadLine : TerminalReadLine + { + private readonly PSReadLineProxy _psrlProxy; + + private readonly PsesInternalHost _psesHost; + + private readonly EngineIntrinsics _engineIntrinsics; + + #region Constructors + + public PsrlReadLine( + PSReadLineProxy psrlProxy, + PsesInternalHost psesHost, + EngineIntrinsics engineIntrinsics, + Func readKeyFunc, + Action onIdleAction) + { + _psrlProxy = psrlProxy; + _psesHost = psesHost; + _engineIntrinsics = engineIntrinsics; + _psrlProxy.OverrideReadKey(readKeyFunc); + _psrlProxy.OverrideIdleHandler(onIdleAction); + } + + #endregion + + #region Public Methods + + public override string ReadLine(CancellationToken cancellationToken) + { + return _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); + } + + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) + { + return ConsoleProxy.ReadKey(intercept: true, cancellationToken); + } + + #endregion + + #region Private Methods + + private string InvokePSReadLine(CancellationToken cancellationToken) + { + EngineIntrinsics engineIntrinsics = _psesHost.IsRunspacePushed ? null : _engineIntrinsics; + return _psrlProxy.ReadLine(_psesHost.Runspace, engineIntrinsics, cancellationToken, /* lastExecutionStatus */ null); + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs new file mode 100644 index 000000000..474c23f1c --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using System.Management.Automation; +using System.Security; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal abstract class TerminalReadLine : IReadLine + { + public abstract string ReadLine(CancellationToken cancellationToken); + + protected abstract ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); + + public SecureString ReadSecureLine(CancellationToken cancellationToken) + { + Console.TreatControlCAsInput = true; + int previousInputLength = 0; + SecureString secureString = new SecureString(); + try + { + bool enterPressed = false; + while (!enterPressed && !cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); + + if (keyInfo.IsCtrlC()) + { + throw new PipelineStoppedException(); + } + + switch (keyInfo.Key) + { + case ConsoleKey.Enter: + // Stop the while loop so we can realign the cursor + // and then return the entered string + enterPressed = true; + continue; + + case ConsoleKey.Tab: + break; + + case ConsoleKey.Backspace: + if (secureString.Length > 0) + { + secureString.RemoveAt(secureString.Length - 1); + } + break; + + default: + if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar)) + { + secureString.AppendChar(keyInfo.KeyChar); + } + break; + } + + // Re-render the secure string characters + int currentInputLength = secureString.Length; + int consoleWidth = Console.WindowWidth; + + if (currentInputLength > previousInputLength) + { + Console.Write('*'); + } + else if (previousInputLength > 0 && currentInputLength < previousInputLength) + { + int row = ConsoleProxy.GetCursorTop(cancellationToken); + int col = ConsoleProxy.GetCursorLeft(cancellationToken); + + // Back up the cursor before clearing the character + col--; + if (col < 0) + { + col = consoleWidth - 1; + row--; + } + + Console.SetCursorPosition(col, row); + Console.Write(' '); + Console.SetCursorPosition(col, row); + } + + previousInputLength = currentInputLength; + } + } + finally + { + Console.TreatControlCAsInput = false; + } + + return secureString; + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 5893b690a..c3ebd9bb8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -625,12 +625,14 @@ private static PowerShell CreatePowerShellForRunspace(Runspace runspace) var engineIntrinsics = (EngineIntrinsics)runspace.SessionStateProxy.GetVariable("ExecutionContext"); - if (hostStartupInfo.ConsoleReplEnabled && !hostStartupInfo.UsesLegacyReadLine) + if (hostStartupInfo.ConsoleReplEnabled) { - var psrlProxy = PSReadLineProxy.LoadAndCreate(_loggerFactory, pwsh); - var readLine = new ConsoleReadLine(psrlProxy, this, engineIntrinsics); - readLine.TryOverrideReadKey(ReadKey); - readLine.TryOverrideIdleHandler(OnPowerShellIdle); + // If we've been configured to use it, or if we can't load PSReadLine, use the legacy readline + if (hostStartupInfo.UsesLegacyReadLine || !TryLoadPSReadLine(pwsh, engineIntrinsics, out IReadLine readLine)) + { + readLine = new LegacyReadLine(this, ReadKey, OnPowerShellIdle); + } + readLineProvider.OverrideReadLine(readLine); System.Console.CancelKeyPress += OnCancelKeyPress; System.Console.InputEncoding = Encoding.UTF8; @@ -747,9 +749,7 @@ private ConsoleKeyInfo ReadKey(bool intercept) private bool LastKeyWasCtrlC() { return _lastKey.HasValue - && _lastKey.Value.Key == ConsoleKey.C - && (_lastKey.Value.Modifiers & ConsoleModifiers.Control) != 0 - && (_lastKey.Value.Modifiers & ConsoleModifiers.Alt) == 0; + && _lastKey.Value.IsCtrlC(); } private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs) @@ -825,6 +825,22 @@ private Task PopOrReinitializeRunspaceAsync() CancellationToken.None); } + private bool TryLoadPSReadLine(PowerShell pwsh, EngineIntrinsics engineIntrinsics, out IReadLine psrlReadLine) + { + psrlReadLine = null; + try + { + var psrlProxy = PSReadLineProxy.LoadAndCreate(_loggerFactory, pwsh); + psrlReadLine = new PsrlReadLine(psrlProxy, this, engineIntrinsics, ReadKey, OnPowerShellIdle); + return true; + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load PSReadLine. Will fall back to legacy readline implementation."); + return false; + } + } + private record RunspaceFrame( Runspace Runspace, RunspaceInfo RunspaceInfo); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs new file mode 100644 index 000000000..24498772c --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility +{ + internal static class ConsoleKeyInfoExtensions + { + public static bool IsCtrlC(this ConsoleKeyInfo keyInfo) + { + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C + && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0 + && (keyInfo.Modifiers & ConsoleModifiers.Shift) == 0 + && (keyInfo.Modifiers & ConsoleModifiers.Alt) == 0; + } + } +}