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)
{
-
}
}
}