From 0f8dd05f2d07d36c7770887745fec7987eaa99eb Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 16:08:15 -0700 Subject: [PATCH 01/12] Add initial ReadLine implementation --- .../PowerShell/Console/LegacyReadLine.cs | 536 ++++++++++++++++++ .../{ConsoleReadLine.cs => PsrlReadLine.cs} | 8 +- .../PowerShell/Host/PsesInternalHost.cs | 2 +- 3 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs rename src/PowerShellEditorServices/Services/PowerShell/Console/{ConsoleReadLine.cs => PsrlReadLine.cs} (99%) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs new file mode 100644 index 000000000..419c7387d --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -0,0 +1,536 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation; +using System.Security; +using System.Text; +using System.Threading; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + + internal class LegacyReadLine : IReadLine + { + private readonly PsesInternalHost _psesHost; + + private readonly IPowerShellDebugContext _debugContext; + + private Func _readKeyFunc; + + public LegacyReadLine( + PsesInternalHost psesHost, + IPowerShellDebugContext debugContext) + { + _psesHost = psesHost; + _debugContext = debugContext; + } + + public string ReadLine(CancellationToken cancellationToken) + { + // TODO: Is inputBeforeCompletion used? + 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); + + // 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(displayKeyInConsole: false, 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; + + //case ConsoleKey.C when ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0): + // throw new PipelineStoppedException(); + + + 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 (_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; + + //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 (IsCtrlC(keyInfo)) + { + 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; + } + + private static bool IsCtrlC(ConsoleKeyInfo keyInfo) + { + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C + && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0 + && (keyInfo.Modifiers & ConsoleModifiers.Alt) == 0; + } + + public SecureString ReadSecureLine(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public bool TryOverrideIdleHandler(Action idleHandler) + { + return true; + } + + public bool TryOverrideReadKey(Func readKeyOverride) + { + _readKeyFunc = readKeyOverride; + return true; + } + + private ConsoleKeyInfo ReadKey(bool displayKeyInConsole, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return _readKeyFunc(!displayKeyInConsole); + } + finally + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + 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/ConsoleReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs similarity index 99% rename from src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleReadLine.cs rename to src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index 55174610c..ff1ff6540 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/ConsoleReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -1,20 +1,20 @@ // 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; +using System.Text; +using System.Threading; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { using System; - internal class ConsoleReadLine : IReadLine + internal class PsrlReadLine : IReadLine { private readonly PSReadLineProxy _psrlProxy; @@ -24,7 +24,7 @@ internal class ConsoleReadLine : IReadLine #region Constructors - public ConsoleReadLine( + public PsrlReadLine( PSReadLineProxy psrlProxy, PsesInternalHost psesHost, EngineIntrinsics engineIntrinsics) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 5893b690a..a0eafa43a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -628,7 +628,7 @@ private static PowerShell CreatePowerShellForRunspace(Runspace runspace) if (hostStartupInfo.ConsoleReplEnabled && !hostStartupInfo.UsesLegacyReadLine) { var psrlProxy = PSReadLineProxy.LoadAndCreate(_loggerFactory, pwsh); - var readLine = new ConsoleReadLine(psrlProxy, this, engineIntrinsics); + var readLine = new PsrlReadLine(psrlProxy, this, engineIntrinsics); readLine.TryOverrideReadKey(ReadKey); readLine.TryOverrideIdleHandler(OnPowerShellIdle); readLineProvider.OverrideReadLine(readLine); From ee874e473fa8cfb0e31e09ccecbaa7b0d28957ff Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 16:21:23 -0700 Subject: [PATCH 02/12] Add common secure string read functionality --- .../PowerShell/Console/LegacyReadLine.cs | 33 +- .../PowerShell/Console/PsrlReadLine.cs | 568 +----------------- .../PowerShell/Console/TerminalReadLine.cs | 114 ++++ 3 files changed, 129 insertions(+), 586 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 419c7387d..42cd060d7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { using System; - internal class LegacyReadLine : IReadLine + internal class LegacyReadLine : TerminalReadLine { private readonly PsesInternalHost _psesHost; @@ -32,7 +32,7 @@ public LegacyReadLine( _debugContext = debugContext; } - public string ReadLine(CancellationToken cancellationToken) + public override string ReadLine(CancellationToken cancellationToken) { // TODO: Is inputBeforeCompletion used? string inputBeforeCompletion = null; @@ -59,7 +59,7 @@ public string ReadLine(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { - ConsoleKeyInfo keyInfo = ReadKey(displayKeyInConsole: false, cancellationToken); + ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); // Do final position calculation after the key has been pressed // because the window could have been resized before then @@ -383,46 +383,31 @@ public string ReadLine(CancellationToken cancellationToken) return null; } - private static bool IsCtrlC(ConsoleKeyInfo keyInfo) - { - if ((int)keyInfo.Key == 3) - { - return true; - } - - return keyInfo.Key == ConsoleKey.C - && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0 - && (keyInfo.Modifiers & ConsoleModifiers.Alt) == 0; - } - - public SecureString ReadSecureLine(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public bool TryOverrideIdleHandler(Action idleHandler) + public override bool TryOverrideIdleHandler(Action idleHandler) { return true; } - public bool TryOverrideReadKey(Func readKeyOverride) + public override bool TryOverrideReadKey(Func readKeyOverride) { _readKeyFunc = readKeyOverride; return true; } - private ConsoleKeyInfo ReadKey(bool displayKeyInConsole, CancellationToken cancellationToken) + protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); try { - return _readKeyFunc(!displayKeyInConsole); + // intercept = false means we display the key in the console + return _readKeyFunc(/* intercept */ false); } finally { cancellationToken.ThrowIfCancellationRequested(); } } + private static int InsertInput( StringBuilder inputLine, int promptStartCol, diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index ff1ff6540..9a8e4da39 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -14,7 +14,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { using System; - internal class PsrlReadLine : IReadLine + internal class PsrlReadLine : TerminalReadLine { private readonly PSReadLineProxy _psrlProxy; @@ -38,594 +38,38 @@ public PsrlReadLine( #region Public Methods - public string ReadLine(CancellationToken cancellationToken) + public override string ReadLine(CancellationToken cancellationToken) { return _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); } - public bool TryOverrideReadKey(Func readKeyFunc) + public override bool TryOverrideReadKey(Func readKeyFunc) { _psrlProxy.OverrideReadKey(readKeyFunc); return true; } - public bool TryOverrideIdleHandler(Action idleHandler) + public override bool TryOverrideIdleHandler(Action idleHandler) { _psrlProxy.OverrideIdleHandler(idleHandler); return true; } - public SecureString ReadSecureLine(CancellationToken cancellationToken) + protected override ConsoleKeyInfo ReadKey(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; + return ConsoleProxy.ReadKey(intercept: true, cancellationToken); } #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/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs new file mode 100644 index 000000000..cd047e8e7 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Security; +using System.Threading; + +namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console +{ + using System; + using System.Management.Automation; + + internal abstract class TerminalReadLine : IReadLine + { + public abstract string ReadLine(CancellationToken cancellationToken); + + public abstract bool TryOverrideIdleHandler(Action idleHandler); + + public abstract bool TryOverrideReadKey(Func readKeyOverride); + + 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 (IsCtrlC(keyInfo)) + { + throw new PipelineStoppedException(); + } + + switch (keyInfo.Key) + { + case ConsoleKey.Enter: + // Break to return the completed 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; + } + + protected static bool IsCtrlC(ConsoleKeyInfo keyInfo) + { + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C + && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0 + && (keyInfo.Modifiers & ConsoleModifiers.Alt) == 0; + } + + } +} From 20b3ed3448966a782f9bd9c95f95c1d192bee778 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 16:34:32 -0700 Subject: [PATCH 03/12] Implement shared Ctrl-C test implementation --- .../PowerShell/Console/LegacyReadLine.cs | 12 +++++----- .../PowerShell/Console/TerminalReadLine.cs | 18 +++------------ .../PowerShell/Host/PsesInternalHost.cs | 4 +--- .../Utility/ConsoleKeyInfoExtensions.cs | 23 +++++++++++++++++++ 4 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Utility/ConsoleKeyInfoExtensions.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 42cd060d7..58566a638 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; -using System.Management.Automation; -using System.Security; -using System.Text; -using System.Threading; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; 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.Management.Automation.Language; +using System.Text; +using System.Threading; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { @@ -345,7 +345,7 @@ public override string ReadLine(CancellationToken cancellationToken) return completedInput; default: - if (IsCtrlC(keyInfo)) + if (keyInfo.IsCtrlC()) { throw new PipelineStoppedException(); } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs index cd047e8e7..31cc8a8fb 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -1,13 +1,14 @@ // 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; - using System.Management.Automation; internal abstract class TerminalReadLine : IReadLine { @@ -31,7 +32,7 @@ public SecureString ReadSecureLine(CancellationToken cancellationToken) { ConsoleKeyInfo keyInfo = ReadKey(cancellationToken); - if (IsCtrlC(keyInfo)) + if (keyInfo.IsCtrlC()) { throw new PipelineStoppedException(); } @@ -97,18 +98,5 @@ public SecureString ReadSecureLine(CancellationToken cancellationToken) return secureString; } - - protected static bool IsCtrlC(ConsoleKeyInfo keyInfo) - { - if ((int)keyInfo.Key == 3) - { - return true; - } - - return keyInfo.Key == ConsoleKey.C - && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0 - && (keyInfo.Modifiers & ConsoleModifiers.Alt) == 0; - } - } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index a0eafa43a..9e478ee39 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -747,9 +747,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) 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; + } + } +} From 8fbfca7747a8afa9cb79962caee2f1ada89e6f5a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 16:35:29 -0700 Subject: [PATCH 04/12] Avoid unused parser call --- .../Services/PowerShell/Console/LegacyReadLine.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 58566a638..6a86c4b79 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -330,12 +330,10 @@ public override string ReadLine(CancellationToken cancellationToken) // // TODO: Start a new line! // continue; //} - - Parser.ParseInput( - completedInput, - out Token[] tokens, - out ParseError[] parseErrors); - + //Parser.ParseInput( + // completedInput, + // out Token[] tokens, + // out ParseError[] parseErrors); //if (parseErrors.Any(e => e.IncompleteInput)) //{ // // TODO: Start a new line! From 7337696e0ce2f7da7151b720faa93e36149cd7d1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 16:48:39 -0700 Subject: [PATCH 05/12] Add idle support for legacy readline --- .../PowerShell/Console/LegacyReadLine.cs | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 6a86c4b79..a980683e7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -15,6 +15,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { using System; + using System.Threading.Tasks; internal class LegacyReadLine : TerminalReadLine { @@ -22,14 +23,19 @@ internal class LegacyReadLine : TerminalReadLine private readonly IPowerShellDebugContext _debugContext; + private readonly Task[] _readKeyTasks; + private Func _readKeyFunc; + private Action _onIdleAction; + public LegacyReadLine( PsesInternalHost psesHost, IPowerShellDebugContext debugContext) { _psesHost = psesHost; _debugContext = debugContext; + _readKeyTasks = new Task[2]; } public override string ReadLine(CancellationToken cancellationToken) @@ -383,6 +389,7 @@ public override string ReadLine(CancellationToken cancellationToken) public override bool TryOverrideIdleHandler(Action idleHandler) { + _onIdleAction = idleHandler; return true; } @@ -397,8 +404,9 @@ protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) cancellationToken.ThrowIfCancellationRequested(); try { - // intercept = false means we display the key in the console - return _readKeyFunc(/* intercept */ false); + return _onIdleAction is null + ? InvokeReadKeyFunc() + : ReadKeyWithIdleSupport(cancellationToken); } finally { @@ -406,6 +414,37 @@ protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) } } + 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(); + _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, From 21401203edd347e19f206010d7bee2bf442f5050 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 16:52:45 -0700 Subject: [PATCH 06/12] Add comment about unimplemented line continuation support --- .../Services/PowerShell/Console/LegacyReadLine.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index a980683e7..cd39682a7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -8,14 +8,13 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation; -using System.Management.Automation.Language; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console { using System; - using System.Threading.Tasks; internal class LegacyReadLine : TerminalReadLine { @@ -331,6 +330,10 @@ public override string ReadLine(CancellationToken cancellationToken) 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! From 9787b58de0d5ef22506d4a80b9fd814cb9363ed6 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 17:03:20 -0700 Subject: [PATCH 07/12] Hook up legacy readline support in the host --- .../PowerShell/Console/LegacyReadLine.cs | 8 ++---- .../PowerShell/Host/PsesInternalHost.cs | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index cd39682a7..b6dfe9150 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -20,8 +20,6 @@ internal class LegacyReadLine : TerminalReadLine { private readonly PsesInternalHost _psesHost; - private readonly IPowerShellDebugContext _debugContext; - private readonly Task[] _readKeyTasks; private Func _readKeyFunc; @@ -29,11 +27,9 @@ internal class LegacyReadLine : TerminalReadLine private Action _onIdleAction; public LegacyReadLine( - PsesInternalHost psesHost, - IPowerShellDebugContext debugContext) + PsesInternalHost psesHost) { _psesHost = psesHost; - _debugContext = debugContext; _readKeyTasks = new Task[2]; } @@ -86,7 +82,7 @@ public override string ReadLine(CancellationToken cancellationToken) // TODO: This logic should be moved to AstOperations or similar! - if (_debugContext.IsStopped) + if (_psesHost.DebugContext.IsStopped) { PSCommand command = new PSCommand() .AddCommand("TabExpansion2") diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 9e478ee39..da7a8ab89 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -625,10 +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 PsrlReadLine(psrlProxy, this, engineIntrinsics); + // 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); + } + readLine.TryOverrideReadKey(ReadKey); readLine.TryOverrideIdleHandler(OnPowerShellIdle); readLineProvider.OverrideReadLine(readLine); @@ -823,6 +827,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); + 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); From 30f58b06653226bc87f86579179ac3d5395f17aa Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 17:04:54 -0700 Subject: [PATCH 08/12] Remove stale TODOs --- .../Services/PowerShell/Console/LegacyReadLine.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index b6dfe9150..883533d3f 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -35,7 +35,6 @@ public LegacyReadLine( public override string ReadLine(CancellationToken cancellationToken) { - // TODO: Is inputBeforeCompletion used? string inputBeforeCompletion = null; string inputAfterCompletion = null; CommandCompletion currentCompletion = null; @@ -48,10 +47,6 @@ public override string ReadLine(CancellationToken cancellationToken) 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; From 20bd69bc12f5614fe8168c38d222367ee6cfd212 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 12 Oct 2021 17:05:15 -0700 Subject: [PATCH 09/12] Remove commented out code --- .../Services/PowerShell/Console/LegacyReadLine.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 883533d3f..33500b4fa 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -63,10 +63,6 @@ public override string ReadLine(CancellationToken cancellationToken) int promptStartRow = initialCursorRow; int consoleWidth = Console.WindowWidth; - //case ConsoleKey.C when ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0): - // throw new PipelineStoppedException(); - - switch (keyInfo.Key) { case ConsoleKey.Tab: From b68c7acab541d400d1f0011af7468c6f087ab618 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 13 Oct 2021 12:25:50 -0700 Subject: [PATCH 10/12] Remove unused usings --- .../Services/PowerShell/Console/LegacyReadLine.cs | 1 - .../Services/PowerShell/Console/PsrlReadLine.cs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 33500b4fa..51d494e35 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index 9a8e4da39..0feeb58b7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -3,11 +3,7 @@ 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; -using System.Text; using System.Threading; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console From 43bddb1d80c63f2a5c06a9a56d0188e9c69f3e88 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 14 Oct 2021 15:13:37 -0700 Subject: [PATCH 11/12] Remove unneeded readline interface methods --- .../Services/PowerShell/Console/IReadLine.cs | 4 ---- .../PowerShell/Console/LegacyReadLine.cs | 24 +++++++------------ .../PowerShell/Console/PsrlReadLine.cs | 18 ++++---------- .../PowerShell/Console/TerminalReadLine.cs | 4 ---- .../PowerShell/Host/PsesInternalHost.cs | 6 ++--- 5 files changed, 15 insertions(+), 41 deletions(-) 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 index 51d494e35..a29fe17f2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -21,15 +21,19 @@ internal class LegacyReadLine : TerminalReadLine private readonly Task[] _readKeyTasks; - private Func _readKeyFunc; + private readonly Func _readKeyFunc; - private Action _onIdleAction; + private readonly Action _onIdleAction; public LegacyReadLine( - PsesInternalHost psesHost) + PsesInternalHost psesHost, + Func readKeyFunc, + Action onIdleAction) { _psesHost = psesHost; _readKeyTasks = new Task[2]; + _readKeyFunc = readKeyFunc; + _onIdleAction = onIdleAction; } public override string ReadLine(CancellationToken cancellationToken) @@ -376,18 +380,6 @@ public override string ReadLine(CancellationToken cancellationToken) return null; } - public override bool TryOverrideIdleHandler(Action idleHandler) - { - _onIdleAction = idleHandler; - return true; - } - - public override bool TryOverrideReadKey(Func readKeyOverride) - { - _readKeyFunc = readKeyOverride; - return true; - } - protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -421,7 +413,7 @@ private ConsoleKeyInfo ReadKeyWithIdleSupport(CancellationToken cancellationToke // The idle timed out case 1: - _onIdleAction(); + _onIdleAction(cancellationToken); _readKeyTasks[1] = Task.Delay(millisecondsDelay: 300, cancellationToken); continue; } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs index 0feeb58b7..1bee46f76 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/PsrlReadLine.cs @@ -23,11 +23,15 @@ internal class PsrlReadLine : TerminalReadLine public PsrlReadLine( PSReadLineProxy psrlProxy, PsesInternalHost psesHost, - EngineIntrinsics engineIntrinsics) + EngineIntrinsics engineIntrinsics, + Func readKeyFunc, + Action onIdleAction) { _psrlProxy = psrlProxy; _psesHost = psesHost; _engineIntrinsics = engineIntrinsics; + _psrlProxy.OverrideReadKey(readKeyFunc); + _psrlProxy.OverrideIdleHandler(onIdleAction); } #endregion @@ -39,18 +43,6 @@ public override string ReadLine(CancellationToken cancellationToken) return _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken); } - public override bool TryOverrideReadKey(Func readKeyFunc) - { - _psrlProxy.OverrideReadKey(readKeyFunc); - return true; - } - - public override bool TryOverrideIdleHandler(Action idleHandler) - { - _psrlProxy.OverrideIdleHandler(idleHandler); - return true; - } - protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) { return ConsoleProxy.ReadKey(intercept: true, cancellationToken); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs index 31cc8a8fb..1e0435b6c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -14,10 +14,6 @@ internal abstract class TerminalReadLine : IReadLine { public abstract string ReadLine(CancellationToken cancellationToken); - public abstract bool TryOverrideIdleHandler(Action idleHandler); - - public abstract bool TryOverrideReadKey(Func readKeyOverride); - protected abstract ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); public SecureString ReadSecureLine(CancellationToken cancellationToken) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index da7a8ab89..c3ebd9bb8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -630,11 +630,9 @@ private static PowerShell CreatePowerShellForRunspace(Runspace runspace) // 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); + readLine = new LegacyReadLine(this, ReadKey, OnPowerShellIdle); } - readLine.TryOverrideReadKey(ReadKey); - readLine.TryOverrideIdleHandler(OnPowerShellIdle); readLineProvider.OverrideReadLine(readLine); System.Console.CancelKeyPress += OnCancelKeyPress; System.Console.InputEncoding = Encoding.UTF8; @@ -833,7 +831,7 @@ private bool TryLoadPSReadLine(PowerShell pwsh, EngineIntrinsics engineIntrinsic try { var psrlProxy = PSReadLineProxy.LoadAndCreate(_loggerFactory, pwsh); - psrlReadLine = new PsrlReadLine(psrlProxy, this, engineIntrinsics); + psrlReadLine = new PsrlReadLine(psrlProxy, this, engineIntrinsics, ReadKey, OnPowerShellIdle); return true; } catch (Exception e) From ff77a6899af251e9384bb3b4e92647d63415126d Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Thu, 14 Oct 2021 15:26:05 -0700 Subject: [PATCH 12/12] Update comment --- .../Services/PowerShell/Console/TerminalReadLine.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs index 1e0435b6c..474c23f1c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/TerminalReadLine.cs @@ -36,7 +36,8 @@ public SecureString ReadSecureLine(CancellationToken cancellationToken) switch (keyInfo.Key) { case ConsoleKey.Enter: - // Break to return the completed string + // Stop the while loop so we can realign the cursor + // and then return the entered string enterPressed = true; continue;