diff --git a/src/Microsoft.PowerShell.Linux.Host/main.cs b/src/Microsoft.PowerShell.Linux.Host/main.cs index be52ea07159..9714a08d5ff 100644 --- a/src/Microsoft.PowerShell.Linux.Host/main.cs +++ b/src/Microsoft.PowerShell.Linux.Host/main.cs @@ -177,6 +177,16 @@ internal class Listener /// private string partialLine = string.Empty; + /// + /// To keep track of whether CTRL-C is causing a command abort + /// + private bool abortCommand = false; + + /// + /// To keep track of whether we are in input mode + /// + private bool awaitingUserInput = false; + /// /// Gets or sets a value indicating whether the host application /// should exit. @@ -342,6 +352,13 @@ private void ExecuteHelper(string cmd, object input) // Ignore empty command lines. if (string.IsNullOrEmpty(cmd)) { + if (this.abortCommand) + { + incompleteLine = false; + partialLine = string.Empty; + this.abortCommand = false; + } + return; } @@ -360,14 +377,8 @@ private void ExecuteHelper(string cmd, object input) { this.currentPowerShell.Runspace = this.myRunSpace; - if (incompleteLine) - { - this.currentPowerShell.AddScript(partialLine + cmd); - } - else - { - this.currentPowerShell.AddScript(cmd); - } + string fullCommand = incompleteLine ? (partialLine + cmd) : cmd; + this.currentPowerShell.AddScript(fullCommand); incompleteLine = false; // Add the default outputter to the end of the pipe and then call the @@ -394,11 +405,7 @@ private void ExecuteHelper(string cmd, object input) catch (IncompleteParseException) { incompleteLine = true; - cmd = cmd.Trim(); - partialLine += cmd; - - partialLine += System.Environment.NewLine; - + partialLine = $"{partialLine}{cmd}{System.Environment.NewLine}"; } finally @@ -514,6 +521,15 @@ private void Execute(string cmd) /// ConsoleCancelEventHandler. private void HandleControlC(object sender, ConsoleCancelEventArgs e) { + e.Cancel = true; + + if (this.awaitingUserInput) + { + this.abortCommand = true; + this.consoleReadLine.Abort(); + return; + } + try { lock (this.instanceLock) @@ -524,7 +540,6 @@ private void HandleControlC(object sender, ConsoleCancelEventArgs e) } } - e.Cancel = true; } catch (Exception exception) { @@ -559,7 +574,12 @@ internal void Run() } this.myHost.UI.Write(ConsoleColor.White, Console.BackgroundColor, prompt); + + // Since Console.TreatControlCAsInput is not implemented, we use awaitingUserInput to + // tell us how control-c should be handled + this.awaitingUserInput = true; string cmd = consoleReadLine.Read(this.myHost.Runspace, false); + this.awaitingUserInput = false; this.Execute(cmd); } } @@ -579,9 +599,23 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) // loop to process Debugger commands. while (resumeAction == null) { - Console.Write("[DBG] PS >> "); + string prompt = incompleteLine ? ">> " : "[DBG] PS >> "; + Console.Write(prompt); + + this.awaitingUserInput = true; string command = consoleReadLine.Read(this.myHost.Runspace, true); - Console.WriteLine(); + this.awaitingUserInput = false; + + if (string.IsNullOrEmpty(command)) + { + if (this.abortCommand) + { + incompleteLine = false; + partialLine = string.Empty; + this.abortCommand = false; + } + continue; + } // Stream output from command processing to console. var output = new PSDataCollection(); @@ -600,10 +634,30 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) // command or script. The returned DebuggerCommandResults object will indicate // whether the command was evaluated by the debugger and if the debugger should // be released with a specific resume action. + PSCommand psCommand = new PSCommand(); - psCommand.AddScript(command).AddCommand("Out-String").AddParameter("Stream", true); - DebuggerCommandResults results = debugger.ProcessCommand(psCommand, output); - if (results.ResumeAction != null) + + string fullCommand = incompleteLine ? (partialLine + command) : command; + psCommand.AddScript(fullCommand).AddCommand("Out-String").AddParameter("Stream", true); + incompleteLine = false; + + DebuggerCommandResults results = null; + try + { + results = debugger.ProcessCommand(psCommand, output); + } + catch (IncompleteParseException) + { + incompleteLine = true; + partialLine = $"{partialLine}{command}{System.Environment.NewLine}"; + } + + if (!incompleteLine) + { + partialLine = string.Empty; + } + + if (!incompleteLine && results.ResumeAction != null) { resumeAction = results.ResumeAction; } diff --git a/src/Microsoft.PowerShell.Linux.Host/readline.cs b/src/Microsoft.PowerShell.Linux.Host/readline.cs index 7a2df6a234d..6b2198cb723 100644 --- a/src/Microsoft.PowerShell.Linux.Host/readline.cs +++ b/src/Microsoft.PowerShell.Linux.Host/readline.cs @@ -6,6 +6,7 @@ namespace Microsoft.PowerShell.Linux.Host using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; + using System.Threading; /// /// This class is used to read the command line and color the text as @@ -90,6 +91,17 @@ internal class ConsoleReadLine /// private string preTabBuffer; + /// + /// To keep track whether we should abort the Console.ReadKey loop + /// + private bool abortReadKey = false; + + /// + /// Amount of time to sleep before checking for user keystroke, + /// in milliseconds + /// + private const int sleepTime = 80; + /// /// Initializes a new instance of the ConsoleReadLine class. /// @@ -128,135 +140,176 @@ public string Read(Runspace runspace, bool nested) { this.powershell.Runspace = runspace; this.Initialize(); + bool finishedTyping = false; - while (true) + while (!abortReadKey) { - ConsoleKeyInfo key = Console.ReadKey(true); - - // Basic Emacs-style readline implementation - if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) + if (Console.KeyAvailable) { - switch (key.Key) + ConsoleKeyInfo key = Console.ReadKey(true); + + // Basic Emacs-style readline implementation + if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) { - case ConsoleKey.A: - this.OnHome(); - break; - case ConsoleKey.E: - this.OnEnd(); - break; - // TODO: save buffer, yank with Ctrl-Y - /* - case ConsoleKey.K: - this.OnEscape(); - break; - */ - case ConsoleKey.D: - this.OnDelete(); - break; - case ConsoleKey.B: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.F: - this.OnRight(key.Modifiers); - break; - case ConsoleKey.P: - // TODO: incremental search - case ConsoleKey.R: - this.OnUpArrow(nested); - break; - case ConsoleKey.N: - // TODO: incremental search - case ConsoleKey.S: - this.OnDownArrow(nested); - break; - case ConsoleKey.J: - this.OnEnter(); - break; - // TODO: save and restore buffer - /* - case ConsoleKey.L: - this.BufferFromString("clear"); - previousKeyPress = key; - return this.OnEnter(); - */ + finishedTyping = ProcessControlKey(key, nested); } - } - else if (key.Modifiers.HasFlag(ConsoleModifiers.Alt)) - { - switch (key.Key) + else if (key.Modifiers.HasFlag(ConsoleModifiers.Alt)) { - // TODO: OnDelete(key) - // TODO: OnBackspace(key) - case ConsoleKey.B: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.F: - this.OnRight(key.Modifiers); - break; + finishedTyping = ProcessAltKey(key); + } + // Unmodified keys + else + { + finishedTyping = ProcessNormalKey(key, nested); } + previousKeyPress = key; } - // Unmodified keys else { - switch (key.Key) - { - case ConsoleKey.Backspace: - this.OnBackspace(); - break; - case ConsoleKey.Delete: - this.OnDelete(); - break; - case ConsoleKey.Enter: - previousKeyPress = key; - return this.OnEnter(); - case ConsoleKey.RightArrow: - this.OnRight(key.Modifiers); - break; - case ConsoleKey.LeftArrow: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.Escape: - this.OnEscape(); - break; - case ConsoleKey.Home: - this.OnHome(); - break; - case ConsoleKey.End: - this.OnEnd(); - break; - case ConsoleKey.Tab: - this.OnTab(); - break; - case ConsoleKey.UpArrow: - this.OnUpArrow(nested); - break; - case ConsoleKey.DownArrow: - this.OnDownArrow(nested); - break; - - // TODO: case ConsoleKey.LeftWindows: not available in linux - // TODO: case ConsoleKey.RightWindows: not available in linux + Thread.Sleep(sleepTime); + } - default: + if (finishedTyping) + { + return this.OnEnter(); + } + } + return string.Empty; + } - if (key.KeyChar == '\x0D') - { - goto case ConsoleKey.Enter; // Ctrl-M - } + /// + /// Process Control-Key combo + /// + private bool ProcessControlKey(ConsoleKeyInfo key, bool nested) + { + switch (key.Key) + { + case ConsoleKey.A: + this.OnHome(); + break; + case ConsoleKey.E: + this.OnEnd(); + break; + // TODO: save buffer, yank with Ctrl-Y + /* + case ConsoleKey.K: + this.OnEscape(); + break; + */ + case ConsoleKey.D: + this.OnDelete(); + break; + case ConsoleKey.B: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.F: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.P: + // TODO: incremental search + case ConsoleKey.R: + this.OnUpArrow(nested); + break; + case ConsoleKey.N: + // TODO: incremental search + case ConsoleKey.S: + this.OnDownArrow(nested); + break; + case ConsoleKey.J: + this.OnEnter(); + break; + // TODO: save and restore buffer + /* + case ConsoleKey.L: + this.BufferFromString("clear"); + previousKeyPress = key; + return this.OnEnter(); + */ + } + return false; + } - if (key.KeyChar == '\x08') - { - goto case ConsoleKey.Backspace; // Ctrl-H - } + /// + /// Process Alt-Key combo + /// + private bool ProcessAltKey(ConsoleKeyInfo key) + { + switch (key.Key) + { + // TODO: OnDelete(key) + // TODO: OnBackspace(key) + case ConsoleKey.B: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.F: + this.OnRight(key.Modifiers); + break; + } + return false; + } - this.Insert(key.KeyChar); + /// + /// Process normal key + /// + private bool ProcessNormalKey(ConsoleKeyInfo key, bool nested) + { + switch (key.Key) + { + case ConsoleKey.Backspace: + this.OnBackspace(); + break; + case ConsoleKey.Delete: + this.OnDelete(); + break; + case ConsoleKey.Enter: + previousKeyPress = key; + return true; + case ConsoleKey.RightArrow: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.LeftArrow: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.Escape: + this.OnEscape(); + break; + case ConsoleKey.Home: + this.OnHome(); + break; + case ConsoleKey.End: + this.OnEnd(); + break; + case ConsoleKey.Tab: + this.OnTab(); + break; + case ConsoleKey.UpArrow: + this.OnUpArrow(nested); + break; + case ConsoleKey.DownArrow: + this.OnDownArrow(nested); + break; + + // TODO: case ConsoleKey.LeftWindows: not available in linux + // TODO: case ConsoleKey.RightWindows: not available in linux + + default: + + if (key.KeyChar == '\x0D') + { + goto case ConsoleKey.Enter; // Ctrl-M + } - this.Render(); - break; + if (key.KeyChar == '\x08') + { + goto case ConsoleKey.Backspace; // Ctrl-H } - } - previousKeyPress = key; + + this.Insert(key.KeyChar); + + this.Render(); + break; } + return false; } /// @@ -270,6 +323,7 @@ private void Initialize() this.current = 0; this.rendered = 0; this.cursor = new Cursor(); + this.abortReadKey = false; } /// @@ -733,6 +787,18 @@ private void Render() this.cursor.Place(this.current); } + /// + /// Abort current command + /// + public void Abort() + { + ConsoleColor saveFGColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.Out.WriteLine("^C"); + Console.ForegroundColor = saveFGColor; + this.abortReadKey = true; + } + /// /// A helper class for maintaining the cursor while editing the command line. ///