diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index d0378f0af..47efd591f 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -109,7 +109,8 @@ function Start-EditorServicesHost { $EnableConsoleRepl.IsPresent, $WaitForDebugger.IsPresent, $AdditionalModules, - $FeatureFlags) + $FeatureFlags, + $Host) # Build the profile paths using the root paths of the current $profile variable $profilePaths = diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 2f3275bf3..4a4acf9c6 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -16,10 +16,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Management.Automation; using System.Management.Automation.Runspaces; +using System.Management.Automation.Host; using System.Reflection; -using System.Threading.Tasks; using System.Runtime.InteropServices; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Host { @@ -61,6 +64,7 @@ public class EditorServicesHost { #region Private Fields + private readonly PSHost internalHost; private string[] additionalModules; private string bundledModulesPath; private DebugAdapter debugAdapter; @@ -93,6 +97,8 @@ public class EditorServicesHost /// The details of the host which is launching PowerShell Editor Services. /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. /// If true, causes the host to wait for the debugger to attach before proceeding. + /// Modules to be loaded when initializing the new runspace. + /// Features to enable for this instance. public EditorServicesHost( HostDetails hostDetails, string bundledModulesPath, @@ -100,8 +106,38 @@ public EditorServicesHost( bool waitForDebugger, string[] additionalModules, string[] featureFlags) + : this( + hostDetails, + bundledModulesPath, + enableConsoleRepl, + waitForDebugger, + additionalModules, + featureFlags, + GetInternalHostFromDefaultRunspace()) + { + } + + /// + /// Initializes a new instance of the EditorServicesHost class and waits for + /// the debugger to attach if waitForDebugger is true. + /// + /// The details of the host which is launching PowerShell Editor Services. + /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. + /// If true, causes the host to wait for the debugger to attach before proceeding. + /// Modules to be loaded when initializing the new runspace. + /// Features to enable for this instance. + /// The value of the $Host variable in the original runspace. + public EditorServicesHost( + HostDetails hostDetails, + string bundledModulesPath, + bool enableConsoleRepl, + bool waitForDebugger, + string[] additionalModules, + string[] featureFlags, + PSHost internalHost) { Validate.IsNotNull(nameof(hostDetails), hostDetails); + Validate.IsNotNull(nameof(internalHost), internalHost); this.hostDetails = hostDetails; this.enableConsoleRepl = enableConsoleRepl; @@ -109,17 +145,18 @@ public EditorServicesHost( this.additionalModules = additionalModules ?? new string[0]; this.featureFlags = new HashSet(featureFlags ?? new string[0]); this.serverCompletedTask = new TaskCompletionSource(); + this.internalHost = internalHost; #if DEBUG if (waitForDebugger) { - if (Debugger.IsAttached) + if (System.Diagnostics.Debugger.IsAttached) { - Debugger.Break(); + System.Diagnostics.Debugger.Break(); } else { - Debugger.Launch(); + System.Diagnostics.Debugger.Launch(); } } #endif @@ -365,6 +402,14 @@ public void WaitForCompletion() #region Private Methods + private static PSHost GetInternalHostFromDefaultRunspace() + { + using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + return pwsh.AddScript("$Host").Invoke().First(); + } + } + private EditorSession CreateSession( HostDetails hostDetails, ProfilePaths profilePaths, @@ -377,7 +422,7 @@ private EditorSession CreateSession( EditorServicesPSHostUserInterface hostUserInterface = enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger, this.internalHost) : new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger); EditorServicesPSHost psHost = @@ -419,7 +464,7 @@ private EditorSession CreateDebugSession( EditorServicesPSHostUserInterface hostUserInterface = enableConsoleRepl - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger) + ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger, this.internalHost) : new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger); EditorServicesPSHost psHost = diff --git a/src/PowerShellEditorServices/Console/ConsoleProxy.cs b/src/PowerShellEditorServices/Console/ConsoleProxy.cs index 3956f6df9..b9312ca7c 100644 --- a/src/PowerShellEditorServices/Console/ConsoleProxy.cs +++ b/src/PowerShellEditorServices/Console/ConsoleProxy.cs @@ -29,30 +29,136 @@ static ConsoleProxy() s_consoleProxy = new UnixConsoleOperations(); } - public static Task ReadKeyAsync(CancellationToken cancellationToken) => - s_consoleProxy.ReadKeyAsync(cancellationToken); + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// An object that describes the constant and Unicode character, if any, + /// that correspond to the pressed console key. The object also + /// describes, in a bitwise combination of values, whether + /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. + /// + public static ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) => + s_consoleProxy.ReadKey(intercept, cancellationToken); + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// A task that will complete with a result of the key pressed by the user. + /// + public static Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) => + s_consoleProxy.ReadKeyAsync(intercept, cancellationToken); + + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The horizontal position of the console cursor. public static int GetCursorLeft() => s_consoleProxy.GetCursorLeft(); + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The horizontal position of the console cursor. public static int GetCursorLeft(CancellationToken cancellationToken) => s_consoleProxy.GetCursorLeft(cancellationToken); + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// public static Task GetCursorLeftAsync() => s_consoleProxy.GetCursorLeftAsync(); + /// + /// Obtains the horizontal position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the horizontal position + /// of the console cursor. + /// public static Task GetCursorLeftAsync(CancellationToken cancellationToken) => s_consoleProxy.GetCursorLeftAsync(cancellationToken); + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The vertical position of the console cursor. public static int GetCursorTop() => s_consoleProxy.GetCursorTop(); + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// The vertical position of the console cursor. public static int GetCursorTop(CancellationToken cancellationToken) => s_consoleProxy.GetCursorTop(cancellationToken); + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// public static Task GetCursorTopAsync() => s_consoleProxy.GetCursorTopAsync(); + /// + /// Obtains the vertical position of the console cursor. Use this method + /// instead of to avoid triggering + /// pending calls to + /// on Unix platforms. + /// + /// The to observe. + /// + /// A representing the asynchronous operation. The + /// property will return the vertical position + /// of the console cursor. + /// public static Task GetCursorTopAsync(CancellationToken cancellationToken) => s_consoleProxy.GetCursorTopAsync(cancellationToken); diff --git a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs index e2824d635..af6ca044c 100644 --- a/src/PowerShellEditorServices/Console/ConsoleReadLine.cs +++ b/src/PowerShellEditorServices/Console/ConsoleReadLine.cs @@ -129,7 +129,7 @@ public async Task ReadSecureLineAsync(CancellationToken cancellati private static async Task ReadKeyAsync(CancellationToken cancellationToken) { - return await ConsoleProxy.ReadKeyAsync(cancellationToken); + return await ConsoleProxy.ReadKeyAsync(intercept: true, cancellationToken); } private async Task ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken) diff --git a/src/PowerShellEditorServices/Console/IConsoleOperations.cs b/src/PowerShellEditorServices/Console/IConsoleOperations.cs index a5556eda5..b3fb58561 100644 --- a/src/PowerShellEditorServices/Console/IConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/IConsoleOperations.cs @@ -18,16 +18,37 @@ public interface IConsoleOperations /// Obtains the next character or function key pressed by the user asynchronously. /// Does not block when other console API's are called. /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The CancellationToken to observe. + /// + /// An object that describes the constant and Unicode character, if any, + /// that correspond to the pressed console key. The object also + /// describes, in a bitwise combination of values, whether + /// one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously with the console key. + /// + ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken); + + /// + /// Obtains the next character or function key pressed by the user asynchronously. + /// Does not block when other console API's are called. + /// + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// /// The CancellationToken to observe. /// /// A task that will complete with a result of the key pressed by the user. /// - Task ReadKeyAsync(CancellationToken cancellationToken); + Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); /// /// Obtains the horizontal position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// The horizontal position of the console cursor. @@ -36,7 +57,7 @@ public interface IConsoleOperations /// /// Obtains the horizontal position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// The to observe. @@ -46,7 +67,7 @@ public interface IConsoleOperations /// /// Obtains the horizontal position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// @@ -59,7 +80,7 @@ public interface IConsoleOperations /// /// Obtains the horizontal position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// The to observe. @@ -73,7 +94,7 @@ public interface IConsoleOperations /// /// Obtains the vertical position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// The vertical position of the console cursor. @@ -82,7 +103,7 @@ public interface IConsoleOperations /// /// Obtains the vertical position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// The to observe. @@ -92,7 +113,7 @@ public interface IConsoleOperations /// /// Obtains the vertical position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// @@ -105,7 +126,7 @@ public interface IConsoleOperations /// /// Obtains the vertical position of the console cursor. Use this method /// instead of to avoid triggering - /// pending calls to + /// pending calls to /// on Unix platforms. /// /// The to observe. diff --git a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs index df5ec2460..199f312f2 100644 --- a/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/UnixConsoleOperations.cs @@ -38,7 +38,7 @@ internal UnixConsoleOperations() WaitForKeyAvailableAsync = LongWaitForKeyAsync; } - internal ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) { s_readKeyHandle.Wait(cancellationToken); @@ -76,7 +76,7 @@ internal ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationTo } } - public async Task ReadKeyAsync(CancellationToken cancellationToken) + public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { await s_readKeyHandle.WaitAsync(cancellationToken); @@ -96,7 +96,7 @@ public async Task ReadKeyAsync(CancellationToken cancellationTok await s_stdInHandle.WaitAsync(cancellationToken); try { - return System.Console.ReadKey(intercept: true); + return System.Console.ReadKey(intercept); } finally { diff --git a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs index 86c543123..493e66930 100644 --- a/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs +++ b/src/PowerShellEditorServices/Console/WindowsConsoleOperations.cs @@ -32,7 +32,7 @@ internal class WindowsConsoleOperations : IConsoleOperations public Task GetCursorTopAsync(CancellationToken cancellationToken) => Task.FromResult(System.Console.CursorTop); - public async Task ReadKeyAsync(CancellationToken cancellationToken) + public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { await _readKeyHandle.WaitAsync(cancellationToken); try @@ -41,7 +41,27 @@ public async Task ReadKeyAsync(CancellationToken cancellationTok _bufferedKey.HasValue ? _bufferedKey.Value : await Task.Factory.StartNew( - () => (_bufferedKey = System.Console.ReadKey(intercept: true)).Value); + () => (_bufferedKey = System.Console.ReadKey(intercept)).Value); + } + finally + { + _readKeyHandle.Release(); + + // Throw if we're cancelled so the buffered key isn't cleared. + cancellationToken.ThrowIfCancellationRequested(); + _bufferedKey = null; + } + } + + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) + { + _readKeyHandle.Wait(cancellationToken); + try + { + return + _bufferedKey.HasValue + ? _bufferedKey.Value + : (_bufferedKey = System.Console.ReadKey(intercept)).Value; } finally { diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 0548d29f1..b2ad85394 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -5,6 +5,7 @@ Provides common PowerShell editor capabilities as a .NET library. netstandard2.0 Microsoft.PowerShell.EditorServices + Latest diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index a1d17547f..24ea1c8db 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -4,6 +4,7 @@ // using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Management.Automation; @@ -32,6 +33,8 @@ public abstract class EditorServicesPSHostUserInterface : { #region Private Fields + private readonly ConcurrentDictionary currentProgressMessages = + new ConcurrentDictionary(); private PromptHandler activePromptHandler; private PSHostRawUserInterface rawUserInterface; private CancellationTokenSource commandLoopCancellationToken; @@ -83,6 +86,11 @@ public abstract class EditorServicesPSHostUserInterface : /// protected ILogger Logger { get; private set; } + /// + /// Gets a value indicating whether writing progress is supported. + /// + internal protected virtual bool SupportsWriteProgress => false; + #endregion #region Constructors @@ -582,17 +590,80 @@ public override void WriteErrorLine(string value) } /// - /// + /// Invoked by to display a progress record. /// - /// - /// - public override void WriteProgress( + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + public sealed override void WriteProgress( long sourceId, ProgressRecord record) { - this.UpdateProgress( - sourceId, - ProgressDetails.Create(record)); + // Maintain old behavior if this isn't overridden. + if (!this.SupportsWriteProgress) + { + this.UpdateProgress(sourceId, ProgressDetails.Create(record)); + return; + } + + // Keep a list of progress records we write so we can automatically + // clean them up after the pipeline ends. + if (record.RecordType == ProgressRecordType.Completed) + { + this.currentProgressMessages.TryRemove(new ProgressKey(sourceId, record), out _); + } + else + { + // Adding with a value of null here because we don't actually need a dictionary. We're + // only using ConcurrentDictionary<,> becuase there is no ConcurrentHashSet<>. + this.currentProgressMessages.TryAdd(new ProgressKey(sourceId, record), null); + } + + this.WriteProgressImpl(sourceId, record); + } + + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + protected virtual void WriteProgressImpl(long sourceId, ProgressRecord record) + { + } + + internal void ClearProgress() + { + const string nonEmptyString = "noop"; + if (!this.SupportsWriteProgress) + { + return; + } + + foreach (ProgressKey key in this.currentProgressMessages.Keys) + { + // This constructor throws if the activity description is empty even + // with completed records. + var record = new ProgressRecord( + key.ActivityId, + activity: nonEmptyString, + statusDescription: nonEmptyString); + + record.RecordType = ProgressRecordType.Completed; + this.WriteProgressImpl(key.SourceId, record); + } + + this.currentProgressMessages.Clear(); } #endregion @@ -917,6 +988,8 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt // The command loop should only be manipulated if it's already started if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted) { + this.ClearProgress(); + // When aborted, cancel any lingering prompts if (this.activePromptHandler != null) { @@ -932,6 +1005,8 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt // the display of the prompt if (eventArgs.ExecutionStatus != ExecutionStatus.Running) { + this.ClearProgress(); + // Execution has completed, start the input prompt this.ShowCommandPrompt(); StartCommandLoop(); @@ -948,11 +1023,48 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt (eventArgs.ExecutionStatus == ExecutionStatus.Failed || eventArgs.HadErrors)) { + this.ClearProgress(); this.WriteOutput(string.Empty, true); var unusedTask = this.WritePromptStringToHostAsync(CancellationToken.None); } } #endregion + + private readonly struct ProgressKey : IEquatable + { + internal readonly long SourceId; + + internal readonly int ActivityId; + + internal readonly int ParentActivityId; + + internal ProgressKey(long sourceId, ProgressRecord record) + { + SourceId = sourceId; + ActivityId = record.ActivityId; + ParentActivityId = record.ParentActivityId; + } + + public bool Equals(ProgressKey other) + { + return SourceId == other.SourceId + && ActivityId == other.ActivityId + && ParentActivityId == other.ParentActivityId; + } + + public override int GetHashCode() + { + // Algorithm from https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations + unchecked + { + int hash = 17; + hash = hash * 31 + SourceId.GetHashCode(); + hash = hash * 31 + ActivityId.GetHashCode(); + hash = hash * 31 + ParentActivityId.GetHashCode(); + return hash; + } + } + } } } diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs index b0f1fe486..be217c9d9 100644 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs @@ -3,9 +3,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Utility; using System; +using System.Management.Automation; using System.Management.Automation.Host; +using System.Threading; namespace Microsoft.PowerShell.EditorServices { @@ -18,10 +21,9 @@ internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface { #region Private Fields - private const int DefaultConsoleHeight = 100; - private const int DefaultConsoleWidth = 120; - + private readonly PSHostRawUserInterface internalRawUI; private ILogger Logger; + private KeyInfo? lastKeyDown; #endregion @@ -32,9 +34,11 @@ internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface /// class with the given IConsoleHost implementation. /// /// The ILogger implementation to use for this instance. - public TerminalPSHostRawUserInterface(ILogger logger) + /// The InternalHost instance from the origin runspace. + public TerminalPSHostRawUserInterface(ILogger logger, PSHost internalHost) { this.Logger = logger; + this.internalRawUI = internalHost.UI.RawUI; } #endregion @@ -64,18 +68,8 @@ public override ConsoleColor ForegroundColor /// public override Size BufferSize { - get - { - return - new Size( - System.Console.BufferWidth, - System.Console.BufferHeight); - } - set - { - System.Console.BufferWidth = value.Width; - System.Console.BufferHeight = value.Height; - } + get => this.internalRawUI.BufferSize; + set => this.internalRawUI.BufferSize = value; } /// @@ -85,16 +79,12 @@ public override Coordinates CursorPosition { get { - return - new Coordinates( - System.Console.CursorLeft, - System.Console.CursorTop); - } - set - { - System.Console.CursorLeft = value.X; - System.Console.CursorTop = value.Y; + return new Coordinates( + ConsoleProxy.GetCursorLeft(), + ConsoleProxy.GetCursorTop()); } + + set => this.internalRawUI.CursorPosition = value; } /// @@ -102,8 +92,8 @@ public override Coordinates CursorPosition /// public override int CursorSize { - get; - set; + get => this.internalRawUI.CursorSize; + set => this.internalRawUI.CursorSize = value; } /// @@ -111,18 +101,8 @@ public override int CursorSize /// public override Coordinates WindowPosition { - get - { - return - new Coordinates( - System.Console.WindowLeft, - System.Console.WindowTop); - } - set - { - System.Console.WindowLeft = value.X; - System.Console.WindowTop = value.Y; - } + get => this.internalRawUI.WindowPosition; + set => this.internalRawUI.WindowPosition = value; } /// @@ -130,18 +110,8 @@ public override Coordinates WindowPosition /// public override Size WindowSize { - get - { - return - new Size( - System.Console.WindowWidth, - System.Console.WindowHeight); - } - set - { - System.Console.WindowWidth = value.Width; - System.Console.WindowHeight = value.Height; - } + get => this.internalRawUI.WindowSize; + set => this.internalRawUI.WindowSize = value; } /// @@ -149,33 +119,24 @@ public override Size WindowSize /// public override string WindowTitle { - get; - set; + get => this.internalRawUI.WindowTitle; + set => this.internalRawUI.WindowTitle = value; } /// /// Gets a boolean that determines whether a keypress is available. /// - public override bool KeyAvailable - { - get { return System.Console.KeyAvailable; } - } + public override bool KeyAvailable => this.internalRawUI.KeyAvailable; /// /// Gets the maximum physical size of the console window. /// - public override Size MaxPhysicalWindowSize - { - get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } - } + public override Size MaxPhysicalWindowSize => this.internalRawUI.MaxPhysicalWindowSize; /// /// Gets the maximum size of the console window. /// - public override Size MaxWindowSize - { - get { return new Size(DefaultConsoleWidth, DefaultConsoleHeight); } - } + public override Size MaxWindowSize => this.internalRawUI.MaxWindowSize; /// /// Reads the current key pressed in the console. @@ -184,11 +145,58 @@ public override Size MaxWindowSize /// A KeyInfo struct with details about the current keypress. public override KeyInfo ReadKey(ReadKeyOptions options) { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.ReadKey was called"); - throw new System.NotImplementedException(); + bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; + + // Key Up was requested and we have a cached key down we can return. + if (includeUp && this.lastKeyDown != null) + { + KeyInfo info = this.lastKeyDown.Value; + this.lastKeyDown = null; + return new KeyInfo( + info.VirtualKeyCode, + info.Character, + info.ControlKeyState, + keyDown: false); + } + + bool intercept = (options & ReadKeyOptions.NoEcho) != 0; + bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0; + if (!(includeDown || includeUp)) + { + throw new PSArgumentException( + "Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.", + nameof(options)); + } + + // Allow ControlC as input so we can emulate pipeline stop requests. We can't actually + // determine if a stop is requested without using non-public API's. + bool oldValue = System.Console.TreatControlCAsInput; + try + { + System.Console.TreatControlCAsInput = true; + ConsoleKeyInfo key = ConsoleProxy.ReadKey(intercept, default(CancellationToken)); + + if (IsCtrlC(key)) + { + // Caller wants CtrlC as input so return it. + if ((options & ReadKeyOptions.AllowCtrlC) != 0) + { + return ProcessKey(key, includeDown); + } + + // Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate + // a real stop. This will not show an exception to a script based caller and it + // will avoid having to return something like default(KeyInfo). + throw new PipelineStoppedException(); + } + + return ProcessKey(key, includeDown); + } + finally + { + System.Console.TreatControlCAsInput = oldValue; + } } /// @@ -208,7 +216,7 @@ public override void FlushInputBuffer() /// A BufferCell array with the requested buffer contents. public override BufferCell[,] GetBufferContents(Rectangle rectangle) { - return new BufferCell[0,0]; + return this.internalRawUI.GetBufferContents(rectangle); } /// @@ -224,9 +232,7 @@ public override void ScrollBufferContents( Rectangle clip, BufferCell fill) { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.ScrollBufferContents was called"); + this.internalRawUI.ScrollBufferContents(source, destination, clip, fill); } /// @@ -245,13 +251,10 @@ public override void SetBufferContents( rectangle.Right == -1) { System.Console.Clear(); + return; } - else - { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.SetBufferContents was called with a specific region"); - } + + this.internalRawUI.SetBufferContents(rectangle, fill); } /// @@ -263,11 +266,66 @@ public override void SetBufferContents( Coordinates origin, BufferCell[,] contents) { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.SetBufferContents was called"); + this.internalRawUI.SetBufferContents(origin, contents); } #endregion + + /// + /// Determines if a key press represents the input Ctrl + C. + /// + /// The key to test. + /// + /// if the key represents the input Ctrl + C, + /// otherwise . + /// + private static bool IsCtrlC(ConsoleKeyInfo keyInfo) + { + // In the VSCode terminal Ctrl C is processed as virtual key code "3", which + // is not a named value in the ConsoleKey enum. + if ((int)keyInfo.Key == 3) + { + return true; + } + + return keyInfo.Key == ConsoleKey.C && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0; + } + + /// + /// Converts objects to objects and caches + /// key down events for the next key up request. + /// + /// The key to convert. + /// + /// A value indicating whether the result should be a key down event. + /// + /// The converted value. + private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) + { + // Translate ConsoleModifiers to ControlKeyStates + ControlKeyStates states = default; + if ((key.Modifiers & ConsoleModifiers.Alt) != 0) + { + states |= ControlKeyStates.LeftAltPressed; + } + + if ((key.Modifiers & ConsoleModifiers.Control) != 0) + { + states |= ControlKeyStates.LeftCtrlPressed; + } + + if ((key.Modifiers & ConsoleModifiers.Shift) != 0) + { + states |= ControlKeyStates.ShiftPressed; + } + + var result = new KeyInfo((int)key.Key, key.KeyChar, states, isDown); + if (isDown) + { + this.lastKeyDown = result; + } + + return result; + } } } diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs index 0d2e839ca..82dab2d1e 100644 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostUserInterface.cs @@ -8,6 +8,8 @@ namespace Microsoft.PowerShell.EditorServices { using System; + using System.Management.Automation; + using System.Management.Automation.Host; using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Utility; @@ -20,6 +22,7 @@ public class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface { #region Private Fields + private readonly PSHostUserInterface internalHostUI; private ConsoleReadLine consoleReadLine; #endregion @@ -32,14 +35,17 @@ public class TerminalPSHostUserInterface : EditorServicesPSHostUserInterface /// /// The PowerShellContext to use for executing commands. /// An ILogger implementation to use for this host. + /// The InternalHost instance from the origin runspace. public TerminalPSHostUserInterface( PowerShellContext powerShellContext, - ILogger logger) + ILogger logger, + PSHost internalHost) : base( powerShellContext, - new TerminalPSHostRawUserInterface(logger), + new TerminalPSHostRawUserInterface(logger, internalHost), logger) { + this.internalHostUI = internalHost.UI; this.consoleReadLine = new ConsoleReadLine(powerShellContext); // Set the output encoding to UTF-8 so that special @@ -60,6 +66,11 @@ public TerminalPSHostUserInterface( #endregion + /// + /// Gets a value indicating whether writing progress is supported. + /// + internal protected override bool SupportsWriteProgress => true; + /// /// Requests that the HostUI implementation read a command line /// from the user to be executed in the integrated console command @@ -139,6 +150,22 @@ public override void WriteOutput( System.Console.BackgroundColor = oldBackgroundColor; } + /// + /// Invoked by to display a progress record. + /// + /// + /// Unique identifier of the source of the record. An int64 is used because typically, + /// the 'this' pointer of the command from whence the record is originating is used, and + /// that may be from a remote Runspace on a 64-bit machine. + /// + /// + /// The record being reported to the host. + /// + protected override void WriteProgressImpl(long sourceId, ProgressRecord record) + { + this.internalHostUI.WriteProgress(sourceId, record); + } + /// /// Sends a progress update event to the user. /// @@ -148,7 +175,6 @@ protected override void UpdateProgress( long sourceId, ProgressDetails progressDetails) { - } } }