Skip to content

Commit b4fb564

Browse files
committed
Add prompt string support for REPL
This change improves the REPL such that the current 'prompt' function is executed and its result written to the host after the user executes a command. Existing tests which deal with console output are also updated to reflect these changes. Resolves #84.
1 parent 62ed52e commit b4fb564

File tree

5 files changed

+178
-42
lines changed

5 files changed

+178
-42
lines changed

src/PowerShellEditorServices/Session/PowerShellContext.cs

Lines changed: 105 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ public async Task<IEnumerable<TResult>> ExecuteCommand<TResult>(
208208
PSCommand psCommand,
209209
bool sendOutputToHost = false)
210210
{
211+
Pipeline nestedPipeline = null;
211212
RunspaceHandle runspaceHandle = null;
213+
IEnumerable<TResult> executionResult = null;
212214

213215
// If the debugger is active and the caller isn't on the pipeline
214216
// thread, send the command over to that thread to be executed.
@@ -253,21 +255,17 @@ public async Task<IEnumerable<TResult>> ExecuteCommand<TResult>(
253255
"Attempting to execute nested pipeline command(s):\r\n\r\n{0}",
254256
GetStringForPSCommand(psCommand)));
255257

256-
using (Pipeline pipeline = this.currentRunspace.CreateNestedPipeline())
258+
nestedPipeline = this.currentRunspace.CreateNestedPipeline();
259+
foreach (var command in psCommand.Commands)
257260
{
258-
foreach (var command in psCommand.Commands)
259-
{
260-
pipeline.Commands.Add(command);
261-
}
262-
263-
IEnumerable<TResult> result =
264-
pipeline
265-
.Invoke()
266-
.Select(pso => pso.BaseObject)
267-
.Cast<TResult>();
268-
269-
return result;
261+
nestedPipeline.Commands.Add(command);
270262
}
263+
264+
executionResult =
265+
nestedPipeline
266+
.Invoke()
267+
.Select(pso => pso.BaseObject)
268+
.Cast<TResult>();
271269
}
272270
else
273271
{
@@ -286,22 +284,19 @@ public async Task<IEnumerable<TResult>> ExecuteCommand<TResult>(
286284

287285
// Invoke the pipeline on a background thread
288286
// TODO: Use built-in async invocation!
289-
var taskResult =
287+
executionResult =
290288
await Task.Factory.StartNew<IEnumerable<TResult>>(
291289
() =>
292290
{
293291
this.powerShell.Commands = psCommand;
294292
Collection<TResult> result = this.powerShell.Invoke<TResult>();
295293
return result;
296294
},
297-
CancellationToken.None, // Might need a cancellation token
298-
TaskCreationOptions.None,
299-
TaskScheduler.Default
295+
CancellationToken.None, // Might need a cancellation token
296+
TaskCreationOptions.None,
297+
TaskScheduler.Default
300298
);
301299

302-
runspaceHandle.Dispose();
303-
runspaceHandle = null;
304-
305300
if (this.powerShell.HadErrors)
306301
{
307302
string errorMessage = "Execution completed with errors:\r\n\r\n";
@@ -320,8 +315,7 @@ await Task.Factory.StartNew<IEnumerable<TResult>>(
320315
"Execution completed successfully.");
321316
}
322317

323-
bool hadErrors = this.powerShell.HadErrors;
324-
return taskResult;
318+
return executionResult;
325319
}
326320
}
327321
catch (RuntimeException e)
@@ -335,15 +329,33 @@ await Task.Factory.StartNew<IEnumerable<TResult>>(
335329
}
336330
finally
337331
{
332+
// Get the new prompt before releasing the runspace handle
333+
if (sendOutputToHost)
334+
{
335+
// Write the prompt
336+
if (runspaceHandle != null)
337+
{
338+
this.WritePromptWithRunspace(runspaceHandle.Runspace);
339+
}
340+
else if (nestedPipeline != null)
341+
{
342+
this.WritePromptWithNestedPipeline();
343+
}
344+
}
345+
346+
// Dispose of the execution context
338347
if (runspaceHandle != null)
339348
{
340349
runspaceHandle.Dispose();
341350
}
351+
else if(nestedPipeline != null)
352+
{
353+
nestedPipeline.Dispose();
354+
}
342355
}
343356
}
344357

345-
// TODO: Better result
346-
return null;
358+
return executionResult;
347359
}
348360

349361
/// <summary>
@@ -741,6 +753,72 @@ private void SetExecutionPolicy(ExecutionPolicy desiredExecutionPolicy)
741753
}
742754
}
743755

