Skip to content

Commit 7c3d38b

Browse files
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)
1 parent 1a86d4b commit 7c3d38b

File tree

5 files changed

+258
-91
lines changed

5 files changed

+258
-91
lines changed

src/PowerShellEditorServices.Host/EditorServicesHost.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
using System.Reflection;
2121
using System.Threading.Tasks;
2222
using System.Runtime.InteropServices;
23+
using System.Management.Automation;
24+
using System.Management.Automation.Host;
25+
using System.Linq;
2326

2427
namespace Microsoft.PowerShell.EditorServices.Host
2528
{
@@ -61,6 +64,7 @@ public class EditorServicesHost
6164
{
6265
#region Private Fields
6366

67+
private readonly PSHost internalHost;
6468
private string[] additionalModules;
6569
private string bundledModulesPath;
6670
private DebugAdapter debugAdapter;
@@ -103,6 +107,11 @@ public EditorServicesHost(
103107
{
104108
Validate.IsNotNull(nameof(hostDetails), hostDetails);
105109

110+
using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace))
111+
{
112+
this.internalHost = pwsh.AddScript("$Host").Invoke<PSHost>().First();
113+
}
114+
106115
this.hostDetails = hostDetails;
107116
this.enableConsoleRepl = enableConsoleRepl;
108117
this.bundledModulesPath = bundledModulesPath;
@@ -113,13 +122,13 @@ public EditorServicesHost(
113122
#if DEBUG
114123
if (waitForDebugger)
115124
{
116-
if (Debugger.IsAttached)
125+
if (System.Diagnostics.Debugger.IsAttached)
117126
{
118-
Debugger.Break();
127+
System.Diagnostics.Debugger.Break();
119128
}
120129
else
121130
{
122-
Debugger.Launch();
131+
System.Diagnostics.Debugger.Launch();
123132
}
124133
}
125134
#endif
@@ -377,7 +386,7 @@ private EditorSession CreateSession(
377386

378387
EditorServicesPSHostUserInterface hostUserInterface =
379388
enableConsoleRepl
380-
? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger)
389+
? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger, this.internalHost)
381390
: new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger);
382391

383392
EditorServicesPSHost psHost =
@@ -419,7 +428,7 @@ private EditorSession CreateDebugSession(
419428

420429
EditorServicesPSHostUserInterface hostUserInterface =
421430
enableConsoleRepl
422-
? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger)
431+
? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, this.logger, this.internalHost)
423432
: new ProtocolPSHostUserInterface(powerShellContext, messageSender, this.logger);
424433

425434
EditorServicesPSHost psHost =

src/PowerShellEditorServices/PowerShellEditorServices.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Description>Provides common PowerShell editor capabilities as a .NET library.</Description>
66
<TargetFrameworks>netstandard2.0</TargetFrameworks>
77
<AssemblyName>Microsoft.PowerShell.EditorServices</AssemblyName>
8+
<LangVersion>Latest</LangVersion>
89
</PropertyGroup>
910
<!-- Fail the release build if there are missing public API documentation comments -->
1011
<PropertyGroup>

src/PowerShellEditorServices/Session/Host/EditorServicesPSHostUserInterface.cs

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public abstract class EditorServicesPSHostUserInterface :
3232
{
3333
#region Private Fields
3434

35+
private readonly HashSet<ProgressKey> currentProgressMessages = new HashSet<ProgressKey>();
3536
private PromptHandler activePromptHandler;
3637
private PSHostRawUserInterface rawUserInterface;
3738
private CancellationTokenSource commandLoopCancellationToken;
@@ -83,6 +84,11 @@ public abstract class EditorServicesPSHostUserInterface :
8384
/// </summary>
8485
protected ILogger Logger { get; private set; }
8586

87+
/// <summary>
88+
/// Gets a value indicating whether writing progress is supported.
89+
/// </summary>
90+
internal protected virtual bool SupportsWriteProgress => false;
91+
8692
#endregion
8793

8894
#region Constructors
@@ -582,17 +588,74 @@ public override void WriteErrorLine(string value)
582588
}
583589

