Skip to content

Commit

Permalink
Delete ConsoleProxy.cs and further simplify ReadKey logic
Browse files Browse the repository at this point in the history
  • Loading branch information
andyleejordan committed Apr 12, 2022
1 parent 9fa13a2 commit e3278a8
Show file tree
Hide file tree
Showing 6 changed files with 32 additions and 256 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Security;
using System.Threading;

namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console
{
// TODO: Do we really need a whole interface for this?
internal interface IReadLine
{
string ReadLine(CancellationToken cancellationToken);

SecureString ReadSecureLine(CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public PsrlReadLine(

public override string ReadLine(CancellationToken cancellationToken) => _psesHost.InvokeDelegate(representation: "ReadLine", new ExecutionOptions { MustRunInForeground = true }, InvokePSReadLine, cancellationToken);

protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => ConsoleProxy.SafeReadKey(intercept: true, cancellationToken);
protected override ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) => _psesHost.ReadKey(intercept: true, cancellationToken);

#endregion

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
using System.Management.Automation;
using System.Security;
using System.Threading;

namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console
Expand All @@ -15,85 +12,5 @@ internal abstract class TerminalReadLine : IReadLine
public abstract string ReadLine(CancellationToken cancellationToken);

protected abstract ConsoleKeyInfo ReadKey(CancellationToken cancellationToken);

public SecureString ReadSecureLine(CancellationToken cancellationToken)
{
Console.TreatControlCAsInput = true;
int previousInputLength = 0;
SecureString secureString = new();
try
{
bool enterPressed = false;
while (!enterPressed && !cancellationToken.IsCancellationRequested)
{
ConsoleKeyInfo keyInfo = ReadKey(cancellationToken);

if (keyInfo.IsCtrlC())
{
throw new PipelineStoppedException();
}

switch (keyInfo.Key)
{
case ConsoleKey.Enter:
// Stop the while loop so we can realign the cursor
// and then return the entered string
enterPressed = true;
continue;

case ConsoleKey.Tab:
break;

case ConsoleKey.Backspace:
if (secureString.Length > 0)
{
secureString.RemoveAt(secureString.Length - 1);
}
break;

default:
if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar))
{
secureString.AppendChar(keyInfo.KeyChar);
}
break;
}

// Re-render the secure string characters
int currentInputLength = secureString.Length;
int consoleWidth = Console.WindowWidth;

if (currentInputLength > previousInputLength)
{
Console.Write('*');
}
else if (previousInputLength > 0 && currentInputLength < previousInputLength)
{
int row = Console.CursorTop;
int col = Console.CursorLeft;

// Back up the cursor before clearing the character
col--;
if (col < 0)
{
col = consoleWidth - 1;
row--;
}

Console.SetCursorPosition(col, row);
Console.Write(' ');
Console.SetCursorPosition(col, row);
}

previousInputLength = currentInputLength;
}
}
finally
{
Console.TreatControlCAsInput = false;
}

return secureString;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
// Licensed under the MIT License.

using System;
using System.Management.Automation;
using System.Management.Automation.Host;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console;

namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host
{
Expand All @@ -15,7 +13,6 @@ internal class EditorServicesConsolePSHostRawUserInterface : PSHostRawUserInterf

private readonly PSHostRawUserInterface _internalRawUI;
private readonly ILogger _logger;
private KeyInfo? _lastKeyDown;

#endregion

Expand Down Expand Up @@ -129,60 +126,7 @@ public override string WindowTitle
/// </summary>
/// <param name="options">Options for reading the current keypress.</param>
/// <returns>A KeyInfo struct with details about the current keypress.</returns>
public override KeyInfo ReadKey(ReadKeyOptions options)
{
bool includeUp = (options & ReadKeyOptions.IncludeKeyUp) != 0;

// Key Up was requested and we have a cached key down we can return.
if (includeUp && _lastKeyDown != null)
{
KeyInfo info = _lastKeyDown.Value;
_lastKeyDown = null;
return new KeyInfo(
info.VirtualKeyCode,
info.Character,
info.ControlKeyState,
keyDown: false);
}

bool intercept = (options & ReadKeyOptions.NoEcho) != 0;
bool includeDown = (options & ReadKeyOptions.IncludeKeyDown) != 0;
if (!(includeDown || includeUp))
{
throw new PSArgumentException(
"Cannot read key options. To read options, set one or both of the following: IncludeKeyDown, IncludeKeyUp.",
nameof(options));
}

// Allow ControlC as input so we can emulate pipeline stop requests. We can't actually
// determine if a stop is requested without using non-public API's.
bool oldValue = System.Console.TreatControlCAsInput;
try
{
System.Console.TreatControlCAsInput = true;
ConsoleKeyInfo key = ConsoleProxy.SafeReadKey(intercept, default);

if (IsCtrlC(key))
{
// Caller wants CtrlC as input so return it.
if ((options & ReadKeyOptions.AllowCtrlC) != 0)
{
return ProcessKey(key, includeDown);
}

// Caller doesn't want CtrlC so throw a PipelineStoppedException to emulate
// a real stop. This will not show an exception to a script based caller and it
// will avoid having to return something like default(KeyInfo).
throw new PipelineStoppedException();
}

return ProcessKey(key, includeDown);
}
finally
{
System.Console.TreatControlCAsInput = oldValue;
}
}
public override KeyInfo ReadKey(ReadKeyOptions options) => _internalRawUI.ReadKey(options);

/// <summary>
/// Flushes the current input buffer.
Expand Down Expand Up @@ -279,62 +223,5 @@ public override void SetBufferContents(
public override int LengthInBufferCells(string source, int offset) => _internalRawUI.LengthInBufferCells(source, offset);

#endregion

/// <summary>
/// Determines if a key press represents the input Ctrl + C.
/// </summary>
/// <param name="keyInfo">The key to test.</param>
/// <returns>
/// <see langword="true" /> if the key represents the input Ctrl + C,
/// otherwise <see langword="false" />.
/// </returns>
private static bool IsCtrlC(ConsoleKeyInfo keyInfo)
{
// In the VSCode terminal Ctrl C is processed as virtual key code "3", which
// is not a named value in the ConsoleKey enum.
if ((int)keyInfo.Key == 3)
{
return true;
}

return keyInfo.Key == ConsoleKey.C && (keyInfo.Modifiers & ConsoleModifiers.Control) != 0;
}

/// <summary>
/// Converts <see cref="ConsoleKeyInfo" /> objects to <see cref="KeyInfo" /> objects and caches
/// key down events for the next key up request.
/// </summary>
/// <param name="key">The key to convert.</param>
/// <param name="isDown">
/// A value indicating whether the result should be a key down event.
/// </param>
/// <returns>The converted value.</returns>
private KeyInfo ProcessKey(ConsoleKeyInfo key, bool isDown)
{
// Translate ConsoleModifiers to ControlKeyStates
ControlKeyStates states = default;
if ((key.Modifiers & ConsoleModifiers.Alt) != 0)
{
states |= ControlKeyStates.LeftAltPressed;
}

if ((key.Modifiers & ConsoleModifiers.Control) != 0)
{
states |= ControlKeyStates.LeftCtrlPressed;
}

if ((key.Modifiers & ConsoleModifiers.Shift) != 0)
{
states |= ControlKeyStates.ShiftPressed;
}

KeyInfo result = new((int)key.Key, key.KeyChar, states, isDown);
if (isDown)
{
_lastKeyDown = result;
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,13 @@ private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args)
}
}

private static readonly ConsoleKeyInfo s_nullKeyInfo = new(
keyChar: ' ',
ConsoleKey.DownArrow,
shift: false,
alt: false,
control: false);

private ConsoleKeyInfo ReadKey(bool intercept)
{
// PSRL doesn't tell us when CtrlC was sent.
Expand All @@ -887,20 +894,34 @@ private ConsoleKeyInfo ReadKey(bool intercept)
// PSReadLine's thread waiting on Console.ReadKey to return. Normally we'd just cancel
// this call, but the .NET API ReadKey is not cancellable, and is stuck until we send
// input. This leads to a myriad of problems, but we circumvent them by pretending to
// press a key.
// press a key, thus allowing ReadKey to return, and us to ignore it.
using CancellationTokenRegistration registration = _readKeyCancellationToken.Register(
() => _languageServer?.SendNotification("powerShell/sendKeyPress")
);
_lastKey = ConsoleProxy.SafeReadKey(intercept, _readKeyCancellationToken);
return _lastKey.Value;
() => _languageServer?.SendNotification("powerShell/sendKeyPress"));

// TODO: We may want to allow users of PSES to override this method call.
_lastKey = System.Console.ReadKey(intercept);

// TODO: After fixing PSReadLine so that when canceled it doesn't read a key, we can
// stop using s_nullKeyInfo (which is a down arrow so we don't change the buffer
// content). Without this, the sent key press is translated to an @ symbol.
return _readKeyCancellationToken.IsCancellationRequested ? s_nullKeyInfo : _lastKey.Value;
}

private bool LastKeyWasCtrlC()
internal ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken)
{
return _lastKey.HasValue
&& _lastKey.Value.IsCtrlC();
try
{
_readKeyCancellationToken = cancellationToken;
return ReadKey(intercept);
}
finally
{
_readKeyCancellationToken = CancellationToken.None;
}
}

private bool LastKeyWasCtrlC() => _lastKey.HasValue && _lastKey.Value.IsCtrlC();

private void StopDebugContext()
{
// We are officially stopping the debugger.
Expand Down

0 comments on commit e3278a8

Please sign in to comment.