756+
private void WritePromptToHost(Func<PSCommand, string> invokeAction)
757+
{
758+
string promptString = null;
759+
760+
try
761+
{
762+
promptString =
763+
invokeAction(
764+
new PSCommand().AddCommand("prompt"));
765+
}
766+
catch(RuntimeException e)
767+
{
768+
Logger.Write(
769+
LogLevel.Verbose,
770+
"Runtime exception occurred while executing prompt command:\r\n\r\n" + e.ToString());
771+
}
772+
finally
773+
{
774+
promptString = promptString ?? "PS >";
775+
}
776+
777+
this.WriteOutput(
778+
Environment.NewLine,
779+
false);
780+
781+
// Write the prompt string
782+
this.WriteOutput(
783+
promptString,
784+
false);
785+
}
786+
787+
private void WritePromptWithRunspace(Runspace runspace)
788+
{
789+
this.WritePromptToHost(
790+
command =>
791+
{
792+
this.powerShell.Commands = command;
793+
794+
return
795+
this.powerShell
796+
.Invoke<string>()
797+
.FirstOrDefault();
798+
});
799+
}
800+
801+
private void WritePromptWithNestedPipeline()
802+
{
803+
using (var pipeline = this.currentRunspace.CreateNestedPipeline())
804+
{
805+
this.WritePromptToHost(
806+
command =>
807+
{
808+
pipeline.Commands.Clear();
809+
pipeline.Commands.Add(command.Commands[0]);
810+
811+
return
812+
pipeline
813+
.Invoke()
814+
.Select(pso => pso.BaseObject)
815+
.Cast<string>()
816+
.FirstOrDefault();
817+
});
818+
819+
}
820+
}
821+
744822
#endregion
745823

746824
#region Events
@@ -769,6 +847,9 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e)
769847
PowerShellExecutionResult.Stopped,
770848
null));
771849

