Skip to content

Commit bb504d5

Browse files
committed
Add integrated console support via custom host implementation
This change enables a true integrated terminal experience for editors like VS Code which have an embedded terminal UI. The change centers primarily around the new ConsoleReadLine class which provides a "good enough" terminal editing and tab completion experience until we can find a way to make PSReadLine work inside of our custom host. The debugging experience has also been updated to share the language service's runspace. In a later change an independent debugging mode will be added.
1 parent 6f3fedd commit bb504d5

File tree

10 files changed

+969
-191
lines changed

10 files changed

+969
-191
lines changed

module/PowerShellEditorServices/PowerShellEditorServices.psm1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ function Start-EditorServicesHost {
4646
[ValidateSet("Normal", "Verbose", "Error")]
4747
$LogLevel = "Normal",
4848

49+
[string]
50+
$DebugServiceOnly,
51+
4952
[switch]
5053
$WaitForDebugger
5154
)
@@ -67,8 +70,14 @@ function Start-EditorServicesHost {
6770
[System.IO.Path]::GetDirectoryName($profile.CurrentUserAllHosts));
6871

6972
$editorServicesHost.StartLogging($LogPath, $LogLevel);
70-
$editorServicesHost.StartLanguageService($LanguageServicePort, $profilePaths);
71-
$editorServicesHost.StartDebugService($DebugServicePort, $profilePaths);
73+
74+
if ($DebugServiceOnly.IsPresent) {
75+
$editorServicesHost.StartDebugService($DebugServicePort, $profilePaths, $false);
76+
}
77+
else {
78+
$editorServicesHost.StartLanguageService($LanguageServicePort, $profilePaths);
79+
$editorServicesHost.StartDebugService($DebugServicePort, $profilePaths, $true);
80+
}
7281
}
7382
catch {
7483
Write-Error "PowerShell Editor Services host initialization failed, terminating."

src/PowerShellEditorServices.Host/EditorServicesHost.cs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,23 +158,41 @@ public void StartLanguageService(int languageServicePort, ProfilePaths profilePa
158158
/// Starts the debug service with the specified TCP socket port.
159159
/// </summary>
160160
/// <param name="debugServicePort">The port number for the debug service.</param>
161-
public void StartDebugService(int debugServicePort, ProfilePaths profilePaths)
161+
public void StartDebugService(
162+
int debugServicePort,
163+
ProfilePaths profilePaths,
164+
bool useExistingSession)
162165
{
163-
this.debugAdapter =
164-
new DebugAdapter(
165-
hostDetails,
166-
profilePaths,
167-
new TcpSocketServerChannel(debugServicePort),
168-
this.languageServer?.EditorOperations);
166+
if (useExistingSession)
167+
{
168+
this.debugAdapter =
169+
new DebugAdapter(
170+
this.languageServer.EditorSession,
171+
new TcpSocketServerChannel(debugServicePort));
172+
}
173+
else
174+
{
175+
this.debugAdapter =
176+
new DebugAdapter(
177+
hostDetails,
178+
profilePaths,
179+
new TcpSocketServerChannel(debugServicePort),
180+
this.languageServer?.EditorOperations);
181+
}
169182

170183
this.debugAdapter.SessionEnded +=
171184
(obj, args) =>
172185
{
173-
Logger.Write(
174-
LogLevel.Normal,
175-
"Previous debug session ended, restarting debug service...");
176-
177-
this.StartDebugService(debugServicePort, profilePaths);
186+
// Only restart if we're reusing the existing session,
187+
// otherwise the process should terminate
188+
if (useExistingSession)
189+
{
190+
Logger.Write(
191+
LogLevel.Normal,
192+
"Previous debug session ended, restarting debug service...");
193+
194+
this.StartDebugService(debugServicePort, profilePaths, true);
195+
}
178196
};
179197

180198
this.debugAdapter.Start().Wait();

src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server
2323
public class DebugAdapter : DebugAdapterBase
2424
{
2525
private EditorSession editorSession;
26-
private OutputDebouncer outputDebouncer;
2726

2827
private bool noDebug;
2928
private bool waitingForAttach;
29+
private bool ownsEditorSession;
3030
private string scriptPathToLaunch;
3131
private string arguments;
3232

@@ -35,21 +35,26 @@ public DebugAdapter(HostDetails hostDetails, ProfilePaths profilePaths)
3535
{
3636
}
3737

38+
public DebugAdapter(EditorSession editorSession, ChannelBase serverChannel)
39+
: base(serverChannel)
40+
{
41+
this.editorSession = editorSession;
42+
this.editorSession.PowerShellContext.RunspaceChanged += this.powerShellContext_RunspaceChanged;
43+
this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped;
44+
}
45+
3846
public DebugAdapter(
3947
HostDetails hostDetails,
4048
ProfilePaths profilePaths,
4149
ChannelBase serverChannel,
4250
IEditorOperations editorOperations)
4351
: base(serverChannel)
4452
{
53+
this.ownsEditorSession = true;
4554
this.editorSession = new EditorSession();
4655
this.editorSession.StartDebugSession(hostDetails, profilePaths, editorOperations);
4756
this.editorSession.PowerShellContext.RunspaceChanged += this.powerShellContext_RunspaceChanged;
4857
this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped;
49-
this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten;
50-
51-
// Set up the output debouncer to throttle output event writes
52-
this.outputDebouncer = new OutputDebouncer(this);
5358
}
5459

5560
protected override void Initialize()
@@ -82,15 +87,12 @@ protected override void Initialize()
8287

8388
protected Task LaunchScript(RequestContext<object> requestContext)
8489
{
85-
return editorSession.PowerShellContext
90+
return editorSession.ConsoleService
8691
.ExecuteScriptAtPath(this.scriptPathToLaunch, this.arguments)
8792
.ContinueWith(
8893
async (t) => {
8994
Logger.Write(LogLevel.Verbose, "Execution completed, flushing output then terminating...");
9095

91-
// Make sure remaining output is flushed before exiting
92-
await this.outputDebouncer.Flush();
93-
9496
await this.SendEvent(
9597
TerminatedEvent.Type,
9698
new TerminatedEvent());
@@ -102,14 +104,18 @@ await this.SendEvent(
102104

103105
protected override void Shutdown()
104106
{
105-
// Make sure remaining output is flushed before exiting
106-
this.outputDebouncer.Flush().Wait();
107-
108107
Logger.Write(LogLevel.Normal, "Debug adapter is shutting down...");
109108

110109
if (this.editorSession != null)
111110
{
112-
this.editorSession.Dispose();
111+
this.editorSession.PowerShellContext.RunspaceChanged -= this.powerShellContext_RunspaceChanged;
112+
this.editorSession.DebugService.DebuggerStopped -= this.DebugService_DebuggerStopped;
113+
114+
if (this.ownsEditorSession)
115+
{
116+
this.editorSession.Dispose();
117+
}
118+
113119
this.editorSession = null;
114120
}
115121
}
@@ -657,26 +663,7 @@ protected async Task HandleEvaluateRequest(
657663
"repl",
658664
StringComparison.CurrentCultureIgnoreCase);
659665

660-
if (isFromRepl)
661-
{
662-
// Check for special commands
663-
if (string.Equals("!ctrlc", evaluateParams.Expression, StringComparison.CurrentCultureIgnoreCase))
664-
{
665-
editorSession.PowerShellContext.AbortExecution();
666-
}
667-
else if (string.Equals("!break", evaluateParams.Expression, StringComparison.CurrentCultureIgnoreCase))
668-
{
669-
editorSession.DebugService.Break();
670-
}
671-
else
672-
{
673-
// Send the input through the console service
674-
editorSession.ConsoleService.ExecuteCommand(
675-
evaluateParams.Expression,
676-
false);
677-
}
678-
}
679-
else
666+
if (!isFromRepl)
680667
{
681668
VariableDetails result =
682669
await editorSession.DebugService.EvaluateExpression(
@@ -692,6 +679,12 @@ await editorSession.DebugService.EvaluateExpression(
692679
result.Id : 0;
693680
}
694681
}
682+
else
683+
{
684+
Logger.Write(
685+
LogLevel.Verbose,
686+
$"Debug adapter client attempted to evaluate command in REPL: {evaluateParams.Expression}");
687+
}
695688

696689
await requestContext.SendResult(
697690
new EvaluateResponseBody
@@ -707,9 +700,6 @@ await requestContext.SendResult(
707700

708701
async void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e)
709702
{
710-
// Flush pending output before sending the event
711-
await this.outputDebouncer.Flush();
712-
713703
// Provide the reason for why the debugger has stopped script execution.
714704
// See https://github.com/Microsoft/vscode/issues/3648
715705
// The reason is displayed in the breakpoints viewlet. Some recommended reasons are:
@@ -766,12 +756,6 @@ await this.SendEvent<ContinuedEvent>(
766756
}
767757
}
768758

769-
async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
770-
{
771-
// Queue the output for writing
772-
await this.outputDebouncer.Invoke(e);
773-
}
774-
775759
#endregion
776760
}
777761
}

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class LanguageServer : LanguageServerBase
2929
private readonly static string DiagnosticSourceName = "PowerShellEditorServices";
3030

3131
private bool profilesLoaded;
32+
private bool consoleReplStarted;
3233
private EditorSession editorSession;
3334
private OutputDebouncer outputDebouncer;
3435
private LanguageServerEditorOperations editorOperations;
@@ -42,6 +43,11 @@ public IEditorOperations EditorOperations
4243
get { return this.editorOperations; }
4344
}
4445

46+
public EditorSession EditorSession
47+
{
48+
get { return this.editorSession; }
49+
}
50+
4551
/// <param name="hostDetails">
4652
/// Provides details about the host application.
4753
/// </param>
@@ -58,7 +64,7 @@ public LanguageServer(HostDetails hostDetails, ProfilePaths profilePaths, Channe
5864
{
5965
this.editorSession = new EditorSession();
6066
this.editorSession.StartSession(hostDetails, profilePaths);
61-
this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten;
67+
//this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten;
6268
this.editorSession.PowerShellContext.RunspaceChanged += PowerShellContext_RunspaceChanged;
6369

6470
// Attach to ExtensionService events
@@ -136,6 +142,9 @@ protected override void Initialize()
136142

137143
protected override async Task Shutdown()
138144
{
145+
// Stop the interactive terminal
146+
this.editorSession.ConsoleService.StopInteractiveConsole();
147+
139148
// Make sure remaining output is flushed before exiting
140149
await this.outputDebouncer.Flush();
141150

@@ -503,6 +512,15 @@ protected async Task HandleDidChangeConfigurationNotification(
503512
this.profilesLoaded = true;
504513
}
505514

515+
// Wait until after profiles are loaded (or not, if that's the
516+
// case) before starting the interactive console.
517+
if (!this.consoleReplStarted)
518+
{
519+
// Start the interactive terminal
520+
this.editorSession.ConsoleService.StartReadLoop();
521+
this.consoleReplStarted = true;
522+
}
523+
506524
// If there is a new settings file path, restart the analyzer with the new settigs.
507525
bool settingsPathChanged = false;
508526
string newSettingsPath = this.currentSettings.ScriptAnalysis.SettingsPath;

0 commit comments

Comments
 (0)