Skip to content

[Revamp pipeline thread handling] Put the legacy readline back #1436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
SydneyhSmith opened this issue Mar 29, 2021 · 2 comments
Closed

[Revamp pipeline thread handling] Put the legacy readline back #1436

SydneyhSmith opened this issue Mar 29, 2021 · 2 comments

Comments

@SydneyhSmith
Copy link
Collaborator

SydneyhSmith commented Mar 29, 2021

We've supported the ability to disable (or otherwise not use) PSReadLine in the extension so far.

The new integrated console work so far makes assumptions about PSReadLine being available and loaded, but should provide a path for not using it.

This requires:

  • Introducing a new code path from the configuration to disable PSReadLine through the creation of the console hosting infrastructure
  • Bringing back the original, PSReadLine-free host implementations
  • Validating that core scenarios work with the non-PSRL host infrastructure

The non-PSRL host code looks like this:

internal class LegacyReadLineContext : IPromptContext
{
private readonly ConsoleReadLine _legacyReadLine;
internal LegacyReadLineContext(PowerShellContextService powerShellContext)
{
_legacyReadLine = new ConsoleReadLine(powerShellContext);
}
public Task AbortReadLineAsync()
{
return Task.FromResult(true);
}
public Task<string> InvokeReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
{
return _legacyReadLine.InvokeLegacyReadLineAsync(isCommandLine, cancellationToken);
}
public Task WaitForReadLineExitAsync()
{
return Task.FromResult(true);
}
public void AddToHistory(string command)
{
// Do nothing, history is managed completely by the PowerShell engine in legacy ReadLine.
}
public void AbortReadLine()
{
// Do nothing, no additional actions are needed to cancel ReadLine.
}
public void WaitForReadLineExit()
{
// Do nothing, ReadLine cancellation is instant or not appliciable.
}
public void ForcePSEventHandling()
{
// Do nothing, the pipeline thread is not occupied by legacy ReadLine.
}
}