584590
/// <summary>
585-
///
591+
/// Invoked by <see cref="Cmdlet.WriteProgress(ProgressRecord)" /> to display a progress record.
586592
/// </summary>
587-
/// <param name="sourceId"></param>
588-
/// <param name="record"></param>
589-
public override void WriteProgress(
593+
/// <param name="sourceId">
594+
/// Unique identifier of the source of the record. An int64 is used because typically,
595+
/// the 'this' pointer of the command from whence the record is originating is used, and
596+
/// that may be from a remote Runspace on a 64-bit machine.
597+
/// </param>
598+
/// <param name="record">
599+
/// The record being reported to the host.
600+
/// </param>
601+
public sealed override void WriteProgress(
590602
long sourceId,
591603
ProgressRecord record)
592604
{
593-
this.UpdateProgress(
594-
sourceId,
595-
ProgressDetails.Create(record));
605+
// Maintain old behavior if this isn't overridden.
606+
if (!this.SupportsWriteProgress)
607+
{
608+
this.UpdateProgress(sourceId, ProgressDetails.Create(record));
609+
return;
610+
}
611+
612+
// Keep a list of progress records we write so we can automatically
613+
// clean them up after the pipeline ends.
614+
if (record.RecordType == ProgressRecordType.Completed)
615+
{
616+
this.currentProgressMessages.Remove(new ProgressKey(sourceId, record));
617+
}
618+
else
619+
{
620+
this.currentProgressMessages.Add(new ProgressKey(sourceId, record));
621+
}
622+
623+
this.WriteProgressImpl(sourceId, record);
624+
}
625+
626+
/// <summary>
627+
/// Invoked by <see cref="Cmdlet.WriteProgress(ProgressRecord)" /> to display a progress record.
628+
/// </summary>
629+
/// <param name="sourceId">
630+
/// Unique identifier of the source of the record. An int64 is used because typically,
631+
/// the 'this' pointer of the command from whence the record is originating is used, and
632+
/// that may be from a remote Runspace on a 64-bit machine.
633+
/// </param>
634+
/// <param name="record">
635+
/// The record being reported to the host.
636+
/// </param>
637+
protected virtual void WriteProgressImpl(long sourceId, ProgressRecord record)
638+
{
639+
}
640+
641+
internal void ClearProgress()
642+
{
643+
const string nonEmptyString = "noop";
644+
if (!this.SupportsWriteProgress)
645+
{
646+
return;
647+
}
648+
649+
foreach (ProgressKey key in this.currentProgressMessages)
650+
{
651+
// This constructor throws if the activity description is empty even
652+
// with completed records.
653+
var record = new ProgressRecord(key.ActivityId, nonEmptyString, nonEmptyString);
654+
record.RecordType = ProgressRecordType.Completed;
655+
this.WriteProgressImpl(key.SourceId, record);
656+
}
657+
658+
this.currentProgressMessages.Clear();
596659
}
597660

598661
#endregion
@@ -917,6 +980,8 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt
917980
// The command loop should only be manipulated if it's already started
918981
if (eventArgs.ExecutionStatus == ExecutionStatus.Aborted)
919982
{
983+
this.ClearProgress();
984+
920985
// When aborted, cancel any lingering prompts
921986
if (this.activePromptHandler != null)
922987
{
@@ -932,6 +997,8 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt
932997
// the display of the prompt
933998
if (eventArgs.ExecutionStatus != ExecutionStatus.Running)
934999
{
1000+
this.ClearProgress();
1001+
9351002
// Execution has completed, start the input prompt
9361003
this.ShowCommandPrompt();
9371004
StartCommandLoop();
@@ -948,11 +1015,48 @@ private void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionSt
9481015
(eventArgs.ExecutionStatus == ExecutionStatus.Failed ||
9491016
eventArgs.HadErrors))
9501017
{
1018+
this.ClearProgress();
9511019
this.WriteOutput(string.Empty, true);
9521020
var unusedTask = this.WritePromptStringToHostAsync(CancellationToken.None);
9531021
}
9541022
}
9551023

9561024
#endregion
1025+
1026+
private readonly struct ProgressKey : IEquatable<ProgressKey>
1027+
{
1028+
internal readonly long SourceId;
1029+
1030+
internal readonly int ActivityId;
1031+
1032+
internal readonly int ParentActivityId;
1033+
1034+
internal ProgressKey(long sourceId, ProgressRecord record)
1035+
{
1036+
SourceId = sourceId;
1037+
ActivityId = record.ActivityId;
1038+
ParentActivityId = record.ParentActivityId;
1039+
}
1040+
1041+
public bool Equals(ProgressKey other)
1042+
{
1043+
return SourceId == other.SourceId
1044+
&& ActivityId == other.ActivityId
1045+
&& ParentActivityId == other.ParentActivityId;
1046+
}
1047+
1048+
public override int GetHashCode()
1049+
{
1050+
// Algorithm from https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations
1051+
unchecked
1052+
{
1053+
int hash = 17;
1054+
hash = hash * 31 + SourceId.GetHashCode();
1055+
hash = hash * 31 + ActivityId.GetHashCode();
1056+
hash = hash * 31 + ParentActivityId.GetHashCode();
1057+
return hash;
1058+
}
1059+
}
1060+
}
9571061
}
9581062
}

0 commit comments

Comments
 (0)