Skip to content

Commit 46960b1

Browse files
SeeminglySciencelzybkr
authored andcommitted
Changes for Editor Hosts (VSCode/Atom) (#626)
1 parent 2a3450d commit 46960b1

File tree

2 files changed

+69
-6
lines changed

2 files changed

+69
-6
lines changed

PSReadLine/ConsoleLib.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ namespace Microsoft.PowerShell.Internal
99
{
1010
internal class VirtualTerminal : IConsole
1111
{
12+
// These two fields are used by PowerShellEditorServices to inject a
13+
// custom ReadKey implementation. This is not a public API, but it is
14+
// part of a private contract with that project.
15+
private static Func<bool, ConsoleKeyInfo> _readKeyOverride;
16+
17+
private static Lazy<Func<bool, ConsoleKeyInfo>> _readKeyMethod = new Lazy<Func<bool, ConsoleKeyInfo>>(
18+
() => _readKeyOverride == null ? Console.ReadKey : _readKeyOverride);
19+
1220
public int CursorLeft
1321
{
1422
get => Console.CursorLeft;
@@ -97,7 +105,7 @@ public Encoding OutputEncoding
97105
set { try { Console.OutputEncoding = value; } catch { } }
98106
}
99107

100-
public ConsoleKeyInfo ReadKey() => Console.ReadKey(true);
108+
public ConsoleKeyInfo ReadKey() => _readKeyMethod.Value(true);
101109
public bool KeyAvailable => Console.KeyAvailable;
102110
public void SetWindowPosition(int left, int top) => Console.SetWindowPosition(left, top);
103111
public void SetCursorPosition(int left, int top) => Console.SetCursorPosition(left, top);

PSReadLine/ReadLine.cs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,16 @@ class ExitException : Exception { }
2828

2929
public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods
3030
{
31+
private const int ConsoleExiting = 1;
32+
33+
private const int CancellationRequested = 2;
34+
35+
private const int EventProcessingRequested = 3;
36+
3137
private static readonly PSConsoleReadLine _singleton = new PSConsoleReadLine();
3238

39+
private static readonly CancellationToken _defaultCancellationToken = new CancellationTokenSource().Token;
40+
3341
private bool _delayedOneTimeInitCompleted;
3442

3543
private IPSConsoleReadLineMockableMethods _mockableMethods;
@@ -41,6 +49,8 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods
4149
private Thread _readKeyThread;
4250
private AutoResetEvent _readKeyWaitHandle;
4351
private AutoResetEvent _keyReadWaitHandle;
52+
private AutoResetEvent _forceEventWaitHandle;
53+
private CancellationToken _cancelReadCancellationToken;
4454
internal ManualResetEvent _closingWaitHandle;
4555
private WaitHandle[] _threadProcWaitHandles;
4656
private WaitHandle[] _requestKeyWaitHandles;
@@ -139,7 +149,12 @@ private void ReadKeyThreadProc()
139149
if (handleId == 1) // It was the _closingWaitHandle that was signaled.
140150
break;
141151

152+
var localCancellationToken = _singleton._cancelReadCancellationToken;
142153
ReadOneOrMoreKeys();
154+
if (localCancellationToken.IsCancellationRequested)
155+
{
156+
continue;
157+
}
143158

144159
// One or more keys were read - let ReadKey know we're done.
145160
_keyReadWaitHandle.Set();
@@ -174,9 +189,10 @@ internal static ConsoleKeyInfo ReadKey()
174189
// - a key is pressed
175190
// - the console is exiting
176191
// - 300ms - to process events if we're idle
177-
192+
// - processing of events is requested externally
193+
// - ReadLine cancellation is requested externally
178194
handleId = WaitHandle.WaitAny(_singleton._requestKeyWaitHandles, 300);
179-
if (handleId != WaitHandle.WaitTimeout)
195+
if (handleId != WaitHandle.WaitTimeout && handleId != EventProcessingRequested)
180196
break;
181197

182198
// If we timed out, check for event subscribers (which is just
@@ -236,7 +252,7 @@ internal static ConsoleKeyInfo ReadKey()
236252
ps?.Dispose();
237253
}
238254

239-
if (handleId == 1)
255+
if (handleId == ConsoleExiting)
240256
{
241257
// The console is exiting - throw an exception to unwind the stack to the point
242258
// where we can return from ReadLine.
@@ -249,6 +265,18 @@ internal static ConsoleKeyInfo ReadKey()
249265
throw new OperationCanceledException();
250266
}
251267

268+
if (handleId == CancellationRequested)
269+
{
270+
// ReadLine was cancelled. Save the current line to be restored next time ReadLine
271+
// is called, clear the buffer and throw an exception so we can return an empty string.
272+
_singleton.SaveCurrentLine();
273+
_singleton._getNextHistoryIndex = _singleton._history.Count;
274+
_singleton._current = 0;
275+
_singleton._buffer.Clear();
276+
_singleton.Render();
277+
throw new OperationCanceledException();
278+
}
279+
252280
var key = _singleton._queuedKeys.Dequeue();
253281
return key;
254282
}
@@ -275,6 +303,18 @@ private void PrependQueuedKeys(ConsoleKeyInfo key)
275303
/// </summary>
276304
/// <returns>The complete command line.</returns>
277305
public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics)
306+
{
307+
// Use a default cancellation token instead of CancellationToken.None because the
308+
// WaitHandle is shared and could be triggered accidently.
309+
return ReadLine(runspace, engineIntrinsics, _defaultCancellationToken);
310+
}
311+
312+
/// <summary>
313+
/// Entry point - called by custom PSHost implementations that require the
314+
/// ability to cancel ReadLine.
315+
/// </summary>
316+
/// <returns>The complete command line.</returns>
317+
public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics, CancellationToken cancellationToken)
278318
{
279319
var console = _singleton._console;
280320

@@ -313,11 +353,14 @@ public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsi
313353
_singleton.Initialize(runspace, engineIntrinsics);
314354
}
315355

356+
_singleton._cancelReadCancellationToken = cancellationToken;
357+
_singleton._requestKeyWaitHandles[2] = _singleton._cancelReadCancellationToken.WaitHandle;
316358
return _singleton.InputLoop();
317359
}
318360
catch (OperationCanceledException)
319361
{
320-
// Console is exiting - return value isn't too critical - null or 'exit' could work equally well.
362+
// Console is either exiting or the cancellation of ReadLine has been requested
363+
// by a custom PSHost implementation.
321364
return "";
322365
}
323366
catch (ExitException)
@@ -720,8 +763,9 @@ private void DelayedOneTimeInitialize()
720763

721764
_singleton._readKeyWaitHandle = new AutoResetEvent(false);
722765
_singleton._keyReadWaitHandle = new AutoResetEvent(false);
766+
_singleton._forceEventWaitHandle = new AutoResetEvent(false);
723767
_singleton._closingWaitHandle = new ManualResetEvent(false);
724-
_singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle};
768+
_singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle, _defaultCancellationToken.WaitHandle, _singleton._forceEventWaitHandle};
725769
_singleton._threadProcWaitHandles = new WaitHandle[] {_singleton._readKeyWaitHandle, _singleton._closingWaitHandle};
726770

727771
// This is for a "being hosted in an alternate appdomain scenario" (the
@@ -741,6 +785,17 @@ private void DelayedOneTimeInitialize()
741785
_singleton._readKeyThread.Start();
742786
}
743787

788+
/// <summary>
789+
/// Used by PowerShellEditorServices to force immediate
790+
/// event handling during the <see cref="PSConsoleReadLine.ReadKey" />
791+
/// method. This is not a public API, but it is part of a private contract
792+
/// with that project.
793+
/// </summary>
794+
private static void ForcePSEventHandling()
795+
{
796+
_singleton._forceEventWaitHandle.Set();
797+
}
798+
744799
private static void Chord(ConsoleKeyInfo? key = null, object arg = null)
745800
{
746801
if (!key.HasValue)

0 commit comments

Comments
 (0)