From 7c3d38b5db28e42cb723360993ca856d1fe0b391 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 4 Mar 2019 23:14:03 -0500 Subject: [PATCH 1/7] Use public InternalHost from origin runspace `ConsoleHost` from `powershell.exe`/`pwsh` still exists within the runspace created at process start. This change grabs the public reference to that host while initializing EditorServicesHost. We can then leverage that host so we can have a much closer to "default" experience. This change adds support for the following host members ## $Host.UI - WriteProgress (including `Write-Progress`) ## $Host.UI.RawUI - CursorSize (still doesn't work in xterm.js though) - WindowTitle - MaxPhysicalWindowSize - MaxWindowSize - ReadKey - GetBufferContents - ScrollBufferContents - SetBufferContents ## TODO [ ] Test RawUI members [ ] Maybe write sync verison of ReadKey [ ] Maybe avoid TerminalPSHost* breaking changes (constructors) --- .../EditorServicesHost.cs | 19 +- .../PowerShellEditorServices.csproj | 1 + .../Host/EditorServicesPSHostUserInterface.cs | 118 +++++++++++- .../Host/TerminalPSHostRawUserInterface.cs | 179 ++++++++++-------- .../Host/TerminalPSHostUserInterface.cs | 32 +++- 5 files changed, 258 insertions(+), 91 deletions(-) diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 2f3275bf3..9763c9037 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -20,6 +20,9 @@ using System.Reflection; using System.Threading.Tasks; using System.Runtime.InteropServices; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Linq; 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; @@ -103,6 +107,11 @@ public EditorServicesHost( { Validate.IsNotNull(nameof(hostDetails), hostDetails); + using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + this.internalHost = pwsh.AddScript("$Host").Invoke().First(); + } + this.hostDetails = hostDetails; this.enableConsoleRepl = enableConsoleRepl; this.bundledModulesPath = bundledModulesPath; @@ -113,13 +122,13 @@ public EditorServicesHost( #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 @@ -377,7 +386,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 +428,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/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..7e2b41edc 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -32,6 +32,7 @@ public abstract class EditorServicesPSHostUserInterface : { #region Private Fields + private readonly HashSet currentProgressMessages = new HashSet(); private PromptHandler activePromptHandler; private PSHostRawUserInterface rawUserInterface; private CancellationTokenSource commandLoopCancellationToken; @@ -83,6 +84,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 +588,74 @@ 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.Remove(new ProgressKey(sourceId, record)); + } + else + { + this.currentProgressMessages.Add(new ProgressKey(sourceId, record)); + } + + 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) + { + // This constructor throws if the activity description is empty even + // with completed records. + var record = new ProgressRecord(key.ActivityId, nonEmptyString, nonEmptyString); + record.RecordType = ProgressRecordType.Completed; + this.WriteProgressImpl(key.SourceId, record); + } + + this.currentProgressMessages.Clear(); } #endregion @@ -917,6 +980,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 +997,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 +1015,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..28bf98ff7 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 { @@ -21,7 +24,9 @@ internal class TerminalPSHostRawUserInterface : PSHostRawUserInterface private const int DefaultConsoleHeight = 100; private const int DefaultConsoleWidth = 120; + private readonly PSHostRawUserInterface internalRawUI; private ILogger Logger; + private KeyInfo? lastKeyDown; #endregion @@ -32,9 +37,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 +71,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 +82,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 +95,8 @@ public override Coordinates CursorPosition /// public override int CursorSize { - get; - set; + get => this.internalRawUI.CursorSize; + set => this.internalRawUI.CursorSize = value; } /// @@ -111,18 +104,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 +113,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,8 +122,8 @@ public override Size WindowSize /// public override string WindowTitle { - get; - set; + get => this.internalRawUI.WindowTitle; + set => this.internalRawUI.WindowTitle = value; } /// @@ -158,24 +131,18 @@ public override string WindowTitle /// public override bool KeyAvailable { - get { return System.Console.KeyAvailable; } + get => 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 +151,78 @@ 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"); + KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) + { + ControlKeyStates states = default; + if ((key.Modifiers & ConsoleModifiers.Alt) != 0) + { + states |= ControlKeyStates.LeftAltPressed; + } - throw new System.NotImplementedException(); + 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; + } + + bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; + 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 ArgumentOutOfRangeException(nameof(options)); + } + + bool oldValue = System.Console.TreatControlCAsInput; + try + { + System.Console.TreatControlCAsInput = true; + ConsoleKeyInfo key = ConsoleProxy + .ReadKeyAsync(default(CancellationToken)) + .ConfigureAwait(continueOnCapturedContext: false) + .GetAwaiter() + .GetResult(); + + if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) + { + if ((options & ReadKeyOptions.AllowCtrlC) == 0) + { + return ProcessKey(key, includeDown); + } + + throw new PipelineStoppedException(); + } + + return ProcessKey(key, includeDown); + } + finally + { + System.Console.TreatControlCAsInput = oldValue; + } } /// @@ -208,7 +242,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 +258,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 +277,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,9 +292,7 @@ public override void SetBufferContents( Coordinates origin, BufferCell[,] contents) { - Logger.Write( - LogLevel.Warning, - "PSHostRawUserInterface.SetBufferContents was called"); + this.internalRawUI.SetBufferContents(origin, contents); } #endregion 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) { - } } } From 24e648973b5c83397a05093c446b942e67ae7649 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 7 Mar 2019 17:47:17 -0500 Subject: [PATCH 2/7] Fix up RawUI.ReadKey - Add the XML documentation comments to ConsoleProxy because it's more likely to be used than the interface itself. - Add a synchronous implementation of ReadKey to ConsoleProxy and use it in RawUI.ReadKey - Fix Ctrl + C not returning as input in VSCode's terminal - Use the exception message from ConsoleHost when ReadKeyOptions do not include either IncludeKeyUp nor IncludeKeyDown --- .../Console/ConsoleProxy.cs | 110 +++++++++++++++++- .../Console/ConsoleReadLine.cs | 2 +- .../Console/IConsoleOperations.cs | 39 +++++-- .../Console/UnixConsoleOperations.cs | 6 +- .../Console/WindowsConsoleOperations.cs | 24 +++- .../Host/TerminalPSHostRawUserInterface.cs | 28 +++-- 6 files changed, 181 insertions(+), 28 deletions(-) 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/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs index 28bf98ff7..e6c7d87ad 100644 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs @@ -21,9 +21,6 @@ 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; @@ -194,20 +191,17 @@ KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0; if (!(includeDown || includeUp)) { - throw new ArgumentOutOfRangeException(nameof(options)); + throw new PSArgumentException( + "Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.", + nameof(options)); } bool oldValue = System.Console.TreatControlCAsInput; try { System.Console.TreatControlCAsInput = true; - ConsoleKeyInfo key = ConsoleProxy - .ReadKeyAsync(default(CancellationToken)) - .ConfigureAwait(continueOnCapturedContext: false) - .GetAwaiter() - .GetResult(); - - if (key.Key == ConsoleKey.C && key.Modifiers == ConsoleModifiers.Control) + ConsoleKeyInfo key = ConsoleProxy.ReadKey(intercept, default(CancellationToken)); + if (IsCtrlC(key)) { if ((options & ReadKeyOptions.AllowCtrlC) == 0) { @@ -296,5 +290,17 @@ public override void SetBufferContents( } #endregion + + 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; + } } } From ee1636eddfd772e2ab14db1eef6c408ec14b1682 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 7 Mar 2019 17:57:12 -0500 Subject: [PATCH 3/7] Pass $Host in the start up script --- .../PowerShellEditorServices.psm1 | 3 +- .../EditorServicesHost.cs | 47 +++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) 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 9763c9037..42fec983c 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -90,6 +90,7 @@ public class EditorServicesHost #region Constructors + /// /// Initializes a new instance of the EditorServicesHost class and waits for /// the debugger to attach if waitForDebugger is true. @@ -97,6 +98,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, @@ -104,13 +107,38 @@ public EditorServicesHost( bool waitForDebugger, string[] additionalModules, string[] featureFlags) + : this( + hostDetails, + bundledModulesPath, + enableConsoleRepl, + waitForDebugger, + additionalModules, + featureFlags, + GetInternalHostFromDefaultRunspace()) { - Validate.IsNotNull(nameof(hostDetails), hostDetails); + } - using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - this.internalHost = pwsh.AddScript("$Host").Invoke().First(); - } + /// + /// 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; @@ -118,6 +146,7 @@ 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) @@ -374,6 +403,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, From 696bd8140f109eed4a4e5e8b3c77ce838b85a211 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Sat, 9 Mar 2019 11:50:15 -0500 Subject: [PATCH 4/7] Address feedback --- .../EditorServicesHost.cs | 9 ++++----- .../Host/EditorServicesPSHostUserInterface.cs | 6 +++++- .../Host/TerminalPSHostRawUserInterface.cs | 17 +++++++++++++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 42fec983c..4a4acf9c6 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -16,13 +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.Management.Automation; -using System.Management.Automation.Host; -using System.Linq; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Host { @@ -90,7 +90,6 @@ public class EditorServicesHost #region Constructors - /// /// Initializes a new instance of the EditorServicesHost class and waits for /// the debugger to attach if waitForDebugger is true. diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index 7e2b41edc..e5ce7444b 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -650,7 +650,11 @@ internal void ClearProgress() { // This constructor throws if the activity description is empty even // with completed records. - var record = new ProgressRecord(key.ActivityId, nonEmptyString, nonEmptyString); + var record = new ProgressRecord( + key.ActivityId, + activity: nonEmptyString, + statusDescription: nonEmptyString); + record.RecordType = ProgressRecordType.Completed; this.WriteProgressImpl(key.SourceId, record); } diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs index e6c7d87ad..9f615b670 100644 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs @@ -126,10 +126,7 @@ public override string WindowTitle /// /// Gets a boolean that determines whether a keypress is available. /// - public override bool KeyAvailable - { - get => this.internalRawUI.KeyAvailable; - } + public override bool KeyAvailable => this.internalRawUI.KeyAvailable; /// /// Gets the maximum physical size of the console window. @@ -148,8 +145,11 @@ public override bool KeyAvailable /// A KeyInfo struct with details about the current keypress. public override KeyInfo ReadKey(ReadKeyOptions options) { + // Converts ConsoleKeyInfo objects to KeyInfo objects and caches key down events + // for the next key up request. KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) { + // Translate ConsoleModifiers to ControlKeyStates ControlKeyStates states = default; if ((key.Modifiers & ConsoleModifiers.Alt) != 0) { @@ -176,6 +176,8 @@ KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) } 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; @@ -196,18 +198,25 @@ KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) 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(); } From acf00906f7e6c2b447dd7d2b8e2f38f219aab9a9 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 11 Mar 2019 17:56:37 -0400 Subject: [PATCH 5/7] Address feedback and also add doc comments --- .../Host/TerminalPSHostRawUserInterface.cs | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs index 9f615b670..be217c9d9 100644 --- a/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/TerminalPSHostRawUserInterface.cs @@ -145,35 +145,6 @@ public override string WindowTitle /// A KeyInfo struct with details about the current keypress. public override KeyInfo ReadKey(ReadKeyOptions options) { - // Converts ConsoleKeyInfo objects to KeyInfo objects and caches key down events - // for the next key up request. - 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; - } bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0; @@ -209,7 +180,7 @@ KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown) if (IsCtrlC(key)) { // Caller wants CtrlC as input so return it. - if ((options & ReadKeyOptions.AllowCtrlC) == 0) + if ((options & ReadKeyOptions.AllowCtrlC) != 0) { return ProcessKey(key, includeDown); } @@ -300,6 +271,14 @@ public override void SetBufferContents( #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 @@ -311,5 +290,42 @@ private static bool IsCtrlC(ConsoleKeyInfo keyInfo) 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; + } } } From 871ff55b9b5793be9de89bf9d8a3681c720eca44 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 12 Mar 2019 22:11:56 -0400 Subject: [PATCH 6/7] Make progress cache thread safe --- .../Session/Host/EditorServicesPSHostUserInterface.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index e5ce7444b..9a4bace17 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,7 +33,8 @@ public abstract class EditorServicesPSHostUserInterface : { #region Private Fields - private readonly HashSet currentProgressMessages = new HashSet(); + private readonly ConcurrentDictionary currentProgressMessages = + new ConcurrentDictionary(); private PromptHandler activePromptHandler; private PSHostRawUserInterface rawUserInterface; private CancellationTokenSource commandLoopCancellationToken; @@ -613,11 +615,11 @@ public sealed override void WriteProgress( // clean them up after the pipeline ends. if (record.RecordType == ProgressRecordType.Completed) { - this.currentProgressMessages.Remove(new ProgressKey(sourceId, record)); + this.currentProgressMessages.TryRemove(new ProgressKey(sourceId, record), out _); } else { - this.currentProgressMessages.Add(new ProgressKey(sourceId, record)); + this.currentProgressMessages.TryAdd(new ProgressKey(sourceId, record), null); } this.WriteProgressImpl(sourceId, record); @@ -646,7 +648,7 @@ internal void ClearProgress() return; } - foreach (ProgressKey key in this.currentProgressMessages) + foreach (ProgressKey key in this.currentProgressMessages.Keys) { // This constructor throws if the activity description is empty even // with completed records. From b1f478163803dfcabdec078a08be26ebc3ca647b Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 21 Mar 2019 17:40:13 -0400 Subject: [PATCH 7/7] Added comment about null --- .../Session/Host/EditorServicesPSHostUserInterface.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs index 9a4bace17..24ea1c8db 100644 --- a/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs @@ -619,6 +619,8 @@ public sealed override void WriteProgress( } 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); }