850+
// Write out the debugger prompt
851+
this.WritePromptWithNestedPipeline();
852+
772853
// Raise the event for the debugger service
773854
if (this.DebuggerStop != null)
774855
{

test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@
88
using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer;
99
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
1010
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
11+
using Nito.AsyncEx;
1112
using System;
13+
using System.Collections.Concurrent;
1214
using System.IO;
1315
using System.Linq;
1416
using System.Threading.Tasks;
1517
using Xunit;
16-
using Xunit.Abstractions;
1718

1819
namespace Microsoft.PowerShell.EditorServices.Test.Host
1920
{
2021
public class LanguageServerTests : IAsyncLifetime
2122
{
2223
private LanguageServiceClient languageServiceClient;
2324

25+
private ConcurrentDictionary<string, AsyncProducerConsumerQueue<object>> eventQueuePerType =
26+
new ConcurrentDictionary<string, AsyncProducerConsumerQueue<object>>();
27+
2428
public Task InitializeAsync()
2529
{
2630
string testLogPath =
@@ -420,22 +424,21 @@ await this.SendRequest(
420424
[Fact]
421425
public async Task ServiceExecutesReplCommandAndReceivesOutput()
422426
{
423-
Task<OutputEventBody> outputEventTask =
424-
this.WaitForEvent(
425-
OutputEvent.Type);
427+
this.QueueEventsForType(OutputEvent.Type);
426428

427-
Task<EvaluateResponseBody> evaluateTask =
429+
await
428430
this.SendRequest(
429431
EvaluateRequest.Type,
430432
new EvaluateRequestArguments
431433
{
432434
Expression = "1 + 2"
433435
});
434436

435-
// Wait for both the evaluate response and the output event
436-
await Task.WhenAll(evaluateTask, outputEventTask);
437+
OutputEventBody outputEvent = await this.WaitForEvent(OutputEvent.Type);
438+
Assert.Equal("1 + 2\r\n\r\n", outputEvent.Output);
439+
Assert.Equal("stdout", outputEvent.Category);
437440

438-
OutputEventBody outputEvent = outputEventTask.Result;
441+
outputEvent = await this.WaitForEvent(OutputEvent.Type);
439442
Assert.Equal("3\r\n", outputEvent.Output);
440443
Assert.Equal("stdout", outputEvent.Category);
441444
}
@@ -451,7 +454,7 @@ await this.SendRequest(
451454
Assert.Equal("Get-ChildItem\r\nGet-Location", expandedText);
452455
}
453456

454-
[Fact]//(Skip = "Choice prompt functionality is currently in transition to a new model.")]
457+
[Fact(Skip = "Choice prompt functionality is currently in transition to a new model.")]
455458
public async Task ServiceExecutesReplCommandAndReceivesChoicePrompt()
456459
{
457460
// TODO: This test is removed until a new choice prompt strategy is determined.
@@ -541,20 +544,49 @@ await this.SendEvent(
541544
}
542545
}
543546

544-
private Task<TParams> WaitForEvent<TParams>(EventType<TParams> eventType)
547+
private void QueueEventsForType<TParams>(EventType<TParams> eventType)
545548
{
546-
TaskCompletionSource<TParams> eventTask = new TaskCompletionSource<TParams>();
549+
var eventQueue =
550+
this.eventQueuePerType.AddOrUpdate(
551+
eventType.MethodName,
552+
new AsyncProducerConsumerQueue<object>(),
553+
(key, queue) => queue);
547554

548555
this.languageServiceClient.SetEventHandler(
549556
eventType,
550557
(p, ctx) =>
551558
{
552-
eventTask.SetResult(p);
553-
return Task.FromResult(true);
554-
},
555-
true); // Override any existing handler
559+
return eventQueue.EnqueueAsync(p);
560+
});
561+
}
556562

557-
return eventTask.Task;
563+
private Task<TParams> WaitForEvent<TParams>(EventType<TParams> eventType)
564+
{
565+
// Use the event queue if one has been registered
566+
AsyncProducerConsumerQueue<object> eventQueue = null;
567+
if (this.eventQueuePerType.TryGetValue(eventType.MethodName, out eventQueue))
568+
{
569+
return
570+
eventQueue
571+
.DequeueAsync()
572+
.ContinueWith<TParams>(
573+
task => (TParams)task.Result);
574+
}
575+
else
576+
{
577+
TaskCompletionSource<TParams> eventTask = new TaskCompletionSource<TParams>();
578+
579+
this.languageServiceClient.SetEventHandler(
580+
eventType,
581+
(p, ctx) =>
582+
{
583+
eventTask.SetResult(p);
584+
return Task.FromResult(true);
585+
},
586+
true); // Override any existing handler
587+
588+
return eventTask.Task;
589+
}
558590
}
559591
}
560592
}

test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@
3939
<HintPath>..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
4040
<Private>True</Private>
4141
</Reference>
42+
<Reference Include="Nito.AsyncEx, Version=3.0.1.0, Culture=neutral, processorArchitecture=MSIL">
43+
<HintPath>..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll</HintPath>
44+
<Private>True</Private>
45+
</Reference>
46+
<Reference Include="Nito.AsyncEx.Concurrent, Version=3.0.1.0, Culture=neutral, processorArchitecture=MSIL">
47+
<HintPath>..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll</HintPath>
48+
<Private>True</Private>
49+
</Reference>
50+
<Reference Include="Nito.AsyncEx.Enlightenment, Version=3.0.1.0, Culture=neutral, processorArchitecture=MSIL">
51+
<HintPath>..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll</HintPath>
52+
<Private>True</Private>
53+
</Reference>
4254
<Reference Include="System" />
4355
<Reference Include="System.Core" />
4456
<Reference Include="System.Xml.Linq" />

test/PowerShellEditorServices.Test.Host/packages.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<packages>
33
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
4+
<package id="Nito.AsyncEx" version="3.0.1" targetFramework="net45" />
45
<package id="xunit" version="2.1.0" targetFramework="net45" />
56
<package id="xunit.abstractions" version="2.0.0" targetFramework="net45" />
67
<package id="xunit.assert" version="2.1.0" targetFramework="net45" />

test/PowerShellEditorServices.Test/Console/PowerShellContextTests.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,19 @@ await this.powerShellContext.ExecuteScriptString(
107107
"\"{0}\"",
108108
TestOutputString));
109109

110+
// Prompt strings are returned as normal output, ignore the prompt
111+
string[] normalOutputLines =
112+
this.GetOutputForType(OutputType.Normal)
113+
.Split(
114+
new string[] { Environment.NewLine },
115+
StringSplitOptions.None);
116+
117+
// The output should be 3 lines: the expected string,
118+
// an empty line, and the prompt string.
119+
Assert.Equal(3, normalOutputLines.Length);
110120
Assert.Equal(
111-
TestOutputString + Environment.NewLine,
112-
this.GetOutputForType(OutputType.Normal));
121+
TestOutputString,
122+
normalOutputLines[0]);
113123
}
114124

115125
[Fact]

0 commit comments

Comments
 (0)