internal class ConsoleReadLine
{
#region Private Field
private readonly PowerShellContextService powerShellContext;
#endregion
#region Constructors
public ConsoleReadLine(PowerShellContextService powerShellContext)
{
this.powerShellContext = powerShellContext;
}
#endregion
#region Public Methods
public Task<string> ReadCommandLineAsync(CancellationToken cancellationToken)
{
return this.ReadLineAsync(true, cancellationToken);
}
public Task<string> ReadSimpleLineAsync(CancellationToken cancellationToken)
{
return this.ReadLineAsync(false, cancellationToken);
}
public async Task<SecureString> ReadSecureLineAsync(CancellationToken cancellationToken)
{
SecureString secureString = new SecureString();
// TODO: Are these values used?
int initialPromptRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false);
int initialPromptCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken).ConfigureAwait(false);
int previousInputLength = 0;
Console.TreatControlCAsInput = true;
try
{
while (!cancellationToken.IsCancellationRequested)
{
ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken).ConfigureAwait(false);
if ((int)keyInfo.Key == 3 ||
keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
{
throw new PipelineStoppedException();
}
if (keyInfo.Key == ConsoleKey.Enter)
{
// Break to return the completed string
break;
}
if (keyInfo.Key == ConsoleKey.Tab)
{
continue;
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (secureString.Length > 0)
{
secureString.RemoveAt(secureString.Length - 1);
}
}
else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar))
{
secureString.AppendChar(keyInfo.KeyChar);
}
// 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 = await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false);
int col = await ConsoleProxy.GetCursorLeftAsync(cancellationToken).ConfigureAwait(false);
// 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;
}
#endregion
#region Private Methods
private static Task<ConsoleKeyInfo> ReadKeyAsync(CancellationToken cancellationToken)
{
return ConsoleProxy.ReadKeyAsync(intercept: true, cancellationToken);
}
private Task<string> ReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
{
return this.powerShellContext.InvokeReadLineAsync(isCommandLine, cancellationToken);
}
/// <summary>
/// Invokes a custom ReadLine method that is similar to but more basic than PSReadLine.
/// This method should be used when PSReadLine is disabled, either by user settings or
/// unsupported PowerShell versions.
/// </summary>
/// <param name="isCommandLine">
/// Indicates whether ReadLine should act like a command line.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token that will be checked prior to completing the returned task.
/// </param>
/// <returns>
/// A task object representing the asynchronus operation. The Result property on
/// the task object returns the user input string.
/// </returns>
internal async Task<string> InvokeLegacyReadLineAsync(bool isCommandLine, CancellationToken cancellationToken)
{
// TODO: Is inputBeforeCompletion used?
string inputBeforeCompletion = null;
string inputAfterCompletion = null;
CommandCompletion currentCompletion = null;
int historyIndex = -1;
Collection<PSObject> currentHistory = null;
StringBuilder inputLine = new StringBuilder();
int initialCursorCol = await ConsoleProxy.GetCursorLeftAsync(cancellationToken).ConfigureAwait(false);
int initialCursorRow = await ConsoleProxy.GetCursorTopAsync(cancellationToken).ConfigureAwait(false);
// TODO: Are these used?
int initialWindowLeft = Console.WindowLeft;
int initialWindowTop = Console.WindowTop;
int currentCursorIndex = 0;
Console.TreatControlCAsInput = true;
try
{
while (!cancellationToken.IsCancellationRequested)
{
ConsoleKeyInfo keyInfo = await ReadKeyAsync(cancellationToken).ConfigureAwait(false);
// Do final position calculation after the key has been pressed
// because the window could have been resized before then
int promptStartCol = initialCursorCol;
int promptStartRow = initialCursorRow;
int consoleWidth = Console.WindowWidth;
if ((int)keyInfo.Key == 3 ||
keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
{
throw new PipelineStoppedException();
}
else if (keyInfo.Key == ConsoleKey.Tab && isCommandLine)
{
if (currentCompletion == null)
{
inputBeforeCompletion = inputLine.ToString();
inputAfterCompletion = null;
// TODO: This logic should be moved to AstOperations or similar!
if (this.powerShellContext.IsDebuggerStopped)
{
PSCommand command = new PSCommand();
command.AddCommand("TabExpansion2");
command.AddParameter("InputScript", inputBeforeCompletion);
command.AddParameter("CursorColumn", currentCursorIndex);
command.AddParameter("Options", null);
var results = await this.powerShellContext
.ExecuteCommandAsync<CommandCompletion>(command, sendOutputToHost: false, sendErrorToHost: false)
.ConfigureAwait(false);
currentCompletion = results.FirstOrDefault();
}
else
{
using (RunspaceHandle runspaceHandle = await this.powerShellContext.GetRunspaceHandleAsync().ConfigureAwait(false))
using (PowerShell powerShell = PowerShell.Create())
{
powerShell.Runspace = runspaceHandle.Runspace;
currentCompletion =
CommandCompletion.CompleteInput(
inputBeforeCompletion,
currentCursorIndex,
null,
powerShell);
if (currentCompletion.CompletionMatches.Count > 0)
{
int replacementEndIndex =
currentCompletion.ReplacementIndex +
currentCompletion.ReplacementLength;
inputAfterCompletion =
inputLine.ToString(
replacementEndIndex,
inputLine.Length - replacementEndIndex);
}
else
{
currentCompletion = null;
}
}
}
}
CompletionResult completion =
currentCompletion?.GetNextResult(
!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift));
if (completion != null)
{
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
$"{completion.CompletionText}{inputAfterCompletion}",
currentCursorIndex,
insertIndex: currentCompletion.ReplacementIndex,
replaceLength: inputLine.Length - currentCompletion.ReplacementIndex,
finalCursorIndex: currentCompletion.ReplacementIndex + completion.CompletionText.Length);
}
}
else if (keyInfo.Key == ConsoleKey.LeftArrow)
{
currentCompletion = null;
if (currentCursorIndex > 0)
{
currentCursorIndex =
this.MoveCursorToIndex(
promptStartCol,
promptStartRow,
consoleWidth,
currentCursorIndex - 1);
}
}
else if (keyInfo.Key == ConsoleKey.Home)
{
currentCompletion = null;
currentCursorIndex =
this.MoveCursorToIndex(
promptStartCol,
promptStartRow,
consoleWidth,
0);
}
else if (keyInfo.Key == ConsoleKey.RightArrow)
{
currentCompletion = null;
if (currentCursorIndex < inputLine.Length)
{
currentCursorIndex =
this.MoveCursorToIndex(
promptStartCol,
promptStartRow,
consoleWidth,
currentCursorIndex + 1);
}
}
else if (keyInfo.Key == ConsoleKey.End)
{
currentCompletion = null;
currentCursorIndex =
this.MoveCursorToIndex(
promptStartCol,
promptStartRow,
consoleWidth,
inputLine.Length);
}
else if (keyInfo.Key == ConsoleKey.UpArrow && isCommandLine)
{
currentCompletion = null;
// TODO: Ctrl+Up should allow navigation in multi-line input
if (currentHistory == null)
{
historyIndex = -1;
PSCommand command = new PSCommand();
command.AddCommand("Get-History");
currentHistory = await this.powerShellContext.ExecuteCommandAsync<PSObject>(command, sendOutputToHost: false, sendErrorToHost: false)
.ConfigureAwait(false)
as Collection<PSObject>;
if (currentHistory != null)
{
historyIndex = currentHistory.Count;
}
}
if (currentHistory != null && currentHistory.Count > 0 && historyIndex > 0)
{
historyIndex--;
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
(string)currentHistory[historyIndex].Properties["CommandLine"].Value,
currentCursorIndex,
insertIndex: 0,
replaceLength: inputLine.Length);
}
}
else if (keyInfo.Key == ConsoleKey.DownArrow && isCommandLine)
{
currentCompletion = null;
// The down arrow shouldn't cause history to be loaded,
// it's only for navigating an active history array
if (historyIndex > -1 && historyIndex < currentHistory.Count &&
currentHistory != null && currentHistory.Count > 0)
{
historyIndex++;
if (historyIndex < currentHistory.Count)
{
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
(string)currentHistory[historyIndex].Properties["CommandLine"].Value,
currentCursorIndex,
insertIndex: 0,
replaceLength: inputLine.Length);
}
else if (historyIndex == currentHistory.Count)
{
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
string.Empty,
currentCursorIndex,
insertIndex: 0,
replaceLength: inputLine.Length);
}
}
}
else if (keyInfo.Key == ConsoleKey.Escape)
{
currentCompletion = null;
historyIndex = currentHistory != null ? currentHistory.Count : -1;
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
string.Empty,
currentCursorIndex,
insertIndex: 0,
replaceLength: inputLine.Length);
}
else if (keyInfo.Key == ConsoleKey.Backspace)
{
currentCompletion = null;
if (currentCursorIndex > 0)
{
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
string.Empty,
currentCursorIndex,
insertIndex: currentCursorIndex - 1,
replaceLength: 1,
finalCursorIndex: currentCursorIndex - 1);
}
}
else if (keyInfo.Key == ConsoleKey.Delete)
{
currentCompletion = null;
if (currentCursorIndex < inputLine.Length)
{
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
string.Empty,
currentCursorIndex,
replaceLength: 1,
finalCursorIndex: currentCursorIndex);
}
}
else if (keyInfo.Key == ConsoleKey.Enter)
{
string completedInput = inputLine.ToString();
currentCompletion = null;
currentHistory = null;
//if ((keyInfo.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift)
//{
// // TODO: Start a new line!
// continue;
//}
Parser.ParseInput(
completedInput,
out Token[] tokens,
out ParseError[] parseErrors);
//if (parseErrors.Any(e => e.IncompleteInput))
//{
// // TODO: Start a new line!
// continue;
//}
return completedInput;
}
else if (keyInfo.KeyChar != 0 && !char.IsControl(keyInfo.KeyChar))
{
// Normal character input
currentCompletion = null;
currentCursorIndex =
this.InsertInput(
inputLine,
promptStartCol,
promptStartRow,
keyInfo.KeyChar.ToString(), // TODO: Determine whether this should take culture into account
currentCursorIndex,
finalCursorIndex: currentCursorIndex + 1);
}
}
}
finally
{
Console.TreatControlCAsInput = false;
}
return null;
}
// TODO: Is this used?
private int CalculateIndexFromCursor(
int promptStartCol,
int promptStartRow,
int consoleWidth)
{
return
((ConsoleProxy.GetCursorTop() - promptStartRow) * consoleWidth) +
ConsoleProxy.GetCursorLeft() - promptStartCol;
}
private void CalculateCursorFromIndex(
int promptStartCol,
int promptStartRow,
int consoleWidth,
int inputIndex,
out int cursorCol,
out int cursorRow)
{
cursorCol = promptStartCol + inputIndex;
cursorRow = promptStartRow + cursorCol / consoleWidth;
cursorCol = cursorCol % consoleWidth;
}
private int InsertInput(
StringBuilder inputLine,
int promptStartCol,
int promptStartRow,
string insertedInput,
int cursorIndex,
int insertIndex = -1,
int replaceLength = 0,
int finalCursorIndex = -1)
{
int consoleWidth = Console.WindowWidth;
int previousInputLength = inputLine.Length;
if (insertIndex == -1)
{
insertIndex = cursorIndex;
}
// Move the cursor to the new insertion point
this.MoveCursorToIndex(
promptStartCol,
promptStartRow,
consoleWidth,
insertIndex);
// Edit the input string based on the insertion
if (insertIndex < inputLine.Length)
{
if (replaceLength > 0)
{
inputLine.Remove(insertIndex, replaceLength);
}
inputLine.Insert(insertIndex, insertedInput);
}
else
{
inputLine.Append(insertedInput);
}
// Re-render affected section
Console.Write(
inputLine.ToString(
insertIndex,
inputLine.Length - insertIndex));
if (inputLine.Length < previousInputLength)
{
Console.Write(
new string(
' ',
previousInputLength - inputLine.Length));
}
// Automatically set the final cursor position to the end
// of the new input string. This is needed if the previous
// input string is longer than the new one and needed to have
// its old contents overwritten. This will position the cursor
// back at the end of the new text
if (finalCursorIndex == -1 && inputLine.Length < previousInputLength)
{
finalCursorIndex = inputLine.Length;
}
if (finalCursorIndex > -1)
{
// Move the cursor to the final position
return
this.MoveCursorToIndex(
promptStartCol,
promptStartRow,
consoleWidth,
finalCursorIndex);
}
else
{
return inputLine.Length;
}
}
private int MoveCursorToIndex(
int promptStartCol,
int promptStartRow,
int consoleWidth,
int newCursorIndex)
{
this.CalculateCursorFromIndex(
promptStartCol,
promptStartRow,
consoleWidth,
newCursorIndex,
out int newCursorCol,
out int newCursorRow);
Console.SetCursorPosition(newCursorCol, newCursorRow);
return newCursorIndex;
}
#endregion
}

In particular look for methods with LegacyReadLine in them

@ghost ghost added the Needs: Triage Maintainer attention needed! label Mar 29, 2021
@SydneyhSmith SydneyhSmith added Area-Engine Area-ReadLine Issue-Bug A bug to squash. and removed Needs: Triage Maintainer attention needed! labels Mar 29, 2021
@rjmholt
Copy link
Contributor

rjmholt commented Oct 11, 2021

To solve in #1459

@rjmholt
Copy link
Contributor

rjmholt commented Oct 28, 2021

Fixed in #1584

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants