Skip to content

Commit b639a13

Browse files
SeeminglySciencerjmholt
authored andcommitted
PSReadLine integration (#672)
* Add infrastructure for managing context Adds classes that manage the state of the prompt, nested contexts, and multiple ReadLine implementations of varying complexity. (cherry picked from commit 7ca8b9b) * Console related classes changes Change ReadLine method to call out to PowerShellContext. This lets the PowerShellContext determine which ReadLine implementation to use based on available modules. Also includes some changes to the System.Console proxy classes to account for PSReadLine. (cherry picked from commit 59bfa3b) * Rewrite command invocation operations for PSRL Refactor PowerShellContext to have a more robust system for tracking the context in which commands are invoked. This is a significant change in that all interactions with the runspace must be done through methods in PowerShellContext. These changes also greatly increase stability. (cherry picked from commit 21e6b5f) * Rewrite direct SessionStateProxy calls All interactions with the runspace must be done through PowerShellContext now that nested PowerShell instances are encountered frequently. Also fix a bunch of race conditions that were made more obvious with the changes. (cherry picked from commit fa2faba) * Pass feature flags to Start-EditorServicesHost * Address feedback and fix travis build error - Address feedback from @bergmeister - Fix a few other similar mistakes I found - Fix travis build failing due to missing documentation comment tag * Fix all tests except ServiceLoadsProfileOnDemand - Fix an issue where intellisense wouldn't finish if PSReadLine was not running - Fix a crash that would occur if the PSHost was not set up for input like the one used in our tests - Fix a compile error when building against PSv3/4 - Fix a hang that occurred when the PromptNest was disposed during a debug session - Fix some XML documentation comment syntax errors * Fix extra new lines outputted after each command Removed a call to WriteOutput where it wasn't required. This was creating extra new lines which failed tests (and obviously didn't look right). * Remove unused field from InvocationEventQueue And also fix spacing between the other fields. * Remove copying of PDB's in build script @rjmholt did a better job of this in a different PR that we can merge into 2.0.0 later. It also doesn't make sense in this PR. * Add AppVeyor tracking to branch 2.0.0 * Fix ambiguous method crash on CoreCLR Simplify delegate creation in PSReadLineProxy and fix the immediate ambiguous method crash the complicated code caused on CoreCLR. * first round of feedback changes * Some more feedback changes * add a bunch of copyright headers I missed * remove KeyAvailable query * Get the latest PSReadLine module installed * Add PSReadLine installation to build script * the file should be downloaded as a .zip * Address remaining feedback * Attempt to fix issue with native apps and input On Unix like platforms some native applications do not work properly if our event subscriber is active. I suspect this is due to PSReadLine querying cursor position prior to checking for events. I believe the cursor position response emitted is being read as input. I've attempted to fix this by hooking into PSHost.NotifyBeginApplication to temporarly remove the event subscriber, and PSHost.NotifyEndApplication to recreate it afterwards. * Revert "Attempt to fix issue with native apps and input" This reverts commit 1682410. * Fix build failure
1 parent 2fe8994 commit b639a13

40 files changed

+3427
-506
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ registered_data.ini
2929
.dotnet/
3030
module/Plaster
3131
module/PSScriptAnalyzer
32+
module/PSReadLine
3233
docs/_site/
3334
docs/_repo/
3435
docs/metadata/

PowerShellEditorServices.build.ps1

+19
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,25 @@ task RestorePsesModules -After Build {
309309

310310
Save-Module @splatParameters
311311
}
312+
313+
# TODO: Replace this with adding a new module to Save when a new PSReadLine release comes out to the Gallery
314+
if (-not (Test-Path $PSScriptRoot/module/PSReadLine))
315+
{
316+
Write-Host "`tInstalling module: PSReadLine"
317+
318+
# Download AppVeyor zip
319+
$jobId = (Invoke-RestMethod https://ci.appveyor.com/api/projects/lzybkr/PSReadLine).build.jobs[0].jobId
320+
Invoke-RestMethod https://ci.appveyor.com/api/buildjobs/$jobId/artifacts/bin%2FRelease%2FPSReadLine.zip -OutFile $PSScriptRoot/module/PSRL.zip
321+
322+
# Position PSReadLine
323+
Expand-Archive $PSScriptRoot/module/PSRL.zip $PSScriptRoot/module/PSRL
324+
Move-Item $PSScriptRoot/module/PSRL/PSReadLine $PSScriptRoot/module
325+
326+
# Clean up
327+
Remove-Item -Force -Recurse $PSScriptRoot/module/PSRL.zip
328+
Remove-Item -Force -Recurse $PSScriptRoot/module/PSRL
329+
}
330+
312331
Write-Host "`n"
313332
}
314333

appveyor.yml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ skip_tags: true
66
branches:
77
only:
88
- master
9+
- 2.0.0
910

1011
environment:
1112
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # Don't download unneeded packages

module/PowerShellEditorServices/Start-EditorServices.ps1

+2-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ try {
314314
-BundledModulesPath $BundledModulesPath `
315315
-EnableConsoleRepl:$EnableConsoleRepl.IsPresent `
316316
-DebugServiceOnly:$DebugServiceOnly.IsPresent `
317-
-WaitForDebugger:$WaitForDebugger.IsPresent
317+
-WaitForDebugger:$WaitForDebugger.IsPresent `
318+
-FeatureFlags $FeatureFlags
318319

319320
# TODO: Verify that the service is started
320321
Log "Start-EditorServicesHost returned $editorServicesHost"

src/PowerShellEditorServices.Host/EditorServicesHost.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ private EditorSession CreateSession(
365365
bool enableConsoleRepl)
366366
{
367367
EditorSession editorSession = new EditorSession(this.logger);
368-
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
368+
PowerShellContext powerShellContext = new PowerShellContext(this.logger, this.featureFlags.Contains("PSReadLine"));
369369

370370
EditorServicesPSHostUserInterface hostUserInterface =
371371
enableConsoleRepl
@@ -405,7 +405,9 @@ private EditorSession CreateDebugSession(
405405
bool enableConsoleRepl)
406406
{
407407
EditorSession editorSession = new EditorSession(this.logger);
408-
PowerShellContext powerShellContext = new PowerShellContext(this.logger);
408+
PowerShellContext powerShellContext = new PowerShellContext(
409+
this.logger,
410+
this.featureFlags.Contains("PSReadLine"));
409411

410412
EditorServicesPSHostUserInterface hostUserInterface =
411413
enableConsoleRepl

src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs

+26-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ protected Task LaunchScript(RequestContext<object> requestContext)
118118

119119
private async Task OnExecutionCompleted(Task executeTask)
120120
{
121+
try
122+
{
123+
await executeTask;
124+
}
125+
catch (Exception e)
126+
{
127+
Logger.Write(
128+
LogLevel.Error,
129+
"Exception occurred while awaiting debug launch task.\n\n" + e.ToString());
130+
}
131+
121132
Logger.Write(LogLevel.Verbose, "Execution completed, terminating...");
122133

123134
this.executionCompleted = true;
@@ -470,7 +481,7 @@ protected async Task HandleDisconnectRequest(
470481
if (this.executionCompleted == false)
471482
{
472483
this.disconnectRequestContext = requestContext;
473-
this.editorSession.PowerShellContext.AbortExecution();
484+
this.editorSession.PowerShellContext.AbortExecution(shouldAbortDebugSession: true);
474485

475486
if (this.isInteractiveDebugSession)
476487
{
@@ -755,6 +766,20 @@ protected async Task HandleStackTraceRequest(
755766
StackFrameDetails[] stackFrames =
756767
editorSession.DebugService.GetStackFrames();
757768

769+
// Handle a rare race condition where the adapter requests stack frames before they've
770+
// begun building.
771+
if (stackFrames == null)
772+
{
773+
await requestContext.SendResult(
774+
new StackTraceResponseBody
775+
{
776+
StackFrames = new StackFrame[0],
777+
TotalFrames = 0
778+
});
779+
780+
return;
781+
}
782+
758783
List<StackFrame> newStackFrames = new List<StackFrame>();
759784

760785
int startFrameIndex = stackTraceParams.StartFrame ?? 0;

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+9
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,15 @@ private static async Task DelayThenInvokeDiagnostics(
15021502
catch (TaskCanceledException)
15031503
{
15041504
// If the task is cancelled, exit directly
1505+
foreach (var script in filesToAnalyze)
1506+
{
1507+
await PublishScriptDiagnostics(
1508+
script,
1509+
script.SyntaxMarkers,
1510+
correctionIndex,
1511+
eventSender);
1512+
}
1513+
15051514
return;
15061515
}
15071516

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Runtime.InteropServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.PowerShell.EditorServices.Console
12+
{
13+
/// <summary>
14+
/// Provides asynchronous implementations of the <see cref="Console" /> API's as well as
15+
/// synchronous implementations that work around platform specific issues.
16+
/// </summary>
17+
internal static class ConsoleProxy
18+
{
19+
private static IConsoleOperations s_consoleProxy;
20+
21+
static ConsoleProxy()
22+
{
23+
// Maybe we should just include the RuntimeInformation package for FullCLR?
24+
#if CoreCLR
25+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
26+
{
27+
s_consoleProxy = new WindowsConsoleOperations();
28+
return;
29+
}
30+
31+
s_consoleProxy = new UnixConsoleOperations();
32+
#else
33+
s_consoleProxy = new WindowsConsoleOperations();
34+
#endif
35+
}
36+
37+
public static Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken) =>
38+
s_consoleProxy.ReadKeyAsync(cancellationToken);
39+
40+
public static int GetCursorLeft() =>
41+
s_consoleProxy.GetCursorLeft();
42+
43+
public static int GetCursorLeft(CancellationToken cancellationToken) =>
44+
s_consoleProxy.GetCursorLeft(cancellationToken);
45+
46+
public static Task<int> GetCursorLeftAsync() =>
47+
s_consoleProxy.GetCursorLeftAsync();
48+
49+
public static Task<int> GetCursorLeftAsync(CancellationToken cancellationToken) =>
50+
s_consoleProxy.GetCursorLeftAsync(cancellationToken);
51+
52+
public static int GetCursorTop() =>
53+
s_consoleProxy.GetCursorTop();
54+
55+
public static int GetCursorTop(CancellationToken cancellationToken) =>
56+
s_consoleProxy.GetCursorTop(cancellationToken);
57+
58+
public static Task<int> GetCursorTopAsync() =>
59+
s_consoleProxy.GetCursorTopAsync();
60+
61+
public static Task<int> GetCursorTopAsync(CancellationToken cancellationToken) =>
62+
s_consoleProxy.GetCursorTopAsync(cancellationToken);
63+
64+
/// <summary>
65+
/// On Unix platforms this method is sent to PSReadLine as a work around for issues
66+
/// with the System.Console implementation for that platform. Functionally it is the
67+
/// same as System.Console.ReadKey, with the exception that it will not lock the
68+
/// standard input stream.
69+
/// </summary>
70+
/// <param name="intercept">
71+
/// Determines whether to display the pressed key in the console window.
72+
/// true to not display the pressed key; otherwise, false.
73+
/// </param>
74+
/// <param name="cancellationToken">
75+
/// The <see cref="CancellationToken" /> that can be used to cancel the request.
76+
/// </param>
77+
/// <returns>
78+
/// An object that describes the ConsoleKey constant and Unicode character, if any,
79+
/// that correspond to the pressed console key. The ConsoleKeyInfo object also describes,
80+
/// in a bitwise combination of ConsoleModifiers values, whether one or more Shift, Alt,
81+
/// or Ctrl modifier keys was pressed simultaneously with the console key.
82+
/// </returns>
83+
internal static ConsoleKeyInfo UnixReadKey(bool intercept, CancellationToken cancellationToken)
84+
{
85+
try
86+
{
87+
return ((UnixConsoleOperations)s_consoleProxy).ReadKey(intercept, cancellationToken);
88+
}
89+
catch (OperationCanceledException)
90+
{
91+
return default(ConsoleKeyInfo);
92+
}
93+
}
94+
}
95+
}

src/PowerShellEditorServices/Console/ConsoleReadLine.cs

+32-26
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Collections.ObjectModel;
77
using System.Linq;
88
using System.Text;
9-
using System.Runtime.InteropServices;
109
using System.Threading;
1110
using System.Threading.Tasks;
1211

@@ -20,27 +19,13 @@ namespace Microsoft.PowerShell.EditorServices.Console
2019
internal class ConsoleReadLine
2120
{
2221
#region Private Field
23-
private static IConsoleOperations s_consoleProxy;
24-
2522
private PowerShellContext powerShellContext;
2623

2724
#endregion
2825

2926
#region Constructors
3027
static ConsoleReadLine()
3128
{
32-
// Maybe we should just include the RuntimeInformation package for FullCLR?
33-
#if CoreCLR
34-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
35-
{
36-
s_consoleProxy = new WindowsConsoleOperations();
37-
return;
38-
}
39-
40-
s_consoleProxy = new UnixConsoleOperations();
41-
#else
42-
s_consoleProxy = new WindowsConsoleOperations();
43-
#endif
4429
}
4530

4631
public ConsoleReadLine(PowerShellContext powerShellContext)
@@ -54,20 +39,20 @@ public ConsoleReadLine(PowerShellContext powerShellContext)
5439

5540
public Task<string> ReadCommandLine(CancellationToken cancellationToken)
5641
{
57-
return this.ReadLine(true, cancellationToken);
42+
return this.ReadLineAsync(true, cancellationToken);
5843
}
5944

6045
public Task<string> ReadSimpleLine(CancellationToken cancellationToken)
6146
{
62-
return this.ReadLine(false, cancellationToken);
47+
return this.ReadLineAsync(false, cancellationToken);
6348
}
6449

6550
public async Task<SecureString> ReadSecureLine(CancellationToken cancellationToken)
6651
{
6752
SecureString secureString = new SecureString();
6853

69-
int initialPromptRow = Console.CursorTop;
70-
int initialPromptCol = Console.CursorLeft;
54+
int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken);
55+
int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken);
7156
int previousInputLength = 0;
7257

7358
Console.TreatControlCAsInput = true;
@@ -114,7 +99,8 @@ public async Task<SecureString> ReadSecureLine(CancellationToken cancellationTok
11499
}
115100
else if (previousInputLength > 0 && currentInputLength < previousInputLength)
116101
{
117-
int row = Console.CursorTop, col = Console.CursorLeft;
102+
int row = await ConsoleProxy.GetCursorTopAsync(cancellationToken);
103+
int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken);
118104

119105
// Back up the cursor before clearing the character
120106
col--;
@@ -146,10 +132,30 @@ public async Task<SecureString> ReadSecureLine(CancellationToken cancellationTok
146132

147133
private static async Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken)
148134
{
149-
return await s_consoleProxy.ReadKeyAsync(cancellationToken);
135+
return await ConsoleProxy.ReadKeyAsync(cancellationToken);
136+
}
137+
138+
private async Task<string> ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
139+
{
140+
return await this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken);
150141
}
151142

152-
private async Task<string> ReadLine(bool isCommandLine, CancellationToken cancellationToken)
143+
/// <summary>
144+
/// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine.
145+
/// This method should be used when PSReadLine is disabled, either by user settings or
146+
/// unsupported PowerShell versions.
147+
/// </summary>
148+
/// <param name="isCommandLine">
149+
/// Indicates whether ReadLine should act like a command line.
150+
/// </param>
151+
/// <param name="cancellationToken">
152+
/// The cancellation token that will be checked prior to completing the returned task.
153+
/// </param>
154+
/// <returns>
155+
/// A task object representing the asynchronus operation. The Result property on
156+
/// the task object returns the user input string.
157+
/// </returns>
158+
internal async Task<string> InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
153159
{
154160
string inputBeforeCompletion = null;
155161
string inputAfterCompletion = null;
@@ -160,8 +166,8 @@ private async Task<string> ReadLine(bool isCommandLine, CancellationToken cancel
160166

161167
StringBuilder inputLine = new StringBuilder();
162168

163-
int initialCursorCol = Console.CursorLeft;
164-
int initialCursorRow = Console.CursorTop;
169+
int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken);
170+
int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken);
165171

166172
int initialWindowLeft = Console.WindowLeft;
167173
int initialWindowTop = Console.WindowTop;
@@ -492,8 +498,8 @@ private int CalculateIndexFromCursor(
492498
int consoleWidth)
493499
{
494500
return
495-
((Console.CursorTop - promptStartRow) * consoleWidth) +
496-
Console.CursorLeft - promptStartCol;
501+
((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) +
502+
ConsoleProxy.GetCursorLeft() - promptStartCol;
497503
}
498504

499505
private void CalculateCursorFromIndex(

0 commit comments

Comments
 (0)