diff --git a/.gitignore b/.gitignore index ebf89a98e..2668cf966 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ ChocolateyPackage/PSReadLine/ # gvim swap files *.swp *.swo + +# Visual Studio cache/options directory +.vs/ diff --git a/PSReadLine/AssemblyInfo.cs b/PSReadLine/AssemblyInfo.cs index fbe383591..f7663f2a2 100644 --- a/PSReadLine/AssemblyInfo.cs +++ b/PSReadLine/AssemblyInfo.cs @@ -14,7 +14,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PSReadLine")] -[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyCopyright("Copyright")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -36,8 +36,8 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.2")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0")] [assembly: SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index 9e8a87341..1a4a57368 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -81,27 +82,10 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.ClearStatusMessage(false); _singleton._current = _singleton._buffer.Length; - // We want to display ^C to show the line was canceled. Instead of appending ^C - // (or (char)3), we append 2 spaces so we don't affect tokenization too much, e.g. - // changing a keyword to a command. - _singleton._buffer.Append(" "); - _singleton.ReallyRender(); - - // Now that we've rendered with this extra spaces, go back and replace the spaces - // with ^C colored in red (so it stands out.) - var coordinates = _singleton.ConvertOffsetToCoordinates(_singleton._current); - var console = _singleton._console; - var consoleBuffer = _singleton._consoleBuffer; - int i = (coordinates.Y - _singleton._initialY) * console.BufferWidth + coordinates.X; - consoleBuffer[i].UnicodeChar = '^'; - consoleBuffer[i].ForegroundColor = ConsoleColor.Red; - consoleBuffer[i].BackgroundColor = console.BackgroundColor; - consoleBuffer[i+1].UnicodeChar = 'C'; - consoleBuffer[i+1].ForegroundColor = ConsoleColor.Red; - consoleBuffer[i+1].BackgroundColor = console.BackgroundColor; - console.WriteBufferLines(consoleBuffer, ref _singleton._initialY); - - _singleton.PlaceCursor(0, coordinates.Y + 1); + _singleton.ForceRender(); + + _singleton._console.Write("\x1b[91m^C\x1b[0m"); + _singleton._buffer.Clear(); // Clear so we don't actually run the input _singleton._current = 0; // If Render is called, _current must be correct. _singleton._currentHistoryIndex = _singleton._history.Count; @@ -248,7 +232,7 @@ private bool AcceptLineImpl(bool validate) if (renderNeeded) { - ReallyRender(); + ForceRender(); } // Only run validation if we haven't before. If we have and status line shows an error, @@ -283,8 +267,8 @@ private bool AcceptLineImpl(bool validate) ClearStatusMessage(render: true); } - var coordinates = ConvertOffsetToCoordinates(_current); - PlaceCursor(0, coordinates.Y + 1); + var point = ConvertOffsetToPoint(_current); + PlaceCursor(0, point.Y + 1); _inputAccepted = true; return true; } @@ -406,8 +390,8 @@ private bool UnresolvedCommandCouldSucceed(string commandName, Ast rootAst) { // The following are debugger commands that should be accepted if we're debugging // because the console host will interpret these commands directly. - case 's': case 'v': case 'o': case 'c': case 'q': case'k': case 'l': - case 'S': case 'V': case 'O': case 'C': case 'Q': case'K': case 'L': + case 's': case 'v': case 'o': case 'c': case 'q': case 'k': case 'l': + case 'S': case 'V': case 'O': case 'C': case 'Q': case 'K': case 'L': case '?': case 'h': case 'H': // Ideally we would check $PSDebugContext, but it is set at function // scope, and because we're in a module, we can't find that variable @@ -482,7 +466,7 @@ public static void AddLine(ConsoleKeyInfo? key = null, object arg = null) /// public static void InsertLineAbove(ConsoleKeyInfo? key = null, object arg = null) { - // Move the current postion to the beginning of the current line and only the current line. + // Move the current position to the beginning of the current line and only the current line. if (_singleton.LineIsMultiLine()) { int i = Math.Max(0, _singleton._current - 1); @@ -512,7 +496,7 @@ public static void InsertLineAbove(ConsoleKeyInfo? key = null, object arg = null /// public static void InsertLineBelow(ConsoleKeyInfo? key = null, object arg = null) { - // Move the current postion to the end of the current line and only the current line. + // Move the current position to the end of the current line and only the current line. if (_singleton.LineIsMultiLine()) { int i = _singleton._current; diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 4d1a15d8b..162d8f890 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,7 +1,12 @@ -### Version 1.? +### Version 2.0 -Bug fixes: +New features: +* Interactive filtering during menu complete. +* Tooltips always on in MenuComplete (only displayed + if they give more information than the completion.) +* Support for VT escape sequences to specify colors. +Bug fixes: * DeleteChar no longer deletes any text left of the cursor in Windows/Emacs and matches vim behavior of working like in command mode. * Fix InvokePrompt when the prompt is > 1 line. @@ -9,6 +14,26 @@ Bug fixes: no support for redirected stdin/stdout though. * Fix YankToPercent off by 1 error. * Fix error reported when running in container. +* Remove Ding from Backspace on an empty line +* Fix occasional hang reading history file +* Honor HistorySearchCursorMovesToEnd in Vi mode +* Fix cursor placement with CJK characters +* Fix key bindings with certain Windows keyboard layouts + +Breaking changes: +* Requires PowerShell V5 or later and .Net 4.6.1 +* Building requires VS2017 +* Set-PSReadlineOption color parameters have changed + - Accept `object` instead of `ConsoleColor` to support accepting both + strings (VT escape sequences) and ConsoleColor + - Parameter names changed - no explicit Foreground/Background options anymore, + but aliases introduced so `-ForegroundColor` is the same as `-Color` and + `-ErrorForegroundColor` is the same as `-ErrorColor`. + To specify background colors, you must now use a VT escape sequence. +* Changing the end of the prompt to red may not work automatically anymore if + your prompt is non-trivial. To fix, use `Set-PSReadlineOption -PromptText "> "`, + replacing the string with the text that should turn red - be sure to include + any whitespace. ### Version 1.2 @@ -166,7 +191,7 @@ New features: * SamplePSReadlineProfile.ps1 added with examples of custom key bindings * Word movement takes DigitArgument * HistoryNoDuplicates now works a little differently - - Dupicates are saved (it was a dubious memory optimization anyway) + - Duplicates are saved (it was a dubious memory optimization anyway) - Recall will recall the most recently executed item instead of the first * When at the last word, NextWord/ForwardWord now move to the end of line instead of the last character of the word. @@ -239,7 +264,7 @@ Bugs fixed: * Ctrl+R with long search lines no longer causes big problems Behavior change: -* Word functions now use delimiters. The previous behavior is availble +* Word functions now use delimiters. The previous behavior is available via a Shell*Word function, e.g. instead of KillWord, use ShellKillWord. ### Version 1.0.0.4 diff --git a/PSReadLine/Clipboard.cs b/PSReadLine/Clipboard.cs new file mode 100644 index 000000000..849817af1 --- /dev/null +++ b/PSReadLine/Clipboard.cs @@ -0,0 +1,266 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Microsoft.PowerShell.Internal +{ + static class Clipboard + { + public static string GetText() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + PSConsoleReadLine.Ding(); + return ""; + } + + string clipboardText = ""; + ExecuteOnStaThread(() => GetTextImpl(out clipboardText)); + + return clipboardText; + } + + public static void SetText(string text) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + PSConsoleReadLine.Ding(); + return; + } + + ExecuteOnStaThread(() => SetClipboardData(Tuple.Create(text, CF_UNICODETEXT))); + } + + public static void SetRtf(string plainText, string rtfText) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + PSConsoleReadLine.Ding(); + return; + } + + if (CF_RTF == 0) + { + CF_RTF = RegisterClipboardFormat("Rich Text Format"); + } + + ExecuteOnStaThread(() => SetClipboardData( + Tuple.Create(plainText, CF_UNICODETEXT), + Tuple.Create(rtfText, CF_RTF))); + } + + private const uint GMEM_MOVEABLE = 0x0002; + private const uint GMEM_ZEROINIT = 0x0040; + const uint GHND = GMEM_MOVEABLE | GMEM_ZEROINIT; + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes); + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalFree(IntPtr hMem); + + [DllImport("kernel32.dll")] + private static extern IntPtr GlobalLock(IntPtr hMem); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GlobalUnlock(IntPtr hMem); + + [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] + private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + [DllImport("user32.dll", SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsClipboardFormatAvailable(uint uFormat); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool OpenClipboard(IntPtr hWndNewOwner); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseClipboard(); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EmptyClipboard(); + + [DllImport("user32.dll", SetLastError = true)] + static extern IntPtr GetClipboardData(uint uFormat); + + [DllImport("user32.dll")] + private static extern IntPtr SetClipboardData(uint uFormat, IntPtr data); + + [DllImport("user32.dll", SetLastError=true)] + static extern uint RegisterClipboardFormat(string lpszFormat); + + private const uint CF_TEXT = 1; + private const uint CF_UNICODETEXT = 13; + private static uint CF_RTF; + + private static bool GetTextImpl(out string text) + { + try + { + if (IsClipboardFormatAvailable(CF_UNICODETEXT)) + { + if (OpenClipboard(IntPtr.Zero)) + { + var data = GetClipboardData(CF_UNICODETEXT); + if (data != IntPtr.Zero) + { + data = GlobalLock(data); + text = Marshal.PtrToStringUni(data); + GlobalUnlock(data); + return true; + } + } + } + else if (IsClipboardFormatAvailable(CF_TEXT)) + { + if (OpenClipboard(IntPtr.Zero)) + { + var data = GetClipboardData(CF_TEXT); + if (data != IntPtr.Zero) + { + data = GlobalLock(data); + text = Marshal.PtrToStringAnsi(data); + GlobalUnlock(data); + return true; + } + } + } + } + catch + { + } + finally + { + CloseClipboard(); + } + + text = ""; + return false; + } + + private static bool SetClipboardData(params Tuple[] data) + { + try + { + if (!OpenClipboard(IntPtr.Zero)) return false; + EmptyClipboard(); + + foreach (var d in data) + { + if (!SetSingleClipboardData(d.Item1, d.Item2)) + return false; + } + } + finally + { + CloseClipboard(); + } + + return true; + } + + private static bool SetSingleClipboardData(string text, uint format) + { + IntPtr hGlobal = IntPtr.Zero; + IntPtr data = IntPtr.Zero; + + try + { + + uint bytes; + if (format == CF_RTF || format == CF_TEXT) + { + bytes = (uint)(text.Length + 1); + data = Marshal.StringToHGlobalAnsi(text); + } + else if (format == CF_UNICODETEXT) + { + bytes = (uint) ((text.Length + 1) * 2); + data = Marshal.StringToHGlobalUni(text); + } + else + { + // Not yet supported format. + return false; + } + + if (data == IntPtr.Zero) return false; + + hGlobal = GlobalAlloc(GHND, (UIntPtr) bytes); + if (hGlobal == IntPtr.Zero) return false; + + IntPtr dataCopy = GlobalLock(hGlobal); + if (dataCopy == IntPtr.Zero) return false; + CopyMemory(dataCopy, data, bytes); + GlobalUnlock(hGlobal); + + if (SetClipboardData(format, hGlobal) != IntPtr.Zero) + { + // The clipboard owns this memory now, so don't free it. + hGlobal = IntPtr.Zero; + } + } + catch + { + } + finally + { + if (data != IntPtr.Zero) + { + Marshal.FreeHGlobal(data); + } + if (hGlobal != IntPtr.Zero) + { + GlobalFree(hGlobal); + } + } + + return true; + } + + private static void ExecuteOnStaThread(Func action) + { + const int retryCount = 5; + int tries = 0; + + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + while (tries++ < retryCount && !action()) + ; + return; + } + + Exception exception = null; + var thread = new Thread(() => + { + try + { + while (tries++ < retryCount && !action()) + ; + } + catch (Exception e) + { + exception = e; + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + + if (exception != null) + { + throw exception; + } + } + } +} diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index a750df82a..52883227b 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -6,10 +6,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Management.Automation; using System.Management.Automation.Language; using System.Reflection; using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -45,7 +48,6 @@ public enum BellStyle Audible } - #region vi public enum ViModeStyle { None, @@ -58,7 +60,6 @@ public enum ViMode Insert, Command } - #endregion vi public enum HistorySaveStyle { @@ -69,20 +70,22 @@ public enum HistorySaveStyle public class PSConsoleReadlineOptions { - public const ConsoleColor DefaultCommentForegroundColor = ConsoleColor.DarkGreen; - public const ConsoleColor DefaultKeywordForegroundColor = ConsoleColor.Green; - public const ConsoleColor DefaultStringForegroundColor = ConsoleColor.DarkCyan; - public const ConsoleColor DefaultOperatorForegroundColor = ConsoleColor.DarkGray; - public const ConsoleColor DefaultVariableForegroundColor = ConsoleColor.Green; - public const ConsoleColor DefaultCommandForegroundColor = ConsoleColor.Yellow; - public const ConsoleColor DefaultParameterForegroundColor = ConsoleColor.DarkGray; - public const ConsoleColor DefaultTypeForegroundColor = ConsoleColor.Gray; - public const ConsoleColor DefaultNumberForegroundColor = ConsoleColor.White; - public const ConsoleColor DefaultMemberForegroundColor = ConsoleColor.Gray; - public const ConsoleColor DefaultEmphasisForegroundColor = ConsoleColor.Cyan; - public const ConsoleColor DefaultErrorForegroundColor = ConsoleColor.Red; - - public const EditMode DefaultEditMode = EditMode.Windows; + public const ConsoleColor DefaultCommentColor = ConsoleColor.DarkGreen; + public const ConsoleColor DefaultKeywordColor = ConsoleColor.Green; + public const ConsoleColor DefaultStringColor = ConsoleColor.DarkCyan; + public const ConsoleColor DefaultOperatorColor = ConsoleColor.DarkGray; + public const ConsoleColor DefaultVariableColor = ConsoleColor.Green; + public const ConsoleColor DefaultCommandColor = ConsoleColor.Yellow; + public const ConsoleColor DefaultParameterColor = ConsoleColor.DarkGray; + public const ConsoleColor DefaultTypeColor = ConsoleColor.Gray; + public const ConsoleColor DefaultNumberColor = ConsoleColor.White; + public const ConsoleColor DefaultMemberColor = ConsoleColor.Gray; + public const ConsoleColor DefaultEmphasisColor = ConsoleColor.Cyan; + public const ConsoleColor DefaultErrorColor = ConsoleColor.Red; + + public static EditMode DefaultEditMode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? EditMode.Windows + : EditMode.Emacs; public const string DefaultContinuationPrompt = ">> "; @@ -105,9 +108,9 @@ public class PSConsoleReadlineOptions /// /// When displaying possible completions, either display - /// tooltips or dipslay just the completions. + /// tooltips or display just the completions. /// - public const bool DefaultShowToolTips = false; + public const bool DefaultShowToolTips = true; /// /// When ringing the bell, what frequency do we use? @@ -135,8 +138,7 @@ public PSConsoleReadlineOptions(string hostName) ResetColors(); EditMode = DefaultEditMode; ContinuationPrompt = DefaultContinuationPrompt; - ContinuationPromptBackgroundColor = Console.BackgroundColor; - ContinuationPromptForegroundColor = Console.ForegroundColor; + ContinuationPromptColor = Console.ForegroundColor; ExtraPromptLineCount = DefaultExtraPromptLineCount; AddToHistoryHandler = null; HistoryNoDuplicates = DefaultHistoryNoDuplicates; @@ -151,8 +153,44 @@ public PSConsoleReadlineOptions(string hostName) WordDelimiters = DefaultWordDelimiters; HistorySearchCaseSensitive = DefaultHistorySearchCaseSensitive; HistorySaveStyle = DefaultHistorySaveStyle; - HistorySavePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) - + @"\Microsoft\Windows\PowerShell\PSReadline\" + hostName + "_history.txt"; + + var historyFileName = hostName + "_history.txt"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + HistorySavePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Microsoft", + "Windows", + "PowerShell", + "PSReadLine", + historyFileName); + } + else + { + // PSReadline can't use Utils.CorePSPlatform (6.0+ only), so do the equivalent: + string historyPath = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + + if (!String.IsNullOrEmpty(historyPath)) + { + HistorySavePath = System.IO.Path.Combine( + historyPath, + "powershell", + "PSReadLine", + historyFileName); + } + else + { + // History is data, so it goes into .local/share/powershell folder + HistorySavePath = System.IO.Path.Combine( + Environment.GetEnvironmentVariable("HOME"), + ".local", + "share", + "powershell", + "PSReadLine", + historyFileName); + } + } + CommandValidationHandler = null; CommandsToValidateScriptBlockArguments = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -174,8 +212,14 @@ public PSConsoleReadlineOptions(string hostName) public EditMode EditMode { get; set; } public string ContinuationPrompt { get; set; } - public ConsoleColor ContinuationPromptForegroundColor { get; set; } - public ConsoleColor ContinuationPromptBackgroundColor { get; set; } + + public object ContinuationPromptColor + { + get => _continuationPromptColor; + set => _continuationPromptColor = VTColorUtils.AsEscapeSequence(value); + } + + internal string _continuationPromptColor; /// /// Prompts are typically 1 line, but sometimes they may span lines. This @@ -235,9 +279,7 @@ public PSConsoleReadlineOptions(string hostName) public bool HistorySearchCaseSensitive { get; set; } internal StringComparison HistoryStringComparison => HistorySearchCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - #region vi public ViModeStyle ViModeIndicator { get; set; } - #endregion vi /// /// The path to the saved history. @@ -245,101 +287,143 @@ public PSConsoleReadlineOptions(string hostName) public string HistorySavePath { get; set; } public HistorySaveStyle HistorySaveStyle { get; set; } - public ConsoleColor DefaultTokenForegroundColor { get; set; } - public ConsoleColor CommentForegroundColor { get; set; } - public ConsoleColor KeywordForegroundColor { get; set; } - public ConsoleColor StringForegroundColor { get; set; } - public ConsoleColor OperatorForegroundColor { get; set; } - public ConsoleColor VariableForegroundColor { get; set; } - public ConsoleColor CommandForegroundColor { get; set; } - public ConsoleColor ParameterForegroundColor { get; set; } - public ConsoleColor TypeForegroundColor { get; set; } - public ConsoleColor NumberForegroundColor { get; set; } - public ConsoleColor MemberForegroundColor { get; set; } - public ConsoleColor DefaultTokenBackgroundColor { get; set; } - public ConsoleColor CommentBackgroundColor { get; set; } - public ConsoleColor KeywordBackgroundColor { get; set; } - public ConsoleColor StringBackgroundColor { get; set; } - public ConsoleColor OperatorBackgroundColor { get; set; } - public ConsoleColor VariableBackgroundColor { get; set; } - public ConsoleColor CommandBackgroundColor { get; set; } - public ConsoleColor ParameterBackgroundColor { get; set; } - public ConsoleColor TypeBackgroundColor { get; set; } - public ConsoleColor NumberBackgroundColor { get; set; } - public ConsoleColor MemberBackgroundColor { get; set; } - public ConsoleColor EmphasisForegroundColor { get; set; } - public ConsoleColor EmphasisBackgroundColor { get; set; } - public ConsoleColor ErrorForegroundColor { get; set; } - public ConsoleColor ErrorBackgroundColor { get; set; } + /// + /// This is the text you want turned red on parse errors, but must + /// occur immediately before the cursor when readline starts. + /// If the prompt function is pure, this value can be inferred, e.g. + /// the default prompt will use "> " for this value. + /// + public string PromptText { get; set; } - internal void ResetColors() + public object DefaultTokenColor { - DefaultTokenForegroundColor = Console.ForegroundColor; - CommentForegroundColor = DefaultCommentForegroundColor; - KeywordForegroundColor = DefaultKeywordForegroundColor; - StringForegroundColor = DefaultStringForegroundColor; - OperatorForegroundColor = DefaultOperatorForegroundColor; - VariableForegroundColor = DefaultVariableForegroundColor; - CommandForegroundColor = DefaultCommandForegroundColor; - ParameterForegroundColor = DefaultParameterForegroundColor; - TypeForegroundColor = DefaultTypeForegroundColor; - NumberForegroundColor = DefaultNumberForegroundColor; - MemberForegroundColor = DefaultNumberForegroundColor; - EmphasisForegroundColor = DefaultEmphasisForegroundColor; - ErrorForegroundColor = DefaultErrorForegroundColor; - DefaultTokenBackgroundColor = Console.BackgroundColor; - CommentBackgroundColor = Console.BackgroundColor; - KeywordBackgroundColor = Console.BackgroundColor; - StringBackgroundColor = Console.BackgroundColor; - OperatorBackgroundColor = Console.BackgroundColor; - VariableBackgroundColor = Console.BackgroundColor; - CommandBackgroundColor = Console.BackgroundColor; - ParameterBackgroundColor = Console.BackgroundColor; - TypeBackgroundColor = Console.BackgroundColor; - NumberBackgroundColor = Console.BackgroundColor; - MemberBackgroundColor = Console.BackgroundColor; - EmphasisBackgroundColor = Console.BackgroundColor; - ErrorBackgroundColor = Console.BackgroundColor; - } - - internal void SetForegroundColor(TokenClassification tokenKind, ConsoleColor color) + get => _defaultTokenColor; + set => _defaultTokenColor = VTColorUtils.AsEscapeSequence(value); + } + + public object CommentColor { - switch (tokenKind) - { - case TokenClassification.None: DefaultTokenForegroundColor = color; break; - case TokenClassification.Comment: CommentForegroundColor = color; break; - case TokenClassification.Keyword: KeywordForegroundColor = color; break; - case TokenClassification.String: StringForegroundColor = color; break; - case TokenClassification.Operator: OperatorForegroundColor = color; break; - case TokenClassification.Variable: VariableForegroundColor = color; break; - case TokenClassification.Command: CommandForegroundColor = color; break; - case TokenClassification.Parameter: ParameterForegroundColor = color; break; - case TokenClassification.Type: TypeForegroundColor = color; break; - case TokenClassification.Number: NumberForegroundColor = color; break; - case TokenClassification.Member: MemberForegroundColor = color; break; - } + get => _commentColor; + set => _commentColor = VTColorUtils.AsEscapeSequence(value); + } + + public object KeywordColor + { + get => _keywordColor; + set => _keywordColor = VTColorUtils.AsEscapeSequence(value); + } + + public object StringColor + { + get => _stringColor; + set => _stringColor = VTColorUtils.AsEscapeSequence(value); + } + + public object OperatorColor + { + get => _operatorColor; + set => _operatorColor = VTColorUtils.AsEscapeSequence(value); + } + + public object VariableColor + { + get => _variableColor; + set => _variableColor = VTColorUtils.AsEscapeSequence(value); + } + + public object CommandColor + { + get => _commandColor; + set => _commandColor = VTColorUtils.AsEscapeSequence(value); + } + + public object ParameterColor + { + get => _parameterColor; + set => _parameterColor = VTColorUtils.AsEscapeSequence(value); + } + + public object TypeColor + { + get => _typeColor; + set => _typeColor = VTColorUtils.AsEscapeSequence(value); + } + + public object NumberColor + { + get => _numberColor; + set => _numberColor = VTColorUtils.AsEscapeSequence(value); } - internal void SetBackgroundColor(TokenClassification tokenKind, ConsoleColor color) + public object MemberColor + { + get => _memberColor; + set => _memberColor = VTColorUtils.AsEscapeSequence(value); + } + + public object EmphasisColor + { + get => _emphasisColor; + set => _emphasisColor = VTColorUtils.AsEscapeSequence(value); + } + + public object ErrorColor + { + get => _errorColor; + set => _errorColor = VTColorUtils.AsEscapeSequence(value); + } + + internal string _defaultTokenColor; + internal string _commentColor; + internal string _keywordColor; + internal string _stringColor; + internal string _operatorColor; + internal string _variableColor; + internal string _commandColor; + internal string _parameterColor; + internal string _typeColor; + internal string _numberColor; + internal string _memberColor; + internal string _emphasisColor; + internal string _errorColor; + + internal void ResetColors() + { + DefaultTokenColor = Console.ForegroundColor; + CommentColor = DefaultCommentColor; + KeywordColor = DefaultKeywordColor; + StringColor = DefaultStringColor; + OperatorColor = DefaultOperatorColor; + VariableColor = DefaultVariableColor; + CommandColor = DefaultCommandColor; + ParameterColor = DefaultParameterColor; + TypeColor = DefaultTypeColor; + NumberColor = DefaultNumberColor; + MemberColor = DefaultNumberColor; + EmphasisColor = DefaultEmphasisColor; + ErrorColor = DefaultErrorColor; + } + + internal void SetColor(TokenClassification tokenKind, object color) { switch (tokenKind) { - case TokenClassification.None: DefaultTokenBackgroundColor = color; break; - case TokenClassification.Comment: CommentBackgroundColor = color; break; - case TokenClassification.Keyword: KeywordBackgroundColor = color; break; - case TokenClassification.String: StringBackgroundColor = color; break; - case TokenClassification.Operator: OperatorBackgroundColor = color; break; - case TokenClassification.Variable: VariableBackgroundColor = color; break; - case TokenClassification.Command: CommandBackgroundColor = color; break; - case TokenClassification.Parameter: ParameterBackgroundColor = color; break; - case TokenClassification.Type: TypeBackgroundColor = color; break; - case TokenClassification.Number: NumberBackgroundColor = color; break; - case TokenClassification.Member: MemberBackgroundColor = color; break; + case TokenClassification.None: DefaultTokenColor = color; break; + case TokenClassification.Comment: CommentColor = color; break; + case TokenClassification.Keyword: KeywordColor = color; break; + case TokenClassification.String: StringColor = color; break; + case TokenClassification.Operator: OperatorColor = color; break; + case TokenClassification.Variable: VariableColor = color; break; + case TokenClassification.Command: CommandColor = color; break; + case TokenClassification.Parameter: ParameterColor = color; break; + case TokenClassification.Type: TypeColor = color; break; + case TokenClassification.Number: NumberColor = color; break; + case TokenClassification.Member: MemberColor = color; break; } } } - [Cmdlet("Get", "PSReadlineOption", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=528808")] + [Cmdlet("Get", "PSReadlineOption", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528808")] [OutputType(typeof(PSConsoleReadlineOptions))] public class GetPSReadlineOption : PSCmdlet { @@ -350,7 +434,7 @@ protected override void EndProcessing() } } - [Cmdlet("Set", "PSReadlineOption", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=528811")] + [Cmdlet("Set", "PSReadlineOption", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528811")] public class SetPSReadlineOption : PSCmdlet { [Parameter(ParameterSetName = "OptionsSet")] @@ -366,52 +450,19 @@ public EditMode EditMode public string ContinuationPrompt { get; set; } [Parameter(ParameterSetName = "OptionsSet")] - public ConsoleColor ContinuationPromptForegroundColor - { - get => _continuationPromptForegroundColor.GetValueOrDefault(); - set => _continuationPromptForegroundColor = value; - } - internal ConsoleColor? _continuationPromptForegroundColor; - - [Parameter(ParameterSetName = "OptionsSet")] - public ConsoleColor ContinuationPromptBackgroundColor - { - get => _continuationPromptBackgroundColor.GetValueOrDefault(); - set => _continuationPromptBackgroundColor = value; - } - internal ConsoleColor? _continuationPromptBackgroundColor; - - [Parameter(ParameterSetName = "OptionsSet")] - public ConsoleColor EmphasisForegroundColor - { - get => _emphasisForegroundColor.GetValueOrDefault(); - set => _emphasisForegroundColor = value; - } - internal ConsoleColor? _emphasisForegroundColor; - - [Parameter(ParameterSetName = "OptionsSet")] - public ConsoleColor EmphasisBackgroundColor - { - get => _emphasisBackgroundColor.GetValueOrDefault(); - set => _emphasisBackgroundColor = value; - } - internal ConsoleColor? _emphasisBackgroundColor; + [Alias("ContinuationPromptForegroundColor")] + [ValidateColor] + public object ContinuationPromptColor { get; set; } [Parameter(ParameterSetName = "OptionsSet")] - public ConsoleColor ErrorForegroundColor - { - get => _errorForegroundColor.GetValueOrDefault(); - set => _errorForegroundColor = value; - } - internal ConsoleColor? _errorForegroundColor; + [Alias("EmphasisForegroundColor")] + [ValidateColor] + public object EmphasisColor { get; set; } [Parameter(ParameterSetName = "OptionsSet")] - public ConsoleColor ErrorBackgroundColor - { - get => _errorBackgroundColor.GetValueOrDefault(); - set => _errorBackgroundColor = value; - } - internal ConsoleColor? _errorBackgroundColor; + [Alias("ErrorForegroundColor")] + [ValidateColor] + public object ErrorColor { get; set; } [Parameter(ParameterSetName = "OptionsSet")] public SwitchParameter HistoryNoDuplicates @@ -553,7 +604,10 @@ public HistorySaveStyle HistorySaveStyle [ValidateNotNullOrEmpty] public string HistorySavePath { get; set; } - #region vi + [Parameter(ParameterSetName = "OptionsSet")] + [ValidateNotNull] + public string PromptText { get; set; } + [Parameter(ParameterSetName = "OptionsSet")] public ViModeStyle ViModeIndicator { @@ -561,7 +615,6 @@ public ViModeStyle ViModeIndicator set => _viModeIndicator = value; } internal ViModeStyle? _viModeIndicator; - #endregion vi [Parameter(ParameterSetName = "ColorSet", Position = 0, Mandatory = true)] public TokenClassification TokenKind @@ -572,20 +625,9 @@ public TokenClassification TokenKind internal TokenClassification? _tokenKind; [Parameter(ParameterSetName = "ColorSet", Position = 1)] - public ConsoleColor ForegroundColor - { - get => _foregroundColor.GetValueOrDefault(); - set => _foregroundColor = value; - } - internal ConsoleColor? _foregroundColor; - - [Parameter(ParameterSetName = "ColorSet", Position = 2)] - public ConsoleColor BackgroundColor - { - get => _backgroundColor.GetValueOrDefault(); - set => _backgroundColor = value; - } - internal ConsoleColor? _backgroundColor; + [Alias("ForegroundColor")] + [ValidateColor] + public object Color { get; set; } [ExcludeFromCodeCoverage] protected override void EndProcessing() @@ -594,6 +636,16 @@ protected override void EndProcessing() } } + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + internal class ValidateColorAttribute : ValidateArgumentsAttribute + { + protected override void Validate(object arguments, EngineIntrinsics engineIntrinsics) + { + if (!VTColorUtils.IsValidColor(arguments)) + throw new ValidationMetadataException(PSReadLineResources.InvalidColorParameter); + } + } + public class ChangePSReadlineKeyHandlerCommandBase : PSCmdlet { [Parameter(Position = 0, Mandatory = true)] @@ -631,7 +683,7 @@ protected IDisposable UseRequestedDispatchTables() } } - [Cmdlet("Set", "PSReadlineKeyHandler", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=528810")] + [Cmdlet("Set", "PSReadlineKeyHandler", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528810")] public class SetPSReadlineKeyHandlerCommand : ChangePSReadlineKeyHandlerCommandBase, IDynamicParameters { [Parameter(Position = 1, Mandatory = true, ParameterSetName = "ScriptBlock")] @@ -656,11 +708,14 @@ protected override void EndProcessing() if (ParameterSetName.Equals(FunctionParameterSet)) { var function = (string)_dynamicParameters.Value[FunctionParameter].Value; + var mi = typeof (PSConsoleReadLine).GetMethod(function, + BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase); var keyHandler = (Action) - Delegate.CreateDelegate(typeof (Action), - typeof (PSConsoleReadLine).GetMethod(function)); - BriefDescription = function; - PSConsoleReadLine.SetKeyHandler(Chord, keyHandler, BriefDescription, Description); + mi.CreateDelegate(typeof (Action)); + var functionName = mi.Name; + var longDescription = PSReadLineResources.ResourceManager.GetString(functionName + "Description"); + + PSConsoleReadLine.SetKeyHandler(Chord, keyHandler, functionName, longDescription); } else { @@ -706,7 +761,7 @@ public object GetDynamicParameters() } } - [Cmdlet("Get", "PSReadlineKeyHandler", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=528807")] + [Cmdlet("Get", "PSReadlineKeyHandler", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528807")] [OutputType(typeof(KeyHandler))] public class GetKeyHandlerCommand : PSCmdlet { @@ -750,7 +805,7 @@ protected override void EndProcessing() } } - [Cmdlet("Remove", "PSReadlineKeyHandler", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=528809")] + [Cmdlet("Remove", "PSReadlineKeyHandler", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=528809")] public class RemoveKeyHandlerCommand : ChangePSReadlineKeyHandlerCommandBase { [ExcludeFromCodeCoverage] @@ -763,6 +818,165 @@ protected override void EndProcessing() } } + public static class VTColorUtils + { + internal static bool IsValidColorImpl(ConsoleColor c) => true; + + internal static bool IsValidColorImpl(string s) => true; + + public static bool IsValidColor(object o) + { + switch (o) + { + case ConsoleColor c: + return true; + + case string s: + if (s.Length > 0) + { + // String can be converted to ConsoleColor, so is it a ConsoleColor? + if (LanguagePrimitives.TryConvertTo(s, out ConsoleColor unused1)) + return true; + + // Escape sequence - assume it's fine as is + if (s[0] == '\x1b') + return true; + + // RGB format with possible '#' + if (s[0] == '#') + s = s.Substring(1); + + if (int.TryParse(s, NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo, out int rgb) && + rgb >= 0 && rgb <= 0x00ffffff) + return true; + } + break; + } + + return false; + } + + public static string AsEscapeSequence(object o) + { + return AsEscapeSequence(o, isBackground: false); + } + + public static string AsEscapeSequence(object o, bool isBackground) + { + switch (o) + { + case ConsoleColor c: + return MapColorToEscapeSequence(c, isBackground); + + case string s: + if (s.Length > 0) + { + // String can be converted to ConsoleColor, so it is a ConsoleColor + if (LanguagePrimitives.TryConvertTo(s, out ConsoleColor c)) + return MapColorToEscapeSequence(c, isBackground); + + // Escape sequence - assume it's fine as is + if (s[0] == '\x1b') + return s; + + // RGB format with possible '#' + if (s[0] == '#') + s = s.Substring(1); + + if (s.Length == 6 && + int.TryParse(s, NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo, out int rgb) && + rgb >= 0 && rgb <= 0x00ffffff) + { + if (rgb < 256) + { + return "\x1b[" + (isBackground ? "4" : "3") + "8;5;" + rgb + "m"; + } + + var r = (rgb >> 16) & 0xff; + var g = (rgb >> 8) & 0xff; + var b = rgb & 0xff; + + return "\x1b[" + (isBackground ? "4" : "3") + "8;2;" + r + ";" + g + ";" + b + "m"; + } + } + break; + } + + throw new ArgumentException("o"); + } + + private static readonly string[] BackgroundColorMap = { + "\x1b[40m", // Black + "\x1b[44m", // DarkBlue + "\x1b[42m", // DarkGreen + "\x1b[46m", // DarkCyan + "\x1b[41m", // DarkRed + "\x1b[45m", // DarkMagenta + "\x1b[43m", // DarkYellow + "\x1b[47m", // Gray + "\x1b[100m", // DarkGray + "\x1b[104m", // Blue + "\x1b[102m", // Green + "\x1b[106m", // Cyan + "\x1b[101m", // Red + "\x1b[105m", // Magenta + "\x1b[103m", // Yellow + "\x1b[107m", // White + }; + + private static readonly string[] ForegroundColorMap = { + "\x1b[30m", // Black + "\x1b[34m", // DarkBlue + "\x1b[32m", // DarkGreen + "\x1b[36m", // DarkCyan + "\x1b[31m", // DarkRed + "\x1b[35m", // DarkMagenta + "\x1b[33m", // DarkYellow + "\x1b[37m", // Gray + "\x1b[90m", // DarkGray + "\x1b[94m", // Blue + "\x1b[92m", // Green + "\x1b[96m", // Cyan + "\x1b[91m", // Red + "\x1b[95m", // Magenta + "\x1b[93m", // Yellow + "\x1b[97m", // White + }; + + internal static string MapColorToEscapeSequence(ConsoleColor color, bool isBackground) + { + int index = (int) color; + if (index < 0) + { + // TODO: light vs. dark + if (isBackground) + { + // Don't change the background - the default (unknown) background + // might be subtly or completely different than what we choose and + // look weird. + return ""; + } + + return ForegroundColorMap[(int) ConsoleColor.Gray]; + } + + if (index > ForegroundColorMap.Length) + { + return ""; + } + return (isBackground ? BackgroundColorMap : ForegroundColorMap)[index]; + } + + public static string FormatColor(object seq) + { + if (seq is ConsoleColor) return seq.ToString(); + + var replacement = (typeof(PSObject).Assembly.GetName().Version.Major < 6) + ? "$([char]0x1b)" + : "`e"; + return "\"" + seq.ToString().Replace("\x1b", replacement) + "\""; + } + } #pragma warning restore 1591 } diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index b2855d7dd..812334bea 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -12,6 +13,7 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -22,7 +24,8 @@ public partial class PSConsoleReadLine private CommandCompletion _tabCompletions; private Runspace _runspace; - private readonly Dictionary doneCompletionKeys = new Dictionary + private static readonly Dictionary KeysEndingCompletion = + new Dictionary { { CompletionResultType.Variable, new[] { Keys.Period } }, { CompletionResultType.Namespace, new[] { Keys.Period } }, @@ -34,11 +37,16 @@ public partial class PSConsoleReadLine { CompletionResultType.ParameterValue, new[] { Keys.Comma } }, }; + private static readonly char[] EolChars = {'\r', '\n'}; + + // String helper for directory paths + private static string DirectorySeparatorString = System.IO.Path.DirectorySeparatorChar.ToString(); + // Stub helper method so completion can be mocked [ExcludeFromCodeCoverage] CommandCompletion IPSConsoleReadLineMockableMethods.CompleteInput(string input, int cursorIndex, Hashtable options, System.Management.Automation.PowerShell powershell) { - return CalloutUsingDefaultConsoleMode( + return CallPossibleExternalApplication( () => CommandCompletion.CompleteInput(input, cursorIndex, options, powershell)); } @@ -60,15 +68,11 @@ public static void TabCompletePrevious(ConsoleKeyInfo? key = null, object arg = _singleton.Complete(forward: false); } - private static bool IsSingleQuote(char c) - { - return c == '\'' || c == (char)8216 || c == (char)8217 || c == (char)8218; - } + private static bool IsSingleQuote(char c) => c == '\'' || c == (char)8216 || c == (char)8217 || c == (char)8218; + private static bool IsDoubleQuote(char c) => c == '"' || c == (char)8220 || c == (char)8221; - private static bool IsDoubleQuote(char c) - { - return c == '"' || c == (char)8220 || c == (char)8221; - } + // variable can be "quoted" like ${env:CommonProgramFiles(x86)} + private static bool IsQuotedVariable(string s) => s.Length > 2 && s[1] == '{' && s[s.Length - 1] == '}'; private static bool IsQuoted(string s) { @@ -78,18 +82,8 @@ private static bool IsQuoted(string s) var first = (s.Length > 4 && s.StartsWith("& ")) ? s[2] : s[0]; var last = s[s.Length - 1]; - return ((IsSingleQuote(first) && IsSingleQuote(last)) - || - (IsDoubleQuote(first) && IsDoubleQuote(last))); - } - return false; - } - private static bool IsQuotedVariable(string s) - { - // variable can be "quoted" like ${env:CommonProgramFiles(x86)} - if (s.Length > 2) - { - return (s[1] == '{' && s[s.Length - 1] == '}'); + return (IsSingleQuote(first) && IsSingleQuote(last)) || + (IsDoubleQuote(first) && IsDoubleQuote(last)); } return false; } @@ -99,7 +93,7 @@ private static string GetUnquotedText(string s, bool consistentQuoting) if (!consistentQuoting && IsQuoted(s)) { //consider possible '& ' prefix - int startindex = (s.StartsWith("& ")) ? 3 : 1; + int startindex = s.StartsWith("& ") ? 3 : 1; s = s.Substring(startindex, s.Length - startindex - 1); } return s; @@ -114,14 +108,25 @@ private static string GetUnquotedText(CompletionResult match, bool consistentQuo { return '$' + s.Substring(2, s.Length - 3); } - else - { - return s; - } + return s; } - else + return GetUnquotedText(s, consistentQuoting); + } + + private void WriteBlankLines(int top, int count) + { + _console.SaveCursor(); + _console.SetCursorPosition(0, top); + WriteBlankLines(count); + _console.RestoreCursor(); + } + + private void WriteBlankLines(int count) + { + var spaces = Spaces(_console.BufferWidth); + for (int i = 0; i < count; i++) { - return GetUnquotedText(s, consistentQuoting); + _console.Write(spaces); } } @@ -133,7 +138,7 @@ private static string GetUnquotedText(CompletionResult match, bool consistentQuo /// public static void Complete(ConsoleKeyInfo? key = null, object arg = null) { - _singleton.CompleteImpl(key, arg, false); + _singleton.CompleteImpl(false); } /// @@ -144,10 +149,10 @@ public static void Complete(ConsoleKeyInfo? key = null, object arg = null) /// public static void MenuComplete(ConsoleKeyInfo? key = null, object arg = null) { - _singleton.CompleteImpl(key, arg, true); + _singleton.CompleteImpl(true); } - private bool IsConsistentQuoting(System.Collections.ObjectModel.Collection matches) + private bool IsConsistentQuoting(Collection matches) { int quotedCompletions = matches.Count(match => IsQuoted(match.CompletionText)); return @@ -157,7 +162,7 @@ private bool IsConsistentQuoting(System.Collections.ObjectModel.Collection m.CompletionText[0] == matches[0].CompletionText[0])); } - private string GetUnambiguousPrefix(System.Collections.ObjectModel.Collection matches, out bool ambiguous) + private string GetUnambiguousPrefix(Collection matches, out bool ambiguous) { // Find the longest unambiguous prefix. This might be the empty // string, in which case we don't want to remove any of the users input, @@ -212,7 +217,7 @@ private string GetUnambiguousPrefix(System.Collections.ObjectModel.Collection /// Display the list of possible completions. /// @@ -388,13 +382,14 @@ public static void PossibleCompletions(ConsoleKeyInfo? key = null, object arg = private static string HandleNewlinesForPossibleCompletions(string s) { s = s.Trim(); - var newlineIndex = s.IndexOfAny(new []{'\r', '\n'}); + var newlineIndex = s.IndexOfAny(EolChars); if (newlineIndex >= 0) { s = s.Substring(0, newlineIndex) + "..."; } return s; } + private static string ShortenLongCompletions(string s, int maxLength) { if (s.Length <= maxLength) return s; @@ -410,135 +405,257 @@ private static string ShortenLongCompletions(string s, int maxLength) return s.Substring(0, maxLength - splitPos - 3) + "..." + s.Substring(s.Length - splitPos, splitPos); } - private static CHAR_INFO[] CreateCompletionMenu(System.Collections.ObjectModel.Collection matches, IConsole console, bool showToolTips, string currentCompletionText, out int currentCompletionIndex, out int MenuColumnWidth, out int DisplayRows) + private class Menu { - var minColWidth = matches.Max(c => c.ListItemText.Length); - minColWidth += 2; - var bufferWidth = console.BufferWidth; - var displayColumns = Math.Max(1, bufferWidth / minColWidth); - currentCompletionIndex = 0; - - ConsoleBufferBuilder cb; - if (displayColumns == 1 || showToolTips) - { - const string seperator = "- "; - var maxTooltipWidth = bufferWidth - minColWidth - seperator.Length; - // switch off tooltips if it too short - if (maxTooltipWidth < 5) + internal PSConsoleReadLine Singleton; + internal int Top; + internal int ColumnWidth; + internal int BufferLines; + internal int Rows; + internal int Columns; + internal int ToolTipLines; + internal Collection MenuItems; + internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; + internal int CurrentSelection; + + void EnsureMenuAndInputIsVisible(IConsole console, int tooltipLineCount) + { + var bottom = this.Top + this.Rows + tooltipLineCount; + if (bottom > console.BufferHeight) { - minColWidth = bufferWidth; - showToolTips = false; + var toScroll = bottom - console.BufferHeight; + console.ScrollBuffer(toScroll); + Singleton._initialY -= toScroll; + this.Top -= toScroll; + + var point = Singleton.ConvertOffsetToPoint(Singleton._current); + Singleton.PlaceCursor(point.X, point.Y); } - DisplayRows = matches.Count; - cb = new ConsoleBufferBuilder(DisplayRows * bufferWidth, console); - for (int index = 0; index < matches.Count; index++) + } + + public void DrawMenu(Menu previousMenu) + { + IConsole console = Singleton._console; + + // Move cursor to the start of the first line after our input. + this.Top = Singleton.ConvertOffsetToPoint(Singleton._buffer.Length).Y + 1; + EnsureMenuAndInputIsVisible(console, tooltipLineCount: 0); + + console.CursorVisible = false; + console.SaveCursor(); + console.SetCursorPosition(0, this.Top); + + var bufferWidth = console.BufferWidth; + var columnWidth = this.ColumnWidth; + + var items = this.MenuItems; + for (var row = 0; row < this.Rows; row++) { - var match = matches[index]; - if (match.CompletionText.Equals(currentCompletionText)) currentCompletionIndex = index; - var listItemText = ShortenLongCompletions(HandleNewlinesForPossibleCompletions(match.ListItemText), minColWidth); - cb.Append(listItemText); - var spacesNeeded = minColWidth - listItemText.Length; - if (spacesNeeded > 0) + var cells = 0; + for (var col = 0; col < this.Columns; col++) { - cb.Append(' ', spacesNeeded); + var index = row + (this.Rows * col); + if (index >= items.Count) + { + break; + } + console.Write(GetMenuItem(items[index].ListItemText, columnWidth)); + cells += columnWidth; } - if (showToolTips) + + // Make sure we always write out exactly 1 buffer width to erase anything + // from a previous menu. + if (cells < bufferWidth) { - cb.Append(seperator); - var toolTip = HandleNewlinesForPossibleCompletions(match.ToolTip); - toolTip = toolTip.Length <= maxTooltipWidth - ? toolTip - : toolTip.Substring(0, maxTooltipWidth); - cb.Append(toolTip); + console.Write(Spaces(bufferWidth - cells)); } + } - // Make sure we always write out exactly 1 buffer width - spacesNeeded = (bufferWidth * (index + 1)) - cb.Length; - if (spacesNeeded > 0) + if (previousMenu != null) + { + if (Rows < previousMenu.Rows + previousMenu.ToolTipLines) { - cb.Append(' ', spacesNeeded); + Singleton.WriteBlankLines(previousMenu.Rows + previousMenu.ToolTipLines - Rows); } } - MenuColumnWidth = bufferWidth; + + console.RestoreCursor(); + console.CursorVisible = true; } - else + + public void Clear() { - DisplayRows = (matches.Count + displayColumns - 1) / displayColumns; - cb = new ConsoleBufferBuilder(DisplayRows * bufferWidth, console); - for (var row = 0; row < DisplayRows; row++) + Singleton.WriteBlankLines(Top, Rows + ToolTipLines); + } + + public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips, string toolTipColor) + { + var console = Singleton._console; + var menuItem = MenuItems[selectedItem]; + var listItem = menuItem.ListItemText; + + string toolTip = null; + if (showTooltips) { - for (var col = 0; col < displayColumns; col++) + toolTip = menuItem.ToolTip.Trim(); + + // Don't bother showing the tooltip if it doesn't add information. + showTooltips = !string.IsNullOrWhiteSpace(toolTip) + && !string.Equals(toolTip, listItem, StringComparison.OrdinalIgnoreCase) + && !string.Equals(toolTip, menuItem.CompletionText, StringComparison.OrdinalIgnoreCase); + } + + // We'll use one blank line to set the tooltip apart from the menu, + // and there will be at least 1 line in the tooltip, possibly more. + var toolTipLines = 2; + if (showTooltips) + { + // Determine if showing the tooltip would scroll the screen, if so, also don't show it. + + int lineLength = 0; + for (var i = 0; i < toolTip.Length; i++) { - var index = row + (DisplayRows * col); - if (index >= matches.Count) + char c = toolTip[i]; + if (c == '\r' && i < toolTip.Length && toolTip[i+1] == '\n') { - break; + // Skip the newline, but handle LF, CRLF, and CR. + i += 1; + } + + if (c == '\r' || c == '\n') + { + toolTipLines += 1; + lineLength = 0; + } + else + { + lineLength += 1; + if (lineLength == console.BufferWidth) + { + toolTipLines += 1; + lineLength = 0; + } } - var match = matches[index]; - if (match.CompletionText.Equals(currentCompletionText)) currentCompletionIndex = index; - var item = HandleNewlinesForPossibleCompletions(match.ListItemText); - cb.Append(item); - cb.Append(' ', minColWidth - item.Length); } - // Make sure we always write out exactly 1 buffer width - var spacesNeeded = (bufferWidth * (row + 1)) - cb.Length; - if (spacesNeeded > 0) + if (BufferLines + Rows + toolTipLines > console.WindowHeight) + { + showTooltips = false; + } + } + + if (showTooltips) + { + EnsureMenuAndInputIsVisible(console, toolTipLines); + } + + console.SaveCursor(); + + var row = Top + selectedItem % Rows; + var col = ColumnWidth * (selectedItem / Rows); + + console.SetCursorPosition(col, row); + + if (select) console.Write("\x001b[7m"); + console.Write(GetMenuItem(listItem, ColumnWidth)); + if (select) console.Write("\x001b[27m"); + + ToolTipLines = 0; + if (showTooltips) + { + console.SetCursorPosition(0, Top + Rows + 1); + console.Write(toolTipColor); + if (select) + { + console.Write(toolTip); + ToolTipLines = toolTipLines; + } + else { - cb.Append(' ', spacesNeeded); + Singleton.WriteBlankLines(toolTipLines); } + + console.Write("\x001b[0m"); } - MenuColumnWidth = minColWidth; + + console.RestoreCursor(); } - return cb.ToArray(); + public void MoveRight() => CurrentSelection = Math.Min(CurrentSelection + Rows, MenuItems.Count - 1); + public void MoveLeft() => CurrentSelection = Math.Max(CurrentSelection - Rows, 0); + public void MoveUp() => CurrentSelection = Math.Max(CurrentSelection - 1, 0); + public void MoveDown() => CurrentSelection = Math.Min(CurrentSelection + 1, MenuItems.Count - 1); + public void MovePageDown() => CurrentSelection = Math.Min(CurrentSelection + Rows - (CurrentSelection % Rows) - 1, + MenuItems.Count - 1); + public void MovePageUp() => CurrentSelection = Math.Max(CurrentSelection - (CurrentSelection % Rows), 0); + + public void MoveN(int n) + { + CurrentSelection = (CurrentSelection + n) % MenuItems.Count; + if (CurrentSelection < 0) + { + CurrentSelection += MenuItems.Count; + } + } } - private System.Collections.ObjectModel.Collection FilterCompletions(System.Collections.ObjectModel.Collection matches, string completionFilter) + + private Menu CreateCompletionMenu(Collection matches) { - var result = new System.Collections.ObjectModel.Collection(); + var bufferWidth = _console.BufferWidth; + var colWidth = Math.Min(matches.Max(c => c.ListItemText.Length) + 2, bufferWidth); + var columns = Math.Max(1, bufferWidth / colWidth); + + return new Menu + { + Singleton = this, + ColumnWidth = colWidth, + Columns = columns, + Rows = (matches.Count + columns - 1) / columns, + MenuItems = matches, + }; + } + + private Collection FilterCompletions(CommandCompletion completion, string completionFilter) + { + var newMatches = new Collection(); + var matches = completion.CompletionMatches; + bool consistentQuoting = IsConsistentQuoting(matches); // add possible first quote to userCompletionText - if (consistentQuoting && - (IsSingleQuote(matches[0].CompletionText[0]) - || IsDoubleQuote(matches[0].CompletionText[0]) - ) - ) + if (consistentQuoting) { - completionFilter = matches[0].CompletionText[0] + completionFilter; + var quote = matches[0].CompletionText[0]; + if (IsSingleQuote(quote) || IsDoubleQuote(quote)) + { + completionFilter = quote + completionFilter; + } } + foreach (CompletionResult item in matches) { if (item.ListItemText.StartsWith(completionFilter, StringComparison.OrdinalIgnoreCase) || - GetUnquotedText(item, consistentQuoting).StartsWith(completionFilter, StringComparison.OrdinalIgnoreCase) - ) + GetUnquotedText(item, consistentQuoting).StartsWith(completionFilter, StringComparison.OrdinalIgnoreCase)) { - result.Add(item); + newMatches.Add(item); } } - return result; + + return newMatches; } - private int FindUserCompletinTextPosition(CompletionResult match, string userCompletionText) + + private int FindUserCompletionTextPosition(CompletionResult match, string userCompletionText) { - if (match.ResultType == CompletionResultType.Variable && - match.CompletionText[1] == '{' - ) - { - return match.CompletionText.IndexOf(userCompletionText.Substring(1), StringComparison.OrdinalIgnoreCase) - 1; - } - else - { - return match.CompletionText.IndexOf(userCompletionText, StringComparison.OrdinalIgnoreCase); - } + return match.ResultType == CompletionResultType.Variable && match.CompletionText[1] == '{' + ? match.CompletionText.IndexOf(userCompletionText.Substring(1), StringComparison.OrdinalIgnoreCase) - 1 + : match.CompletionText.IndexOf(userCompletionText, StringComparison.OrdinalIgnoreCase); } private bool IsDoneWithCompletions(CompletionResult currentCompletion, ConsoleKeyInfo nextKey) { - if (nextKey == Keys.Space || nextKey == Keys.Enter) return true; - if (doneCompletionKeys.TryGetValue(currentCompletion.ResultType, out var doneKeys)) - { - return doneKeys.Contains(nextKey); - } - else return false; + return nextKey.EqualsNormalized(Keys.Space) + || nextKey.EqualsNormalized(Keys.Enter) + || KeysEndingCompletion.TryGetValue(currentCompletion.ResultType, out var doneKeys) + && doneKeys.Contains(nextKey, ConsoleKeyInfoComparer.Instance); } private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSelect) @@ -557,19 +674,16 @@ private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSel } } - var matches = completions.CompletionMatches; - - var menuBuffer = CreateCompletionMenu(matches, _console, Options.ShowToolTips, - "", out var selectedItem, out var menuColumnWidth, out var displayRows); + var menu = CreateCompletionMenu(completions.CompletionMatches); if (menuSelect) { // Make sure the menu and line can appear on the screen at the same time, // if not, we'll skip the menu. - var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length); - var bufferLines = endBufferCoords.Y - _initialY + 1; - if ((bufferLines + displayRows) > _console.WindowHeight) + var endBufferPoint = ConvertOffsetToPoint(_buffer.Length); + menu.BufferLines = endBufferPoint.Y - _initialY + 1; + if (menu.BufferLines + menu.Rows > _console.WindowHeight) { menuSelect = false; } @@ -577,301 +691,325 @@ private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSel if (menuSelect) { - RemoveEditsAfterUndo(); - var undoPoint = _edits.Count; + MenuCompleteImpl(menu, completions); + } + else + { + menu.DrawMenu(null); + InvokePrompt(key: null, arg: menu.Top + menu.Rows); + } + } - bool undo = false; + private static string GetMenuItem(string item, int columnWidth) + { + item = HandleNewlinesForPossibleCompletions(item); + var spacesNeeded = columnWidth - LengthInBufferCells(item); + if (spacesNeeded > 0) + { + item = item + Spaces(spacesNeeded); + } - int savedUserMark = _mark; - _visualSelectionCommandCount++; + return item; + } + + private void MenuCompleteImpl(Menu menu, CommandCompletion completions) + { + var menuStack = new Stack(10); + menuStack.Push(null); // Used to help detect excess backspaces + + RemoveEditsAfterUndo(); + var undoPoint = _edits.Count; - var userCompletionText = GetUnambiguousPrefix(matches, out var ambiguous); - // remove possible first quote - if (userCompletionText.Length > 0 && - ( IsSingleQuote(userCompletionText[0]) - || IsDoubleQuote(userCompletionText[0]) - ) - ) + bool undo = false; + + int savedUserMark = _mark; + _visualSelectionCommandCount++; + + // Get the unambiguous prefix, possibly removing the first quote + var userCompletionText = GetUnambiguousPrefix(menu.MenuItems, out var ambiguous); + if (userCompletionText.Length > 0) + { + var c = userCompletionText[0]; + if (IsSingleQuote(c) || IsDoubleQuote(c)) { userCompletionText = userCompletionText.Substring(1); } - int userInitialCompletionLength = userCompletionText.Length; + } + + var userInitialCompletionLength = userCompletionText.Length; - DoReplacementForCompletion(matches[0], completions); - // Recompute end of buffer coordinates as the replacement could have - // added a line. - var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length); - var menuAreaTop = endBufferCoords.Y + 1; - var previousMenuTop = menuAreaTop; + completions.CurrentMatchIndex = 0; + menu.DrawMenu(null); - int previousItem = -1; + bool processingKeys = true; + int previousSelection = -1; - bool processingKeys = true; - int backspaceCounter = 0; - while (processingKeys) + while (processingKeys) + { + if (menu.CurrentSelection != previousSelection) { - int cycleBackspaceCounter = backspaceCounter; - if (selectedItem != previousItem) + var currentMenuItem = menu.CurrentMenuItem; + int curPos = FindUserCompletionTextPosition(currentMenuItem, userCompletionText); + if (userCompletionText.Length == 0 && + (IsSingleQuote(currentMenuItem.CompletionText[0]) || IsDoubleQuote(currentMenuItem.CompletionText[0]))) { - int curPos = FindUserCompletinTextPosition(matches[selectedItem], userCompletionText); - if (userCompletionText.Length == 0 && - ( IsSingleQuote(matches[selectedItem].CompletionText[0]) - || IsDoubleQuote(matches[selectedItem].CompletionText[0]) - ) - ) - { - curPos++; - } - // set mark to the end of UserCompletion but in real completion (because of .\ and so on) - _mark = completions.ReplacementIndex + curPos + userCompletionText.Length; - DoReplacementForCompletion(matches[selectedItem], completions); - - endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length); - menuAreaTop = endBufferCoords.Y + 1; + curPos++; + } - if (previousItem != -1) - InvertSelectedCompletion(menuBuffer, previousItem, menuColumnWidth, displayRows); - InvertSelectedCompletion(menuBuffer, selectedItem, menuColumnWidth, displayRows); + // set mark to the end of UserCompletion but in real completion (because of .\ and so on) + _mark = completions.ReplacementIndex + curPos + userCompletionText.Length; + DoReplacementForCompletion(currentMenuItem, completions); - var blanklinesCount = previousMenuTop - menuAreaTop; - previousMenuTop = menuAreaTop; - _console.WriteBufferLines(menuBuffer, ref menuAreaTop); + ExchangePointAndMark(); - // Showing the menu may have scrolled the screen or moved the cursor, update initialY to reflect that. - _initialY -= (previousMenuTop - menuAreaTop); - // return cursor in place even if screen was scrolled - ExchangePointAndMark(); + // After replacement, the menu might be misplaced from the command line + // getting shorter or longer. + var endOfCommandLine = ConvertOffsetToPoint(_buffer.Length); + var topAdjustment = (endOfCommandLine.Y + 1) - menu.Top; - if (previousItem != -1 && blanklinesCount > 0) - { - WriteBlankLines(blanklinesCount, menuAreaTop + displayRows); - } - previousMenuTop = menuAreaTop; - previousItem = selectedItem; - } - - var nextKey = ReadKey(); - if (nextKey == Keys.RightArrow) + if (topAdjustment != 0) { - selectedItem = Math.Min(selectedItem + displayRows, matches.Count - 1); + menu.Top += topAdjustment; + menu.DrawMenu(null); } - else if (nextKey == Keys.LeftArrow) + if (topAdjustment > 0) { - selectedItem = Math.Max(selectedItem - displayRows, 0); + // Render did not clear the rest of the command line which flowed + // into the menu, so we must do that here. + _console.SaveCursor(); + _console.SetCursorPosition(endOfCommandLine.X, endOfCommandLine.Y); + _console.Write(Spaces(_console.BufferWidth - endOfCommandLine.X)); + _console.RestoreCursor(); } - else if (nextKey == Keys.DownArrow) + else if (topAdjustment < 0) { - selectedItem = Math.Min(selectedItem + 1, matches.Count - 1); + // The menu moved up, clear the extra stuff at the bottom. + WriteBlankLines(menu.Top + menu.Rows - topAdjustment - 1, -topAdjustment); } - else if (nextKey == Keys.UpArrow) + + if (previousSelection != -1) { - selectedItem = Math.Max(selectedItem - 1, 0); + menu.UpdateMenuSelection(previousSelection, /*select*/ false, + Options.ShowToolTips, VTColorUtils.AsEscapeSequence(Options.EmphasisColor)); } - else if (nextKey == Keys.PageDown) + menu.UpdateMenuSelection(menu.CurrentSelection, /*select*/ true, + Options.ShowToolTips, VTColorUtils.AsEscapeSequence(Options.EmphasisColor)); + + previousSelection = menu.CurrentSelection; + } + + var nextKey = ReadKey(); + if (nextKey.EqualsNormalized(Keys.RightArrow)) { menu.MoveRight(); } + else if (nextKey.EqualsNormalized(Keys.LeftArrow)) { menu.MoveLeft(); } + else if (nextKey.EqualsNormalized(Keys.DownArrow)) { menu.MoveDown(); } + else if (nextKey.EqualsNormalized(Keys.UpArrow)) { menu.MoveUp(); } + else if (nextKey.EqualsNormalized(Keys.PageDown)) { menu.MovePageDown(); } + else if (nextKey.EqualsNormalized(Keys.PageUp)) { menu.MovePageUp(); } + else if (nextKey.EqualsNormalized(Keys.Tab)) + { + // Search for possible unambiguous common prefix. + string unAmbiguousText = GetUnambiguousPrefix(menu.MenuItems, out ambiguous); + int userComplPos = unAmbiguousText.IndexOf(userCompletionText, StringComparison.OrdinalIgnoreCase); + + // ... If found - advance IncrementalCompletion ... + if (unAmbiguousText.Length > 0 && userComplPos >= 0 && + unAmbiguousText.Length > (userComplPos + userCompletionText.Length)) { - selectedItem = Math.Min(selectedItem + displayRows - (selectedItem % displayRows) - 1, matches.Count - 1); + userCompletionText = unAmbiguousText.Substring(userComplPos); + _current = completions.ReplacementIndex + + FindUserCompletionTextPosition(menu.MenuItems[menu.CurrentSelection], userCompletionText) + + userCompletionText.Length; + Render(); + Ding(); } - else if (nextKey == Keys.PageUp) + // ... if no - usual Tab behaviour + else { - selectedItem = Math.Max(selectedItem - (selectedItem % displayRows), 0); + menu.MoveN(1); } - else if (nextKey == Keys.Tab) + } + else if (nextKey.EqualsNormalized(Keys.ShiftTab)) + { + menu.MoveN(-1); + } + else if (nextKey.EqualsNormalized(Keys.CtrlG) + || nextKey.EqualsNormalized(Keys.Escape)) + { + undo = true; + processingKeys = false; + _visualSelectionCommandCount = 0; + _mark = savedUserMark; + } + else if (nextKey.EqualsNormalized(Keys.Backspace)) + { + // TODO: Shift + Backspace does not fail here? + if (menuStack.Count > 1) { - // Search for possible unAmbiguous common prefix. ... - string unAmbiguousText = GetUnambiguousPrefix(matches, out ambiguous); - int userComplPos = unAmbiguousText.IndexOf(userCompletionText, StringComparison.OrdinalIgnoreCase); - // ... If found - advance IncrementalCompletion ... - if (unAmbiguousText.Length > 0 && userComplPos >= 0 && unAmbiguousText.Length > (userComplPos + userCompletionText.Length)) - { - userCompletionText = unAmbiguousText.Substring(userComplPos); - _current = completions.ReplacementIndex + FindUserCompletinTextPosition(matches[selectedItem], userCompletionText) + userCompletionText.Length; - Render(); - Ding(); - } - // ... if no - usual Tab behaviour - else - { - selectedItem = (selectedItem + 1) % matches.Count; - } + var newMenu = menuStack.Pop(); + + newMenu.DrawMenu(menu); + previousSelection = -1; + + menu = newMenu; + + userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); } - else if (nextKey == Keys.ShiftTab) + else if (menuStack.Count == 1) { - selectedItem = (selectedItem - 1) % matches.Count; - if (selectedItem < 0) - { - selectedItem += matches.Count; - } + Ding(); + + Debug.Assert(menuStack.Peek() == null, "sentinel value expected"); + // Pop so the next backspace sends us to the else block and out of the loop. + menuStack.Pop(); } - else if (nextKey == Keys.CtrlG || nextKey == Keys.Escape) + else { - undo = true; processingKeys = false; + undo = true; _visualSelectionCommandCount = 0; _mark = savedUserMark; + PrependQueuedKeys(nextKey); } - else - { - bool prependNextKey = false; - int cursorAdjustment = 0; - bool truncateCurrentCompletion = false; - bool keepSelection = false; - - if (IsDoneWithCompletions(matches[selectedItem], nextKey)) - { - processingKeys = false; - ExchangePointAndMark(); // cursor to the end of Completion - if (nextKey != Keys.Enter) - { - if (matches[selectedItem].ResultType == CompletionResultType.ProviderContainer) - userCompletionText = GetUnquotedText(GetReplacementTextForDirectory(matches[selectedItem].CompletionText, ref cursorAdjustment), consistentQuoting: false); - else - userCompletionText = GetUnquotedText(matches[selectedItem], consistentQuoting: false); + } + else + { + bool prependNextKey = false; + int cursorAdjustment = 0; + bool truncateCurrentCompletion = false; + bool keepSelection = false; - // do not append the same char as last char in CompletionText (works for for '(', '\') - prependNextKey = userCompletionText[userCompletionText.Length - 1] != nextKey.KeyChar; - } - } - else if (nextKey == Keys.Backspace || - (nextKey.KeyChar > 0 && !char.IsControl(nextKey.KeyChar) )) + var currentMenuItem = menu.CurrentMenuItem; + if (IsDoneWithCompletions(currentMenuItem, nextKey)) + { + processingKeys = false; + ExchangePointAndMark(); // cursor to the end of Completion + if (!nextKey.EqualsNormalized(Keys.Enter)) { - // TODO: Shift + Backspace does not fall here ? - if (nextKey == Keys.Backspace) + if (currentMenuItem.ResultType == CompletionResultType.ProviderContainer) { - if (userCompletionText.Length > userInitialCompletionLength) - { - userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); - } - else if (backspaceCounter == 0) - { - Ding(); - backspaceCounter++; - } - else - { - processingKeys = false; - prependNextKey = true; - // we exit loop with current completion up to cursor - truncateCurrentCompletion = true; - if (userInitialCompletionLength == 0) - { - undo = true; - } - } + userCompletionText = GetUnquotedText( + GetReplacementTextForDirectory(currentMenuItem.CompletionText, ref cursorAdjustment), + consistentQuoting: false); } else { - userCompletionText += nextKey.KeyChar; + userCompletionText = GetUnquotedText(currentMenuItem, consistentQuoting: false); } - if (processingKeys && backspaceCounter == 0) // do not rebuild menu on backspace with Ding() + + // do not append the same char as last char in CompletionText (works for for '(', '\') + prependNextKey = userCompletionText[userCompletionText.Length - 1] != nextKey.KeyChar; + } + } + else if (nextKey.KeyChar > 0 && !char.IsControl(nextKey.KeyChar)) + { + userCompletionText += nextKey.KeyChar; + // filter out matches and redraw menu + var newMatches = FilterCompletions(completions, userCompletionText); + if (newMatches.Count > 0) + { + var newMenu = CreateCompletionMenu(newMatches); + + newMenu.DrawMenu(menu); + previousSelection = -1; + + // Remember the current menu for when we see Backspace. + menu.ToolTipLines = 0; + if (menuStack.Count == 0) { - // filter out matches and redraw menu - var tmpMatches = FilterCompletions(completions.CompletionMatches, userCompletionText); - if (tmpMatches.Count > 0) - { - WriteBlankLines(displayRows, menuAreaTop); - var selectedCompletionText = matches[selectedItem].CompletionText; - matches = tmpMatches; - previousItem = -1; - menuBuffer = CreateCompletionMenu(matches, _console, Options.ShowToolTips, - selectedCompletionText, out selectedItem, out menuColumnWidth, out displayRows); - } - else - { - processingKeys = false; - prependNextKey = true; - // we exit loop with current completion up to cursor - truncateCurrentCompletion = true; - if (userInitialCompletionLength == 0) - { - undo = true; - } - } + // The user hit backspace before there were any items on the stack + // and we removed the sentinel - so put it back now. + menuStack.Push(null); } + + menuStack.Push(menu); + menu = newMenu; } - else // exit with any other Key chord + else { processingKeys = false; prependNextKey = true; - - // without this branch experience doesnt look naturally - if (_dispatchTable.TryGetValue(nextKey, out var handler) && - ( - handler.Action == CopyOrCancelLine || - handler.Action == Cut || - handler.Action == DeleteChar || - handler.Action == Paste - ) - ) + // we exit loop with current completion up to cursor + truncateCurrentCompletion = true; + if (userInitialCompletionLength == 0) { - keepSelection = true; + undo = true; } } - if (!processingKeys) // time to exit loop + } + else // exit with any other Key chord + { + processingKeys = false; + prependNextKey = true; + + // without this branch experience doesnt look naturally + if (_dispatchTable.TryGetValue(nextKey, out var handler) && + ( + handler.Action == CopyOrCancelLine || + handler.Action == Cut || + handler.Action == DeleteChar || + handler.Action == Paste + ) + ) { - if (truncateCurrentCompletion && ! undo) - { - CompletionResult r = new CompletionResult(matches[selectedItem].CompletionText.Substring(0, _current - completions.ReplacementIndex)); - DoReplacementForCompletion(r, completions); - } - if (keepSelection) - { - _visualSelectionCommandCount = 1; - } - else - { - _visualSelectionCommandCount = 0; - // if mark was set after cursor, it restored in uninspected position, because text before mark now longer - // should we correct it ? I think not, beause any other text insertion does not correct it - _mark = savedUserMark; - } - // without render all key chords that just move cursor leave selection visible, but it can be wrong - if (!undo && !keepSelection) - { - Render(); - } - if (prependNextKey) - { - _current -= cursorAdjustment; - PrependQueuedKeys(nextKey); - } + keepSelection = true; + } + } + + if (!processingKeys) // time to exit loop + { + if (truncateCurrentCompletion && !undo) + { + CompletionResult r = new CompletionResult(currentMenuItem + .CompletionText.Substring(0, _current - completions.ReplacementIndex)); + DoReplacementForCompletion(r, completions); + } + if (keepSelection) + { + _visualSelectionCommandCount = 1; + } + else + { + _visualSelectionCommandCount = 0; + // if mark was set after cursor, it restored in uninspected position, because text before mark now longer + // should we correct it ? I think not, beause any other text insertion does not correct it + _mark = savedUserMark; + } + // without render all key chords that just move cursor leave selection visible, but it can be wrong + if (!undo && !keepSelection) + { + Render(); + } + if (prependNextKey) + { + _current -= cursorAdjustment; + PrependQueuedKeys(nextKey); } } - if (backspaceCounter == cycleBackspaceCounter) - backspaceCounter = 0; } + } - WriteBlankLines(displayRows, menuAreaTop); + menu.Clear(); - var lastInsert = ((GroupedEdit)_edits[_edits.Count - 1])._groupedEditItems[1]; - Debug.Assert(lastInsert is EditItemInsertString, "The only edits possible here are pairs of Delete/Insert"); - var firstDelete = ((GroupedEdit)_edits[undoPoint])._groupedEditItems[0]; - Debug.Assert(firstDelete is EditItemDelete, "The only edits possible here are pairs of Delete/Insert"); + var lastInsert = ((GroupedEdit) _edits[_edits.Count - 1])._groupedEditItems[1]; + Debug.Assert(lastInsert is EditItemInsertString, "The only edits possible here are pairs of Delete/Insert"); + var firstDelete = ((GroupedEdit) _edits[undoPoint])._groupedEditItems[0]; + Debug.Assert(firstDelete is EditItemDelete, "The only edits possible here are pairs of Delete/Insert"); - var groupEditCount = _edits.Count - undoPoint; - _edits.RemoveRange(undoPoint, groupEditCount); - _undoEditIndex = undoPoint; + var groupEditCount = _edits.Count - undoPoint; + _edits.RemoveRange(undoPoint, groupEditCount); + _undoEditIndex = undoPoint; - if (undo) - { - // Pretend it never happened. - lastInsert.Undo(); - firstDelete.Undo(); - Render(); - } - else - { - // Leave one edit instead of possibly many to undo - SaveEditItem(GroupedEdit.Create(new List { firstDelete, lastInsert })); - } + if (undo) + { + // Pretend it never happened. + lastInsert.Undo(); + firstDelete.Undo(); + Render(); } else { - var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length); - var menuAreaTop = endBufferCoords.Y + 1; - - _console.WriteBufferLines(menuBuffer, ref menuAreaTop); - _initialY = menuAreaTop + displayRows; - Render(); + // Leave one edit instead of possibly many to undo + SaveEditItem(GroupedEdit.Create(new List {firstDelete, lastInsert})); } } } diff --git a/PSReadLine/ConsoleBufferBuilder.cs b/PSReadLine/ConsoleBufferBuilder.cs deleted file mode 100644 index 5a38e8ae3..000000000 --- a/PSReadLine/ConsoleBufferBuilder.cs +++ /dev/null @@ -1,55 +0,0 @@ -/********************************************************************++ -Copyright (c) Microsoft Corporation. All rights reserved. ---********************************************************************/ - -using System.Collections.Generic; -using Microsoft.PowerShell.Internal; - -namespace Microsoft.PowerShell -{ - internal class ConsoleBufferBuilder - { - private readonly List buffer; - private readonly IConsole _console; - - public ConsoleBufferBuilder(int capacity, IConsole console) - { - buffer = new List(capacity); - _console = console; - } - - CHAR_INFO NewCharInfo(char c) - { - return new CHAR_INFO - { - UnicodeChar = c, - BackgroundColor = _console.BackgroundColor, - ForegroundColor = _console.ForegroundColor - }; - } - - public void Append(string s) - { - foreach (char c in s) - { - buffer.Add(NewCharInfo(c)); - } - } - - public void Append(char c, int count) - { - while (count-- > 0) - { - buffer.Add(NewCharInfo(c)); - } - } - - public CHAR_INFO[] ToArray() - { - return buffer.ToArray(); - } - - public int Length => buffer.Count; - } -} - diff --git a/PSReadLine/ConsoleKeyChordConverter.cs b/PSReadLine/ConsoleKeyChordConverter.cs index 591e7a3f5..8609e99b0 100644 --- a/PSReadLine/ConsoleKeyChordConverter.cs +++ b/PSReadLine/ConsoleKeyChordConverter.cs @@ -5,9 +5,8 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Runtime.InteropServices; -using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -16,10 +15,15 @@ namespace Microsoft.PowerShell /// public static class ConsoleKeyChordConverter { + private static Exception CantConvert(string s, params object[] args) + { + return new ArgumentException(string.Format(CultureInfo.CurrentCulture, s, args)); + } + /// /// Converts a string to a sequence of ConsoleKeyInfo. - /// Sequences are separated by ','. Modifiers - /// appear before the modified key and are separated by '+'. + /// Sequences are separated by ','. + /// Modifiers appear before the modified key and are separated by '+'. /// Examples: /// Ctrl+X /// Ctrl+D,M @@ -32,207 +36,250 @@ public static ConsoleKeyInfo[] Convert(string chord) throw new ArgumentNullException(nameof(chord)); } - var tokens = chord.Split(new[] {','}); + int start = 0; + var first = GetOneKey(chord, ref start); - if (tokens.Length > 2) - { - throw new ArgumentException(PSReadLineResources.ChordWithTooManyKeys); - } + if (start >= chord.Length) + return new [] { first }; - var result = new ConsoleKeyInfo[tokens.Length]; - for (int i = 0; i < tokens.Length; i++) - { - result[i] = ConvertOneSequence(tokens[i]); - } + if (chord[start++] != ',') + throw CantConvert(PSReadLineResources.InvalidSequence, chord); + + var second = GetOneKey(chord, ref start); + if (start < chord.Length) + throw CantConvert(PSReadLineResources.InvalidSequence, chord); - return result; + return new [] { first, second }; } - private static ConsoleKeyInfo ConvertOneSequence(string sequence) + private static ConsoleKeyInfo GetOneKey(string chord, ref int start) { - Stack tokens = null; - ConsoleModifiers modifiers = 0; - ConsoleKey key = 0; - - bool valid = !String.IsNullOrEmpty(sequence); - - if (valid) + ConsoleModifiers mods = 0; + while (start < chord.Length) { - tokens = new Stack( - (sequence.Split(new[] {'+'}) - .Select( - part => part.ToLowerInvariant().Trim()))); - } - - while (valid && tokens.Count > 0) - { - string token = tokens.Pop(); - - // sequence was something silly like "shift++" - if (token == String.Empty) - { - valid = false; - break; - } - - // key should be first token to be popped - if (key == 0) + var thisMod = GetModifier(chord, ref start); + if (thisMod != 0) { - // Enum.TryParse accepts arbitrary integers. We shouldn't, - // but single digits need to map to the correct key, e.g. - // ConsoleKey.D1 - if (long.TryParse(token, out var tokenAsLong)) + // modifier already set? + if ((mods & thisMod) != 0) { - if (tokenAsLong >= 0 && tokenAsLong <= 9) - { - token = "D" + token; - } - else - { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.UnrecognizedKey, token)); - } + // Found a duplicate modifier + throw CantConvert(PSReadLineResources.InvalidModifier, thisMod, chord); } - // try simple parse for ConsoleKey enum name - valid = Enum.TryParse(token, ignoreCase: true, result: out key); + mods |= thisMod; - // doesn't map to ConsoleKey so convert to virtual key from char - if (!valid && token.Length == 1) + if (start >= chord.Length) { - valid = TryParseCharLiteral(token[0], ref modifiers, ref key, out var failReason); - - if (!valid) - { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.CantTranslateKey, token[0], failReason)); - } + // Ends with a modifier? + throw CantConvert(PSReadLineResources.UnrecognizedKey, chord); } - if (!valid) + if (chord[start] != '+' && chord[start] != '-') { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.UnrecognizedKey, token)); + // No character between mod and key + throw CantConvert(PSReadLineResources.InvalidSequence, chord); } + + start++; + continue; + } + + // Not a modifier + var comma = chord.IndexOf(",", start, StringComparison.Ordinal); + string input; + if (comma == start) + { + input = ","; + } + else if (comma == -1) + { + input = chord.Substring(start); } else { - // now, parse modifier(s) + input = chord.Substring(start, comma - start); + } + start += input.Length; + + if (!MapKeyChar(input, (mods & ConsoleModifiers.Control) != 0, out var key, out var keyChar)) + { + throw CantConvert(PSReadLineResources.UnrecognizedKey, input); + } + + return new ConsoleKeyInfo(keyChar, key, + (mods & ConsoleModifiers.Shift) != 0, + (mods & ConsoleModifiers.Alt) != 0, + (mods & ConsoleModifiers.Control) != 0); + } - // courtesy translation - if (token == "ctrl") + throw CantConvert(PSReadLineResources.InvalidSequence, chord); + } + + private static ConsoleModifiers GetModifier(string sequence, ref int start) + { + switch (sequence[start]) + { + case 's': + case 'S': + if ((sequence.Length - start >= 5) && + (sequence[start + 1] == 'h' || sequence[start + 1] == 'H') && + (sequence[start + 2] == 'i' || sequence[start + 2] == 'I') && + (sequence[start + 3] == 'f' || sequence[start + 3] == 'F') && + (sequence[start + 4] == 't' || sequence[start + 4] == 'T')) { - token = "control"; + start += 5; + return ConsoleModifiers.Shift; } - - if (Enum.TryParse(token, ignoreCase: true, result: out var modifier)) + goto default; + + case 'c': + case 'C': + if (sequence.Length - start >= 7 && + (sequence[start + 1] == 'o' || sequence[start + 1] == 'O') && + (sequence[start + 2] == 'n' || sequence[start + 2] == 'N') && + (sequence[start + 3] == 't' || sequence[start + 3] == 'T') && + (sequence[start + 4] == 'r' || sequence[start + 4] == 'R') && + (sequence[start + 5] == 'o' || sequence[start + 5] == 'O') && + (sequence[start + 6] == 'l' || sequence[start + 6] == 'L')) { - // modifier already set? - if ((modifiers & modifier) != 0) - { - // either found duplicate modifier token or shift state - // was already implied from char, e.g. char is "}", which is "shift+]" - throw new ArgumentException( - String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidModifier, modifier, key)); - } - modifiers |= modifier; + start += 7; + return ConsoleModifiers.Control; } - else + else if (sequence.Length - start >= 4 && + (sequence[start + 1] == 't' || sequence[start + 1] == 'T') && + (sequence[start + 2] == 'r' || sequence[start + 2] == 'R') && + (sequence[start + 3] == 'l' || sequence[start + 3] == 'L')) { - throw new ArgumentException( - String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidModifier, token, key)); + start += 4; + return ConsoleModifiers.Control; } - } - } - - if (!valid) - { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidSequence, sequence)); - } + goto default; - char keyChar = GetCharFromConsoleKey(key, modifiers); + case 'a': + case 'A': + if (sequence.Length - start >= 3 && + (sequence[start + 1] == 'l' || sequence[start + 1] == 'L') && + (sequence[start + 2] == 't' || sequence[start + 2] == 'T')) + { + start += 3; + return ConsoleModifiers.Alt; + } + goto default; - return new ConsoleKeyInfo(keyChar, key, - shift: ((modifiers & ConsoleModifiers.Shift) != 0), - alt: ((modifiers & ConsoleModifiers.Alt) != 0), - control: ((modifiers & ConsoleModifiers.Control) != 0)); + default: + return 0; + } } - private static bool TryParseCharLiteral(char literal, ref ConsoleModifiers modifiers, ref ConsoleKey key, out string failReason) + struct KeyPair { - bool valid = false; - - // shift state will be in MSB - short virtualKey = NativeMethods.VkKeyScan(literal); - int hresult = Marshal.GetLastWin32Error(); + public char KeyChar; + public ConsoleKey Key; + } - if (virtualKey != 0) + private static readonly Dictionary KeyMappings = + new Dictionary(StringComparer.OrdinalIgnoreCase) { - // e.g. "}" = 0x01dd but "]" is 0x00dd, ergo } = shift+]. - // shift = 1, control = 2, alt = 4, hankaku = 8 (ignored) - int state = virtualKey >> 8; - - if ((state & 1) == 1) + { "Backspace", new KeyPair { Key = ConsoleKey.Backspace } }, // Not '\b' - ^H and BS differ on Linux + { "Delete", new KeyPair { Key = ConsoleKey.Delete } }, + { "DownArrow", new KeyPair { Key = ConsoleKey.DownArrow} }, + { "End", new KeyPair { Key = ConsoleKey.End } }, + { "Enter", new KeyPair { Key = ConsoleKey.Enter } }, // Platforms differ - '\r' or '\n' + { "Escape", new KeyPair { Key = ConsoleKey.Escape, KeyChar = '\x1b' } }, + { "F1", new KeyPair { Key = ConsoleKey.F1 } }, + { "F2", new KeyPair { Key = ConsoleKey.F2 } }, + { "F3", new KeyPair { Key = ConsoleKey.F3 } }, + { "F4", new KeyPair { Key = ConsoleKey.F4 } }, + { "F5", new KeyPair { Key = ConsoleKey.F5 } }, + { "F6", new KeyPair { Key = ConsoleKey.F6 } }, + { "F7", new KeyPair { Key = ConsoleKey.F7 } }, + { "F8", new KeyPair { Key = ConsoleKey.F8 } }, + { "F9", new KeyPair { Key = ConsoleKey.F9 } }, + { "F10", new KeyPair { Key = ConsoleKey.F10 } }, + { "F11", new KeyPair { Key = ConsoleKey.F11 } }, + { "F12", new KeyPair { Key = ConsoleKey.F12 } }, + { "F13", new KeyPair { Key = ConsoleKey.F13 } }, + { "F14", new KeyPair { Key = ConsoleKey.F14 } }, + { "F15", new KeyPair { Key = ConsoleKey.F15 } }, + { "F16", new KeyPair { Key = ConsoleKey.F16 } }, + { "F17", new KeyPair { Key = ConsoleKey.F17 } }, + { "F18", new KeyPair { Key = ConsoleKey.F18 } }, + { "F19", new KeyPair { Key = ConsoleKey.F19 } }, + { "F20", new KeyPair { Key = ConsoleKey.F20 } }, + { "F21", new KeyPair { Key = ConsoleKey.F21 } }, + { "F22", new KeyPair { Key = ConsoleKey.F22 } }, + { "F23", new KeyPair { Key = ConsoleKey.F23 } }, + { "F24", new KeyPair { Key = ConsoleKey.F24 } }, + { "Home", new KeyPair { Key = ConsoleKey.Home } }, + { "Insert", new KeyPair { Key = ConsoleKey.Insert } }, + { "LeftArrow", new KeyPair { Key = ConsoleKey.LeftArrow} }, + { "PageDown", new KeyPair { Key = ConsoleKey.PageDown} }, + { "PageUp", new KeyPair { Key = ConsoleKey.PageUp} }, + { "RightArrow", new KeyPair { Key = ConsoleKey.RightArrow} }, + { "Spacebar", new KeyPair { Key = ConsoleKey.Spacebar, KeyChar = ' ' } }, + { "Tab", new KeyPair { Key = ConsoleKey.Tab, KeyChar = '\t' } }, + { "UpArrow", new KeyPair { Key = ConsoleKey.UpArrow} }, + }; + + static bool MapKeyChar(string input, bool isCtrl, out ConsoleKey key, out char keyChar) + { + if (input.Length == 1) + { + keyChar = input[0]; + if (keyChar >= 'a' && keyChar <= 'z') { - modifiers |= ConsoleModifiers.Shift; + key = ConsoleKey.A + (keyChar - 'a'); } - if ((state & 2) == 2) + else if (keyChar >= 'A' && keyChar <= 'Z') { - modifiers |= ConsoleModifiers.Control; + key = ConsoleKey.A + (keyChar - 'A'); } - if ((state & 4) == 4) + else if (keyChar >= '0' && keyChar <= '9') { - modifiers |= ConsoleModifiers.Alt; + key = ConsoleKey.D0 + (keyChar - '0'); } - - virtualKey &= 0xff; - - if (Enum.IsDefined(typeof (ConsoleKey), (int) virtualKey)) + else { - failReason = null; - key = (ConsoleKey) virtualKey; - valid = true; + key = 0; } - else + if (isCtrl) { - // haven't seen this happen yet, but possible - failReason = String.Format(CultureInfo.CurrentCulture, PSReadLineResources.UnrecognizedKey, virtualKey); - } - } - else - { - Exception e = Marshal.GetExceptionForHR(hresult); - failReason = e.Message; - } - - return valid; - } - - internal static char GetCharFromConsoleKey(ConsoleKey key, ConsoleModifiers modifiers) - { - // default for unprintables and unhandled - char keyChar = '\u0000'; - - // emulate GetKeyboardState bitmap - set high order bit for relevant modifier virtual keys - var state = new byte[256]; - state[NativeMethods.VK_SHIFT] = (byte)(((modifiers & ConsoleModifiers.Shift) != 0) ? 0x80 : 0); - state[NativeMethods.VK_CONTROL] = (byte)(((modifiers & ConsoleModifiers.Control) != 0) ? 0x80 : 0); - state[NativeMethods.VK_ALT] = (byte)(((modifiers & ConsoleModifiers.Alt) != 0) ? 0x80 : 0); - - // a ConsoleKey enum's value is a virtual key code - uint virtualKey = (uint)key; - - // get corresponding scan code - uint scanCode = NativeMethods.MapVirtualKey(virtualKey, NativeMethods.MAPVK_VK_TO_VSC); + if (keyChar >= 'a' && keyChar <= 'z') + { + keyChar = (char)(keyChar - 'a' + 1); + } + else if (keyChar >= 'A' && keyChar <= 'Z') + { + keyChar = (char)(keyChar - 'A' + 1); + } + else + { + keyChar = '\0'; + } + } - // get corresponding character - maybe be 0, 1 or 2 in length (diacriticals) - var chars = new char[2]; - int charCount = NativeMethods.ToUnicode( - virtualKey, scanCode, state, chars, chars.Length, NativeMethods.MENU_IS_INACTIVE); + return true; + } - // TODO: support diacriticals (charCount == 2) - if (charCount == 1) + if (KeyMappings.TryGetValue(input, out var keyPair)) { - keyChar = chars[0]; + key = keyPair.Key; + + if (key == ConsoleKey.Backspace) + { + keyChar = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == isCtrl + ? '\x7f' : '\x08'; + } + else + { + keyChar = keyPair.KeyChar; + } + return true; } - return keyChar; + key = 0; + keyChar = '\0'; + return false; } } } diff --git a/PSReadLine/ConsoleLib.cs b/PSReadLine/ConsoleLib.cs index 7b7a4c6f9..169c27d49 100644 --- a/PSReadLine/ConsoleLib.cs +++ b/PSReadLine/ConsoleLib.cs @@ -3,452 +3,11 @@ --********************************************************************/ using System; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Win32.SafeHandles; namespace Microsoft.PowerShell.Internal { -#pragma warning disable 1591 - - internal static class NativeMethods - { - public const uint MAPVK_VK_TO_VSC = 0x00; - public const uint MAPVK_VSC_TO_VK = 0x01; - public const uint MAPVK_VK_TO_CHAR = 0x02; - - public const byte VK_SHIFT = 0x10; - public const byte VK_CONTROL = 0x11; - public const byte VK_ALT = 0x12; - public const uint MENU_IS_ACTIVE = 0x01; - public const uint MENU_IS_INACTIVE = 0x00; // windows key - - public const uint ENABLE_PROCESSED_INPUT = 0x0001; - public const uint ENABLE_LINE_INPUT = 0x0002; - public const uint ENABLE_WINDOW_INPUT = 0x0008; - public const uint ENABLE_MOUSE_INPUT = 0x0010; - - public const int FontTypeMask = 0x06; - public const int TrueTypeFont = 0x04; - - internal static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); // WinBase.h - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr GetStdHandle(uint handleId); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool ScrollConsoleScreenBuffer(IntPtr hConsoleOutput, - ref SMALL_RECT lpScrollRectangle, - IntPtr lpClipRectangle, - COORD dwDestinationOrigin, - ref CHAR_INFO lpFill); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool WriteConsole(IntPtr hConsoleOutput, string lpBuffer, uint nNumberOfCharsToWrite, out uint lpNumberOfCharsWritten, IntPtr lpReserved); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); - - [DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool WriteConsoleOutput(IntPtr consoleOutput, CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT writeRegion); - - [DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool ReadConsoleOutput(IntPtr consoleOutput, [Out] CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT readRegion); - - [DllImport("user32.dll", SetLastError = true)] - public static extern uint MapVirtualKey(uint uCode, uint uMapType); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int ToUnicode(uint uVirtKey, uint uScanCode, byte[] lpKeyState, - [MarshalAs(UnmanagedType.LPArray)] [Out] char[] chars, int charMaxCount, uint flags); - - [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern short VkKeyScan(char @char); - - [DllImport("kernel32.dll", SetLastError = false, CharSet = CharSet.Unicode)] - public static extern uint GetConsoleOutputCP(); - - [DllImport("User32.dll", SetLastError = false, CharSet = CharSet.Unicode)] - public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr GetConsoleWindow(); - - [DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr GetDC(IntPtr hwnd); - - [DllImport("GDI32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool GetTextMetrics(IntPtr hdc, out TEXTMETRIC tm); - - [DllImport("GDI32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool GetCharWidth32(IntPtr hdc, uint first, uint last, out int width); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr CreateFile - ( - string fileName, - uint desiredAccess, - uint ShareModes, - IntPtr securityAttributes, - uint creationDisposition, - uint flagsAndAttributes, - IntPtr templateFileWin32Handle - ); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern bool GetCurrentConsoleFontEx(IntPtr consoleOutput, bool bMaximumWindow, ref CONSOLE_FONT_INFO_EX consoleFontInfo); - - [StructLayout(LayoutKind.Sequential)] - internal struct COLORREF - { - internal uint ColorDWORD; - - internal uint R => ColorDWORD & 0xff; - internal uint G => (ColorDWORD >> 8) & 0xff; - internal uint B => (ColorDWORD >> 16) & 0xff; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct CONSOLE_SCREEN_BUFFER_INFO_EX - { - internal int cbSize; - internal COORD dwSize; - internal COORD dwCursorPosition; - internal ushort wAttributes; - internal SMALL_RECT srWindow; - internal COORD dwMaximumWindowSize; - internal ushort wPopupAttributes; - internal bool bFullscreenSupported; - internal COLORREF Black; - internal COLORREF DarkBlue; - internal COLORREF DarkGreen; - internal COLORREF DarkCyan; - internal COLORREF DarkRed; - internal COLORREF DarkMagenta; - internal COLORREF DarkYellow; - internal COLORREF Gray; - internal COLORREF DarkGray; - internal COLORREF Blue; - internal COLORREF Green; - internal COLORREF Cyan; - internal COLORREF Red; - internal COLORREF Magenta; - internal COLORREF Yellow; - internal COLORREF White; - } - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool GetConsoleScreenBufferInfoEx(IntPtr hConsoleOutput, - ref CONSOLE_SCREEN_BUFFER_INFO_EX csbe); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool SetConsoleScreenBufferInfoEx(IntPtr hConsoleOutput, - ref CONSOLE_SCREEN_BUFFER_INFO_EX csbe); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern int GetFileType(IntPtr handle); - internal const int FILE_TYPE_CHAR = 0x0002; - } - - public delegate bool BreakHandler(ConsoleBreakSignal ConsoleBreakSignal); - - public enum ConsoleBreakSignal : uint - { - CtrlC = 0, - CtrlBreak = 1, - Close = 2, - Logoff = 5, - Shutdown = 6, - None = 255, - } - - internal enum CHAR_INFO_Attributes : ushort - { - COMMON_LVB_LEADING_BYTE = 0x0100, - COMMON_LVB_TRAILING_BYTE = 0x0200 - } - - public enum StandardHandleId : uint - { - Error = unchecked((uint)-12), - Output = unchecked((uint)-11), - Input = unchecked((uint)-10), - } - - [Flags] - internal enum AccessQualifiers : uint - { - // From winnt.h - GenericRead = 0x80000000, - GenericWrite = 0x40000000 - } - - internal enum CreationDisposition : uint - { - // From winbase.h - CreateNew = 1, - CreateAlways = 2, - OpenExisting = 3, - OpenAlways = 4, - TruncateExisting = 5 - } - - [Flags] - internal enum ShareModes : uint - { - // From winnt.h - ShareRead = 0x00000001, - ShareWrite = 0x00000002 - } - - public struct SMALL_RECT - { - public short Left; - public short Top; - public short Right; - public short Bottom; - - [ExcludeFromCodeCoverage] - public override string ToString() - { - return String.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", Left, Top, Right, Bottom); - } - } - - internal struct COORD - { - public short X; - public short Y; - - [ExcludeFromCodeCoverage] - public override string ToString() - { - return String.Format(CultureInfo.InvariantCulture, "{0},{1}", X, Y); - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct FONTSIGNATURE - { - //From public\sdk\inc\wingdi.h - - // fsUsb*: A 128-bit Unicode subset bitfield (USB) identifying up to 126 Unicode subranges - internal uint fsUsb0; - internal uint fsUsb1; - internal uint fsUsb2; - internal uint fsUsb3; - // fsCsb*: A 64-bit, code-page bitfield (CPB) that identifies a specific character set or code page. - internal uint fsCsb0; - internal uint fsCsb1; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal struct TEXTMETRIC - { - //From public\sdk\inc\wingdi.h - public int tmHeight; - public int tmAscent; - public int tmDescent; - public int tmInternalLeading; - public int tmExternalLeading; - public int tmAveCharWidth; - public int tmMaxCharWidth; - public int tmWeight; - public int tmOverhang; - public int tmDigitizedAspectX; - public int tmDigitizedAspectY; - public char tmFirstChar; - public char tmLastChar; - public char tmDefaultChar; - public char tmBreakChar; - public byte tmItalic; - public byte tmUnderlined; - public byte tmStruckOut; - public byte tmPitchAndFamily; - public byte tmCharSet; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal struct CONSOLE_FONT_INFO_EX - { - internal int cbSize; - internal int nFont; - internal short FontWidth; - internal short FontHeight; - internal int FontFamily; - internal int FontWeight; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] - internal string FontFace; - } - - public struct CHAR_INFO - { - public ushort UnicodeChar; - public ushort Attributes; - - public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background) - { - UnicodeChar = c; - Attributes = (ushort)(((int)background << 4) | (int)foreground); - } - - [ExcludeFromCodeCoverage] - public ConsoleColor ForegroundColor - { - get => (ConsoleColor)(Attributes & 0xf); - set => Attributes = (ushort)((Attributes & 0xfff0) | ((int)value & 0xf)); - } - - [ExcludeFromCodeCoverage] - public ConsoleColor BackgroundColor - { - get => (ConsoleColor)((Attributes & 0xf0) >> 4); - set => Attributes = (ushort)((Attributes & 0xff0f) | (((int)value & 0xf) << 4)); - } - - [ExcludeFromCodeCoverage] - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append((char)UnicodeChar); - if (ForegroundColor != Console.ForegroundColor) - sb.AppendFormat(" fg: {0}", ForegroundColor); - if (BackgroundColor != Console.BackgroundColor) - sb.AppendFormat(" bg: {0}", BackgroundColor); - return sb.ToString(); - } - - [ExcludeFromCodeCoverage] - public override bool Equals(object obj) - { - if (!(obj is CHAR_INFO)) - { - return false; - } - - var other = (CHAR_INFO)obj; - return this.UnicodeChar == other.UnicodeChar && this.Attributes == other.Attributes; - } - - [ExcludeFromCodeCoverage] - public override int GetHashCode() - { - return UnicodeChar.GetHashCode() + Attributes.GetHashCode(); - } - - } - - internal static class ConsoleKeyInfoExtension - { - public static string ToGestureString(this ConsoleKeyInfo key) - { - var mods = key.Modifiers; - - var sb = new StringBuilder(); - if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) - { - sb.Append("Ctrl"); - } - if (key.Modifiers.HasFlag(ConsoleModifiers.Alt)) - { - if (sb.Length > 0) - sb.Append("+"); - sb.Append("Alt"); - } - - char c = ConsoleKeyChordConverter.GetCharFromConsoleKey(key.Key, - (mods & ConsoleModifiers.Shift) != 0 ? ConsoleModifiers.Shift : 0); - if (char.IsControl(c) || char.IsWhiteSpace(c)) - { - if (key.Modifiers.HasFlag(ConsoleModifiers.Shift)) - { - if (sb.Length > 0) - sb.Append("+"); - sb.Append("Shift"); - } - if (sb.Length > 0) - sb.Append("+"); - sb.Append(key.Key); - } - else - { - if (sb.Length > 0) - sb.Append("+"); - sb.Append(c); - } - return sb.ToString(); - } - } - internal class ConhostConsole : IConsole { - private readonly Lazy _outputHandle = new Lazy(() => - { - // We use CreateFile here instead of GetStdWin32Handle, as GetStdWin32Handle will return redirected handles - var handle = NativeMethods.CreateFile( - "CONOUT$", - (UInt32)(AccessQualifiers.GenericRead | AccessQualifiers.GenericWrite), - (UInt32)ShareModes.ShareWrite, - (IntPtr)0, - (UInt32)CreationDisposition.OpenExisting, - 0, - (IntPtr)0); - - if (handle == NativeMethods.INVALID_HANDLE_VALUE) - { - int err = Marshal.GetLastWin32Error(); - Win32Exception innerException = new Win32Exception(err); - throw new Exception("Failed to retreive the input console handle.", innerException); - } - - return new SafeFileHandle(handle, true); - } - ); - - private readonly Lazy _inputHandle = new Lazy(() => - { - // We use CreateFile here instead of GetStdWin32Handle, as GetStdWin32Handle will return redirected handles - var handle = NativeMethods.CreateFile( - "CONIN$", - (UInt32)(AccessQualifiers.GenericRead | AccessQualifiers.GenericWrite), - (UInt32)ShareModes.ShareWrite, - (IntPtr)0, - (UInt32)CreationDisposition.OpenExisting, - 0, - (IntPtr)0); - - if (handle == NativeMethods.INVALID_HANDLE_VALUE) - { - int err = Marshal.GetLastWin32Error(); - Win32Exception innerException = new Win32Exception(err); - throw new Exception("Failed to retreive the input console handle.", innerException); - } - - return new SafeFileHandle(handle, true); - }); - - public uint GetConsoleInputMode() - { - var handle = _inputHandle.Value.DangerousGetHandle(); - NativeMethods.GetConsoleMode(handle, out var result); - return result; - } - - public void SetConsoleInputMode(uint mode) - { - var handle = _inputHandle.Value.DangerousGetHandle(); - NativeMethods.SetConsoleMode(handle, mode); - } - public ConsoleKeyInfo ReadKey() { return Console.ReadKey(true); @@ -474,6 +33,12 @@ public int CursorSize set => Console.CursorSize = value; } + public bool CursorVisible + { + get => Console.CursorVisible; + set => Console.CursorVisible = value; + } + public int BufferWidth { get => Console.BufferWidth; @@ -536,98 +101,22 @@ public void WriteLine(string value) Console.WriteLine(value); } - public void WriteBufferLines(CHAR_INFO[] buffer, ref int top) - { - WriteBufferLines(buffer, ref top, true); - } - - public void WriteBufferLines(CHAR_INFO[] buffer, ref int top, bool ensureBottomLineVisible) - { - var handle = NativeMethods.GetStdHandle((uint) StandardHandleId.Output); - - int bufferWidth = Console.BufferWidth; - int bufferLineCount = buffer.Length / bufferWidth; - if ((top + bufferLineCount) > Console.BufferHeight) - { - var scrollCount = (top + bufferLineCount) - Console.BufferHeight; - ScrollBuffer(scrollCount); - top -= scrollCount; - } - var bufferSize = new COORD - { - X = (short) bufferWidth, - Y = (short) bufferLineCount - }; - var bufferCoord = new COORD {X = 0, Y = 0}; - var bottom = top + bufferLineCount - 1; - var writeRegion = new SMALL_RECT - { - Top = (short) top, - Left = 0, - Bottom = (short) bottom, - Right = (short) (bufferWidth - 1) - }; - NativeMethods.WriteConsoleOutput(handle, buffer, - bufferSize, bufferCoord, ref writeRegion); - - // Now make sure the bottom line is visible - if (ensureBottomLineVisible && - (bottom >= (Console.WindowTop + Console.WindowHeight))) - { - Console.CursorTop = bottom; - } - } - public void ScrollBuffer(int lines) { - var handle = NativeMethods.GetStdHandle((uint) StandardHandleId.Output); - - var scrollRectangle = new SMALL_RECT - { - Top = (short) lines, - Left = 0, - Bottom = (short)(Console.BufferHeight - 1), - Right = (short)Console.BufferWidth - }; - var destinationOrigin = new COORD {X = 0, Y = 0}; - var fillChar = new CHAR_INFO(' ', Console.ForegroundColor, Console.BackgroundColor); - NativeMethods.ScrollConsoleScreenBuffer(handle, ref scrollRectangle, IntPtr.Zero, destinationOrigin, ref fillChar); + Console.Write("\x1b[" + lines + "S"); } - public CHAR_INFO[] ReadBufferLines(int top, int count) - { - var result = new CHAR_INFO[BufferWidth * count]; - var handle = NativeMethods.GetStdHandle((uint) StandardHandleId.Output); + private int _savedX, _savedY; - var readBufferSize = new COORD { - X = (short)BufferWidth, - Y = (short)count}; - var readBufferCoord = new COORD {X = 0, Y = 0}; - var readRegion = new SMALL_RECT - { - Top = (short)top, - Left = 0, - Bottom = (short)(top + count), - Right = (short)(BufferWidth - 1) - }; - NativeMethods.ReadConsoleOutput(handle, result, - readBufferSize, readBufferCoord, ref readRegion); - return result; + public void SaveCursor() + { + _savedX = Console.CursorLeft; + _savedY = Console.CursorTop; } - public bool IsHandleRedirected(bool stdIn) + public void RestoreCursor() { - var handle = NativeMethods.GetStdHandle((uint)(stdIn ? StandardHandleId.Input : StandardHandleId.Output)); - - // If handle is not to a character device, we must be redirected: - int fileType = NativeMethods.GetFileType(handle); - if ((fileType & NativeMethods.FILE_TYPE_CHAR) != NativeMethods.FILE_TYPE_CHAR) - return true; - - // Char device - if GetConsoleMode succeeds, we are NOT redirected. - return !NativeMethods.GetConsoleMode(handle, out var unused); + Console.SetCursorPosition(_savedX, _savedY); } } - -#pragma warning restore 1591 } diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 5539c87fa..3d927435e 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text; using System.Threading; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -112,16 +113,13 @@ private void ReportHistoryFileError(Exception e) return; historyErrorReportedCount += 1; - var fgColor = Console.ForegroundColor; - var bgColor = Console.BackgroundColor; - Console.ForegroundColor = Options.ErrorForegroundColor; + Console.Write(_options._errorColor); Console.WriteLine(PSReadLineResources.HistoryFileErrorMessage, Options.HistorySavePath, e.Message); if (historyErrorReportedCount == 2) { Console.WriteLine(PSReadLineResources.HistoryFileErrorFinalMessage); } - Console.ForegroundColor = fgColor; - Console.BackgroundColor = bgColor; + Console.Write("\x1b0m"); } private bool WithHistoryFileMutexDo(int timeout, Action action) @@ -600,7 +598,9 @@ private void InteractiveHistorySearchLoop(int direction) { UpdateHistoryDuringInteractiveSearch(toMatch.ToString(), +1, ref searchFromPoint); } - else if (function == BackwardDeleteChar || key == Keys.Backspace || key == Keys.CtrlH) + else if (function == BackwardDeleteChar + || key.EqualsNormalized(Keys.Backspace) + || key.EqualsNormalized(Keys.CtrlH)) { if (toMatch.Length > 0) { @@ -641,7 +641,7 @@ private void InteractiveHistorySearchLoop(int direction) Ding(); } } - else if (key == Keys.Escape) + else if (key.EqualsNormalized(Keys.Escape)) { // End search break; @@ -652,7 +652,7 @@ private void InteractiveHistorySearchLoop(int direction) EndOfHistory(); break; } - else if (EndInteractiveHistorySearch(key, function)) + else if (EndInteractiveHistorySearch(key)) { PrependQueuedKeys(key); break; @@ -680,20 +680,10 @@ private void InteractiveHistorySearchLoop(int direction) } } - private static bool EndInteractiveHistorySearch(ConsoleKeyInfo key, Action function) + private static bool EndInteractiveHistorySearch(ConsoleKeyInfo key) { - // Keys < ' ' are control characters - if (key.KeyChar < ' ') - { - return true; - } - - if ((key.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) - { - return true; - } - - return false; + return char.IsControl(key.KeyChar) + || (key.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0; } private void InteractiveHistorySearch(int direction) diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index ace0629c0..dbe047550 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -6,8 +6,10 @@ using System.Collections.Generic; using System.Globalization; using System.Management.Automation; +using System.Runtime.InteropServices; using System.Text; using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -54,7 +56,7 @@ class KeyHandler // should be included on all handlers that ignore their parameters so they // can be called from PowerShell without passing anything. // - // The first arugment is the key that caused the action to be called + // The first argument is the key that caused the action to be called // (the second key when it's a 2 key chord). The default is null (it's nullable) // because PowerShell can't handle default(ConsoleKeyInfo) as a default. // Most actions will ignore this argument. @@ -76,14 +78,18 @@ public string LongDescription internal class ConsoleKeyInfoComparer : IEqualityComparer { + public static ConsoleKeyInfoComparer Instance { get; } = new ConsoleKeyInfoComparer(); + + private ConsoleKeyInfoComparer() { } + public bool Equals(ConsoleKeyInfo x, ConsoleKeyInfo y) { - return x.Key == y.Key && x.KeyChar == y.KeyChar && x.Modifiers == y.Modifiers; + return x.EqualsNormalized(y); } public int GetHashCode(ConsoleKeyInfo obj) { - return obj.GetHashCode(); + return obj.GetNormalizedHashCode(); } } @@ -99,11 +105,30 @@ static KeyHandler MakeKeyHandler(Action action, string } private Dictionary _dispatchTable; - private Dictionary> _chordDispatchTable; + private Dictionary> _chordDispatchTable; + + /// + /// Helper to set bindings based on EditMode + /// + void SetDefaultBindings(EditMode editMode) + { + switch (editMode) + { + case EditMode.Emacs: + SetDefaultEmacsBindings(); + break; + case EditMode.Vi: + SetDefaultViBindings(); + break; + case EditMode.Windows: + SetDefaultWindowsBindings(); + break; + } + } void SetDefaultWindowsBindings() { - _dispatchTable = new Dictionary(new ConsoleKeyInfoComparer()) + _dispatchTable = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.Enter, MakeKeyHandler(AcceptLine, "AcceptLine") }, { Keys.ShiftEnter, MakeKeyHandler(AddLine, "AddLine") }, @@ -116,8 +141,8 @@ void SetDefaultWindowsBindings() { Keys.CtrlRightArrow, MakeKeyHandler(NextWord, "NextWord") }, { Keys.ShiftLeftArrow, MakeKeyHandler(SelectBackwardChar, "SelectBackwardChar") }, { Keys.ShiftRightArrow, MakeKeyHandler(SelectForwardChar, "SelectForwardChar") }, - { Keys.ShiftCtrlLeftArrow, MakeKeyHandler(SelectBackwardWord, "SelectBackwardWord") }, - { Keys.ShiftCtrlRightArrow, MakeKeyHandler(SelectNextWord, "SelectNextWord") }, + { Keys.CtrlShiftLeftArrow, MakeKeyHandler(SelectBackwardWord, "SelectBackwardWord") }, + { Keys.CtrlShiftRightArrow, MakeKeyHandler(SelectNextWord, "SelectNextWord") }, { Keys.UpArrow, MakeKeyHandler(PreviousHistory, "PreviousHistory") }, { Keys.DownArrow, MakeKeyHandler(NextHistory, "NextHistory") }, { Keys.Home, MakeKeyHandler(BeginningOfLine, "BeginningOfLine") }, @@ -126,12 +151,8 @@ void SetDefaultWindowsBindings() { Keys.ShiftEnd, MakeKeyHandler(SelectLine, "SelectLine") }, { Keys.Delete, MakeKeyHandler(DeleteChar, "DeleteChar") }, { Keys.Backspace, MakeKeyHandler(BackwardDeleteChar, "BackwardDeleteChar") }, - { Keys.CtrlSpace, MakeKeyHandler(MenuComplete, "MenuComplete") }, { Keys.Tab, MakeKeyHandler(TabCompleteNext, "TabCompleteNext") }, { Keys.ShiftTab, MakeKeyHandler(TabCompletePrevious, "TabCompletePrevious") }, - { Keys.VolumeDown, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeUp, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeMute, MakeKeyHandler(Ignore, "Ignore") }, { Keys.CtrlA, MakeKeyHandler(SelectAll, "SelectAll") }, { Keys.CtrlC, MakeKeyHandler(CopyOrCancelLine, "CopyOrCancelLine") }, { Keys.CtrlShiftC, MakeKeyHandler(Copy, "Copy") }, @@ -143,8 +164,6 @@ void SetDefaultWindowsBindings() { Keys.CtrlY, MakeKeyHandler(Redo, "Redo") }, { Keys.CtrlZ, MakeKeyHandler(Undo, "Undo") }, { Keys.CtrlBackspace, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, - { Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord") }, - { Keys.CtrlEnd, MakeKeyHandler(ForwardDeleteLine, "ForwardDeleteLine") }, { Keys.CtrlHome, MakeKeyHandler(BackwardDeleteLine, "BackwardDeleteLine") }, { Keys.CtrlRBracket, MakeKeyHandler(GotoBrace, "GotoBrace") }, { Keys.CtrlAltQuestion, MakeKeyHandler(ShowKeyBindings, "ShowKeyBindings") }, @@ -161,7 +180,6 @@ void SetDefaultWindowsBindings() { Keys.Alt9, MakeKeyHandler(DigitArgument, "DigitArgument") }, { Keys.AltMinus, MakeKeyHandler(DigitArgument, "DigitArgument") }, { Keys.AltQuestion, MakeKeyHandler(WhatIsKey, "WhatIsKey") }, - { Keys.AltF7, MakeKeyHandler(ClearHistory, "ClearHistory") }, { Keys.F3, MakeKeyHandler(CharacterSearch, "CharacterSearch") }, { Keys.ShiftF3, MakeKeyHandler(CharacterSearchBackward, "CharacterSearchBackward") }, { Keys.F8, MakeKeyHandler(HistorySearchBackward, "HistorySearchBackward") }, @@ -172,12 +190,21 @@ void SetDefaultWindowsBindings() { Keys.CtrlPageDown, MakeKeyHandler(ScrollDisplayDownLine, "ScrollDisplayDownLine") }, }; - _chordDispatchTable = new Dictionary>(); + // Some bindings are not available on certain platforms + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _dispatchTable.Add(Keys.CtrlSpace, MakeKeyHandler(MenuComplete, "MenuComplete")); + _dispatchTable.Add(Keys.AltF7, MakeKeyHandler(ClearHistory, "ClearHistory")); + _dispatchTable.Add(Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord")); + _dispatchTable.Add(Keys.CtrlEnd, MakeKeyHandler(ForwardDeleteLine, "ForwardDeleteLine")); + } + + _chordDispatchTable = new Dictionary>(ConsoleKeyInfoComparer.Instance); } void SetDefaultEmacsBindings() { - _dispatchTable = new Dictionary(new ConsoleKeyInfoComparer()) + _dispatchTable = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.Backspace, MakeKeyHandler(BackwardDeleteChar, "BackwardDeleteChar") }, { Keys.Enter, MakeKeyHandler(AcceptLine, "AcceptLine") }, @@ -204,7 +231,6 @@ void SetDefaultEmacsBindings() { Keys.CtrlE, MakeKeyHandler(EndOfLine, "EndOfLine") }, { Keys.CtrlF, MakeKeyHandler(ForwardChar, "ForwardChar") }, { Keys.CtrlG, MakeKeyHandler(Abort, "Abort") }, - { Keys.CtrlH, MakeKeyHandler(BackwardDeleteChar, "BackwardDeleteChar") }, { Keys.CtrlL, MakeKeyHandler(ClearScreen, "ClearScreen") }, { Keys.CtrlK, MakeKeyHandler(KillLine, "KillLine") }, { Keys.CtrlM, MakeKeyHandler(ValidateAndAcceptLine,"ValidateAndAcceptLine") }, @@ -221,7 +247,7 @@ void SetDefaultEmacsBindings() { Keys.CtrlAt, MakeKeyHandler(SetMark, "SetMark") }, { Keys.CtrlUnderbar, MakeKeyHandler(Undo, "Undo") }, { Keys.CtrlRBracket, MakeKeyHandler(CharacterSearch, "CharacterSearch") }, - { Keys.AltCtrlRBracket, MakeKeyHandler(CharacterSearchBackward,"CharacterSearchBackward") }, + { Keys.CtrlAltRBracket, MakeKeyHandler(CharacterSearchBackward,"CharacterSearchBackward") }, { Keys.Alt0, MakeKeyHandler(DigitArgument, "DigitArgument") }, { Keys.Alt1, MakeKeyHandler(DigitArgument, "DigitArgument") }, { Keys.Alt2, MakeKeyHandler(DigitArgument, "DigitArgument") }, @@ -242,28 +268,35 @@ void SetDefaultEmacsBindings() { Keys.AltY, MakeKeyHandler(YankPop, "YankPop") }, { Keys.AltBackspace, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, { Keys.AltEquals, MakeKeyHandler(PossibleCompletions, "PossibleCompletions") }, - { Keys.CtrlSpace, MakeKeyHandler(MenuComplete, "MenuComplete") }, { Keys.CtrlAltQuestion, MakeKeyHandler(ShowKeyBindings, "ShowKeyBindings") }, { Keys.AltQuestion, MakeKeyHandler(WhatIsKey, "WhatIsKey") }, - { Keys.AltSpace, MakeKeyHandler(SetMark, "SetMark") }, // useless entry here for completeness - brings up system menu on Windows { Keys.AltPeriod, MakeKeyHandler(YankLastArg, "YankLastArg") }, { Keys.AltUnderbar, MakeKeyHandler(YankLastArg, "YankLastArg") }, - { Keys.AltCtrlY, MakeKeyHandler(YankNthArg, "YankNthArg") }, - { Keys.VolumeDown, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeUp, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeMute, MakeKeyHandler(Ignore, "Ignore") }, + { Keys.CtrlAltY, MakeKeyHandler(YankNthArg, "YankNthArg") }, { Keys.PageUp, MakeKeyHandler(ScrollDisplayUp, "ScrollDisplayUp") }, - { Keys.CtrlPageUp, MakeKeyHandler(ScrollDisplayUpLine, "ScrollDisplayUpLine") }, { Keys.PageDown, MakeKeyHandler(ScrollDisplayDown, "ScrollDisplayDown") }, - { Keys.CtrlPageDown, MakeKeyHandler(ScrollDisplayDownLine,"ScrollDisplayDownLine") }, - { Keys.CtrlHome, MakeKeyHandler(ScrollDisplayTop, "ScrollDisplayTop") }, - { Keys.CtrlEnd, MakeKeyHandler(ScrollDisplayToCursor,"ScrollDisplayToCursor") }, }; - _chordDispatchTable = new Dictionary> + // Some bindings are not available on certain platforms + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _dispatchTable.Add(Keys.CtrlSpace, MakeKeyHandler(MenuComplete, "MenuComplete")); + _dispatchTable.Add(Keys.CtrlEnd, MakeKeyHandler(ScrollDisplayToCursor, "ScrollDisplayToCursor")); + _dispatchTable.Add(Keys.CtrlHome, MakeKeyHandler(ScrollDisplayTop, "ScrollDisplayTop")); + _dispatchTable.Add(Keys.CtrlPageDown, MakeKeyHandler(ScrollDisplayDownLine, "ScrollDisplayDownLine")); + _dispatchTable.Add(Keys.CtrlPageUp, MakeKeyHandler(ScrollDisplayUpLine, "ScrollDisplayUpLine")); + } + else + { + // Ctrl+H is the same KeyChar as Backspace on Windows, but not on Linux, so we need another entry. + _dispatchTable.Add(Keys.CtrlH, MakeKeyHandler(BackwardDeleteChar, "BackwardDeleteChar")); + _dispatchTable.Add(Keys.AltSpace, MakeKeyHandler(SetMark, "SetMark")); + } + + _chordDispatchTable = new Dictionary>(ConsoleKeyInfoComparer.Instance) { // Escape, table (meta key) - [Keys.Escape] = new Dictionary(new ConsoleKeyInfoComparer()) + [Keys.Escape] = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.B, MakeKeyHandler(BackwardWord, "BackwardWord") }, { Keys.D, MakeKeyHandler(KillWord, "KillWord")}, @@ -277,7 +310,7 @@ void SetDefaultEmacsBindings() }, // Ctrl+X, table - [Keys.CtrlX] = new Dictionary(new ConsoleKeyInfoComparer()) + [Keys.CtrlX] = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.Backspace, MakeKeyHandler(BackwardKillLine, "BackwardKillLine") }, { Keys.CtrlU, MakeKeyHandler(Undo, "Undo") }, @@ -310,12 +343,11 @@ public static void ShowKeyBindings(ConsoleKeyInfo? key = null, object arg = null } // Don't overwrite any of the line - so move to first line after the end of our buffer. - var coords = _singleton.ConvertOffsetToCoordinates(_singleton._buffer.Length); - _singleton.PlaceCursor(0, coords.Y + 1); + var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + _singleton.PlaceCursor(0, point.Y + 1); console.WriteLine(buffer.ToString()); - _singleton._initialY = console.CursorTop; - _singleton.Render(); + InvokePrompt(key: null, arg: _singleton._console.CursorTop); } /// @@ -366,12 +398,11 @@ public static void WhatIsKey(ConsoleKeyInfo? key = null, object arg = null) _singleton.ClearStatusMessage(render: false); // Don't overwrite any of the line - so move to first line after the end of our buffer. - var coords = _singleton.ConvertOffsetToCoordinates(_singleton._buffer.Length); - _singleton.PlaceCursor(0, coords.Y + 1); + var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + _singleton.PlaceCursor(0, point.Y + 1); _singleton._console.WriteLine(buffer.ToString()); - _singleton._initialY = _singleton._console.CursorTop; - _singleton.Render(); + InvokePrompt(key: null, arg: _singleton._console.CursorTop); } } } diff --git a/PSReadLine/KeyBindings.vi.cs b/PSReadLine/KeyBindings.vi.cs index 8ad658785..dad87eab7 100644 --- a/PSReadLine/KeyBindings.vi.cs +++ b/PSReadLine/KeyBindings.vi.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; namespace Microsoft.PowerShell { @@ -50,7 +51,7 @@ internal static ConsoleColor AlternateBackground(ConsoleColor bg) /// private void SetDefaultViBindings() { - _viInsKeyMap = new Dictionary(new ConsoleKeyInfoComparer()) + _viInsKeyMap = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.Enter, MakeKeyHandler(AcceptLine, "AcceptLine" ) }, { Keys.CtrlD, MakeKeyHandler(ViAcceptLineOrExit, "ViAcceptLineOrExit" ) }, @@ -70,16 +71,12 @@ private void SetDefaultViBindings() { Keys.Tab, MakeKeyHandler(ViTabCompleteNext, "ViTabCompleteNext") }, { Keys.ShiftTab, MakeKeyHandler(ViTabCompletePrevious, "ViTabCompletePrevious") }, { Keys.CtrlV, MakeKeyHandler(Paste, "Paste") }, - { Keys.VolumeDown, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeUp, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeMute, MakeKeyHandler(Ignore, "Ignore") }, { Keys.CtrlC, MakeKeyHandler(CancelLine, "CancelLine") }, { Keys.CtrlL, MakeKeyHandler(ClearScreen, "ClearScreen") }, { Keys.CtrlT, MakeKeyHandler(SwapCharacters, "SwapCharacters") }, { Keys.CtrlY, MakeKeyHandler(Redo, "Redo") }, { Keys.CtrlZ, MakeKeyHandler(Undo, "Undo") }, { Keys.CtrlBackspace, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, - { Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord") }, { Keys.CtrlEnd, MakeKeyHandler(ForwardDeleteLine, "ForwardDeleteLine") }, { Keys.CtrlHome, MakeKeyHandler(BackwardDeleteLine, "BackwardDeleteLine") }, { Keys.CtrlRBracket, MakeKeyHandler(GotoBrace, "GotoBrace") }, @@ -89,7 +86,14 @@ private void SetDefaultViBindings() { Keys.CtrlR, MakeKeyHandler(ViSearchHistoryBackward,"ViSearchHistoryBackward") }, { Keys.CtrlS, MakeKeyHandler(SearchForward, "SearchForward") } }; - _viCmdKeyMap = new Dictionary(new ConsoleKeyInfoComparer()) + + // Some bindings are not available on certain platforms + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _viInsKeyMap.Add(Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord")); + } + + _viCmdKeyMap = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.Enter, MakeKeyHandler(ViAcceptLine, "ViAcceptLine") }, { Keys.CtrlD, MakeKeyHandler(ViAcceptLineOrExit, "ViAcceptLineOrExit") }, @@ -110,9 +114,6 @@ private void SetDefaultViBindings() { Keys.Tab, MakeKeyHandler(TabCompleteNext, "TabCompleteNext") }, { Keys.ShiftTab, MakeKeyHandler(TabCompletePrevious, "TabCompletePrevious") }, { Keys.CtrlV, MakeKeyHandler(Paste, "Paste") }, - { Keys.VolumeDown, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeUp, MakeKeyHandler(Ignore, "Ignore") }, - { Keys.VolumeMute, MakeKeyHandler(Ignore, "Ignore") }, { Keys.CtrlC, MakeKeyHandler(CancelLine, "CancelLine") }, { Keys.CtrlL, MakeKeyHandler(ClearScreen, "ClearScreen") }, { Keys.CtrlT, MakeKeyHandler(SwapCharacters, "SwapCharacters") }, @@ -121,7 +122,6 @@ private void SetDefaultViBindings() { Keys.CtrlY, MakeKeyHandler(Redo, "Redo") }, { Keys.CtrlZ, MakeKeyHandler(Undo, "Undo") }, { Keys.CtrlBackspace, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, - { Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord") }, { Keys.CtrlEnd, MakeKeyHandler(ForwardDeleteLine, "ForwardDeleteLine") }, { Keys.CtrlHome, MakeKeyHandler(BackwardDeleteLine, "BackwardDeleteLine") }, { Keys.CtrlRBracket, MakeKeyHandler(GotoBrace, "GotoBrace") }, @@ -206,7 +206,14 @@ private void SetDefaultViBindings() { Keys.Comma, MakeKeyHandler(RepeatLastCharSearchBackwards, "RepeatLastCharSearchBackwards") } }; - _viChordDTable = new Dictionary(new ConsoleKeyInfoComparer()) + // Some bindings are not available on certain platforms + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _viCmdKeyMap.Add(Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord")); + } + + + _viChordDTable = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.D, MakeKeyHandler( DeleteLine, "DeleteLine") }, { Keys.Dollar, MakeKeyHandler( DeleteToEnd, "DeleteToEnd") }, @@ -228,7 +235,7 @@ private void SetDefaultViBindings() { Keys.ucT, MakeKeyHandler( ViDeleteToBeforeCharBackward, "ViDeleteToBeforeCharBackward") }, }; - _viChordCTable = new Dictionary(new ConsoleKeyInfoComparer()) + _viChordCTable = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.C, MakeKeyHandler( ViReplaceLine, "ViReplaceLine") }, { Keys.Dollar, MakeKeyHandler( ViReplaceToEnd, "ViReplaceToEnd") }, @@ -250,7 +257,7 @@ private void SetDefaultViBindings() { Keys.ucT, MakeKeyHandler( ViReplaceToBeforeCharBackward, "ViReplaceToBeforeCharBackward") }, }; - _viChordYTable = new Dictionary(new ConsoleKeyInfoComparer()) + _viChordYTable = new Dictionary(ConsoleKeyInfoComparer.Instance) { { Keys.Y, MakeKeyHandler( ViYankLine, "ViYankLine") }, { Keys.Dollar, MakeKeyHandler( ViYankToEndOfLine, "ViYankToEndOfLine") }, @@ -268,8 +275,8 @@ private void SetDefaultViBindings() { Keys.Percent, MakeKeyHandler( ViYankPercent, "ViYankPercent") }, }; - _viCmdChordTable = new Dictionary>(); - _viInsChordTable = new Dictionary>(); + _viCmdChordTable = new Dictionary>(ConsoleKeyInfoComparer.Instance); + _viInsChordTable = new Dictionary>(ConsoleKeyInfoComparer.Instance); _dispatchTable = _viInsKeyMap; _chordDispatchTable = _viInsChordTable; diff --git a/PSReadLine/Keys.cs b/PSReadLine/Keys.cs index 57f2248f3..83687dee0 100644 --- a/PSReadLine/Keys.cs +++ b/PSReadLine/Keys.cs @@ -3,295 +3,615 @@ --********************************************************************/ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +[assembly: DebuggerDisplay("Key={Key}, KeyChar={KeyChar,x}, Mods={Modifiers}", Target = typeof(ConsoleKeyInfo))] namespace Microsoft.PowerShell { internal static class Keys { - public static ConsoleKeyInfo A = new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false); - public static ConsoleKeyInfo B = new ConsoleKeyInfo('b', ConsoleKey.B, false, false, false); - public static ConsoleKeyInfo C = new ConsoleKeyInfo('c', ConsoleKey.C, false, false, false); - public static ConsoleKeyInfo D = new ConsoleKeyInfo('d', ConsoleKey.D, false, false, false); - public static ConsoleKeyInfo E = new ConsoleKeyInfo('e', ConsoleKey.E, false, false, false); - public static ConsoleKeyInfo F = new ConsoleKeyInfo('f', ConsoleKey.F, false, false, false); - public static ConsoleKeyInfo G = new ConsoleKeyInfo('g', ConsoleKey.G, false, false, false); - public static ConsoleKeyInfo H = new ConsoleKeyInfo('h', ConsoleKey.H, false, false, false); - public static ConsoleKeyInfo I = new ConsoleKeyInfo('i', ConsoleKey.I, false, false, false); - public static ConsoleKeyInfo J = new ConsoleKeyInfo('j', ConsoleKey.J, false, false, false); - public static ConsoleKeyInfo K = new ConsoleKeyInfo('k', ConsoleKey.K, false, false, false); - public static ConsoleKeyInfo L = new ConsoleKeyInfo('l', ConsoleKey.L, false, false, false); - public static ConsoleKeyInfo M = new ConsoleKeyInfo('m', ConsoleKey.M, false, false, false); - public static ConsoleKeyInfo N = new ConsoleKeyInfo('n', ConsoleKey.N, false, false, false); - public static ConsoleKeyInfo O = new ConsoleKeyInfo('o', ConsoleKey.O, false, false, false); - public static ConsoleKeyInfo P = new ConsoleKeyInfo('p', ConsoleKey.P, false, false, false); - public static ConsoleKeyInfo Q = new ConsoleKeyInfo('q', ConsoleKey.Q, false, false, false); - public static ConsoleKeyInfo R = new ConsoleKeyInfo('r', ConsoleKey.R, false, false, false); - public static ConsoleKeyInfo S = new ConsoleKeyInfo('s', ConsoleKey.S, false, false, false); - public static ConsoleKeyInfo T = new ConsoleKeyInfo('t', ConsoleKey.T, false, false, false); - public static ConsoleKeyInfo U = new ConsoleKeyInfo('u', ConsoleKey.U, false, false, false); - public static ConsoleKeyInfo V = new ConsoleKeyInfo('v', ConsoleKey.V, false, false, false); - public static ConsoleKeyInfo W = new ConsoleKeyInfo('w', ConsoleKey.W, false, false, false); - public static ConsoleKeyInfo X = new ConsoleKeyInfo('x', ConsoleKey.X, false, false, false); - public static ConsoleKeyInfo Y = new ConsoleKeyInfo('y', ConsoleKey.Y, false, false, false); - public static ConsoleKeyInfo Z = new ConsoleKeyInfo('z', ConsoleKey.Z, false, false, false); - public static ConsoleKeyInfo ucA = new ConsoleKeyInfo('A', ConsoleKey.A, true, false, false); - public static ConsoleKeyInfo ucB = new ConsoleKeyInfo('B', ConsoleKey.B, true, false, false); - public static ConsoleKeyInfo ucC = new ConsoleKeyInfo('C', ConsoleKey.C, true, false, false); - public static ConsoleKeyInfo ucD = new ConsoleKeyInfo('D', ConsoleKey.D, true, false, false); - public static ConsoleKeyInfo ucE = new ConsoleKeyInfo('E', ConsoleKey.E, true, false, false); - public static ConsoleKeyInfo ucF = new ConsoleKeyInfo('F', ConsoleKey.F, true, false, false); - public static ConsoleKeyInfo ucG = new ConsoleKeyInfo('G', ConsoleKey.G, true, false, false); - public static ConsoleKeyInfo ucH = new ConsoleKeyInfo('H', ConsoleKey.H, true, false, false); - public static ConsoleKeyInfo ucI = new ConsoleKeyInfo('I', ConsoleKey.I, true, false, false); - public static ConsoleKeyInfo ucJ = new ConsoleKeyInfo('J', ConsoleKey.J, true, false, false); - public static ConsoleKeyInfo ucK = new ConsoleKeyInfo('K', ConsoleKey.K, true, false, false); - public static ConsoleKeyInfo ucL = new ConsoleKeyInfo('L', ConsoleKey.L, true, false, false); - public static ConsoleKeyInfo ucM = new ConsoleKeyInfo('M', ConsoleKey.M, true, false, false); - public static ConsoleKeyInfo ucN = new ConsoleKeyInfo('N', ConsoleKey.N, true, false, false); - public static ConsoleKeyInfo ucO = new ConsoleKeyInfo('O', ConsoleKey.O, true, false, false); - public static ConsoleKeyInfo ucP = new ConsoleKeyInfo('P', ConsoleKey.P, true, false, false); - public static ConsoleKeyInfo ucQ = new ConsoleKeyInfo('Q', ConsoleKey.Q, true, false, false); - public static ConsoleKeyInfo ucR = new ConsoleKeyInfo('R', ConsoleKey.R, true, false, false); - public static ConsoleKeyInfo ucS = new ConsoleKeyInfo('S', ConsoleKey.S, true, false, false); - public static ConsoleKeyInfo ucT = new ConsoleKeyInfo('T', ConsoleKey.T, true, false, false); - public static ConsoleKeyInfo ucU = new ConsoleKeyInfo('U', ConsoleKey.U, true, false, false); - public static ConsoleKeyInfo ucV = new ConsoleKeyInfo('V', ConsoleKey.V, true, false, false); - public static ConsoleKeyInfo ucW = new ConsoleKeyInfo('W', ConsoleKey.W, true, false, false); - public static ConsoleKeyInfo ucX = new ConsoleKeyInfo('X', ConsoleKey.X, true, false, false); - public static ConsoleKeyInfo ucY = new ConsoleKeyInfo('Y', ConsoleKey.Y, true, false, false); - public static ConsoleKeyInfo ucZ = new ConsoleKeyInfo('Z', ConsoleKey.Z, true, false, false); - - public static ConsoleKeyInfo _0 = new ConsoleKeyInfo('0', ConsoleKey.D0, false, false, false); - public static ConsoleKeyInfo _1 = new ConsoleKeyInfo('1', ConsoleKey.D1, false, false, false); - public static ConsoleKeyInfo _2 = new ConsoleKeyInfo('2', ConsoleKey.D2, false, false, false); - public static ConsoleKeyInfo _3 = new ConsoleKeyInfo('3', ConsoleKey.D3, false, false, false); - public static ConsoleKeyInfo _4 = new ConsoleKeyInfo('4', ConsoleKey.D4, false, false, false); - public static ConsoleKeyInfo _5 = new ConsoleKeyInfo('5', ConsoleKey.D5, false, false, false); - public static ConsoleKeyInfo _6 = new ConsoleKeyInfo('6', ConsoleKey.D6, false, false, false); - public static ConsoleKeyInfo _7 = new ConsoleKeyInfo('7', ConsoleKey.D7, false, false, false); - public static ConsoleKeyInfo _8 = new ConsoleKeyInfo('8', ConsoleKey.D8, false, false, false); - public static ConsoleKeyInfo _9 = new ConsoleKeyInfo('9', ConsoleKey.D9, false, false, false); - - public static ConsoleKeyInfo RParen = new ConsoleKeyInfo(')', ConsoleKey.D0, true, false, false); - public static ConsoleKeyInfo Bang = new ConsoleKeyInfo('!', ConsoleKey.D1, true, false, false); - public static ConsoleKeyInfo At = new ConsoleKeyInfo('@', ConsoleKey.D2, true, false, false); - public static ConsoleKeyInfo Pound = new ConsoleKeyInfo('#', ConsoleKey.D3, true, false, false); - public static ConsoleKeyInfo Dollar = new ConsoleKeyInfo('$', ConsoleKey.D4, true, false, false); - public static ConsoleKeyInfo Percent = new ConsoleKeyInfo('%', ConsoleKey.D5, true, false, false); - public static ConsoleKeyInfo Uphat = new ConsoleKeyInfo('^', ConsoleKey.D6, true, false, false); - public static ConsoleKeyInfo Ampersand = new ConsoleKeyInfo('&', ConsoleKey.D7, true, false, false); - public static ConsoleKeyInfo Star = new ConsoleKeyInfo('*', ConsoleKey.D8, true, false, false); - public static ConsoleKeyInfo LParen = new ConsoleKeyInfo('(', ConsoleKey.D9, true, false, false); - - public static ConsoleKeyInfo Colon = new ConsoleKeyInfo(':', ConsoleKey.Oem1, true, false, false); - public static ConsoleKeyInfo Semicolon = new ConsoleKeyInfo(';', ConsoleKey.Oem1, false, false, false); - public static ConsoleKeyInfo Question = new ConsoleKeyInfo('?', ConsoleKey.Oem2, true, false, false); - public static ConsoleKeyInfo Slash = new ConsoleKeyInfo('/', ConsoleKey.Oem2, false, false, false); - public static ConsoleKeyInfo Tilde = new ConsoleKeyInfo('~', ConsoleKey.Oem3, true, false, false); - public static ConsoleKeyInfo Backtick = new ConsoleKeyInfo('`', ConsoleKey.Oem3, false, false, false); - public static ConsoleKeyInfo LCurly = new ConsoleKeyInfo('{', ConsoleKey.Oem4, true, false, false); - public static ConsoleKeyInfo LBracket = new ConsoleKeyInfo('[', ConsoleKey.Oem4, false, false, false); - public static ConsoleKeyInfo Pipe = new ConsoleKeyInfo('|', ConsoleKey.Oem5, true, false, false); - public static ConsoleKeyInfo Backslash = new ConsoleKeyInfo('\\', ConsoleKey.Oem5, false, false, false); - public static ConsoleKeyInfo RCurly = new ConsoleKeyInfo('}', ConsoleKey.Oem6, true, false, false); - public static ConsoleKeyInfo RBracket = new ConsoleKeyInfo(']', ConsoleKey.Oem6, false, false, false); - public static ConsoleKeyInfo SQuote = new ConsoleKeyInfo('\'', ConsoleKey.Oem7, false, false, false); - public static ConsoleKeyInfo DQuote = new ConsoleKeyInfo('"', ConsoleKey.Oem7, true, false, false); - public static ConsoleKeyInfo LessThan = new ConsoleKeyInfo('<', ConsoleKey.OemComma, true, false, false); - public static ConsoleKeyInfo Comma = new ConsoleKeyInfo(',', ConsoleKey.OemComma, false, false, false); - public static ConsoleKeyInfo GreaterThan = new ConsoleKeyInfo('>', ConsoleKey.OemPeriod, true, false, false); - public static ConsoleKeyInfo Period = new ConsoleKeyInfo('.', ConsoleKey.OemPeriod, false, false, false); - public static ConsoleKeyInfo Underbar = new ConsoleKeyInfo('_', ConsoleKey.OemMinus, true, false, false); - public static ConsoleKeyInfo Minus = new ConsoleKeyInfo('-', ConsoleKey.OemMinus, false, false, false); - public static ConsoleKeyInfo AltMinus = new ConsoleKeyInfo('-', ConsoleKey.OemMinus, false, true, false); - public static ConsoleKeyInfo Plus = new ConsoleKeyInfo('+', ConsoleKey.OemPlus, true, false, false); - new - public static ConsoleKeyInfo Equals = new ConsoleKeyInfo('=', ConsoleKey.OemPlus, false, false, false); - - public static ConsoleKeyInfo CtrlAt = new ConsoleKeyInfo((char)0, ConsoleKey.D2, true, false, true); - public static ConsoleKeyInfo AltUnderbar = new ConsoleKeyInfo('_', ConsoleKey.OemMinus, true, true, false); - public static ConsoleKeyInfo CtrlUnderbar = new ConsoleKeyInfo((char)31, ConsoleKey.OemMinus, true, false, true); - public static ConsoleKeyInfo AltEquals = new ConsoleKeyInfo('=', ConsoleKey.OemPlus, false, true, false); - public static ConsoleKeyInfo Space = new ConsoleKeyInfo(' ', ConsoleKey.Spacebar, false, false, false); - // Useless because it's caught by the console to bring up the system menu. - public static ConsoleKeyInfo AltSpace = new ConsoleKeyInfo(' ', ConsoleKey.Spacebar, false, true, false); - public static ConsoleKeyInfo CtrlSpace = new ConsoleKeyInfo(' ', ConsoleKey.Spacebar, false, false, true); - public static ConsoleKeyInfo AltLess = new ConsoleKeyInfo('<', ConsoleKey.OemComma, true, true, false); - public static ConsoleKeyInfo AltGreater = new ConsoleKeyInfo('>', ConsoleKey.OemPeriod, true, true, false); - public static ConsoleKeyInfo CtrlRBracket = new ConsoleKeyInfo((char)29, ConsoleKey.Oem6, false, false, true); - public static ConsoleKeyInfo AltCtrlRBracket = new ConsoleKeyInfo((char)0, ConsoleKey.Oem6, false, true, true); - public static ConsoleKeyInfo AltPeriod = new ConsoleKeyInfo('.', ConsoleKey.OemPeriod, false, true, false); - public static ConsoleKeyInfo CtrlAltQuestion = new ConsoleKeyInfo((char)0, ConsoleKey.Oem2, true, true, true); - public static ConsoleKeyInfo AltQuestion = new ConsoleKeyInfo('?', ConsoleKey.Oem2, true, true, false); - - public static ConsoleKeyInfo Alt0 = new ConsoleKeyInfo('0', ConsoleKey.D0, false, true, false); - public static ConsoleKeyInfo Alt1 = new ConsoleKeyInfo('1', ConsoleKey.D1, false, true, false); - public static ConsoleKeyInfo Alt2 = new ConsoleKeyInfo('2', ConsoleKey.D2, false, true, false); - public static ConsoleKeyInfo Alt3 = new ConsoleKeyInfo('3', ConsoleKey.D3, false, true, false); - public static ConsoleKeyInfo Alt4 = new ConsoleKeyInfo('4', ConsoleKey.D4, false, true, false); - public static ConsoleKeyInfo Alt5 = new ConsoleKeyInfo('5', ConsoleKey.D5, false, true, false); - public static ConsoleKeyInfo Alt6 = new ConsoleKeyInfo('6', ConsoleKey.D6, false, true, false); - public static ConsoleKeyInfo Alt7 = new ConsoleKeyInfo('7', ConsoleKey.D7, false, true, false); - public static ConsoleKeyInfo Alt8 = new ConsoleKeyInfo('8', ConsoleKey.D8, false, true, false); - public static ConsoleKeyInfo Alt9 = new ConsoleKeyInfo('9', ConsoleKey.D9, false, true, false); - - public static ConsoleKeyInfo AltA = new ConsoleKeyInfo((char)97, ConsoleKey.A, false, true, false); - public static ConsoleKeyInfo AltB = new ConsoleKeyInfo((char)98, ConsoleKey.B, false, true, false); - public static ConsoleKeyInfo AltC = new ConsoleKeyInfo((char)99, ConsoleKey.C, false, true, false); - public static ConsoleKeyInfo AltD = new ConsoleKeyInfo((char)100, ConsoleKey.D, false, true, false); - public static ConsoleKeyInfo AltE = new ConsoleKeyInfo((char)101, ConsoleKey.E, false, true, false); - public static ConsoleKeyInfo AltF = new ConsoleKeyInfo((char)102, ConsoleKey.F, false, true, false); - public static ConsoleKeyInfo AltG = new ConsoleKeyInfo((char)103, ConsoleKey.G, false, true, false); - public static ConsoleKeyInfo AltH = new ConsoleKeyInfo((char)104, ConsoleKey.H, false, true, false); - public static ConsoleKeyInfo AltI = new ConsoleKeyInfo((char)105, ConsoleKey.I, false, true, false); - public static ConsoleKeyInfo AltJ = new ConsoleKeyInfo((char)106, ConsoleKey.J, false, true, false); - public static ConsoleKeyInfo AltK = new ConsoleKeyInfo((char)107, ConsoleKey.K, false, true, false); - public static ConsoleKeyInfo AltL = new ConsoleKeyInfo((char)108, ConsoleKey.L, false, true, false); - public static ConsoleKeyInfo AltM = new ConsoleKeyInfo((char)109, ConsoleKey.M, false, true, false); - public static ConsoleKeyInfo AltN = new ConsoleKeyInfo((char)110, ConsoleKey.N, false, true, false); - public static ConsoleKeyInfo AltO = new ConsoleKeyInfo((char)111, ConsoleKey.O, false, true, false); - public static ConsoleKeyInfo AltP = new ConsoleKeyInfo((char)112, ConsoleKey.P, false, true, false); - public static ConsoleKeyInfo AltQ = new ConsoleKeyInfo((char)113, ConsoleKey.Q, false, true, false); - public static ConsoleKeyInfo AltR = new ConsoleKeyInfo((char)114, ConsoleKey.R, false, true, false); - public static ConsoleKeyInfo AltS = new ConsoleKeyInfo((char)115, ConsoleKey.S, false, true, false); - public static ConsoleKeyInfo AltT = new ConsoleKeyInfo((char)116, ConsoleKey.T, false, true, false); - public static ConsoleKeyInfo AltU = new ConsoleKeyInfo((char)117, ConsoleKey.U, false, true, false); - public static ConsoleKeyInfo AltV = new ConsoleKeyInfo((char)118, ConsoleKey.V, false, true, false); - public static ConsoleKeyInfo AltW = new ConsoleKeyInfo((char)119, ConsoleKey.W, false, true, false); - public static ConsoleKeyInfo AltX = new ConsoleKeyInfo((char)120, ConsoleKey.X, false, true, false); - public static ConsoleKeyInfo AltY = new ConsoleKeyInfo((char)121, ConsoleKey.Y, false, true, false); - public static ConsoleKeyInfo AltZ = new ConsoleKeyInfo((char)122, ConsoleKey.Z, false, true, false); - - public static ConsoleKeyInfo CtrlA = new ConsoleKeyInfo((char)1, ConsoleKey.A, false, false, true); - public static ConsoleKeyInfo CtrlB = new ConsoleKeyInfo((char)2, ConsoleKey.B, false, false, true); - public static ConsoleKeyInfo CtrlC = new ConsoleKeyInfo((char)3, ConsoleKey.C, false, false, true); - public static ConsoleKeyInfo CtrlD = new ConsoleKeyInfo((char)4, ConsoleKey.D, false, false, true); - public static ConsoleKeyInfo CtrlE = new ConsoleKeyInfo((char)5, ConsoleKey.E, false, false, true); - public static ConsoleKeyInfo CtrlF = new ConsoleKeyInfo((char)6, ConsoleKey.F, false, false, true); - public static ConsoleKeyInfo CtrlG = new ConsoleKeyInfo((char)7, ConsoleKey.G, false, false, true); - public static ConsoleKeyInfo CtrlH = new ConsoleKeyInfo((char)8, ConsoleKey.H, false, false, true); - public static ConsoleKeyInfo CtrlI = new ConsoleKeyInfo((char)9, ConsoleKey.I, false, false, true); - public static ConsoleKeyInfo CtrlJ = new ConsoleKeyInfo((char)10, ConsoleKey.J, false, false, true); - public static ConsoleKeyInfo CtrlK = new ConsoleKeyInfo((char)11, ConsoleKey.K, false, false, true); - public static ConsoleKeyInfo CtrlL = new ConsoleKeyInfo((char)12, ConsoleKey.L, false, false, true); - public static ConsoleKeyInfo CtrlM = new ConsoleKeyInfo((char)13, ConsoleKey.M, false, false, true); - public static ConsoleKeyInfo CtrlN = new ConsoleKeyInfo((char)14, ConsoleKey.N, false, false, true); - public static ConsoleKeyInfo CtrlO = new ConsoleKeyInfo((char)15, ConsoleKey.O, false, false, true); - public static ConsoleKeyInfo CtrlP = new ConsoleKeyInfo((char)16, ConsoleKey.P, false, false, true); - public static ConsoleKeyInfo CtrlQ = new ConsoleKeyInfo((char)17, ConsoleKey.Q, false, false, true); - public static ConsoleKeyInfo CtrlR = new ConsoleKeyInfo((char)18, ConsoleKey.R, false, false, true); - public static ConsoleKeyInfo CtrlS = new ConsoleKeyInfo((char)19, ConsoleKey.S, false, false, true); - public static ConsoleKeyInfo CtrlT = new ConsoleKeyInfo((char)20, ConsoleKey.T, false, false, true); - public static ConsoleKeyInfo CtrlU = new ConsoleKeyInfo((char)21, ConsoleKey.U, false, false, true); - public static ConsoleKeyInfo CtrlV = new ConsoleKeyInfo((char)22, ConsoleKey.V, false, false, true); - public static ConsoleKeyInfo CtrlW = new ConsoleKeyInfo((char)23, ConsoleKey.W, false, false, true); - public static ConsoleKeyInfo CtrlX = new ConsoleKeyInfo((char)24, ConsoleKey.X, false, false, true); - public static ConsoleKeyInfo CtrlY = new ConsoleKeyInfo((char)25, ConsoleKey.Y, false, false, true); - public static ConsoleKeyInfo CtrlZ = new ConsoleKeyInfo((char)26, ConsoleKey.Z, false, false, true); - - public static ConsoleKeyInfo CtrlShiftC = new ConsoleKeyInfo((char)3, ConsoleKey.C, true, false, true); - - public static ConsoleKeyInfo AltShiftB = new ConsoleKeyInfo('B', ConsoleKey.B, true, true, false); - public static ConsoleKeyInfo AltShiftF = new ConsoleKeyInfo('F', ConsoleKey.F, true, true, false); - - public static ConsoleKeyInfo AltCtrlY = new ConsoleKeyInfo((char)0, ConsoleKey.Y, false, true, true); - - public static ConsoleKeyInfo Backspace = new ConsoleKeyInfo((char)8, ConsoleKey.Backspace, false, false, false); - public static ConsoleKeyInfo CtrlBackspace = new ConsoleKeyInfo((char)0x7f, ConsoleKey.Backspace, false, false, true); - public static ConsoleKeyInfo AltBackspace = new ConsoleKeyInfo((char)8, ConsoleKey.Backspace, false, true, false); - public static ConsoleKeyInfo Delete = new ConsoleKeyInfo((char)0, ConsoleKey.Delete, false, false, false); - public static ConsoleKeyInfo CtrlDelete = new ConsoleKeyInfo((char)0, ConsoleKey.Delete, false, false, true); - public static ConsoleKeyInfo DownArrow = new ConsoleKeyInfo((char)0, ConsoleKey.DownArrow, false, false, false); - public static ConsoleKeyInfo End = new ConsoleKeyInfo((char)0, ConsoleKey.End, false, false, false); - public static ConsoleKeyInfo CtrlEnd = new ConsoleKeyInfo((char)0, ConsoleKey.End, false, false, true); - public static ConsoleKeyInfo ShiftEnd = new ConsoleKeyInfo((char)0, ConsoleKey.End, true, false, false); - public static ConsoleKeyInfo Enter = new ConsoleKeyInfo((char)13, ConsoleKey.Enter, false, false, false); - public static ConsoleKeyInfo Escape = new ConsoleKeyInfo((char)27, ConsoleKey.Escape, false, false, false); - public static ConsoleKeyInfo Home = new ConsoleKeyInfo((char)0, ConsoleKey.Home, false, false, false); - public static ConsoleKeyInfo CtrlHome = new ConsoleKeyInfo((char)0, ConsoleKey.Home, false, false, true); - public static ConsoleKeyInfo ShiftHome = new ConsoleKeyInfo((char)0, ConsoleKey.Home, true, false, false); - public static ConsoleKeyInfo LeftArrow = new ConsoleKeyInfo((char)0, ConsoleKey.LeftArrow, false, false, false); - public static ConsoleKeyInfo RightArrow = new ConsoleKeyInfo((char)0, ConsoleKey.RightArrow, false, false, false); - public static ConsoleKeyInfo Tab = new ConsoleKeyInfo((char)9, ConsoleKey.Tab, false, false, false); - public static ConsoleKeyInfo UpArrow = new ConsoleKeyInfo((char)0, ConsoleKey.UpArrow, false, false, false); - public static ConsoleKeyInfo PageUp = new ConsoleKeyInfo((char)0, ConsoleKey.PageUp, false, false, false); - public static ConsoleKeyInfo PageDown = new ConsoleKeyInfo((char)0, ConsoleKey.PageDown, false, false, false); - public static ConsoleKeyInfo ShiftPageUp = new ConsoleKeyInfo((char)0, ConsoleKey.PageUp, true, false, false); - public static ConsoleKeyInfo ShiftPageDown = new ConsoleKeyInfo((char)0, ConsoleKey.PageDown, true, false, false); - public static ConsoleKeyInfo CtrlPageUp = new ConsoleKeyInfo((char)0, ConsoleKey.PageUp, false, false, true); - public static ConsoleKeyInfo CtrlPageDown = new ConsoleKeyInfo((char)0, ConsoleKey.PageDown, false, false, true); - public static ConsoleKeyInfo AltPageUp = new ConsoleKeyInfo((char)0, ConsoleKey.PageUp, false, true, false); - public static ConsoleKeyInfo AltPageDown = new ConsoleKeyInfo((char)0, ConsoleKey.PageDown, false, true, false); - - public static ConsoleKeyInfo ShiftLeftArrow = new ConsoleKeyInfo((char)0, ConsoleKey.LeftArrow, true, false, false); - public static ConsoleKeyInfo ShiftRightArrow = new ConsoleKeyInfo((char)0, ConsoleKey.RightArrow, true, false, false); - public static ConsoleKeyInfo CtrlLeftArrow = new ConsoleKeyInfo((char)0, ConsoleKey.LeftArrow, false, false, true); - public static ConsoleKeyInfo CtrlRightArrow = new ConsoleKeyInfo((char)0, ConsoleKey.RightArrow, false, false, true); - public static ConsoleKeyInfo ShiftCtrlLeftArrow = new ConsoleKeyInfo((char)0, ConsoleKey.LeftArrow, true, false, true); - public static ConsoleKeyInfo ShiftCtrlRightArrow = new ConsoleKeyInfo((char)0, ConsoleKey.RightArrow, true, false, true); - - public static ConsoleKeyInfo ShiftTab = new ConsoleKeyInfo((char)9, ConsoleKey.Tab, true, false, false); - - public static ConsoleKeyInfo CtrlEnter = new ConsoleKeyInfo((char)10, ConsoleKey.Enter, false, false, true); - public static ConsoleKeyInfo CtrlShiftEnter = new ConsoleKeyInfo((char)0, ConsoleKey.Enter, true, false, true); - public static ConsoleKeyInfo ShiftEnter = new ConsoleKeyInfo((char)13, ConsoleKey.Enter, true, false, false); - - public static ConsoleKeyInfo F1 = new ConsoleKeyInfo((char)0, ConsoleKey.F1, false, false, false); - public static ConsoleKeyInfo F2 = new ConsoleKeyInfo((char)0, ConsoleKey.F2, false, false, false); - public static ConsoleKeyInfo F3 = new ConsoleKeyInfo((char)0, ConsoleKey.F3, false, false, false); - public static ConsoleKeyInfo F4 = new ConsoleKeyInfo((char)0, ConsoleKey.F4, false, false, false); - public static ConsoleKeyInfo F5 = new ConsoleKeyInfo((char)0, ConsoleKey.F5, false, false, false); - public static ConsoleKeyInfo F6 = new ConsoleKeyInfo((char)0, ConsoleKey.F6, false, false, false); - public static ConsoleKeyInfo F7 = new ConsoleKeyInfo((char)0, ConsoleKey.F7, false, false, false); - public static ConsoleKeyInfo F8 = new ConsoleKeyInfo((char)0, ConsoleKey.F8, false, false, false); - public static ConsoleKeyInfo F9 = new ConsoleKeyInfo((char)0, ConsoleKey.F9, false, false, false); - public static ConsoleKeyInfo Fl0 = new ConsoleKeyInfo((char)0, ConsoleKey.F10, false, false, false); - public static ConsoleKeyInfo F11 = new ConsoleKeyInfo((char)0, ConsoleKey.F11, false, false, false); - public static ConsoleKeyInfo F12 = new ConsoleKeyInfo((char)0, ConsoleKey.F12, false, false, false); - public static ConsoleKeyInfo F13 = new ConsoleKeyInfo((char)0, ConsoleKey.F13, false, false, false); - public static ConsoleKeyInfo F14 = new ConsoleKeyInfo((char)0, ConsoleKey.F14, false, false, false); - public static ConsoleKeyInfo F15 = new ConsoleKeyInfo((char)0, ConsoleKey.F15, false, false, false); - public static ConsoleKeyInfo F16 = new ConsoleKeyInfo((char)0, ConsoleKey.F16, false, false, false); - public static ConsoleKeyInfo F17 = new ConsoleKeyInfo((char)0, ConsoleKey.F17, false, false, false); - public static ConsoleKeyInfo F18 = new ConsoleKeyInfo((char)0, ConsoleKey.F18, false, false, false); - public static ConsoleKeyInfo F19 = new ConsoleKeyInfo((char)0, ConsoleKey.F19, false, false, false); - public static ConsoleKeyInfo F20 = new ConsoleKeyInfo((char)0, ConsoleKey.F20, false, false, false); - public static ConsoleKeyInfo F21 = new ConsoleKeyInfo((char)0, ConsoleKey.F21, false, false, false); - public static ConsoleKeyInfo F22 = new ConsoleKeyInfo((char)0, ConsoleKey.F22, false, false, false); - public static ConsoleKeyInfo F23 = new ConsoleKeyInfo((char)0, ConsoleKey.F23, false, false, false); - public static ConsoleKeyInfo F24 = new ConsoleKeyInfo((char)0, ConsoleKey.F24, false, false, false); - - public static ConsoleKeyInfo AltF1 = new ConsoleKeyInfo((char)0, ConsoleKey.F1, false, true, false); - public static ConsoleKeyInfo AltF2 = new ConsoleKeyInfo((char)0, ConsoleKey.F2, false, true, false); - public static ConsoleKeyInfo AltF3 = new ConsoleKeyInfo((char)0, ConsoleKey.F3, false, true, false); - public static ConsoleKeyInfo AltF4 = new ConsoleKeyInfo((char)0, ConsoleKey.F4, false, true, false); - public static ConsoleKeyInfo AltF5 = new ConsoleKeyInfo((char)0, ConsoleKey.F5, false, true, false); - public static ConsoleKeyInfo AltF6 = new ConsoleKeyInfo((char)0, ConsoleKey.F6, false, true, false); - public static ConsoleKeyInfo AltF7 = new ConsoleKeyInfo((char)0, ConsoleKey.F7, false, true, false); - public static ConsoleKeyInfo AltF8 = new ConsoleKeyInfo((char)0, ConsoleKey.F8, false, true, false); - public static ConsoleKeyInfo AltF9 = new ConsoleKeyInfo((char)0, ConsoleKey.F9, false, true, false); - public static ConsoleKeyInfo AltFl0 = new ConsoleKeyInfo((char)0, ConsoleKey.F10, false, true, false); - public static ConsoleKeyInfo AltF11 = new ConsoleKeyInfo((char)0, ConsoleKey.F11, false, true, false); - public static ConsoleKeyInfo AltF12 = new ConsoleKeyInfo((char)0, ConsoleKey.F12, false, true, false); - public static ConsoleKeyInfo AltF13 = new ConsoleKeyInfo((char)0, ConsoleKey.F13, false, true, false); - public static ConsoleKeyInfo AltF14 = new ConsoleKeyInfo((char)0, ConsoleKey.F14, false, true, false); - public static ConsoleKeyInfo AltF15 = new ConsoleKeyInfo((char)0, ConsoleKey.F15, false, true, false); - public static ConsoleKeyInfo AltF16 = new ConsoleKeyInfo((char)0, ConsoleKey.F16, false, true, false); - public static ConsoleKeyInfo AltF17 = new ConsoleKeyInfo((char)0, ConsoleKey.F17, false, true, false); - public static ConsoleKeyInfo AltF18 = new ConsoleKeyInfo((char)0, ConsoleKey.F18, false, true, false); - public static ConsoleKeyInfo AltF19 = new ConsoleKeyInfo((char)0, ConsoleKey.F19, false, true, false); - public static ConsoleKeyInfo AltF20 = new ConsoleKeyInfo((char)0, ConsoleKey.F20, false, true, false); - public static ConsoleKeyInfo AltF21 = new ConsoleKeyInfo((char)0, ConsoleKey.F21, false, true, false); - public static ConsoleKeyInfo AltF22 = new ConsoleKeyInfo((char)0, ConsoleKey.F22, false, true, false); - public static ConsoleKeyInfo AltF23 = new ConsoleKeyInfo((char)0, ConsoleKey.F23, false, true, false); - public static ConsoleKeyInfo AltF24 = new ConsoleKeyInfo((char)0, ConsoleKey.F24, false, true, false); - - public static ConsoleKeyInfo ShiftF3 = new ConsoleKeyInfo((char)0, ConsoleKey.F3, true, false, false); - public static ConsoleKeyInfo ShiftF8 = new ConsoleKeyInfo((char)0, ConsoleKey.F8, true, false, false); - - // Keys to ignore - public static ConsoleKeyInfo VolumeUp = new ConsoleKeyInfo((char)0, ConsoleKey.VolumeUp, false, false, false); - public static ConsoleKeyInfo VolumeDown = new ConsoleKeyInfo((char)0, ConsoleKey.VolumeDown, false, false, false); - public static ConsoleKeyInfo VolumeMute = new ConsoleKeyInfo((char)0, ConsoleKey.VolumeMute, false, false, false); + static ConsoleKeyInfo Key(char c) + { + return new ConsoleKeyInfo(c, 0, shift: false, alt: false, control: false); + } + + static ConsoleKeyInfo Key(ConsoleKey key) + { + return new ConsoleKeyInfo('\0', key, shift: false, alt: false, control: false); + } + + static ConsoleKeyInfo Alt(char c) + { + return new ConsoleKeyInfo(c, 0, shift: false, alt: true, control: false); + } + + static ConsoleKeyInfo Alt(ConsoleKey key) + { + return new ConsoleKeyInfo('\0', key, shift: false, alt: true, control: false); + } + + static ConsoleKeyInfo Ctrl(char c) + { + return new ConsoleKeyInfo(c, 0, shift: false, alt: false, control: true); + } + + static ConsoleKeyInfo Ctrl(ConsoleKey key) + { + return new ConsoleKeyInfo('\0', key, shift: false, alt: false, control: true); + } + + static ConsoleKeyInfo Shift(ConsoleKey key) + { + return new ConsoleKeyInfo('\0', key, shift: true, alt: false, control: false); + } + + static ConsoleKeyInfo CtrlShift(char c) + { + return new ConsoleKeyInfo(c, 0, shift: true, alt: false, control: true); + } + + static ConsoleKeyInfo CtrlShift(ConsoleKey key) + { + return new ConsoleKeyInfo('\0', key, shift: true, alt: false, control: true); + } + + static ConsoleKeyInfo CtrlAlt(char c) + { + ConsoleKey key = 0; + bool shift = false; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var scan = VkKeyScan((short) c); + if ((scan & 0x100) != 0) { shift = true; } + key = (ConsoleKey)(scan & 0xff); + c = '\0'; + } + else + { + if (c >= 'a' && c <= 'z') + { + c = (char)(c - 'a' + 1); + } + else if (c >= 'A' && c <= 'Z') + { + c = (char)(c - 'A' + 1); + } + } + return new ConsoleKeyInfo(c, key, shift, alt: true, control: true); + } + + public static ConsoleKeyInfo F1 = Key(ConsoleKey.F1); + public static ConsoleKeyInfo F2 = Key(ConsoleKey.F2); + public static ConsoleKeyInfo F3 = Key(ConsoleKey.F3); + public static ConsoleKeyInfo F4 = Key(ConsoleKey.F4); + public static ConsoleKeyInfo F5 = Key(ConsoleKey.F5); + public static ConsoleKeyInfo F6 = Key(ConsoleKey.F6); + public static ConsoleKeyInfo F7 = Key(ConsoleKey.F7); + public static ConsoleKeyInfo F8 = Key(ConsoleKey.F8); + public static ConsoleKeyInfo F9 = Key(ConsoleKey.F9); + public static ConsoleKeyInfo Fl0 = Key(ConsoleKey.F10); + public static ConsoleKeyInfo F11 = Key(ConsoleKey.F11); + public static ConsoleKeyInfo F12 = Key(ConsoleKey.F12); + public static ConsoleKeyInfo F13 = Key(ConsoleKey.F13); + public static ConsoleKeyInfo F14 = Key(ConsoleKey.F14); + public static ConsoleKeyInfo F15 = Key(ConsoleKey.F15); + public static ConsoleKeyInfo F16 = Key(ConsoleKey.F16); + public static ConsoleKeyInfo F17 = Key(ConsoleKey.F17); + public static ConsoleKeyInfo F18 = Key(ConsoleKey.F18); + public static ConsoleKeyInfo F19 = Key(ConsoleKey.F19); + public static ConsoleKeyInfo F20 = Key(ConsoleKey.F20); + public static ConsoleKeyInfo F21 = Key(ConsoleKey.F21); + public static ConsoleKeyInfo F22 = Key(ConsoleKey.F22); + public static ConsoleKeyInfo F23 = Key(ConsoleKey.F23); + public static ConsoleKeyInfo F24 = Key(ConsoleKey.F24); + public static ConsoleKeyInfo _0 = Key('0'); + public static ConsoleKeyInfo _1 = Key('1'); + public static ConsoleKeyInfo _2 = Key('2'); + public static ConsoleKeyInfo _3 = Key('3'); + public static ConsoleKeyInfo _4 = Key('4'); + public static ConsoleKeyInfo _5 = Key('5'); + public static ConsoleKeyInfo _6 = Key('6'); + public static ConsoleKeyInfo _7 = Key('7'); + public static ConsoleKeyInfo _8 = Key('8'); + public static ConsoleKeyInfo _9 = Key('9'); + public static ConsoleKeyInfo A = Key('a'); + public static ConsoleKeyInfo B = Key('b'); + public static ConsoleKeyInfo C = Key('c'); + public static ConsoleKeyInfo D = Key('d'); + public static ConsoleKeyInfo E = Key('e'); + public static ConsoleKeyInfo F = Key('f'); + public static ConsoleKeyInfo G = Key('g'); + public static ConsoleKeyInfo H = Key('h'); + public static ConsoleKeyInfo I = Key('i'); + public static ConsoleKeyInfo J = Key('j'); + public static ConsoleKeyInfo K = Key('k'); + public static ConsoleKeyInfo L = Key('l'); + public static ConsoleKeyInfo M = Key('m'); + public static ConsoleKeyInfo N = Key('n'); + public static ConsoleKeyInfo O = Key('o'); + public static ConsoleKeyInfo P = Key('p'); + public static ConsoleKeyInfo Q = Key('q'); + public static ConsoleKeyInfo R = Key('r'); + public static ConsoleKeyInfo S = Key('s'); + public static ConsoleKeyInfo T = Key('t'); + public static ConsoleKeyInfo U = Key('u'); + public static ConsoleKeyInfo V = Key('v'); + public static ConsoleKeyInfo W = Key('w'); + public static ConsoleKeyInfo X = Key('x'); + public static ConsoleKeyInfo Y = Key('y'); + public static ConsoleKeyInfo Z = Key('z'); + public static ConsoleKeyInfo ucA = Key('A'); + public static ConsoleKeyInfo ucB = Key('B'); + public static ConsoleKeyInfo ucC = Key('C'); + public static ConsoleKeyInfo ucD = Key('D'); + public static ConsoleKeyInfo ucE = Key('E'); + public static ConsoleKeyInfo ucF = Key('F'); + public static ConsoleKeyInfo ucG = Key('G'); + public static ConsoleKeyInfo ucH = Key('H'); + public static ConsoleKeyInfo ucI = Key('I'); + public static ConsoleKeyInfo ucJ = Key('J'); + public static ConsoleKeyInfo ucK = Key('K'); + public static ConsoleKeyInfo ucL = Key('L'); + public static ConsoleKeyInfo ucM = Key('M'); + public static ConsoleKeyInfo ucN = Key('N'); + public static ConsoleKeyInfo ucO = Key('O'); + public static ConsoleKeyInfo ucP = Key('P'); + public static ConsoleKeyInfo ucQ = Key('Q'); + public static ConsoleKeyInfo ucR = Key('R'); + public static ConsoleKeyInfo ucS = Key('S'); + public static ConsoleKeyInfo ucT = Key('T'); + public static ConsoleKeyInfo ucU = Key('U'); + public static ConsoleKeyInfo ucV = Key('V'); + public static ConsoleKeyInfo ucW = Key('W'); + public static ConsoleKeyInfo ucX = Key('X'); + public static ConsoleKeyInfo ucY = Key('Y'); + public static ConsoleKeyInfo ucZ = Key('Z'); + public static ConsoleKeyInfo Bang = Key('!'); + public static ConsoleKeyInfo At = Key('@'); + public static ConsoleKeyInfo Pound = Key('#'); + public static ConsoleKeyInfo Dollar = Key('$'); + public static ConsoleKeyInfo Percent = Key('%'); + public static ConsoleKeyInfo Uphat = Key('^'); + public static ConsoleKeyInfo Ampersand = Key('&'); + public static ConsoleKeyInfo Star = Key('*'); + public static ConsoleKeyInfo LParen = Key('('); + public static ConsoleKeyInfo RParen = Key(')'); + public static ConsoleKeyInfo Colon = Key(':'); + public static ConsoleKeyInfo Semicolon = Key(';'); + public static ConsoleKeyInfo Question = Key('?'); + public static ConsoleKeyInfo Slash = Key('/'); + public static ConsoleKeyInfo Tilde = Key('~'); + public static ConsoleKeyInfo Backtick = Key('`'); + public static ConsoleKeyInfo LCurly = Key('{'); + public static ConsoleKeyInfo LBracket = Key('['); + public static ConsoleKeyInfo Pipe = Key('|'); + public static ConsoleKeyInfo Backslash = Key('\\'); + public static ConsoleKeyInfo RCurly = Key('}'); + public static ConsoleKeyInfo RBracket = Key(']'); + public static ConsoleKeyInfo SQuote = Key('\''); + public static ConsoleKeyInfo DQuote = Key('"'); + public static ConsoleKeyInfo LessThan = Key('<'); + public static ConsoleKeyInfo Comma = Key(','); + public static ConsoleKeyInfo GreaterThan = Key('>'); + public static ConsoleKeyInfo Period = Key('.'); + public static ConsoleKeyInfo Underbar = Key('_'); + public static ConsoleKeyInfo Minus = Key('-'); + public static ConsoleKeyInfo Plus = Key('+'); + public static ConsoleKeyInfo Eq = Key('='); + public static ConsoleKeyInfo Space = Key(' '); + public static ConsoleKeyInfo Backspace = Key(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\x08' : '\x7f'); + public static ConsoleKeyInfo Delete = Key(ConsoleKey.Delete); + public static ConsoleKeyInfo DownArrow = Key(ConsoleKey.DownArrow); + public static ConsoleKeyInfo End = Key(ConsoleKey.End); + public static ConsoleKeyInfo Enter = Key(ConsoleKey.Enter); + public static ConsoleKeyInfo Escape = Key('\x1b'); + public static ConsoleKeyInfo Home = Key(ConsoleKey.Home); + public static ConsoleKeyInfo LeftArrow = Key(ConsoleKey.LeftArrow); + public static ConsoleKeyInfo PageUp = Key(ConsoleKey.PageUp); + public static ConsoleKeyInfo PageDown = Key(ConsoleKey.PageDown); + public static ConsoleKeyInfo RightArrow = Key(ConsoleKey.RightArrow); + public static ConsoleKeyInfo Tab = Key(ConsoleKey.Tab); + public static ConsoleKeyInfo UpArrow = Key(ConsoleKey.UpArrow); + + + // Alt+Fn is unavailable or doesn't work on Linux. + // If you boot to a TTY, Alt+Fn switches to TTYn for n=1-6, + // otherwise the key is ignored (showkey -a never receives the key) + // If you boot to X, many Alt+Fn keys are bound by the system, + // those that aren't don't work because .Net doesn't handle them. + public static ConsoleKeyInfo AltF1 = Alt(ConsoleKey.F1); + public static ConsoleKeyInfo AltF2 = Alt(ConsoleKey.F2); + public static ConsoleKeyInfo AltF3 = Alt(ConsoleKey.F3); + public static ConsoleKeyInfo AltF4 = Alt(ConsoleKey.F4); + public static ConsoleKeyInfo AltF5 = Alt(ConsoleKey.F5); + public static ConsoleKeyInfo AltF6 = Alt(ConsoleKey.F6); + public static ConsoleKeyInfo AltF7 = Alt(ConsoleKey.F7); + public static ConsoleKeyInfo AltF8 = Alt(ConsoleKey.F8); + public static ConsoleKeyInfo AltF9 = Alt(ConsoleKey.F9); + public static ConsoleKeyInfo AltFl0 = Alt(ConsoleKey.F10); + public static ConsoleKeyInfo AltF11 = Alt(ConsoleKey.F11); + public static ConsoleKeyInfo AltF12 = Alt(ConsoleKey.F12); + public static ConsoleKeyInfo AltF13 = Alt(ConsoleKey.F13); + public static ConsoleKeyInfo AltF14 = Alt(ConsoleKey.F14); + public static ConsoleKeyInfo AltF15 = Alt(ConsoleKey.F15); + public static ConsoleKeyInfo AltF16 = Alt(ConsoleKey.F16); + public static ConsoleKeyInfo AltF17 = Alt(ConsoleKey.F17); + public static ConsoleKeyInfo AltF18 = Alt(ConsoleKey.F18); + public static ConsoleKeyInfo AltF19 = Alt(ConsoleKey.F19); + public static ConsoleKeyInfo AltF20 = Alt(ConsoleKey.F20); + public static ConsoleKeyInfo AltF21 = Alt(ConsoleKey.F21); + public static ConsoleKeyInfo AltF22 = Alt(ConsoleKey.F22); + public static ConsoleKeyInfo AltF23 = Alt(ConsoleKey.F23); + public static ConsoleKeyInfo AltF24 = Alt(ConsoleKey.F24); + public static ConsoleKeyInfo Alt0 = Alt('0'); + public static ConsoleKeyInfo Alt1 = Alt('1'); + public static ConsoleKeyInfo Alt2 = Alt('2'); + public static ConsoleKeyInfo Alt3 = Alt('3'); + public static ConsoleKeyInfo Alt4 = Alt('4'); + public static ConsoleKeyInfo Alt5 = Alt('5'); + public static ConsoleKeyInfo Alt6 = Alt('6'); + public static ConsoleKeyInfo Alt7 = Alt('7'); + public static ConsoleKeyInfo Alt8 = Alt('8'); + public static ConsoleKeyInfo Alt9 = Alt('9'); + public static ConsoleKeyInfo AltA = Alt('a'); + public static ConsoleKeyInfo AltB = Alt('b'); + public static ConsoleKeyInfo AltC = Alt('c'); + public static ConsoleKeyInfo AltD = Alt('d'); + public static ConsoleKeyInfo AltE = Alt('e'); + public static ConsoleKeyInfo AltF = Alt('f'); + public static ConsoleKeyInfo AltG = Alt('g'); + public static ConsoleKeyInfo AltH = Alt('h'); + public static ConsoleKeyInfo AltI = Alt('i'); + public static ConsoleKeyInfo AltJ = Alt('j'); + public static ConsoleKeyInfo AltK = Alt('k'); + public static ConsoleKeyInfo AltL = Alt('l'); + public static ConsoleKeyInfo AltM = Alt('m'); + public static ConsoleKeyInfo AltN = Alt('n'); + public static ConsoleKeyInfo AltO = Alt('o'); + public static ConsoleKeyInfo AltP = Alt('p'); + public static ConsoleKeyInfo AltQ = Alt('q'); + public static ConsoleKeyInfo AltR = Alt('r'); + public static ConsoleKeyInfo AltS = Alt('s'); + public static ConsoleKeyInfo AltT = Alt('t'); + public static ConsoleKeyInfo AltU = Alt('u'); + public static ConsoleKeyInfo AltV = Alt('v'); + public static ConsoleKeyInfo AltW = Alt('w'); + public static ConsoleKeyInfo AltX = Alt('x'); + public static ConsoleKeyInfo AltY = Alt('y'); + public static ConsoleKeyInfo AltZ = Alt('z'); + public static ConsoleKeyInfo AltShiftB = Alt('B'); + public static ConsoleKeyInfo AltShiftF = Alt('F'); + public static ConsoleKeyInfo AltSpace = Alt(' '); // !Windows, system menu. + public static ConsoleKeyInfo AltPeriod = Alt('.'); // !Linux, CLR bug + public static ConsoleKeyInfo AltEquals = Alt('='); // !Linux, CLR bug + public static ConsoleKeyInfo AltMinus = Alt('-'); + public static ConsoleKeyInfo AltUnderbar = Alt('_'); // !Linux, CLR bug + public static ConsoleKeyInfo AltBackspace = Alt(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\x08' : '\x7f'); + public static ConsoleKeyInfo AltLess = Alt('<'); // !Linux, CLR bug + public static ConsoleKeyInfo AltGreater = Alt('>'); // !Linux, CLR bug + public static ConsoleKeyInfo AltQuestion = Alt('?'); // !Linux, CLR bug + + public static ConsoleKeyInfo CtrlAt = Ctrl('\x00'); + public static ConsoleKeyInfo CtrlSpace = Ctrl(' '); // !Linux, same as CtrlAt + public static ConsoleKeyInfo CtrlA = Ctrl('\x01'); + public static ConsoleKeyInfo CtrlB = Ctrl('\x02'); + public static ConsoleKeyInfo CtrlC = Ctrl('\x03'); + public static ConsoleKeyInfo CtrlD = Ctrl('\x04'); + public static ConsoleKeyInfo CtrlE = Ctrl('\x05'); + public static ConsoleKeyInfo CtrlF = Ctrl('\x06'); + public static ConsoleKeyInfo CtrlG = Ctrl('\a'); + public static ConsoleKeyInfo CtrlH = Ctrl('\b'); + public static ConsoleKeyInfo CtrlI = Ctrl('\t'); + public static ConsoleKeyInfo CtrlJ = Ctrl('\n'); + public static ConsoleKeyInfo CtrlK = Ctrl('\v'); + public static ConsoleKeyInfo CtrlL = Ctrl('\f'); + public static ConsoleKeyInfo CtrlM = Ctrl('\r'); + public static ConsoleKeyInfo CtrlN = Ctrl('\x0e'); + public static ConsoleKeyInfo CtrlO = Ctrl('\x0f'); + public static ConsoleKeyInfo CtrlP = Ctrl('\x10'); + public static ConsoleKeyInfo CtrlQ = Ctrl('\x11'); + public static ConsoleKeyInfo CtrlR = Ctrl('\x12'); + public static ConsoleKeyInfo CtrlS = Ctrl('\x13'); + public static ConsoleKeyInfo CtrlT = Ctrl('\x14'); + public static ConsoleKeyInfo CtrlU = Ctrl('\x15'); + public static ConsoleKeyInfo CtrlV = Ctrl('\x16'); + public static ConsoleKeyInfo CtrlW = Ctrl('\x17'); + public static ConsoleKeyInfo CtrlX = Ctrl('\x18'); + public static ConsoleKeyInfo CtrlY = Ctrl('\x19'); + public static ConsoleKeyInfo CtrlZ = Ctrl('\x1a'); + public static ConsoleKeyInfo CtrlLBracket = Ctrl('\x1b'); + public static ConsoleKeyInfo CtrlBackslash = Ctrl('\x1c'); + public static ConsoleKeyInfo CtrlRBracket = Ctrl('\x1d'); + public static ConsoleKeyInfo CtrlUnderbar = Ctrl('\x1f'); + public static ConsoleKeyInfo CtrlBackspace = Ctrl(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\x7f' : '\x08'); + public static ConsoleKeyInfo CtrlDelete = Ctrl(ConsoleKey.Delete); // !Linux + public static ConsoleKeyInfo CtrlEnd = Ctrl(ConsoleKey.End); // !Linux + public static ConsoleKeyInfo CtrlHome = Ctrl(ConsoleKey.Home); // !Linux + public static ConsoleKeyInfo CtrlPageUp = Ctrl(ConsoleKey.PageUp); // !Linux + public static ConsoleKeyInfo CtrlPageDown = Ctrl(ConsoleKey.PageDown); // !Linux + public static ConsoleKeyInfo CtrlLeftArrow = Ctrl(ConsoleKey.LeftArrow); // !Linux + public static ConsoleKeyInfo CtrlRightArrow = Ctrl(ConsoleKey.RightArrow); // !Linux + public static ConsoleKeyInfo CtrlEnter = Ctrl(ConsoleKey.Enter); // !Linux + + + public static ConsoleKeyInfo ShiftF3 = Shift(ConsoleKey.F3); + public static ConsoleKeyInfo ShiftF8 = Shift(ConsoleKey.F8); + public static ConsoleKeyInfo ShiftEnd = Shift(ConsoleKey.End); + public static ConsoleKeyInfo ShiftEnter = Shift(ConsoleKey.Enter); + public static ConsoleKeyInfo ShiftHome = Shift(ConsoleKey.Home); + public static ConsoleKeyInfo ShiftPageUp = Shift(ConsoleKey.PageUp); + public static ConsoleKeyInfo ShiftPageDown = Shift(ConsoleKey.PageDown); + public static ConsoleKeyInfo ShiftLeftArrow = Shift(ConsoleKey.LeftArrow); + public static ConsoleKeyInfo ShiftRightArrow = Shift(ConsoleKey.RightArrow); + public static ConsoleKeyInfo ShiftTab = Shift(ConsoleKey.Tab); // !Linux, same as Tab + + public static ConsoleKeyInfo CtrlShiftC = CtrlShift('\x03'); // !Linux + public static ConsoleKeyInfo CtrlShiftEnter = CtrlShift(ConsoleKey.Enter); + public static ConsoleKeyInfo CtrlShiftLeftArrow = CtrlShift(ConsoleKey.LeftArrow); + public static ConsoleKeyInfo CtrlShiftRightArrow = CtrlShift(ConsoleKey.RightArrow); + + public static ConsoleKeyInfo CtrlAltY = CtrlAlt('y'); + public static ConsoleKeyInfo CtrlAltRBracket = CtrlAlt(']'); + public static ConsoleKeyInfo CtrlAltQuestion = CtrlAlt('?'); + + + [DllImport("user32.dll")] + public static extern int VkKeyScan(short wAsciiVal); + + struct KeyWithModifiers + { + public KeyWithModifiers(ConsoleKey key, ConsoleModifiers mods = 0) + { + Key = key; + Modifiers = mods; + } + public ConsoleKey Key { get; } + public ConsoleModifiers Modifiers { get; } + + public override bool Equals(object other) + { + if (!(other is KeyWithModifiers)) return false; + var obj = (KeyWithModifiers)other; + return Key == obj.Key && Modifiers == obj.Modifiers; + } + + public override int GetHashCode() + { + int h1 = Key.GetHashCode(); + int h2 = Modifiers.GetHashCode(); + // This is based on Tuple.GetHashCode + return unchecked(((h1 << 5) + h1) ^ h2); + } + } + + private static readonly Dictionary CtrlKeyToKeyCharMap + = new Dictionary(); + + static Keys() + { + int i; + for (i = 0; i < 26; i++) + { + CtrlKeyToKeyCharMap.Add(new KeyWithModifiers(ConsoleKey.A + i), (char)(i + 1)); + } + + for (i = 0; i < 10; i++) + { + char c = i == 2 ? '\0' : i == 6 ? '\x1e' : (char) ('0' + i); + CtrlKeyToKeyCharMap.Add(new KeyWithModifiers(ConsoleKey.D0 + i), c); + CtrlKeyToKeyCharMap.Add(new KeyWithModifiers(ConsoleKey.NumPad0 + i), c); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // These mapping are needed to support different keyboard + // layouts, e.g. ']' is Oem6 with an English keyboard, but + // Oem5 with a Portuguese keyboard. + foreach (char c in "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?") + { + var scan = VkKeyScan((short) c); + ConsoleModifiers mods = 0; + if ((scan & 0x100) != 0) { mods |= ConsoleModifiers.Shift; } + if ((scan & 0x200) != 0) { mods |= ConsoleModifiers.Control; } + if ((scan & 0x400) != 0) { mods |= ConsoleModifiers.Alt; } + var key = new KeyWithModifiers((ConsoleKey)(scan & 0xff), mods); + CtrlKeyToKeyCharMap.Add(key, c); + } + } + } + + internal static bool IgnoreKeyChar(this ConsoleKeyInfo key) + { + switch (key.Key) + { + case ConsoleKey.F1: + case ConsoleKey.F2: + case ConsoleKey.F3: + case ConsoleKey.F4: + case ConsoleKey.F5: + case ConsoleKey.F6: + case ConsoleKey.F7: + case ConsoleKey.F8: + case ConsoleKey.F9: + case ConsoleKey.F10: + case ConsoleKey.F11: + case ConsoleKey.F12: + case ConsoleKey.F13: + case ConsoleKey.F14: + case ConsoleKey.F15: + case ConsoleKey.F16: + case ConsoleKey.F17: + case ConsoleKey.F18: + case ConsoleKey.F19: + case ConsoleKey.F20: + case ConsoleKey.F21: + case ConsoleKey.F22: + case ConsoleKey.F23: + case ConsoleKey.F24: + case ConsoleKey.Delete: + case ConsoleKey.DownArrow: + case ConsoleKey.End: + case ConsoleKey.Enter: + case ConsoleKey.Home: + case ConsoleKey.LeftArrow: + case ConsoleKey.PageUp: + case ConsoleKey.PageDown: + case ConsoleKey.RightArrow: + case ConsoleKey.Tab: + case ConsoleKey.UpArrow: + return true; + } + + return false; + } + + private static ConsoleModifiers NormalizeModifiers(this ConsoleKeyInfo key) + { + var result = key.Modifiers; + if (!char.IsControl(key.KeyChar)) + { + // Ignore Shift state unless it's a control character. + result = result & ~ConsoleModifiers.Shift; + } + + return result; + } + + internal static char NormalizeKeyChar(this ConsoleKeyInfo key) + { + if (key.KeyChar == '\0' && (key.Modifiers & ConsoleModifiers.Control) != 0) + { + // On Windows, ConsoleKeyInfo.Key is something we wanted to ignore, but + // a few bindings force us to do something with it. + var mods = key.Modifiers & ConsoleModifiers.Shift; + var keyWithMods = new KeyWithModifiers(key.Key, mods); + if (CtrlKeyToKeyCharMap.TryGetValue(keyWithMods, out var keyChar)) + return keyChar; + + if (key.Key >= ConsoleKey.A && key.Key <= ConsoleKey.Z) + { + return (char)(key.Key - ConsoleKey.A + 1); + } + + } + return key.KeyChar; + } + + internal static bool EqualsNormalized(this ConsoleKeyInfo x, ConsoleKeyInfo y) + { + // In the common case, we mask something out of the ConsoleKeyInfo + // e.g. Shift or somewhat meaningless Key (like Oem6) which might vary + // in different keyboard layouts. + // + // That said, if all fields compare, it's a match and we return that. + if (x.Key == y.Key && x.KeyChar == y.KeyChar && x.Modifiers == y.Modifiers) + return true; + + // We ignore Shift state as that can vary in different keyboard layouts. + var xMods = NormalizeModifiers(x); + var yMods = NormalizeModifiers(y); + + if (xMods != yMods) + return false; + + // If we don't have a character, we probably masked it out (except for Ctrl+@) + // when building our key bindings, so compare Key instead. + return x.IgnoreKeyChar() || y.IgnoreKeyChar() + ? x.Key == y.Key + : x.NormalizeKeyChar() == y.NormalizeKeyChar(); + } + + internal static int GetNormalizedHashCode(this ConsoleKeyInfo obj) + { + // Because a comparison of two ConsoleKeyInfo objects is a comparison of the + // combination of the ConsoleKey and Modifiers, we must combine their hashes. + // Note that if the ConsoleKey is default, we must fall back to the KeyChar, + // otherwise every non-special key will compare as the same. + int h1 = obj.IgnoreKeyChar() + ? obj.Key.GetHashCode() + : obj.NormalizeKeyChar().GetHashCode(); + + int h2 = NormalizeModifiers(obj).GetHashCode(); + + // This is based on Tuple.GetHashCode + return unchecked(((h1 << 5) + h1) ^ h2); + } + + internal static bool ShouldInsert(this ConsoleKeyInfo key) + { + return key.KeyChar != '\0' && + (key.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) == 0; + } + + internal static bool IsUnmodifiedChar(this ConsoleKeyInfo key, char c) + { + return key.KeyChar == c && + (key.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) == 0; + } + + public static string ToGestureString(this ConsoleKeyInfo key) + { + var useKeyEnum = key.IgnoreKeyChar(); + + var mods = key.Modifiers; + + var sb = new StringBuilder(); + + if (useKeyEnum && (mods & ConsoleModifiers.Shift) != 0) + { + sb.Append("Shift"); + } + + if ((mods & ConsoleModifiers.Control) != 0) + { + if (sb.Length > 0) sb.Append("+"); + sb.Append("Ctrl"); + } + + if ((mods & ConsoleModifiers.Alt) != 0) + { + if (sb.Length > 0) sb.Append("+"); + sb.Append("Alt"); + } + + if (sb.Length > 0) sb.Append("+"); + + if (useKeyEnum) + { + sb.Append(key.Key); + } + else + { + var c = key.NormalizeKeyChar(); + string s; + switch (c) + { + case ' ' : s = "Space"; break; + case '\x1b': s = "Escape"; break; + case '\x1c': s = "\\"; break; + case '\x1d': s = "]"; break; + case '\x1f': s = "_"; break; + case '\x7f': s = "Backspace"; break; + case '\x08': + s = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Backspace" : "Ctrl+Backspace"; + break; + + case char _ when c <= 26: + s = ((char) ('a' + c - 1)).ToString(); + break; + + default: + s = c.ToString(); + break; + } + sb.Append(s); + } + + return sb.ToString(); + } } } diff --git a/PSReadLine/KillYank.cs b/PSReadLine/KillYank.cs index 3c35d9cd3..88015c4a8 100644 --- a/PSReadLine/KillYank.cs +++ b/PSReadLine/KillYank.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Management.Automation.Language; -using System.Windows.Forms; +using Microsoft.PowerShell.Internal; namespace Microsoft.PowerShell { @@ -29,7 +29,7 @@ class YankLastArgState private int _visualSelectionCommandCount; /// - /// Mark the current loction of the cursor for use in a subsequent editing command. + /// Mark the current location of the cursor for use in a subsequent editing command. /// public static void SetMark(ConsoleKeyInfo? key = null, object arg = null) { @@ -444,13 +444,7 @@ public static void SelectBackwardsLine(ConsoleKeyInfo? key = null, object arg = /// public static void Paste(ConsoleKeyInfo? key = null, object arg = null) { - string textToPaste = null; - ExecuteOnSTAThread(() => { - if (Clipboard.ContainsText()) - { - textToPaste = Clipboard.GetText(); - } - }); + string textToPaste = Clipboard.GetText(); if (textToPaste != null) { @@ -484,7 +478,7 @@ public static void Copy(ConsoleKeyInfo? key = null, object arg = null) } if (!string.IsNullOrEmpty(textToSet)) { - ExecuteOnSTAThread(() => Clipboard.SetText(textToSet)); + Clipboard.SetText(textToSet); } } @@ -511,7 +505,7 @@ public static void Cut(ConsoleKeyInfo? key = null, object arg = null) if (_singleton._visualSelectionCommandCount > 0) { _singleton.GetRegion(out var start, out var length); - ExecuteOnSTAThread(() => Clipboard.SetText(_singleton._buffer.ToString(start, length))); + Clipboard.SetText(_singleton._buffer.ToString(start, length)); Delete(start, length); } } diff --git a/PSReadLine/Movement.cs b/PSReadLine/Movement.cs index d4b034024..26d279f75 100644 --- a/PSReadLine/Movement.cs +++ b/PSReadLine/Movement.cs @@ -95,25 +95,25 @@ private void MoveToLine(int numericArg) const int endOfLine = int.MaxValue; _moveToLineCommandCount += 1; - var coords = ConvertOffsetToCoordinates(_current); + var point = ConvertOffsetToPoint(_current); if (_moveToLineCommandCount == 1) { _moveToLineDesiredColumn = (_current == _buffer.Length || _buffer[_current] == '\n') ? endOfLine - : coords.X; + : point.X; } - var topLine = _initialY + Options.ExtraPromptLineCount; + var topLine = _initialY; - var newY = coords.Y + numericArg; - coords.Y = (short)Math.Max(newY, topLine); + var newY = point.Y + numericArg; + point.Y = Math.Max(newY, topLine); if (_moveToLineDesiredColumn != endOfLine) { - coords.X = (short)_moveToLineDesiredColumn; + point.X = _moveToLineDesiredColumn; } - var newCurrent = ConvertLineAndColumnToOffset(coords); + var newCurrent = ConvertLineAndColumnToOffset(point); if (newCurrent != -1) { if (_moveToLineDesiredColumn == endOfLine) @@ -420,7 +420,7 @@ private static char TryGetArgAsChar(object arg) return s[0]; } - return (char)0; + return '\0'; } /// @@ -438,7 +438,7 @@ public static void CharacterSearch(ConsoleKeyInfo? key = null, object arg = null } char toFind = TryGetArgAsChar(arg); - if (toFind == (char)0) + if (toFind == '\0') { // Should we prompt? toFind = ReadKey().KeyChar; @@ -476,7 +476,7 @@ public static void CharacterSearchBackward(ConsoleKeyInfo? key = null, object ar } char toFind = TryGetArgAsChar(arg); - if (toFind == (char)0) + if (toFind == '\0') { // Should we prompt? toFind = ReadKey().KeyChar; diff --git a/PSReadLine/Movement.vi.cs b/PSReadLine/Movement.vi.cs index 13823ae4c..7e639ff0c 100644 --- a/PSReadLine/Movement.vi.cs +++ b/PSReadLine/Movement.vi.cs @@ -97,6 +97,9 @@ public static void ViNextGlob(ConsoleKeyInfo? key = null, object arg = null) _singleton.MoveCursor(Math.Min(i, _singleton._buffer.Length - 1)); } + /// + /// Moves the cursor to the end of the word, using only white space as delimiters. + /// public static void ViEndOfGlob(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) @@ -116,6 +119,9 @@ public static void ViEndOfGlob(ConsoleKeyInfo? key = null, object arg = null) } } + /// + /// Moves to the end of the previous word, using only white space as a word delimiter. + /// public static void ViEndOfPreviousGlob(ConsoleKeyInfo? key = null, object arg = null) { if (!TryGetArgAsInt(arg, out var numericArg, 1)) diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 2670c8175..08011b36f 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -19,33 +19,25 @@ public partial class PSConsoleReadLine private void SetOptionsInternal(SetPSReadlineOption options) { - if (options.ContinuationPrompt != null) - { - Options.ContinuationPrompt = options.ContinuationPrompt; - } - if (options._continuationPromptForegroundColor.HasValue) - { - Options.ContinuationPromptForegroundColor = options.ContinuationPromptForegroundColor; - } - if (options._continuationPromptBackgroundColor.HasValue) + if (options.ResetTokenColors) { - Options.ContinuationPromptBackgroundColor = options.ContinuationPromptBackgroundColor; + Options.ResetColors(); } - if (options._emphasisBackgroundColor.HasValue) + if (options.ContinuationPrompt != null) { - Options.EmphasisBackgroundColor = options.EmphasisBackgroundColor; + Options.ContinuationPrompt = options.ContinuationPrompt; } - if (options._emphasisForegroundColor.HasValue) + if (options.ContinuationPromptColor != null) { - Options.EmphasisForegroundColor = options.EmphasisForegroundColor; + Options.ContinuationPromptColor = options.ContinuationPromptColor; } - if (options._errorBackgroundColor.HasValue) + if (options.EmphasisColor != null) { - Options.ErrorBackgroundColor = options.ErrorBackgroundColor; + Options.EmphasisColor = options.EmphasisColor; } - if (options._errorForegroundColor.HasValue) + if (options.ErrorColor != null) { - Options.ErrorForegroundColor = options.ErrorForegroundColor; + Options.ErrorColor = options.ErrorColor; } if (options._historyNoDuplicates.HasValue) { @@ -93,18 +85,7 @@ private void SetOptionsInternal(SetPSReadlineOption options) // Switching/resetting modes - clear out chord dispatch table _chordDispatchTable.Clear(); - switch (options._editMode) - { - case EditMode.Emacs: - SetDefaultEmacsBindings(); - break; - case EditMode.Vi: - SetDefaultViBindings(); - break; - case EditMode.Windows: - SetDefaultWindowsBindings(); - break; - } + SetDefaultBindings(Options.EditMode); } if (options._showToolTips.HasValue) { @@ -142,12 +123,10 @@ private void SetOptionsInternal(SetPSReadlineOption options) { Options.HistorySaveStyle = options.HistorySaveStyle; } - #region vi if (options._viModeIndicator.HasValue) { Options.ViModeIndicator = options.ViModeIndicator; } - #endregion if (options.HistorySavePath != null) { Options.HistorySavePath = options.HistorySavePath; @@ -155,19 +134,15 @@ private void SetOptionsInternal(SetPSReadlineOption options) _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); _historyFileLastSavedSize = 0; } - if (options.ResetTokenColors) + if (options.PromptText != null) { - Options.ResetColors(); + Options.PromptText = options.PromptText; } if (options._tokenKind.HasValue) { - if (options._foregroundColor.HasValue) - { - Options.SetForegroundColor(options.TokenKind, options.ForegroundColor); - } - if (options._backgroundColor.HasValue) + if (options.Color != null) { - Options.SetBackgroundColor(options.TokenKind, options.BackgroundColor); + Options.SetColor(options.TokenKind, options.Color); } } } @@ -186,7 +161,7 @@ private void SetKeyHandlerInternal(string[] keys, Action(); + secondDispatchTable = new Dictionary(ConsoleKeyInfoComparer.Instance); _chordDispatchTable[chord[0]] = secondDispatchTable; } secondDispatchTable[chord[1]] = MakeKeyHandler(handler, briefDescription, longDescription, scriptBlock); @@ -275,7 +250,6 @@ public static void SetKeyHandler(string[] key, Action h /// /// Helper function for the Remove-PSReadlineKeyHandler cmdlet. /// - /// public static void RemoveKeyHandler(string[] key) { _singleton.RemoveKeyHandlerInternal(key); diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index a56a67f9e..b39f1926f 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -1,5 +1,5 @@  - + Debug @@ -7,11 +7,12 @@ {615788CB-1B9A-4B34-97B3-4608686E59CA} Library Properties - Microsoft.PowerShell + Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine - v4.0 + v4.6.1 512 - Client + + true @@ -38,10 +39,12 @@ - packages\Microsoft.PowerShell.3.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll + packages\Microsoft.PowerShell.5.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll True - + + packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + @@ -51,10 +54,10 @@ + - @@ -67,6 +70,7 @@ + True True @@ -91,9 +95,9 @@ ResXFileCodeGenerator - Microsoft.PowerShell.PSReadLineResources.resources + Microsoft.PowerShell.PSReadLine.PSReadLineResources.resources PSReadlineResources.Designer.cs - Microsoft.PowerShell + Microsoft.PowerShell.PSReadLine Designer diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml new file mode 100644 index 000000000..9b4db4797 --- /dev/null +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -0,0 +1,137 @@ + + + + PSReadLine-Options + + Microsoft.PowerShell.PSConsoleReadlineOptions + + + + + + + EditMode + + + AddToHistoryHandler + + + HistoryNoDuplicates + + + HistorySavePath + + + HistorySaveStyle + + + HistorySearchCaseSensitive + + + HistorySearchCursorMovesToEnd + + + MaximumHistoryCount + + + ContinuationPrompt + + + ExtraPromptLineCount + + + PromptText + + + BellStyle + + + DingDuration + + + DingTone + + + CommandsToValidateScriptBlockArguments + + + CommandValidationHandler + + + CompletionQueryItems + + + MaximumKillRingCount + + + ShowToolTips + + + ViModeIndicator + + + WordDelimiters + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.CommandColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.CommentColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.ContinuationPromptColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.DefaultTokenColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.EmphasisColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.ErrorColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.KeywordColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.MemberColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.NumberColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.OperatorColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.ParameterColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.StringColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.TypeColor) + + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.VariableColor) + + + + + + + + diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 28e0f3cfd..3591f6cd6 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,18 +1,19 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '1.2' +ModuleVersion = '2.0' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' Copyright = '(c) Microsoft Corporation. All rights reserved.' Description = 'Great command line editing in the PowerShell console host' -PowerShellVersion = '3.0' -DotNetFrameworkVersion = '4.0' -CLRVersion = '4.0' +PowerShellVersion = '5.0' +DotNetFrameworkVersion = '4.6.1' +CLRVersion = '4.0.0' +FormatsToProcess = 'PSReadLine.format.ps1xml' AliasesToExport = @() FunctionsToExport = 'PSConsoleHostReadline' CmdletsToExport = 'Get-PSReadlineKeyHandler','Set-PSReadlineKeyHandler','Remove-PSReadlineKeyHandler', 'Get-PSReadlineOption','Set-PSReadlineOption' -HelpInfoURI = 'http://go.microsoft.com/fwlink/?LinkId=528806' +HelpInfoURI = 'https://go.microsoft.com/fwlink/?LinkId=528806' } diff --git a/PSReadLine/PSReadLine.sln b/PSReadLine/PSReadLine.sln index bfdff5961..b45d05b01 100644 --- a/PSReadLine/PSReadLine.sln +++ b/PSReadLine/PSReadLine.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.12 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine", "PSReadLine.csproj", "{615788CB-1B9A-4B34-97B3-4608686E59CA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPSReadLine", "..\TestPSReadLine\TestPSReadLine.csproj", "{08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}" @@ -9,24 +11,41 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestPSReadLine", "..\Un EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + CodeCoverage|Any CPU = CodeCoverage|Any CPU Debug|Any CPU = Debug|Any CPU + Linux|Any CPU = Linux|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.Build.0 = Release|Any CPU {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.Build.0 = Release|Any CPU {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.ActiveCfg = Release|Any CPU {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.Build.0 = Release|Any CPU {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.ActiveCfg = Release|Any CPU {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA13C704-483F-4CE4-A3FB-8F79295F1071} + EndGlobalSection EndGlobal diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 1470cd00f..9c6ed0c6f 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.PowerShell { +namespace Microsoft.PowerShell.PSReadLine { using System; @@ -19,7 +19,7 @@ namespace Microsoft.PowerShell { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class PSReadLineResources { @@ -39,7 +39,7 @@ internal PSReadLineResources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerShell.PSReadLineResources", typeof(PSReadLineResources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PowerShell.PSReadLine.PSReadLineResources", typeof(PSReadLineResources).Assembly); resourceMan = temp; } return resourceMan; @@ -106,7 +106,7 @@ internal static string BackwardCharDescription { } /// - /// Looks up a localized string similar to Delete the charcter before the cursor. + /// Looks up a localized string similar to Delete the character before the cursor. /// internal static string BackwardDeleteCharDescription { get { @@ -322,7 +322,7 @@ internal static string DeleteBraceDescription { } /// - /// Looks up a localized string similar to Delete the character under the cusor. + /// Looks up a localized string similar to Delete the character under the cursor. /// internal static string DeleteCharDescription { get { @@ -331,7 +331,7 @@ internal static string DeleteCharDescription { } /// - /// Looks up a localized string similar to Delete the character under the cusor, or if the line is empty, exit the process.. + /// Looks up a localized string similar to Delete the character under the cursor, or if the line is empty, exit the process.. /// internal static string DeleteCharOrExitDescription { get { @@ -502,7 +502,7 @@ internal static string GotoBraceDescription { } /// - /// Looks up a localized string similar to Moves the cursor to the perscribed column.. + /// Looks up a localized string similar to Moves the cursor to the prescribed column.. /// internal static string GotoColumnDescription { get { @@ -594,6 +594,15 @@ internal static string InsertLineBelowDescription { } } + /// + /// Looks up a localized string similar to Parameter must be a ConsoleColor, ANSI escape sequence, or RGB value with optional leading '#'.. + /// + internal static string InvalidColorParameter { + get { + return ResourceManager.GetString("InvalidColorParameter", resourceCulture); + } + } + /// /// Looks up a localized string similar to Duplicate or invalid modifier token '{0}' for key '{1}'.. /// @@ -990,7 +999,7 @@ internal static string ScrollDisplayUpLineDescription { } /// - /// Looks up a localized string similar to Searches backward for the perscribed character.. + /// Looks up a localized string similar to Searches backward for the prescribed character.. /// internal static string SearchBackwardCharDescription { get { @@ -999,7 +1008,7 @@ internal static string SearchBackwardCharDescription { } /// - /// Looks up a localized string similar to Move to the previous occurance of the specified character.. + /// Looks up a localized string similar to Move to the previous occurrence of the specified character.. /// internal static string SearchCharBackwardDescription { get { @@ -1008,7 +1017,7 @@ internal static string SearchCharBackwardDescription { } /// - /// Looks up a localized string similar to Move to the previous occurance of the specified character and then forward one character.. + /// Looks up a localized string similar to Move to the previous occurrence of the specified character and then forward one character.. /// internal static string SearchCharBackwardWithBackoffDescription { get { @@ -1017,7 +1026,7 @@ internal static string SearchCharBackwardWithBackoffDescription { } /// - /// Looks up a localized string similar to Move to the next occurance of the specified character.. + /// Looks up a localized string similar to Move to the next occurrence of the specified character.. /// internal static string SearchCharDescription { get { @@ -1026,7 +1035,7 @@ internal static string SearchCharDescription { } /// - /// Looks up a localized string similar to Move to he next occurance of the specified character and then back one character.. + /// Looks up a localized string similar to Move to he next occurrence of the specified character and then back one character.. /// internal static string SearchCharWithBackoffDescription { get { @@ -1035,7 +1044,7 @@ internal static string SearchCharWithBackoffDescription { } /// - /// Looks up a localized string similar to Searches for the perscribed character in the perscribed direction.. + /// Looks up a localized string similar to Searches for the prescribed character in the prescribed direction.. /// internal static string SearchDescription { get { @@ -1585,7 +1594,7 @@ internal static string ViReplaceEndOfGlobDescription { } /// - /// Looks up a localized string similar to Delete to the end of the word, as delimited by white space and common delimters, and enter insert mode.. + /// Looks up a localized string similar to Delete to the end of the word, as delimited by white space and common delimiters, and enter insert mode.. /// internal static string ViReplaceEndOfWordDescription { get { @@ -1639,7 +1648,7 @@ internal static string ViReplaceWordDescription { } /// - /// Looks up a localized string similar to Starts a new seach backward in the history.. + /// Looks up a localized string similar to Starts a new search backward in the history.. /// internal static string ViSearchHistoryBackwardDescription { get { @@ -1756,7 +1765,7 @@ internal static string ViYankPreviousGlobDescription { } /// - /// Looks up a localized string similar to Place all characters from before the cursor to the beginning of the previous word, as delimted by white space and common delimiters, into the local clipboard.. + /// Looks up a localized string similar to Place all characters from before the cursor to the beginning of the previous word, as delimited by white space and common delimiters, into the local clipboard.. /// internal static string ViYankPreviousWordDescription { get { diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 6348877b3..a6e3478e4 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -127,7 +127,7 @@ Move the cursor back one character - Delete the charcter before the cursor + Delete the character before the cursor Delete text from the cursor to the start of the line @@ -154,7 +154,7 @@ Complete the input if there is a single completion, otherwise complete the input with common prefix for all completions. Show possible completions if pressed a second time. - Delete the character under the cusor + Delete the character under the cursor Move the cursor to the beginning of the current or previous token or start of the line @@ -412,7 +412,7 @@ Exception: If there are other parse errors, unresolved commands, or incorrect parameters, show the error and continue editing. - Delete the character under the cusor, or if the line is empty, exit the process. + Delete the character under the cursor, or if the line is empty, exit the process. Unable to translate '{0}' to virtual key code: {1}. @@ -451,10 +451,10 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Repeats the last search. - Searches backward for the perscribed character. + Searches backward for the prescribed character. - Searches for the perscribed character in the perscribed direction. + Searches for the prescribed character in the prescribed direction. Switches to insert mode after positioning the cursor past the end of the line. @@ -496,7 +496,7 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Moves the cursor forward to the end of the next word. - Moves the cursor to the perscribed column. + Moves the cursor to the prescribed column. Switches to insert mode. @@ -547,7 +547,7 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Delete the current character and insert the next character. - Starts a new seach backward in the history. + Starts a new search backward in the history. Transposes the current character with the next character in the line. @@ -565,16 +565,16 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Repeat the last recorded character search. - Move to the previous occurance of the specified character. + Move to the previous occurrence of the specified character. - Move to the previous occurance of the specified character and then forward one character. + Move to the previous occurrence of the specified character and then forward one character. - Move to the next occurance of the specified character. + Move to the next occurrence of the specified character. - Move to he next occurance of the specified character and then back one character. + Move to he next occurrence of the specified character and then back one character. Replace the previous word. @@ -622,7 +622,7 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Delete to the beginning of the next word, as delimited by white space, and enter insert mode. - Delete to the end of the word, as delimited by white space and common delimters, and enter insert mode. + Delete to the end of the word, as delimited by white space and common delimiters, and enter insert mode. Delete to the end of the word, as delimited by white space, and enter insert mode. @@ -634,7 +634,7 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Place all characters at and after the cursor into the local clipboard. - Place all characters from before the cursor to the beginning of the previous word, as delimted by white space and common delimiters, into the local clipboard. + Place all characters from before the cursor to the beginning of the previous word, as delimited by white space and common delimiters, into the local clipboard. Place all characters from before the cursor to the beginning of the previous word, as delimited by white space, into the local clipboard. @@ -717,4 +717,7 @@ Or not saving history with: An exception occurred in custom key handler, see $error for more information: {0} + + Parameter must be a ConsoleColor, ANSI escape sequence, or RGB value with optional leading '#'. + \ No newline at end of file diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs new file mode 100644 index 000000000..1d958ca52 --- /dev/null +++ b/PSReadLine/PlatformWindows.cs @@ -0,0 +1,203 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.PowerShell; +using Microsoft.PowerShell.Internal; +using Microsoft.Win32.SafeHandles; + +static class PlatformWindows +{ + public enum ConsoleBreakSignal : uint + { + CtrlC = 0, + CtrlBreak = 1, + Close = 2, + Logoff = 5, + Shutdown = 6, + None = 255, + } + + public enum StandardHandleId : uint + { + Error = unchecked((uint)-12), + Output = unchecked((uint)-11), + Input = unchecked((uint)-10), + } + + [Flags] + internal enum AccessQualifiers : uint + { + // From winnt.h + GenericRead = 0x80000000, + GenericWrite = 0x40000000 + } + + internal enum CreationDisposition : uint + { + // From winbase.h + CreateNew = 1, + CreateAlways = 2, + OpenExisting = 3, + OpenAlways = 4, + TruncateExisting = 5 + } + + [Flags] + internal enum ShareModes : uint + { + // From winnt.h + ShareRead = 0x00000001, + ShareWrite = 0x00000002 + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr CreateFile + ( + string fileName, + uint desiredAccess, + uint ShareModes, + IntPtr securityAttributes, + uint creationDisposition, + uint flagsAndAttributes, + IntPtr templateFileWin32Handle + ); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern int GetFileType(IntPtr handle); + + internal const int FILE_TYPE_CHAR = 0x0002; + + internal static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); // WinBase.h + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr GetStdHandle(uint handleId); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); + + delegate bool BreakHandler(ConsoleBreakSignal ConsoleBreakSignal); + + private static bool OnBreak(ConsoleBreakSignal signal) + { + if (signal == ConsoleBreakSignal.Close || signal == ConsoleBreakSignal.Shutdown) + { + // Set the event so ReadKey throws an exception to unwind. + _singleton._closingWaitHandle.Set(); + } + + return false; + } + + private static PSConsoleReadLine _singleton; + internal static void OneTimeInit(PSConsoleReadLine singleton) + { + _singleton = singleton; + var breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(OnBreak)); + SetConsoleCtrlHandler((BreakHandler)breakHandlerGcHandle.Target, true); + } + + const uint ENABLE_PROCESSED_INPUT = 0x0001; + const uint ENABLE_LINE_INPUT = 0x0002; + const uint ENABLE_WINDOW_INPUT = 0x0008; + const uint ENABLE_MOUSE_INPUT = 0x0010; + + static uint _prePSReadlineConsoleMode; + internal static void Init() + { + // If either stdin or stdout is redirected, PSReadline doesn't really work, so throw + // and let PowerShell call Console.ReadLine or do whatever else it decides to do. + if (IsHandleRedirected(stdin: false) || IsHandleRedirected(stdin: true)) + { + throw new NotSupportedException(); + } + + _prePSReadlineConsoleMode = GetConsoleInputMode(); + + // Clear a couple flags so we can actually receive certain keys: + // ENABLE_PROCESSED_INPUT - enables Ctrl+C + // ENABLE_LINE_INPUT - enables Ctrl+S + // Also clear a couple flags so we don't mask the input that we ignore: + // ENABLE_MOUSE_INPUT - mouse events + // ENABLE_WINDOW_INPUT - window resize events + var mode = _prePSReadlineConsoleMode & + ~(ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT); + SetConsoleInputMode(mode); + } + + internal static void Complete() + { + SetConsoleInputMode(_prePSReadlineConsoleMode); + } + + internal static T CallPossibleExternalApplication(Func func) + { + uint psReadlineConsoleMode = GetConsoleInputMode(); + try + { + SetConsoleInputMode(_prePSReadlineConsoleMode); + return func(); + } + finally + { + SetConsoleInputMode(psReadlineConsoleMode); + } + } + + private static readonly Lazy _inputHandle = new Lazy(() => + { + // We use CreateFile here instead of GetStdWin32Handle, as GetStdWin32Handle will return redirected handles + var handle = CreateFile( + "CONIN$", + (UInt32)(AccessQualifiers.GenericRead | AccessQualifiers.GenericWrite), + (UInt32)ShareModes.ShareWrite, + (IntPtr)0, + (UInt32)CreationDisposition.OpenExisting, + 0, + (IntPtr)0); + + if (handle == INVALID_HANDLE_VALUE) + { + int err = Marshal.GetLastWin32Error(); + Win32Exception innerException = new Win32Exception(err); + throw new Exception("Failed to retrieve the input console handle.", innerException); + } + + return new SafeFileHandle(handle, true); + }); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode); + + private static uint GetConsoleInputMode() + { + var handle = _inputHandle.Value.DangerousGetHandle(); + GetConsoleMode(handle, out var result); + return result; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode); + + private static void SetConsoleInputMode(uint mode) + { + var handle = _inputHandle.Value.DangerousGetHandle(); + SetConsoleMode(handle, mode); + } + + private static bool IsHandleRedirected(bool stdin) + { + var handle = GetStdHandle((uint)(stdin ? StandardHandleId.Input : StandardHandleId.Output)); + + // If handle is not to a character device, we must be redirected: + int fileType = GetFileType(handle); + if ((fileType & FILE_TYPE_CHAR) != FILE_TYPE_CHAR) + return true; + + // Char device - if GetConsoleMode succeeds, we are NOT redirected. + return !GetConsoleMode(handle, out var unused); + } +} diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index f5bdc2bbd..b52b436f5 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -8,6 +8,7 @@ using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell @@ -28,13 +29,12 @@ public interface IPSConsoleReadLineMockableMethods [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")] public interface IConsole { - uint GetConsoleInputMode(); - void SetConsoleInputMode(uint mode); ConsoleKeyInfo ReadKey(); bool KeyAvailable { get; } int CursorLeft { get; set; } int CursorTop { get; set;} int CursorSize { get; set; } + bool CursorVisible { get; set; } int BufferWidth { get; set; } int BufferHeight { get; set;} int WindowWidth { get; set; } @@ -46,12 +46,10 @@ public interface IConsole void SetCursorPosition(int left, int top); void WriteLine(string s); void Write(string s); - void WriteBufferLines(CHAR_INFO[] buffer, ref int top); - void WriteBufferLines(CHAR_INFO[] buffer, ref int top, bool ensureBottomLineVisible); void ScrollBuffer(int lines); - CHAR_INFO[] ReadBufferLines(int top, int count); - bool IsHandleRedirected(bool stdin); + void SaveCursor(); + void RestoreCursor(); } #pragma warning restore 1591 @@ -119,7 +117,7 @@ public static void Delete(int start, int length) /// The length to replace /// The replacement text /// The action that initiated the replace (used for undo) - /// The argument to the action that initiaed the replace (used for undo) + /// The argument to the action that initiated the replace (used for undo) public static void Replace(int start, int length, string replacement, Action instigator = null, object instigatorArg = null) { if (start < 0 || start > _singleton._buffer.Length) @@ -217,7 +215,7 @@ public static void SetCursorPosition(int cursor) /// /// A helper method when your function expects an optional int argument (e.g. from DigitArgument) - /// If there is not argument (it's null), returns true and sets numericArg to derfaultNumericArg. + /// If there is not argument (it's null), returns true and sets numericArg to defaultNumericArg. /// Dings and returns false if the argument is not an int (no conversion is attempted) /// Otherwise returns true, and numericArg has the result. /// diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 0b191f97b..8138ca9a2 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -15,6 +15,7 @@ using System.Threading; using Microsoft.PowerShell.Commands; using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; [module: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")] [module: SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] @@ -33,16 +34,15 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private IPSConsoleReadLineMockableMethods _mockableMethods; private IConsole _console; + private Encoding _initialOutputEncoding; private EngineIntrinsics _engineIntrinsics; - private static GCHandle _breakHandlerGcHandle; private Thread _readKeyThread; private AutoResetEvent _readKeyWaitHandle; private AutoResetEvent _keyReadWaitHandle; - private ManualResetEvent _closingWaitHandle; + internal ManualResetEvent _closingWaitHandle; private WaitHandle[] _threadProcWaitHandles; private WaitHandle[] _requestKeyWaitHandles; - private uint _prePSReadlineConsoleMode; private readonly StringBuilder _buffer; private readonly StringBuilder _statusBuffer; @@ -109,7 +109,7 @@ private void ReadKeyThreadProc() } } - private static ConsoleKeyInfo ReadKey() + internal static ConsoleKeyInfo ReadKey() { // Reading a key is handled on a different thread. During process shutdown, // PowerShell will wait in it's ConsoleCtrlHandler until the pipeline has completed. @@ -155,7 +155,7 @@ private static ConsoleKeyInfo ReadKey() // There is an OnIdle event. We're idle because we timed out. Normally // PowerShell generates this event, but PowerShell assumes the engine is not // idle because it called PSConsoleHostReadline which isn't returning. - // So we generate the event intstead. + // So we generate the event instead. _singleton._engineIntrinsics.Events.GenerateEvent("PowerShell.OnIdle", null, null, null); runPipelineForEventProcessing = true; break; @@ -232,17 +232,6 @@ private void PrependQueuedKeys(ConsoleKeyInfo key) } } - private bool BreakHandler(ConsoleBreakSignal signal) - { - if (signal == ConsoleBreakSignal.Close || signal == ConsoleBreakSignal.Shutdown) - { - // Set the event so ReadKey throws an exception to unwind. - _closingWaitHandle.Set(); - } - - return false; - } - /// /// Entry point - called from the PowerShell function PSConsoleHostReadline /// after the prompt has been displayed. @@ -252,32 +241,16 @@ public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsi { var console = _singleton._console; - // If either stdin or stdout is redirected, PSReadline doesn't really work, so throw - // and let PowerShell call Console.ReadLine or do whatever else it decides to do. - if (console.IsHandleRedirected(stdin: false) || console.IsHandleRedirected(stdin: true)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - throw new NotSupportedException(); + PlatformWindows.Init(); } - _singleton._prePSReadlineConsoleMode = console.GetConsoleInputMode(); bool firstTime = true; while (true) { try { - // Clear a couple flags so we can actually receive certain keys: - // ENABLE_PROCESSED_INPUT - enables Ctrl+C - // ENABLE_LINE_INPUT - enables Ctrl+S - // Also clear a couple flags so we don't mask the input that we ignore: - // ENABLE_MOUSE_INPUT - mouse events - // ENABLE_WINDOW_INPUT - window resize events - var mode = _singleton._prePSReadlineConsoleMode & - ~(NativeMethods.ENABLE_PROCESSED_INPUT | - NativeMethods.ENABLE_LINE_INPUT | - NativeMethods.ENABLE_WINDOW_INPUT | - NativeMethods.ENABLE_MOUSE_INPUT); - console.SetConsoleInputMode(mode); - if (firstTime) { firstTime = false; @@ -346,7 +319,11 @@ public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsi } finally { - console.SetConsoleInputMode(_singleton._prePSReadlineConsoleMode); + Console.OutputEncoding = _singleton._initialOutputEncoding; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + PlatformWindows.Complete(); + } } } } @@ -432,27 +409,35 @@ private string InputLoop() } } - T CalloutUsingDefaultConsoleMode(Func func) + T CallPossibleExternalApplication(Func func) { - uint psReadlineConsoleMode = _console.GetConsoleInputMode(); try { - _console.SetConsoleInputMode(_prePSReadlineConsoleMode); - return func(); + Console.OutputEncoding = _initialOutputEncoding; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? PlatformWindows.CallPossibleExternalApplication(func) + : func(); } finally { - _console.SetConsoleInputMode(psReadlineConsoleMode); + Console.OutputEncoding = Encoding.Unicode; } } - void CalloutUsingDefaultConsoleMode(Action action) + void CallPossibleExternalApplication(Action action) { - CalloutUsingDefaultConsoleMode(() => { action(); return null; }); + CallPossibleExternalApplication(() => { action(); return null; }); } void ProcessOneKey(ConsoleKeyInfo key, Dictionary dispatchTable, bool ignoreIfNoAction, object arg) { + // Our dispatch tables are built as much as possible in a portable way, so for example, + // we avoid depending on scan codes like ConsoleKey.Oem6 and instead look at the + // ConsoleKeyInfo.Key. We also want to ignore the shift state as that may differ on + // different keyboard layouts. + // + // That said, we first look up exactly what we get from Console.ReadKey - that will fail + // most of the time, and when it does, we normalize the key. if (!dispatchTable.TryGetValue(key, out var handler)) { // If we see a control character where Ctrl wasn't used but shift was, treat that like @@ -467,14 +452,14 @@ void ProcessOneKey(ConsoleKeyInfo key, Dictionary di { if (handler.ScriptBlock != null) { - CalloutUsingDefaultConsoleMode(() => handler.Action(key, arg)); + CallPossibleExternalApplication(() => handler.Action(key, arg)); } else { handler.Action(key, arg); } } - else if (!ignoreIfNoAction && key.KeyChar != 0) + else if (!ignoreIfNoAction && key.ShouldInsert()) { SelfInsert(key, arg); } @@ -485,8 +470,6 @@ private PSConsoleReadLine() _mockableMethods = this; _console = new ConhostConsole(); - SetDefaultWindowsBindings(); - _buffer = new StringBuilder(8 * 1024); _statusBuffer = new StringBuilder(256); _savedCurrentLine = new HistoryItem(); @@ -516,6 +499,7 @@ private PSConsoleReadLine() hostName = "PSReadline"; } _options = new PSConsoleReadlineOptions(hostName); + SetDefaultBindings(_options.EditMode); } private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) @@ -529,6 +513,7 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _delayedOneTimeInitCompleted = true; } + _previousRender = _initialPrevRender; _buffer.Clear(); _edits = new List(); _undoEditIndex = 0; @@ -541,11 +526,7 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _parseErrors = null; _inputAccepted = false; _initialX = _console.CursorLeft; - _initialY = _console.CursorTop - Options.ExtraPromptLineCount; - _initialBackgroundColor = _console.BackgroundColor; - _initialForegroundColor = _console.ForegroundColor; - _space = new CHAR_INFO(' ', _initialForegroundColor, _initialBackgroundColor); - _bufferWidth = _console.BufferWidth; + _initialY = _console.CursorTop; _killCommandCount = 0; _yankCommandCount = 0; _yankLastArgCommandCount = 0; @@ -553,7 +534,8 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _visualSelectionCommandCount = 0; _statusIsErrorMessage = false; - _consoleBuffer = ReadBufferLines(_initialY, 1 + Options.ExtraPromptLineCount); + _initialOutputEncoding = Console.OutputEncoding; + Console.OutputEncoding = Encoding.Unicode; _lastRenderTime = Stopwatch.StartNew(); _killCommandCount = 0; @@ -598,6 +580,37 @@ private void DelayedOneTimeInitialize() _options.MaximumHistoryCount = historyCountValue; } + if (_options.PromptText == null && + _engineIntrinsics?.InvokeCommand.GetCommand("prompt", CommandTypes.Function) is FunctionInfo promptCommand) + { + var promptIsPure = null == + promptCommand.ScriptBlock.Ast.Find(ast => ast is CommandAst || + ast is InvokeMemberExpressionAst, + searchNestedScriptBlocks: true); + if (promptIsPure) + { + var res = promptCommand.ScriptBlock.InvokeReturnAsIs(Array.Empty()); + string evaluatedPrompt = res as string; + if (evaluatedPrompt == null && res is PSObject psobject) + { + evaluatedPrompt = psobject.BaseObject as string; + } + if (evaluatedPrompt != null) + { + int i; + for (i = evaluatedPrompt.Length - 1; i >= 0; i--) + { + if (!char.IsWhiteSpace(evaluatedPrompt[i])) break; + } + + if (i >= 0) + { + _options.PromptText = evaluatedPrompt.Substring(i); + } + } + } + } + _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); _history = new HistoryQueue(Options.MaximumHistoryCount); @@ -631,14 +644,17 @@ private void DelayedOneTimeInitialize() _killIndex = -1; // So first add indexes 0. _killRing = new List(Options.MaximumKillRingCount); - _breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(_singleton.BreakHandler)); - NativeMethods.SetConsoleCtrlHandler((BreakHandler)_breakHandlerGcHandle.Target, true); _singleton._readKeyWaitHandle = new AutoResetEvent(false); _singleton._keyReadWaitHandle = new AutoResetEvent(false); _singleton._closingWaitHandle = new ManualResetEvent(false); _singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle}; _singleton._threadProcWaitHandles = new WaitHandle[] {_singleton._readKeyWaitHandle, _singleton._closingWaitHandle}; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + PlatformWindows.OneTimeInit(_singleton); + } + // This is for a "being hosted in an alternate appdomain scenario" (the // DomainUnload event is not raised for the default appdomain). It allows us // to exit cleanly when the appdomain is unloaded but the process is not going @@ -670,43 +686,6 @@ private static void Chord(ConsoleKeyInfo? key = null, object arg = null) } } - private static void Ignore(ConsoleKeyInfo? key = null, object arg = null) - { - } - - private static void ExecuteOnSTAThread(Action action) - { - if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) - { - action(); - return; - } - - Exception exception = null; - var thread = new Thread(() => - { - try - { - action(); - } - catch (Exception e) - { - exception = e; - } - }); - - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); - thread.Join(); - - if (exception != null) - { - throw exception; - } - } - - #region Miscellaneous bindable functions - /// /// Abort current action, e.g. incremental history search /// @@ -725,13 +704,11 @@ public static void DigitArgument(ConsoleKeyInfo? key = null, object arg = null) return; } - #region VI special case if (_singleton._options.EditMode == EditMode.Vi && key.Value.KeyChar == '0') { BeginningOfLine(); return; } - #endregion VI special case bool sawDigit = false; _singleton._statusLinePrompt = "digit-argument: "; @@ -814,57 +791,102 @@ public static void DigitArgument(ConsoleKeyInfo? key = null, object arg = null) /// public static void InvokePrompt(ConsoleKeyInfo? key = null, object arg = null) { - var currentBuffer = _singleton._buffer.ToString(); - var currentPos = _singleton._current; - _singleton._buffer.Clear(); - _singleton._current = 0; - for (int i = 0; i < _singleton._consoleBuffer.Length; i++) - { - _singleton._consoleBuffer[i].UnicodeChar = ' '; - _singleton._consoleBuffer[i].ForegroundColor = _singleton._console.ForegroundColor; - _singleton._consoleBuffer[i].BackgroundColor = _singleton._console.BackgroundColor; - } - _singleton.Render(); - _singleton._console.CursorLeft = 0; - _singleton._console.CursorTop = _singleton._initialY; + var console = _singleton._console; + console.CursorVisible = false; - var runspaceIsRemote = _singleton._mockableMethods.RunspaceIsRemote(_singleton._runspace); - System.Management.Automation.PowerShell ps; - if (!runspaceIsRemote) + if (arg is int newY) { - ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + if (newY >= console.BufferHeight) + { + var toScroll = newY - console.BufferHeight + 1; + console.ScrollBuffer(toScroll); + newY -= toScroll; + } + console.SetCursorPosition(0, newY); } else { - ps = System.Management.Automation.PowerShell.Create(); - ps.Runspace = _singleton._runspace; - } - string newPrompt; - using (ps) - { - ps.AddCommand("prompt"); - var result = ps.Invoke(); - newPrompt = result.Count == 1 ? result[0] : "PS>"; - } + newY = _singleton._initialY - _singleton._options.ExtraPromptLineCount; - if (runspaceIsRemote) - { - var connectionInfo = _singleton._runspace.ConnectionInfo; - if (!string.IsNullOrEmpty(connectionInfo.ComputerName)) + console.SetCursorPosition(0, newY); + + // We need to rewrite the prompt, so blank out everything from a previous prompt invocation + // in case the next one is shorter. + var spaces = Spaces(console.BufferWidth); + for (int i = 0; i < _singleton._options.ExtraPromptLineCount + 1; i++) { - newPrompt = string.Format(CultureInfo.InvariantCulture, "[{0}]: {1}", connectionInfo.ComputerName, newPrompt); + console.Write(spaces); } + + console.SetCursorPosition(0, newY); } - _singleton._console.Write(newPrompt); - _singleton._initialX = _singleton._console.CursorLeft; - _singleton._consoleBuffer = ReadBufferLines(_singleton._initialY, 1 + _singleton.Options.ExtraPromptLineCount); - _singleton._buffer.Append(currentBuffer); - _singleton._current = currentPos; + string newPrompt = GetPrompt(); + + console.Write(newPrompt); + _singleton._initialX = console.CursorLeft; + _singleton._initialY = console.CursorTop; + _singleton._previousRender = _initialPrevRender; + _singleton.Render(); + console.CursorVisible = true; } - #endregion Miscellaneous bindable functions + private static string GetPrompt() + { + string newPrompt = null; + if (_singleton._runspace?.Debugger != null && _singleton._runspace.Debugger.InBreakpoint) + { + // Run prompt command in debugger API to ensure it is run correctly on the runspace. + // This handles remote runspace debugging and nested debugger scenarios. + PSDataCollection results = new PSDataCollection(); + var command = new PSCommand(); + command.AddCommand("prompt"); + _singleton._runspace.Debugger.ProcessCommand( + command, + results); + + if (results.Count == 1) + newPrompt = results[0].BaseObject as string; + } + else + { + var runspaceIsRemote = _singleton._mockableMethods.RunspaceIsRemote(_singleton._runspace); + + System.Management.Automation.PowerShell ps; + if (!runspaceIsRemote) + { + ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + } + else + { + ps = System.Management.Automation.PowerShell.Create(); + ps.Runspace = _singleton._runspace; + } + + using (ps) + { + ps.AddCommand("prompt"); + var result = ps.Invoke(); + if (result.Count == 1) + { + newPrompt = result[0]; + if (runspaceIsRemote) + { + if (!string.IsNullOrEmpty(_singleton._runspace?.ConnectionInfo?.ComputerName)) + { + newPrompt = "[" + (_singleton._runspace?.ConnectionInfo).ComputerName + "]: " + newPrompt; + } + } + } + } + } + + if (string.IsNullOrEmpty(newPrompt)) + newPrompt = "PS>"; + + return newPrompt; + } } } diff --git a/PSReadLine/ReadLine.vi.cs b/PSReadLine/ReadLine.vi.cs index a0eead512..42ba250fb 100644 --- a/PSReadLine/ReadLine.vi.cs +++ b/PSReadLine/ReadLine.vi.cs @@ -170,7 +170,7 @@ public static void SearchChar(ConsoleKeyInfo? key = null, object arg = null) } /// - /// Read the next character and then find it, going backard, and then back off a character. + /// Read the next character and then find it, going backward, and then back off a character. /// This is for 'T' functionality. /// public static void SearchCharBackward(ConsoleKeyInfo? key = null, object arg = null) @@ -192,7 +192,7 @@ public static void SearchCharWithBackoff(ConsoleKeyInfo? key = null, object arg } /// - /// Read the next character and then find it, going backard, and then back off a character. + /// Read the next character and then find it, going backward, and then back off a character. /// This is for 'T' functionality. /// public static void SearchCharBackwardWithBackoff(ConsoleKeyInfo? key = null, object arg = null) @@ -377,14 +377,15 @@ public static void ViDeleteEndOfGlob(ConsoleKeyInfo? key = null, object arg = nu /// /// Deletes until given character /// - /// - /// public static void ViDeleteToChar(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToChar(keyChar, key, arg); } + /// + /// Deletes until given character + /// public static void ViDeleteToChar(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.Set(keyChar, isBackward: false, isBackoff: false); @@ -392,16 +393,17 @@ public static void ViDeleteToChar(char keyChar, ConsoleKeyInfo? key = null, obje } /// - /// Deletes until given character + /// Deletes backwards until given character /// - /// - /// public static void ViDeleteToCharBackward(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToCharBack(keyChar, key, arg); } + /// + /// Deletes backwards until given character + /// public static void ViDeleteToCharBack(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.SearchBackwardDelete(keyChar, arg, backoff: false, instigator: (_key, _arg) => ViDeleteToCharBack(keyChar, _key, _arg)); @@ -410,14 +412,15 @@ public static void ViDeleteToCharBack(char keyChar, ConsoleKeyInfo? key = null, /// /// Deletes until given character /// - /// - /// public static void ViDeleteToBeforeChar(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; ViDeleteToBeforeChar(keyChar, key, arg); } + /// + /// Deletes until given character + /// public static void ViDeleteToBeforeChar(char keyChar, ConsoleKeyInfo? key = null, object arg = null) { ViCharacterSearcher.Set(keyChar, isBackward: false, isBackoff: true); @@ -427,8 +430,6 @@ public static void ViDeleteToBeforeChar(char keyChar, ConsoleKeyInfo? key = null /// /// Deletes until given character /// - /// - /// public static void ViDeleteToBeforeCharBackward(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; @@ -538,7 +539,7 @@ private void ViIndicateInsertMode() } /// - /// Switch to Insert mode and position the cursor at the begining of the line. + /// Switch to Insert mode and position the cursor at the beginning of the line. /// public static void ViInsertAtBegining(ConsoleKeyInfo? key = null, object arg = null) { @@ -846,7 +847,7 @@ public static void RepeatSearchBackward(ConsoleKeyInfo? key = null, object arg = /// /// Prompts for a string for history searching. /// - /// True for seaching backward in the history. + /// True for searching backward in the history. private void StartSearch(bool backward) { _statusLinePrompt = "find: "; @@ -856,18 +857,18 @@ private void StartSearch(bool backward) while (true) { var nextKey = ReadKey(); - if (nextKey.Key == Keys.Enter.Key || nextKey.Key == Keys.Tab.Key) + if (nextKey.EqualsNormalized(Keys.Enter) || nextKey.EqualsNormalized(Keys.Tab)) { _searchHistoryPrefix = argBuffer.ToString(); _searchHistoryBackward = backward; HistorySearch(); break; } - if (nextKey.Key == Keys.Escape.Key) + if (nextKey.EqualsNormalized(Keys.Escape)) { break; } - if (nextKey.Key == Keys.Backspace.Key) + if (nextKey.EqualsNormalized(Keys.Backspace)) { if (argBuffer.Length > 0) { @@ -940,8 +941,6 @@ public static void RepeatLastCommand(ConsoleKeyInfo? key = null, object arg = nu /// /// Chords in vi needs special handling because a numeric argument can be input between the 1st and 2nd key. /// - /// - /// private static void ViChord(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue) @@ -961,7 +960,7 @@ private static void ViChord(ConsoleKeyInfo? key = null, object arg = null) { _singleton.ProcessOneKey(secondKey, secondKeyDispatchTable, ignoreIfNoAction: true, arg: arg); } - else if (!IsNumberic(secondKey)) + else if (!IsNumeric(secondKey)) { _singleton.ProcessOneKey(secondKey, secondKeyDispatchTable, ignoreIfNoAction: true, arg: arg); } @@ -970,7 +969,7 @@ private static void ViChord(ConsoleKeyInfo? key = null, object arg = null) var argBuffer = _singleton._statusBuffer; argBuffer.Clear(); _singleton._statusLinePrompt = "digit-argument: "; - while (IsNumberic(secondKey)) + while (IsNumeric(secondKey)) { argBuffer.Append(secondKey.KeyChar); _singleton.Render(); @@ -991,9 +990,9 @@ private static void ViChord(ConsoleKeyInfo? key = null, object arg = null) } } - private static bool IsNumberic(ConsoleKeyInfo key) + private static bool IsNumeric(ConsoleKeyInfo key) { - return char.IsNumber(key.KeyChar); + return key.KeyChar >= '0' && key.KeyChar <= '9' && key.Modifiers == 0; } /// @@ -1007,13 +1006,11 @@ public static void ViDigitArgumentInChord(ConsoleKeyInfo? key = null, object arg return; } - #region VI special case if (_singleton._options.EditMode == EditMode.Vi && key.Value.KeyChar == '0') { BeginningOfLine(); return; } - #endregion VI special case bool sawDigit = false; _singleton._statusLinePrompt = "digit-argument: "; @@ -1034,7 +1031,7 @@ public static void ViDigitArgumentInChord(ConsoleKeyInfo? key = null, object arg var nextKey = ReadKey(); if (_singleton._dispatchTable.TryGetValue(nextKey, out var handler) && handler.Action == DigitArgument) { - if (nextKey.KeyChar == '-') + if (nextKey.EqualsNormalized(Keys.Minus)) { if (argBuffer[0] == '-') { @@ -1048,7 +1045,7 @@ public static void ViDigitArgumentInChord(ConsoleKeyInfo? key = null, object arg continue; } - if (nextKey.KeyChar >= '0' && nextKey.KeyChar <= '9') + if (IsNumeric(nextKey)) { if (!sawDigit && argBuffer.Length > 0) { diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index ef5764f86..33e3e3d16 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -5,21 +5,50 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Management.Automation.Language; +using System.Runtime.InteropServices; +using System.Text; using Microsoft.PowerShell.Internal; namespace Microsoft.PowerShell { + internal struct Point + { + public int X; + public int Y; + + public override string ToString() + { + return String.Format(CultureInfo.InvariantCulture, "{0},{1}", X, Y); + } + } + public partial class PSConsoleReadLine { - private CHAR_INFO[] _consoleBuffer; + struct RenderedLineData + { + public string line; + public int columns; + } + + class RenderData + { + public bool errorPrompt; + public RenderedLineData[] lines; + } + + private const int COMMON_WIDEST_CONSOLE_WIDTH = 160; + private readonly List _consoleBufferLines = new List(1) {new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)}; + private static readonly string[] _spaces = new string[80]; + private RenderData _previousRender; + private static readonly RenderData _initialPrevRender = new RenderData + { + lines = new[] { new RenderedLineData{ columns = 0, line = ""}} + }; private int _initialX; private int _initialY; - private int _bufferWidth; - private ConsoleColor _initialBackgroundColor; - private ConsoleColor _initialForegroundColor; - private CHAR_INFO _space; private int _current; private int _emphasisStart; private int _emphasisLength; @@ -28,8 +57,7 @@ private class SavedTokenState { internal Token[] Tokens { get; set; } internal int Index { get; set; } - internal ConsoleColor BackgroundColor { get; set; } - internal ConsoleColor ForegroundColor { get; set; } + internal string Color { get; set; } } private void MaybeParseInput() @@ -65,60 +93,106 @@ private void Render() if (_queuedKeys.Count > 10 && (_lastRenderTime.ElapsedMilliseconds < 50)) { // We won't render, but most likely the tokens will be different, so make - // sure we don't use old tokens. + // sure we don't use old tokens, also allow garbage to get collected. _tokens = null; _ast = null; + _parseErrors = null; return; } - ReallyRender(); + ForceRender(); } - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults")] - private void ReallyRender() + private void ForceRender() + { + var defaultColor = VTColorUtils.MapColorToEscapeSequence(_console.ForegroundColor, isBackground: false) + + VTColorUtils.MapColorToEscapeSequence(_console.BackgroundColor, isBackground: true); + + // Geneate a sequence of logical lines with escape sequences for coloring. + int logicalLineCount = GenerateRender(defaultColor); + + // Now write that out (and remember what we did so we can clear previous renders + // and minimize writing more than necessary on the next render.) + + var renderLines = new RenderedLineData[logicalLineCount]; + var renderData = new RenderData {lines = renderLines}; + for (var i = 0; i < logicalLineCount; i++) + { + var line = _consoleBufferLines[i].ToString(); + renderLines[i].line = line; + renderLines[i].columns = LengthInBufferCells(line); + } + + // And then do the real work of writing to the screen. + // Rendering data is in reused + ReallyRender(renderData, defaultColor); + + // Cleanup some excess buffers, saving a few because we know we'll use them. + var bufferCount = _consoleBufferLines.Count; + var excessBuffers = bufferCount - renderLines.Length; + if (excessBuffers > 5) + { + _consoleBufferLines.RemoveRange(renderLines.Length, excessBuffers); + } + } + + private int GenerateRender(string defaultColor) { var text = ParseInput(); - int statusLineCount = GetStatusLineCount(); - int j = _initialX + (_bufferWidth * Options.ExtraPromptLineCount); - var backgroundColor = _initialBackgroundColor; - var foregroundColor = _initialForegroundColor; + string color = defaultColor; + string activeColor = ""; bool afterLastToken = false; - int totalBytes = j; - int bufferWidth = _console.BufferWidth; + int currentLogicalLine = 0; + + void UpdateColorsIfNecessary(string newColor) + { + if (!object.ReferenceEquals(newColor, activeColor)) + { + _consoleBufferLines[currentLogicalLine].Append(newColor); + activeColor = newColor; + } + } + + void MaybeEmphasize(int i, string currColor) + { + if (i >= _emphasisStart && i < (_emphasisStart + _emphasisLength)) + { + currColor = _options._emphasisColor; + } + UpdateColorsIfNecessary(currColor); + } + + foreach (var buf in _consoleBufferLines) + { + buf.Clear(); + } var tokenStack = new Stack(); tokenStack.Push(new SavedTokenState { Tokens = _tokens, Index = 0, - BackgroundColor = _initialBackgroundColor, - ForegroundColor = _initialForegroundColor + Color = defaultColor }); - int bufferLineCount; - - bufferLineCount = ConvertOffsetToCoordinates(text.Length).Y - _initialY + 1 + statusLineCount; - if (_consoleBuffer.Length != bufferLineCount * bufferWidth) + bool selectionNeedsTerminating = false; + for (int i = 0; i < text.Length; i++) { - var newBuffer = new CHAR_INFO[bufferLineCount * bufferWidth]; - Array.Copy(_consoleBuffer, newBuffer, _initialX + (Options.ExtraPromptLineCount * _bufferWidth)); - if (_consoleBuffer.Length > bufferLineCount * bufferWidth) + if (_visualSelectionCommandCount > 0) { - int consoleBufferOffset = ConvertOffsetToConsoleBufferOffset(text.Length, _initialX + (Options.ExtraPromptLineCount * _bufferWidth)); - // Need to erase the extra lines that we won't draw again - for (int i = consoleBufferOffset; i < _consoleBuffer.Length; i++) + GetRegion(out int start, out int length); + if (i == start) + { + _consoleBufferLines[currentLogicalLine].Append("\x1b[7m"); + selectionNeedsTerminating = true; + } + else if (i == start + length) { - _consoleBuffer[i] = _space; + _consoleBufferLines[currentLogicalLine].Append("\x1b[27m"); + selectionNeedsTerminating = false; } - _console.WriteBufferLines(_consoleBuffer, ref _initialY); } - _consoleBuffer = newBuffer; - } - - for (int i = 0; i < text.Length; i++) - { - totalBytes = totalBytes % bufferWidth; if (!afterLastToken) { // Figure out the color of the character - if it's in a token, @@ -134,8 +208,7 @@ private void ReallyRender() { afterLastToken = true; token = null; - foregroundColor = _initialForegroundColor; - backgroundColor = _initialBackgroundColor; + color = defaultColor; } else { @@ -145,16 +218,14 @@ private void ReallyRender() if (!afterLastToken) { - foregroundColor = state.ForegroundColor; - backgroundColor = state.BackgroundColor; - + color = state.Color; token = state.Tokens[++state.Index]; } } if (!afterLastToken && i == token.Extent.StartOffset) { - GetTokenColors(token, out foregroundColor, out backgroundColor); + color = GetTokenColor(token); if (token is StringExpandableToken stringToken) { @@ -171,13 +242,12 @@ private void ReallyRender() { Tokens = tokens, Index = 0, - BackgroundColor = backgroundColor, - ForegroundColor = foregroundColor + Color = color }); if (i == tokens[0].Extent.StartOffset) { - GetTokenColors(tokens[0], out foregroundColor, out backgroundColor); + color = GetTokenColor(tokens[0]); } } } @@ -187,128 +257,252 @@ private void ReallyRender() var charToRender = text[i]; if (charToRender == '\n') { - while ((j % bufferWidth) != 0) + currentLogicalLine += 1; + if (currentLogicalLine > _consoleBufferLines.Count - 1) { - _consoleBuffer[j++] = _space; + _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); } - for (int k = 0; k < Options.ContinuationPrompt.Length; k++, j++) + UpdateColorsIfNecessary(Options._continuationPromptColor); + foreach (char c in Options.ContinuationPrompt) { - _consoleBuffer[j].UnicodeChar = Options.ContinuationPrompt[k]; - _consoleBuffer[j].ForegroundColor = Options.ContinuationPromptForegroundColor; - _consoleBuffer[j].BackgroundColor = Options.ContinuationPromptBackgroundColor; + _consoleBufferLines[currentLogicalLine].Append(c); } } else { - int size = LengthInBufferCells(charToRender); - totalBytes += size; - - //if there is no enough space for the character at the edge, fill in spaces at the end and - //put the character to next line. - int filling = totalBytes > bufferWidth ? (totalBytes - bufferWidth) % size : 0; - for (int f = 0; f < filling; f++) - { - _consoleBuffer[j++] = _space; - totalBytes++; - } - if (char.IsControl(charToRender)) { - _consoleBuffer[j].UnicodeChar = '^'; - MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor); - _consoleBuffer[j].UnicodeChar = (char)('@' + charToRender); - MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor); - - } - else if (size > 1) - { - _consoleBuffer[j].UnicodeChar = charToRender; - _consoleBuffer[j].Attributes = (ushort)(_consoleBuffer[j].Attributes | - (uint)CHAR_INFO_Attributes.COMMON_LVB_LEADING_BYTE); - MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor); - _consoleBuffer[j].UnicodeChar = charToRender; - _consoleBuffer[j].Attributes = (ushort)(_consoleBuffer[j].Attributes | - (uint)CHAR_INFO_Attributes.COMMON_LVB_TRAILING_BYTE); - MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor); + MaybeEmphasize(i, color); + _consoleBufferLines[currentLogicalLine].Append('^'); + _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender)); } else { - _consoleBuffer[j].UnicodeChar = charToRender; - MaybeEmphasize(ref _consoleBuffer[j++], i, foregroundColor, backgroundColor); + MaybeEmphasize(i, color); + _consoleBufferLines[currentLogicalLine].Append(charToRender); } } } - for (; j < (_consoleBuffer.Length - (statusLineCount * _bufferWidth)); j++) + if (selectionNeedsTerminating) { - _consoleBuffer[j] = _space; + _consoleBufferLines[currentLogicalLine].Append("\x1b[27m"); } if (_statusLinePrompt != null) { - foregroundColor = _statusIsErrorMessage ? Options.ErrorForegroundColor : _console.ForegroundColor; - backgroundColor = _statusIsErrorMessage ? Options.ErrorBackgroundColor : _console.BackgroundColor; - - for (int i = 0; i < _statusLinePrompt.Length; i++, j++) + currentLogicalLine += 1; + if (currentLogicalLine > _consoleBufferLines.Count - 1) { - _consoleBuffer[j].UnicodeChar = _statusLinePrompt[i]; - _consoleBuffer[j].ForegroundColor = foregroundColor; - _consoleBuffer[j].BackgroundColor = backgroundColor; + _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); } - for (int i = 0; i < _statusBuffer.Length; i++, j++) + + color = _statusIsErrorMessage ? Options._errorColor : defaultColor; + UpdateColorsIfNecessary(color); + + foreach (char c in _statusLinePrompt) { - _consoleBuffer[j].UnicodeChar = _statusBuffer[i]; - _consoleBuffer[j].ForegroundColor = foregroundColor; - _consoleBuffer[j].BackgroundColor = backgroundColor; + _consoleBufferLines[currentLogicalLine].Append(c); } - for (; j < _consoleBuffer.Length; j++) + _consoleBufferLines[currentLogicalLine].Append(_statusBuffer); + } + + return currentLogicalLine + 1; + } + + private void ReallyRender(RenderData renderData, string defaultColor) + { + string activeColor = ""; + + void UpdateColorsIfNecessary(string newColor) + { + if (!object.ReferenceEquals(newColor, activeColor)) { - _consoleBuffer[j] = _space; + _console.Write(newColor); + activeColor = newColor; } } - bool rendered = false; - if (_parseErrors.Length > 0) + // TODO: avoid writing everything. + + // Move the cursor to where we started, but make cursor invisible while we're rendering. + _console.CursorVisible = false; + PlaceCursor(_initialX, _initialY); + + // Possibly need to flip the color on the prompt if the error state changed. + if (!string.IsNullOrEmpty(_options.PromptText)) { - int promptChar = _initialX - 1 + (_bufferWidth * Options.ExtraPromptLineCount); + renderData.errorPrompt = (_parseErrors != null && _parseErrors.Length > 0); + if (renderData.errorPrompt != _previousRender.errorPrompt) + { + // We need to update the prompt + _console.CursorLeft -= _options.PromptText.Length; + UpdateColorsIfNecessary(renderData.errorPrompt ? _options._errorColor : defaultColor); + _console.Write(_options.PromptText); + _console.Write("\x1b[0m"); + } + } - while (promptChar >= 0) + var bufferWidth = _console.BufferWidth; + + int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysicalLine) + { + int cnt = 1; + if (isFirstLogicalLine) { - var c = (char)_consoleBuffer[promptChar].UnicodeChar; - if (char.IsWhiteSpace(c)) + // The first logical line has the user prompt that we don't touch + // (except where we turn part to red, but we've finished that + // before getting here.) + var maxFirstLine = bufferWidth - _initialX; + if (columns > maxFirstLine) + { + cnt += 1; + columns -= maxFirstLine; + } + else { - promptChar -= 1; - continue; + lenLastPhysicalLine = columns; + return 1; } + } + + lenLastPhysicalLine = columns % bufferWidth; + return cnt + columns / bufferWidth; + } + + var previousRenderLines = _previousRender.lines; + var previousLogicalLine = 0; + var previousPhysicalLine = 0; + + var renderLines = renderData.lines; + var logicalLine = 0; + var physicalLine = 0; + var lenPrevLastLine = 0; + + for (; logicalLine < renderLines.Length; logicalLine++) + { + if (logicalLine != 0) _console.Write("\n"); + + var lineData = renderLines[logicalLine]; + _console.Write(lineData.line); + + int lenLastLine; + physicalLine += PhysicalLineCount(lineData.columns, logicalLine == 0, out lenLastLine); + + // Find the previous logical line (if any) that would have rendered + // the current physical line because we may need to clear it. + // We don't clear it unconditionally to allow things like a prompt + // on the right side of the line. - ConsoleColor prevColor = _consoleBuffer[promptChar].ForegroundColor; - _consoleBuffer[promptChar].ForegroundColor = ConsoleColor.Red; - _console.WriteBufferLines(_consoleBuffer, ref _initialY); - rendered = true; - _consoleBuffer[promptChar].ForegroundColor = prevColor; - break; + while (physicalLine > previousPhysicalLine + && previousLogicalLine < previousRenderLines.Length) + { + previousPhysicalLine += PhysicalLineCount(previousRenderLines[previousLogicalLine].columns, + previousLogicalLine == 0, + out lenPrevLastLine); + previousLogicalLine += 1; + } + + // Our current physical line might be in the middle of the + // previous logical line, in which case we need to blank + // the rest of the line, otherwise we blank just the end + // of what was written. + int lenToClear = 0; + if (physicalLine == previousPhysicalLine) + { + // We're on the end of the previous logical line, so we + // only need to clear any extra. + + if (lenPrevLastLine > lenLastLine) + lenToClear = lenPrevLastLine - lenLastLine; + } + else if (physicalLine < previousPhysicalLine) + { + // We're in the middle of a previous logical line, we + // need to clear to the end of the line. + lenToClear = bufferWidth - (lenLastLine % bufferWidth); + if (physicalLine == 1) + lenToClear -= _initialX; + } + + if (lenToClear > 0) + { + UpdateColorsIfNecessary(defaultColor); + _console.Write(Spaces(lenToClear)); + } + } + + UpdateColorsIfNecessary(defaultColor); + + while (previousPhysicalLine > physicalLine) + { + _console.Write("\n"); + physicalLine += 1; + var lenToClear = physicalLine == previousPhysicalLine ? lenPrevLastLine : bufferWidth; + if (lenToClear > 0) + { + _console.Write(Spaces(lenToClear)); } } - if (!rendered) + // Fewer lines than our last render? Clear them. + for (; previousLogicalLine < previousRenderLines.Length; previousLogicalLine++) { - _console.WriteBufferLines(_consoleBuffer, ref _initialY); + _console.Write("\n"); + _console.Write(Spaces(previousRenderLines[previousLogicalLine].columns)); } - var coordinates = ConvertOffsetToCoordinates(_current); - PlaceCursor(coordinates.X, coordinates.Y); + _previousRender = renderData; + + // Reset the colors after we've finished all our rendering. + _console.Write("\x1b[0m"); - if ((_initialY + bufferLineCount) > (_console.WindowTop + _console.WindowHeight)) + if (_initialY + physicalLine >= _console.BufferHeight) { - _console.WindowTop = _initialY + bufferLineCount - _console.WindowHeight; + // We had to scroll to render everything, update _initialY + _initialY = _console.BufferHeight - physicalLine; } + var point = ConvertOffsetToPoint(_current); + PlaceCursor(point.X, point.Y); + _console.CursorVisible = true; + + // TODO: set WindowTop if necessary + _lastRenderTime.Restart(); } - private int LengthInBufferCells(char c) + private static string Spaces(int cnt) + { + return cnt < _spaces.Length + ? (_spaces[cnt] ?? (_spaces[cnt] = new string(' ', cnt))) + : new string(' ', cnt); + } + + private static int LengthInBufferCells(string str) + { + var sum = 0; + var len = str.Length; + for (var i = 0; i < len; i++) + { + var c = str[i]; + if (c == 0x1b && (i+1) < len && str[i+1] == '[') + { + // Simple escape sequence skipping + i += 2; + while (i < len && str[i] != 'm') + i++; + + continue; + } + sum += LengthInBufferCells(c); + } + return sum; + } + + private static int LengthInBufferCells(char c) { if (c < 256) { @@ -337,95 +531,56 @@ private int LengthInBufferCells(char c) return 1 + (isWide ? 1 : 0); } - private static void WriteBlankLines(int count, int top) - { - var console = _singleton._console; - var blanks = new CHAR_INFO[count * console.BufferWidth]; - for (int i = 0; i < blanks.Length; i++) - { - blanks[i].BackgroundColor = console.BackgroundColor; - blanks[i].ForegroundColor = console.ForegroundColor; - blanks[i].UnicodeChar = ' '; - } - console.WriteBufferLines(blanks, ref top); - } - - private static CHAR_INFO[] ReadBufferLines(int top, int count) - { - return _singleton._console.ReadBufferLines(top, count); - } - - private void GetTokenColors(Token token, out ConsoleColor foregroundColor, out ConsoleColor backgroundColor) + private string GetTokenColor(Token token) { switch (token.Kind) { case TokenKind.Comment: - foregroundColor = _options.CommentForegroundColor; - backgroundColor = _options.CommentBackgroundColor; - return; + return _options._commentColor; case TokenKind.Parameter: - foregroundColor = _options.ParameterForegroundColor; - backgroundColor = _options.ParameterBackgroundColor; - return; + return _options._parameterColor; case TokenKind.Variable: case TokenKind.SplattedVariable: - foregroundColor = _options.VariableForegroundColor; - backgroundColor = _options.VariableBackgroundColor; - return; + return _options._variableColor; case TokenKind.StringExpandable: case TokenKind.StringLiteral: case TokenKind.HereStringExpandable: case TokenKind.HereStringLiteral: - foregroundColor = _options.StringForegroundColor; - backgroundColor = _options.StringBackgroundColor; - return; + return _options._stringColor; case TokenKind.Number: - foregroundColor = _options.NumberForegroundColor; - backgroundColor = _options.NumberBackgroundColor; - return; + return _options._numberColor; } if ((token.TokenFlags & TokenFlags.CommandName) != 0) { - foregroundColor = _options.CommandForegroundColor; - backgroundColor = _options.CommandBackgroundColor; - return; + return _options._commandColor; } if ((token.TokenFlags & TokenFlags.Keyword) != 0) { - foregroundColor = _options.KeywordForegroundColor; - backgroundColor = _options.KeywordBackgroundColor; - return; + return _options._keywordColor; } if ((token.TokenFlags & (TokenFlags.BinaryOperator | TokenFlags.UnaryOperator | TokenFlags.AssignmentOperator)) != 0) { - foregroundColor = _options.OperatorForegroundColor; - backgroundColor = _options.OperatorBackgroundColor; - return; + return _options._operatorColor; } if ((token.TokenFlags & TokenFlags.TypeName) != 0) { - foregroundColor = _options.TypeForegroundColor; - backgroundColor = _options.TypeBackgroundColor; - return; + return _options._typeColor; } if ((token.TokenFlags & TokenFlags.MemberName) != 0) { - foregroundColor = _options.MemberForegroundColor; - backgroundColor = _options.MemberBackgroundColor; - return; + return _options._memberColor; } - foregroundColor = _options.DefaultTokenForegroundColor; - backgroundColor = _options.DefaultTokenBackgroundColor; + return _options._defaultTokenColor; } private void GetRegion(out int start, out int length) @@ -442,66 +597,30 @@ private void GetRegion(out int start, out int length) } } - private bool InRegion(int i) - { - int start, end; - if (_mark > _current) - { - start = _current; - end = _mark; - } - else - { - start = _mark; - end = _current; - } - return i >= start && i < end; - } - - private void MaybeEmphasize(ref CHAR_INFO charInfo, int i, ConsoleColor foregroundColor, ConsoleColor backgroundColor) - { - if (i >= _emphasisStart && i < (_emphasisStart + _emphasisLength)) - { - backgroundColor = _options.EmphasisBackgroundColor; - foregroundColor = _options.EmphasisForegroundColor; - } - else if (_visualSelectionCommandCount > 0 && InRegion(i)) - { - // We can't quite emulate real console selection because it inverts - // based on actual screen colors, our pallete is limited. The choice - // to invert only the lower 3 bits to change the color is somewhat - // but looks best with the 2 default color schemes - starting PowerShell - // from it's shortcut or from a cmd shortcut. - foregroundColor = (ConsoleColor)((int)foregroundColor ^ 7); - backgroundColor = (ConsoleColor)((int)backgroundColor ^ 7); - } - - charInfo.ForegroundColor = foregroundColor; - charInfo.BackgroundColor = backgroundColor; - } - private void PlaceCursor(int x, int y) { int statusLineCount = GetStatusLineCount(); if ((y + statusLineCount) >= _console.BufferHeight) { - _console.ScrollBuffer((y + statusLineCount) - _console.BufferHeight + 1); - y = _console.BufferHeight - 1; + var scrollCount = y + statusLineCount - _console.BufferHeight + 1; + _console.ScrollBuffer(scrollCount); + _initialY -= scrollCount; + y -= scrollCount; } _console.SetCursorPosition(x, y); } private void MoveCursor(int newCursor) { - var coordinates = ConvertOffsetToCoordinates(newCursor); - PlaceCursor(coordinates.X, coordinates.Y); + var point = ConvertOffsetToPoint(newCursor); + PlaceCursor(point.X, point.Y); _current = newCursor; } - private COORD ConvertOffsetToCoordinates(int offset) + internal Point ConvertOffsetToPoint(int offset) { int x = _initialX; - int y = _initialY + Options.ExtraPromptLineCount; + int y = _initialY; int bufferWidth = _console.BufferWidth; var continuationPromptLength = Options.ContinuationPrompt.Length; @@ -547,40 +666,15 @@ private COORD ConvertOffsetToCoordinates(int offset) y++; } } - - return new COORD {X = (short)x, Y = (short)y}; - } - private int ConvertOffsetToConsoleBufferOffset(int offset, int startIndex) - { - int j = startIndex; - for (int i = 0; i < offset; i++) - { - var c = _buffer[i]; - if (c == '\n') - { - for (int k = 0; k < Options.ContinuationPrompt.Length; k++) - { - j++; - } - } - else if (LengthInBufferCells(c) > 1) - { - j += 2; - } - else - { - j++; - } - } - return j; + return new Point {X = x, Y = y}; } - private int ConvertLineAndColumnToOffset(COORD coord) + private int ConvertLineAndColumnToOffset(Point point) { int offset; int x = _initialX; - int y = _initialY + Options.ExtraPromptLineCount; + int y = _initialY; int bufferWidth = _console.BufferWidth; var continuationPromptLength = Options.ContinuationPrompt.Length; @@ -588,7 +682,7 @@ private int ConvertLineAndColumnToOffset(COORD coord) { // If we are on the correct line, return when we find // the correct column - if (coord.Y == y && coord.X <= x) + if (point.Y == y && point.X <= x) { return offset; } @@ -597,7 +691,7 @@ private int ConvertLineAndColumnToOffset(COORD coord) { // If we are about to move off of the correct line, // the line was shorter than the column we wanted so return. - if (coord.Y == y) + if (point.Y == y) { return offset; } @@ -627,7 +721,7 @@ private int ConvertLineAndColumnToOffset(COORD coord) // Return -1 if y is out of range, otherwise the last line was shorter // than we wanted, but still in range so just return the last offset. - return (coord.Y == y) ? offset : -1; + return (point.Y == y) ? offset : -1; } private bool LineIsMultiLine() @@ -656,7 +750,10 @@ void IPSConsoleReadLineMockableMethods.Ding() case BellStyle.None: break; case BellStyle.Audible: - Console.Beep(Options.DingTone, Options.DingDuration); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Console.Beep(Options.DingTone, Options.DingDuration); + else + Console.Beep(); break; case BellStyle.Visual: // TODO: flash prompt? command line? @@ -681,11 +778,9 @@ private bool PromptYesOrNo(string s) _statusLinePrompt = null; Render(); - return key.Key == ConsoleKey.Y; + return key.IsUnmodifiedChar('y') || key.IsUnmodifiedChar('Y'); } - #region Screen scrolling - /// /// Scroll the display up one screen. /// @@ -760,10 +855,10 @@ public static void ScrollDisplayTop(ConsoleKeyInfo? key = null, object arg = nul public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg = null) { // Ideally, we'll put the last input line at the bottom of the window - var coordinates = _singleton.ConvertOffsetToCoordinates(_singleton._buffer.Length); + var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); var console = _singleton._console; - var newTop = coordinates.Y - console.WindowHeight + 1; + var newTop = point.Y - console.WindowHeight + 1; // If the cursor is already visible, and we're on the first // page-worth of the buffer, then just scroll to the top (we can't @@ -793,7 +888,5 @@ public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg } console.SetWindowPosition(0, newTop); } - - #endregion Screen scrolling } } diff --git a/PSReadLine/Replace.vi.cs b/PSReadLine/Replace.vi.cs index a06713a74..c6cdb238f 100644 --- a/PSReadLine/Replace.vi.cs +++ b/PSReadLine/Replace.vi.cs @@ -21,23 +21,9 @@ private static void ViReplaceUntilEsc(ConsoleKeyInfo? key, object arg) StringBuilder deletedStr = new StringBuilder(); ConsoleKeyInfo nextKey = ReadKey(); - while (nextKey.Key != ConsoleKey.Escape && nextKey.Key != ConsoleKey.Enter) + while (!nextKey.EqualsNormalized(Keys.Escape) && !nextKey.EqualsNormalized(Keys.Enter)) { - if (nextKey.Key != ConsoleKey.Backspace && nextKey.KeyChar != '\u0000') - { - if (_singleton._current >= _singleton._buffer.Length) - { - _singleton._buffer.Append(nextKey.KeyChar); - } - else - { - deletedStr.Append(_singleton._buffer[_singleton._current]); - _singleton._buffer[_singleton._current] = nextKey.KeyChar; - } - _singleton._current++; - _singleton.Render(); - } - if (nextKey.Key == ConsoleKey.Backspace) + if (nextKey.EqualsNormalized(Keys.Backspace)) { if (_singleton._current == startingCursor) { @@ -58,6 +44,20 @@ private static void ViReplaceUntilEsc(ConsoleKeyInfo? key, object arg) _singleton.Render(); } } + else if (nextKey.ShouldInsert()) + { + if (_singleton._current >= _singleton._buffer.Length) + { + _singleton._buffer.Append(nextKey.KeyChar); + } + else + { + deletedStr.Append(_singleton._buffer[_singleton._current]); + _singleton._buffer[_singleton._current] = nextKey.KeyChar; + } + _singleton._current++; + _singleton.Render(); + } nextKey = ReadKey(); } @@ -70,7 +70,7 @@ private static void ViReplaceUntilEsc(ConsoleKeyInfo? key, object arg) _singleton.EndEditGroup(); } - if (nextKey.Key == ConsoleKey.Enter) + if (nextKey.EqualsNormalized(Keys.Enter)) { ViAcceptLine(nextKey); } @@ -148,7 +148,7 @@ private static void ViReplaceWord(ConsoleKeyInfo? key, object arg) } _singleton._lastWordDelimiter = char.MinValue; } - if (_singleton._current == _singleton._buffer.Length - 1 + if (_singleton._current == _singleton._buffer.Length - 1 && !_singleton.IsDelimiter(_singleton._lastWordDelimiter, _singleton.Options.WordDelimiters) && _singleton._shouldAppend) { @@ -239,8 +239,6 @@ private static void ReplaceCharInPlace(ConsoleKeyInfo? key, object arg) /// /// Deletes until given character /// - /// - /// public static void ViReplaceToChar(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; @@ -269,8 +267,6 @@ private static void ViReplaceToChar(char keyChar, ConsoleKeyInfo? key = null, ob /// /// Replaces until given character /// - /// - /// public static void ViReplaceToCharBackward(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; @@ -289,8 +285,6 @@ private static void ViReplaceToCharBack(char keyChar, ConsoleKeyInfo? key = null /// /// Replaces until given character /// - /// - /// public static void ViReplaceToBeforeChar(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; @@ -310,8 +304,6 @@ private static void ViReplaceToBeforeChar(char keyChar, ConsoleKeyInfo? key = nu /// /// Replaces until given character /// - /// - /// public static void ViReplaceToBeforeCharBackward(ConsoleKeyInfo? key = null, object arg = null) { var keyChar = ReadKey().KeyChar; diff --git a/PSReadLine/ScreenCapture.cs b/PSReadLine/ScreenCapture.cs index b7868a38a..b8bf7e51b 100644 --- a/PSReadLine/ScreenCapture.cs +++ b/PSReadLine/ScreenCapture.cs @@ -3,146 +3,200 @@ --********************************************************************/ using System; +using System.Globalization; using System.Runtime.InteropServices; using System.Text; -using System.Windows.Forms; using Microsoft.PowerShell.Internal; namespace Microsoft.PowerShell { - public partial class PSConsoleReadLine + internal static class ScreenCapture { - private static void InvertLines(int start, int count) + internal struct COORD { - var buffer = ReadBufferLines(start, count); - for (int i = 0; i < buffer.Length; i++) + public short X; + public short Y; + + public override string ToString() { - buffer[i].ForegroundColor = (ConsoleColor)((int)buffer[i].ForegroundColor ^ 7); - buffer[i].BackgroundColor = (ConsoleColor)((int)buffer[i].BackgroundColor ^ 7); + return String.Format(CultureInfo.InvariantCulture, "{0},{1}", X, Y); } - _singleton._console.WriteBufferLines(buffer, ref start, false); } - /// - /// Start interactive screen capture - up/down arrows select lines, enter copies - /// selected text to clipboard as text and html - /// - public static void CaptureScreen(ConsoleKeyInfo? key = null, object arg = null) + internal struct CHAR_INFO { - int selectionTop = _singleton._console.CursorTop; - int selectionHeight = 1; - int currentY = selectionTop; - IConsole console = _singleton._console; + public ushort UnicodeChar; + public ushort Attributes; - // We'll keep the current selection line (currentY) at least 4 lines - // away from the top or bottom of the window. - const int margin = 5; - bool TooCloseToTop() => (currentY - console.WindowTop) < margin; - bool TooCloseToBottom() => ((console.WindowTop + console.WindowHeight) - currentY) < margin; + public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background) + { + UnicodeChar = c; + Attributes = (ushort)(((int)background << 4) | (int)foreground); + } - // Current lines starts out selected - InvertLines(selectionTop, selectionHeight); - bool done = false; - while (!done) + public ConsoleColor ForegroundColor { - var k = ReadKey(); - switch (k.Key) + get => (ConsoleColor)(Attributes & 0xf); + set => Attributes = (ushort)((Attributes & 0xfff0) | ((int)value & 0xf)); + } + + public ConsoleColor BackgroundColor + { + get => (ConsoleColor)((Attributes & 0xf0) >> 4); + set => Attributes = (ushort)((Attributes & 0xff0f) | (((int)value & 0xf) << 4)); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append((char)UnicodeChar); + if (ForegroundColor != Console.ForegroundColor) + sb.AppendFormat(" fg: {0}", ForegroundColor); + if (BackgroundColor != Console.BackgroundColor) + sb.AppendFormat(" bg: {0}", BackgroundColor); + return sb.ToString(); + } + + public override bool Equals(object obj) + { + if (!(obj is CHAR_INFO)) { - case ConsoleKey.K: - case ConsoleKey.UpArrow: - if (TooCloseToTop()) - ScrollDisplayUpLine(); + return false; + } - if (currentY > 0) - { - currentY -= 1; - if ((k.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) - { - if (currentY < selectionTop) - { - // Extend selection up, only invert newly selected line. - InvertLines(currentY, 1); - selectionTop = currentY; - selectionHeight += 1; - } - else if (currentY >= selectionTop) - { - // Selection shortend 1 line, invert unselected line. - InvertLines(currentY + 1, 1); - selectionHeight -= 1; - } - break; - } - goto updateSelectionCommon; - } - break; + var other = (CHAR_INFO)obj; + return this.UnicodeChar == other.UnicodeChar && this.Attributes == other.Attributes; + } - case ConsoleKey.J: - case ConsoleKey.DownArrow: - if (TooCloseToBottom()) - ScrollDisplayDownLine(); + public override int GetHashCode() + { + return UnicodeChar.GetHashCode() + Attributes.GetHashCode(); + } + } - if (currentY < (console.BufferHeight - 1)) - { - currentY += 1; - if ((k.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) - { - if (currentY == (selectionTop + selectionHeight)) - { - // Extend selection down, only invert newly selected line. - InvertLines(selectionTop + selectionHeight, 1); - selectionHeight += 1; - } - else if (currentY == (selectionTop + 1)) - { - // Selection shortend 1 line, invert unselected line. - InvertLines(selectionTop, 1); - selectionTop = currentY; - selectionHeight -= 1; - } - break; - } - goto updateSelectionCommon; - } - break; + [DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool WriteConsoleOutput(IntPtr consoleOutput, CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT writeRegion); - updateSelectionCommon: - // Shift not pressed - unselect current selection - InvertLines(selectionTop, selectionHeight); - selectionTop = currentY; - selectionHeight = 1; - InvertLines(selectionTop, selectionHeight); - break; + [DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool ReadConsoleOutput(IntPtr consoleOutput, [Out] CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT readRegion); - case ConsoleKey.Enter: - InvertLines(selectionTop, selectionHeight); - DumpScreenToClipboard(selectionTop, selectionHeight); - ScrollDisplayToCursor(); - return; + [StructLayout(LayoutKind.Sequential)] + struct COLORREF + { + internal uint ColorDWORD; - case ConsoleKey.Escape: - done = true; - continue; + internal uint R => ColorDWORD & 0xff; + internal uint G => (ColorDWORD >> 8) & 0xff; + internal uint B => (ColorDWORD >> 16) & 0xff; + } - case ConsoleKey.C: - case ConsoleKey.G: - if (k.Modifiers == ConsoleModifiers.Control) - { - done = true; - continue; - } - Ding(); - break; - default: - Ding(); - break; - } + public struct SMALL_RECT + { + public short Left; + public short Top; + public short Right; + public short Bottom; + + public override string ToString() + { + return String.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", Left, Top, Right, Bottom); } - InvertLines(selectionTop, selectionHeight); - ScrollDisplayToCursor(); } - private const string CmdColorTable = @" + [StructLayout(LayoutKind.Sequential)] + struct CONSOLE_SCREEN_BUFFER_INFO_EX + { + internal int cbSize; + internal COORD dwSize; + internal COORD dwCursorPosition; + internal ushort wAttributes; + internal SMALL_RECT srWindow; + internal COORD dwMaximumWindowSize; + internal ushort wPopupAttributes; + internal bool bFullscreenSupported; + internal COLORREF Black; + internal COLORREF DarkBlue; + internal COLORREF DarkGreen; + internal COLORREF DarkCyan; + internal COLORREF DarkRed; + internal COLORREF DarkMagenta; + internal COLORREF DarkYellow; + internal COLORREF Gray; + internal COLORREF DarkGray; + internal COLORREF Blue; + internal COLORREF Green; + internal COLORREF Cyan; + internal COLORREF Red; + internal COLORREF Magenta; + internal COLORREF Yellow; + internal COLORREF White; + } + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool GetConsoleScreenBufferInfoEx(IntPtr hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO_EX csbe); + + internal static CHAR_INFO[] ReadBufferLines(int top, int count, int bufferWidth) + { + var result = new CHAR_INFO[bufferWidth * count]; + var handle = PlatformWindows.GetStdHandle((uint) PlatformWindows.StandardHandleId.Output); + + var readBufferSize = new COORD { + X = (short)bufferWidth, + Y = (short)count}; + var readBufferCoord = new COORD {X = 0, Y = 0}; + var readRegion = new SMALL_RECT + { + Top = (short)top, + Left = 0, + Bottom = (short)(top + count), + Right = (short)(bufferWidth - 1) + }; + ReadConsoleOutput(handle, result, readBufferSize, readBufferCoord, ref readRegion); + return result; + } + + internal static void WriteBufferLines(CHAR_INFO[] buffer, int top, IConsole console) + { + var handle = PlatformWindows.GetStdHandle((uint) PlatformWindows.StandardHandleId.Output); + + int bufferWidth = Console.BufferWidth; + int bufferLineCount = buffer.Length / bufferWidth; + if ((top + bufferLineCount) > Console.BufferHeight) + { + var scrollCount = (top + bufferLineCount) - Console.BufferHeight; + console.ScrollBuffer(scrollCount); + top -= scrollCount; + } + var bufferSize = new COORD + { + X = (short) bufferWidth, + Y = (short) bufferLineCount + }; + var bufferCoord = new COORD {X = 0, Y = 0}; + var bottom = top + bufferLineCount - 1; + var writeRegion = new SMALL_RECT + { + Top = (short) top, + Left = 0, + Bottom = (short) bottom, + Right = (short) (bufferWidth - 1) + }; + WriteConsoleOutput(handle, buffer, + bufferSize, bufferCoord, ref writeRegion); + } + + internal static void InvertLines(int start, int count, IConsole console) + { + var buffer = ReadBufferLines(start, count, console.BufferWidth); + for (int i = 0; i < buffer.Length; i++) + { + buffer[i].ForegroundColor = (ConsoleColor)((int)buffer[i].ForegroundColor ^ 7); + buffer[i].BackgroundColor = (ConsoleColor)((int)buffer[i].BackgroundColor ^ 7); + } + WriteBufferLines(buffer, start, console); + } + + internal const string CmdColorTable = @" \red0\green0\blue0; \red0\green0\blue128; \red0\green128\blue0; @@ -161,7 +215,7 @@ public static void CaptureScreen(ConsoleKeyInfo? key = null, object arg = null) \red255\green255\blue255; "; - private const string PowerShellColorTable = @" + internal const string PowerShellColorTable = @" \red1\green36\blue86; \red0\green0\blue128; \red0\green128\blue0; @@ -180,59 +234,43 @@ public static void CaptureScreen(ConsoleKeyInfo? key = null, object arg = null) \red255\green255\blue255; "; - private static string GetRTFColorFromColorRef(NativeMethods.COLORREF colorref) + static string GetRTFColorFromColorRef(COLORREF colorref) { - return string.Concat("\\red", colorref.R.ToString("D"), - "\\green", colorref.G.ToString("D"), - "\\blue", colorref.B.ToString("D"), ";"); + return String.Concat("\\red", colorref.R.ToString("D"), + "\\green", colorref.G.ToString("D"), + "\\blue", colorref.B.ToString("D"), ";"); } - private static string GetColorTable() + internal static string GetColorTable(IConsole console) { - var handle = NativeMethods.GetStdHandle((uint) StandardHandleId.Output); - var csbe = new NativeMethods.CONSOLE_SCREEN_BUFFER_INFO_EX + var handle = PlatformWindows.GetStdHandle((uint) PlatformWindows.StandardHandleId.Output); + var csbe = new CONSOLE_SCREEN_BUFFER_INFO_EX { - cbSize = Marshal.SizeOf(typeof(NativeMethods.CONSOLE_SCREEN_BUFFER_INFO_EX)) + cbSize = Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFO_EX)) }; - if (NativeMethods.GetConsoleScreenBufferInfoEx(handle, ref csbe)) + if (GetConsoleScreenBufferInfoEx(handle, ref csbe)) { - return GetRTFColorFromColorRef(csbe.Black) + - GetRTFColorFromColorRef(csbe.DarkBlue) + - GetRTFColorFromColorRef(csbe.DarkGreen) + - GetRTFColorFromColorRef(csbe.DarkCyan) + - GetRTFColorFromColorRef(csbe.DarkRed) + - GetRTFColorFromColorRef(csbe.DarkMagenta) + - GetRTFColorFromColorRef(csbe.DarkYellow) + - GetRTFColorFromColorRef(csbe.Gray) + - GetRTFColorFromColorRef(csbe.DarkGray) + - GetRTFColorFromColorRef(csbe.Blue) + - GetRTFColorFromColorRef(csbe.Green) + - GetRTFColorFromColorRef(csbe.Cyan) + - GetRTFColorFromColorRef(csbe.Red) + - GetRTFColorFromColorRef(csbe.Magenta) + - GetRTFColorFromColorRef(csbe.Yellow) + - GetRTFColorFromColorRef(csbe.White); + return GetRTFColorFromColorRef(csbe.Black) + GetRTFColorFromColorRef(csbe.DarkBlue) + GetRTFColorFromColorRef(csbe.DarkGreen) + GetRTFColorFromColorRef(csbe.DarkCyan) + GetRTFColorFromColorRef(csbe.DarkRed) + GetRTFColorFromColorRef(csbe.DarkMagenta) + GetRTFColorFromColorRef(csbe.DarkYellow) + GetRTFColorFromColorRef(csbe.Gray) + GetRTFColorFromColorRef(csbe.DarkGray) + GetRTFColorFromColorRef(csbe.Blue) + GetRTFColorFromColorRef(csbe.Green) + GetRTFColorFromColorRef(csbe.Cyan) + GetRTFColorFromColorRef(csbe.Red) + GetRTFColorFromColorRef(csbe.Magenta) + GetRTFColorFromColorRef(csbe.Yellow) + GetRTFColorFromColorRef(csbe.White); } // A bit of a hack if the above failed - assume PowerShell's color scheme if the // background color is Magenta, otherwise we assume the default scheme. - return _singleton._console.BackgroundColor == ConsoleColor.DarkMagenta + return console.BackgroundColor == ConsoleColor.DarkMagenta ? PowerShellColorTable : CmdColorTable; } - private static void DumpScreenToClipboard(int top, int count) + internal static void DumpScreenToClipboard(int top, int count, IConsole console) { - var buffer = ReadBufferLines(top, count); - var bufferWidth = _singleton._console.BufferWidth; + var buffer = ReadBufferLines(top, count, console.BufferWidth); + var bufferWidth = console.BufferWidth; - var dataObject = new DataObject(); var textBuffer = new StringBuilder(buffer.Length + count); var rtfBuffer = new StringBuilder(); rtfBuffer.Append(@"{\rtf\ansi{\fonttbl{\f0 Consolas;}}"); - var colorTable = GetColorTable(); + var colorTable = GetColorTable(console); rtfBuffer.AppendFormat(@"{{\colortbl;{0}}}{1}", colorTable, Environment.NewLine); rtfBuffer.Append(@"\f0 \fs18 "); @@ -283,11 +321,11 @@ private static void DumpScreenToClipboard(int top, int count) textBuffer.Append(c); switch (c) { - case '\\': rtfBuffer.Append(@"\\"); break; - case '\t': rtfBuffer.Append(@"\tab"); break; - case '{': rtfBuffer.Append(@"\{"); break; - case '}': rtfBuffer.Append(@"\}"); break; - default: rtfBuffer.Append(c); break; + case '\\': rtfBuffer.Append(@"\\"); break; + case '\t': rtfBuffer.Append(@"\tab"); break; + case '{': rtfBuffer.Append(@"\{"); break; + case '}': rtfBuffer.Append(@"\}"); break; + default: rtfBuffer.Append(c); break; } } } @@ -296,9 +334,135 @@ private static void DumpScreenToClipboard(int top, int count) } rtfBuffer.Append("}}"); - dataObject.SetData(DataFormats.Text, textBuffer.ToString()); - dataObject.SetData(DataFormats.Rtf, rtfBuffer.ToString()); - ExecuteOnSTAThread(() => Clipboard.SetDataObject(dataObject, copy: true)); + Clipboard.SetRtf(textBuffer.ToString(), rtfBuffer.ToString()); + } + } + + public partial class PSConsoleReadLine + { + /// + /// Start interactive screen capture - up/down arrows select lines, enter copies + /// selected text to clipboard as text and html + /// + public static void CaptureScreen(ConsoleKeyInfo? key = null, object arg = null) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Ding(); + return; + } + + int selectionTop = _singleton._console.CursorTop; + int selectionHeight = 1; + int currentY = selectionTop; + IConsole console = _singleton._console; + + // We'll keep the current selection line (currentY) at least 4 lines + // away from the top or bottom of the window. + const int margin = 5; + bool TooCloseToTop() => (currentY - console.WindowTop) < margin; + bool TooCloseToBottom() => ((console.WindowTop + console.WindowHeight) - currentY) < margin; + + // Current lines starts out selected + ScreenCapture.InvertLines(selectionTop, selectionHeight, console); + bool done = false; + while (!done) + { + var k = ReadKey(); + switch (k.Key) + { + case ConsoleKey.K: + case ConsoleKey.UpArrow: + if (TooCloseToTop()) + ScrollDisplayUpLine(); + + if (currentY > 0) + { + currentY -= 1; + if ((k.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) + { + if (currentY < selectionTop) + { + // Extend selection up, only invert newly selected line. + ScreenCapture.InvertLines(currentY, 1, console); + selectionTop = currentY; + selectionHeight += 1; + } + else if (currentY >= selectionTop) + { + // Selection shortend 1 line, invert unselected line. + ScreenCapture.InvertLines(currentY + 1, 1, console); + selectionHeight -= 1; + } + break; + } + goto updateSelectionCommon; + } + break; + + case ConsoleKey.J: + case ConsoleKey.DownArrow: + if (TooCloseToBottom()) + ScrollDisplayDownLine(); + + if (currentY < (console.BufferHeight - 1)) + { + currentY += 1; + if ((k.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift) + { + if (currentY == (selectionTop + selectionHeight)) + { + // Extend selection down, only invert newly selected line. + ScreenCapture.InvertLines(selectionTop + selectionHeight, 1, console); + selectionHeight += 1; + } + else if (currentY == (selectionTop + 1)) + { + // Selection shortend 1 line, invert unselected line. + ScreenCapture.InvertLines(selectionTop, 1, console); + selectionTop = currentY; + selectionHeight -= 1; + } + break; + } + goto updateSelectionCommon; + } + break; + + updateSelectionCommon: + // Shift not pressed - unselect current selection + ScreenCapture.InvertLines(selectionTop, selectionHeight, console); + selectionTop = currentY; + selectionHeight = 1; + ScreenCapture.InvertLines(selectionTop, selectionHeight, console); + break; + + case ConsoleKey.Enter: + ScreenCapture.InvertLines(selectionTop, selectionHeight, console); + ScreenCapture.DumpScreenToClipboard(selectionTop, selectionHeight, console); + ScrollDisplayToCursor(); + return; + + case ConsoleKey.Escape: + done = true; + continue; + + case ConsoleKey.C: + case ConsoleKey.G: + if (k.Modifiers == ConsoleModifiers.Control) + { + done = true; + continue; + } + Ding(); + break; + default: + Ding(); + break; + } + } + ScreenCapture.InvertLines(selectionTop, selectionHeight, console); + ScrollDisplayToCursor(); } } } diff --git a/PSReadLine/UndoRedo.cs b/PSReadLine/UndoRedo.cs index 98989fd9a..5aefb7705 100644 --- a/PSReadLine/UndoRedo.cs +++ b/PSReadLine/UndoRedo.cs @@ -74,7 +74,6 @@ public static void Undo(ConsoleKeyInfo? key = null, object arg = null) _singleton.ClearStatusMessage(render: false); } _singleton._edits[_singleton._undoEditIndex - 1].Undo(); - _singleton._edits.RemoveAt(_singleton._undoEditIndex - 1); _singleton._undoEditIndex--; if (_singleton._options.EditMode == EditMode.Vi && _singleton._current >= _singleton._buffer.Length) { diff --git a/PSReadLine/Words.vi.cs b/PSReadLine/Words.vi.cs index 929be017c..413f72979 100644 --- a/PSReadLine/Words.vi.cs +++ b/PSReadLine/Words.vi.cs @@ -121,7 +121,7 @@ private int ViFindPreviousWordPoint(string wordDelimiters) /// Returns the beginning of the current/next word as defined by wordDelimiters and whitespace. /// /// Current cursor location. - /// Characters used to deliminate words. + /// Characters used to delimit words. /// Location of the beginning of the previous word. private int ViFindPreviousWordPoint(int i, string wordDelimiters) { diff --git a/PSReadLine/packages.config b/PSReadLine/packages.config index 0dc84546b..a85d3fe7e 100644 --- a/PSReadLine/packages.config +++ b/PSReadLine/packages.config @@ -1,4 +1,5 @@  - + + \ No newline at end of file diff --git a/PSReadline.build.ps1 b/PSReadline.build.ps1 index 982b1ff4f..994174d33 100644 --- a/PSReadline.build.ps1 +++ b/PSReadline.build.ps1 @@ -56,7 +56,7 @@ task CheckPlatyPSInstalled ` $restoreNugetParameters = @{ Inputs = "PSReadLine/packages.config" # We could look for other files, but this is probably good enough. - Outputs = "PSReadLine/packages/Microsoft.PowerShell.3.ReferenceAssemblies.1.0.0/Microsoft.PowerShell.3.ReferenceAssemblies.1.0.0.nupkg" + Outputs = "PSReadLine/packages/Microsoft.PowerShell.5.ReferenceAssemblies.1.0.0/Microsoft.PowerShell.5.ReferenceAssemblies.1.0.0.nupkg" } <# @@ -89,7 +89,7 @@ $binaryModuleParams = @{ Synopsis: Build main binary module #> task BuildMainModule @binaryModuleParams RestoreNugetPackages, { - exec { msbuild PSReadline/PSReadLine.csproj /t:Rebuild /p:Configuration=$Configuration } + exec { msbuild PSReadline/PSReadLine.csproj /t:Rebuild /p:Configuration=$Configuration /p:Platform=AnyCPU } } @@ -102,7 +102,7 @@ $buildTestParams = @{ Synopsis: Build executable for interactive testing/development #> task BuildTestHost @buildTestParams BuildMainModule, { - exec { msbuild TestPSReadLine/TestPSReadLine.csproj /t:Rebuild /p:Configuration=$Configuration } + exec { msbuild TestPSReadLine/TestPSReadLine.csproj /t:Rebuild /p:Configuration=$Configuration /p:Platform=AnyCPU } } @@ -116,7 +116,7 @@ $buildUnitTestParams = @{ Synopsis: Build the unit tests #> task BuildUnitTests @buildUnitTestParams BuildMainModule, { - exec { msbuild UnitTestPSReadLine/UnitTestPSReadLine.csproj /t:Rebuild /p:Configuration=$Configuration } + exec { msbuild UnitTestPSReadLine/UnitTestPSReadLine.csproj /t:Rebuild /p:Configuration=$Configuration /p:Platform=AnyCPU } } @@ -135,6 +135,7 @@ task LayoutModule BuildMainModule, BuildMamlHelp, { 'PSReadLine/Changes.txt', 'PSReadLine/License.txt', 'PSReadLine/SamplePSReadlineProfile.ps1', + 'PSReadLine/PSReadLine.format.ps1xml', 'PSReadLine/PSReadLine.psm1' foreach ($file in $extraFiles) @@ -144,6 +145,7 @@ task LayoutModule BuildMainModule, BuildMamlHelp, { Copy-Item PSReadLine/bin/$Configuration/Microsoft.PowerShell.PSReadLine.dll $targetDir + Copy-Item PSReadLine/bin/$Configuration/System.Runtime.InteropServices.RuntimeInformation.dll $targetDir Copy-Item PSReadLine/en-US/about_PSReadline.help.txt $targetDir/en-US # Copy module manifest, but fix the version to match what we've specified in the binary module. diff --git a/TestPSReadLine/App.config b/TestPSReadLine/App.config index 8e1564635..bae5d6d81 100644 --- a/TestPSReadLine/App.config +++ b/TestPSReadLine/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/TestPSReadLine/Program.cs b/TestPSReadLine/Program.cs index 76ee6cd47..abf61b197 100644 --- a/TestPSReadLine/Program.cs +++ b/TestPSReadLine/Program.cs @@ -50,6 +50,9 @@ static void Main() EditMode = EditMode.Emacs, HistoryNoDuplicates = true, }); + var options = PSConsoleReadLine.GetOptions(); + options.CommandColor = "#8181f7"; + options.StringColor = "\x1b[38;5;100m"; PSConsoleReadLine.SetKeyHandler(new[] {"Ctrl+LeftArrow"}, PSConsoleReadLine.ShellBackwardWord, "", ""); PSConsoleReadLine.SetKeyHandler(new[] {"Ctrl+RightArrow"}, PSConsoleReadLine.ShellNextWord, "", ""); PSConsoleReadLine.SetKeyHandler(new[] {"F4"}, PSConsoleReadLine.HistorySearchBackward, "", ""); @@ -74,25 +77,28 @@ static void Main() // uncomment if you hit a hang, run it once, then comment it out again. //ps.Commands.Clear(); //ps.AddCommand("Get-Command").Invoke(); - } - while (true) - { - Console.Write("TestHostPS> "); + executionContext.InvokeProvider.Item.Set("function:prompt", ScriptBlock.Create("'TestHostPS> '")); + + while (true) + { + ps.Commands.Clear(); + Console.Write(string.Join("", ps.AddCommand("prompt").Invoke())); - var line = PSConsoleReadLine.ReadLine(null, executionContext); - Console.WriteLine(line); - line = line.Trim(); - if (line.Equals("exit")) - Environment.Exit(0); - if (line.Equals("cmd")) - PSConsoleReadLine.SetOptions(new SetPSReadlineOption {EditMode = EditMode.Windows}); - if (line.Equals("emacs")) - PSConsoleReadLine.SetOptions(new SetPSReadlineOption {EditMode = EditMode.Emacs}); - if (line.Equals("vi")) - PSConsoleReadLine.SetOptions(new SetPSReadlineOption {EditMode = EditMode.Vi}); - if (line.Equals("nodupes")) - PSConsoleReadLine.SetOptions(new SetPSReadlineOption {HistoryNoDuplicates = true}); + var line = PSConsoleReadLine.ReadLine(rs, executionContext); + Console.WriteLine(line); + line = line.Trim(); + if (line.Equals("exit")) + Environment.Exit(0); + if (line.Equals("cmd")) + PSConsoleReadLine.SetOptions(new SetPSReadlineOption {EditMode = EditMode.Windows}); + if (line.Equals("emacs")) + PSConsoleReadLine.SetOptions(new SetPSReadlineOption {EditMode = EditMode.Emacs}); + if (line.Equals("vi")) + PSConsoleReadLine.SetOptions(new SetPSReadlineOption {EditMode = EditMode.Vi}); + if (line.Equals("nodupes")) + PSConsoleReadLine.SetOptions(new SetPSReadlineOption {HistoryNoDuplicates = true}); + } } } } diff --git a/TestPSReadLine/TestPSReadLine.csproj b/TestPSReadLine/TestPSReadLine.csproj index 2606687ae..89c596082 100644 --- a/TestPSReadLine/TestPSReadLine.csproj +++ b/TestPSReadLine/TestPSReadLine.csproj @@ -1,5 +1,5 @@  - + Debug @@ -9,8 +9,9 @@ Properties TestPSReadLine TestPSReadLine - v4.5 + v4.6.1 512 + AnyCPU @@ -37,7 +38,7 @@ - ..\PSReadLine\packages\Microsoft.PowerShell.3.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll + ..\PSReadLine\packages\Microsoft.PowerShell.5.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll True @@ -71,4 +72,4 @@ --> - \ No newline at end of file + diff --git a/TestPSReadLine/packages.config b/TestPSReadLine/packages.config index cae6c6db8..b4ef22ebc 100644 --- a/TestPSReadLine/packages.config +++ b/TestPSReadLine/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/UnitTestPSReadLine/BasicEditingTest.cs b/UnitTestPSReadLine/BasicEditingTest.cs index 5b782d172..f6da79b0c 100644 --- a/UnitTestPSReadLine/BasicEditingTest.cs +++ b/UnitTestPSReadLine/BasicEditingTest.cs @@ -25,7 +25,7 @@ public void TestInput() public void TestRevertLine() { // Add one test for chords - TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+X,Escape", PSConsoleReadLine.RevertLine)); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+x,Escape", PSConsoleReadLine.RevertLine)); Test("ls", Keys("di", _.Escape, "ls")); Test("ls", Keys("di", _.CtrlX, _.Escape, "ls")); @@ -246,37 +246,37 @@ public void TestInsertLineAbove() Test("56\n1234", Keys("1234", _.Home, _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(0, 0)), "56")); // Test case - start with single line, cursor in middle - Test("56\n1234", Keys("1234", - _.LeftArrow, _.LeftArrow, _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(0, 0)), + Test("56\n1234", Keys("1234", + _.LeftArrow, _.LeftArrow, _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(0, 0)), "56")); // Test case - start with multi-line, cursor at end of second line (end of input) - Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", + Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "9ABC")); // Test case - start with multi-line, cursor at beginning of second line - Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", + Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "9ABC")); // Test case - start with multi-line, cursor at end of first line - Test("9ABC\n1234\n5678", Keys("1234", _.ShiftEnter, "5678", + Test("9ABC\n1234\n5678", Keys("1234", _.ShiftEnter, "5678", _.UpArrow, _.LeftArrow, _.End, CheckThat(() => AssertCursorLeftTopIs(4, 0)), _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(0, 0)), "9ABC")); // Test case - start with multi-line, cursor at beginning of first line - temporarily having to press Home twice to // work around bug in home handler. - Test("9ABC\n1234\n5678", Keys("1234", _.ShiftEnter, "5678", + Test("9ABC\n1234\n5678", Keys("1234", _.ShiftEnter, "5678", _.UpArrow, _.LeftArrow, _.Home, _.Home, CheckThat(() => AssertCursorLeftTopIs(0, 0)), _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(0, 0)), "9ABC")); // Test case - insert multiple blank lines - Test("1234\n9ABC\n\n5678", Keys("1234", _.ShiftEnter, "5678", + Test("1234\n9ABC\n\n5678", Keys("1234", _.ShiftEnter, "5678", _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), _.CtrlEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "9ABC")); @@ -290,42 +290,42 @@ public void TestInsertLineBelow() var continutationPromptLength = PSConsoleReadlineOptions.DefaultContinuationPrompt.Length; // Test case - start with single line, cursor at end - Test("1234\n56", Keys("1234", - _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + Test("1234\n56", Keys("1234", + _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "56")); // Test case - start with single line, cursor in home position - Test("1234\n56", Keys("1234", - _.Home, _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + Test("1234\n56", Keys("1234", + _.Home, _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "56")); // Test case - start with single line, cursor in middle - Test("1234\n56", Keys("1234", - _.LeftArrow, _.LeftArrow, _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + Test("1234\n56", Keys("1234", + _.LeftArrow, _.LeftArrow, _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "56")); // Test case - start with multi-line, cursor at end of second line (end of input) - Test("1234\n5678\n9ABC", Keys("1234", _.ShiftEnter, "5678", - _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), + Test("1234\n5678\n9ABC", Keys("1234", _.ShiftEnter, "5678", + _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), "9ABC")); // Test case - start with multi-line, cursor at beginning of second line - Test("1234\n5678\n9ABC", Keys("1234", _.ShiftEnter, "5678", - _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), + Test("1234\n5678\n9ABC", Keys("1234", _.ShiftEnter, "5678", + _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), "9ABC")); // Test case - start with multi-line, cursor at end of first line - Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", - _.UpArrow, _.LeftArrow, _.End, CheckThat(() => AssertCursorLeftTopIs(4, 0)), - _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", + _.UpArrow, _.LeftArrow, _.End, CheckThat(() => AssertCursorLeftTopIs(4, 0)), + _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "9ABC")); // Test case - start with multi-line, cursor at beginning of first line - temporarily having to press Home twice to // work around bug in home handler. - Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", - _.UpArrow, _.LeftArrow, _.Home, _.Home, CheckThat(() => AssertCursorLeftTopIs(0, 0)), - _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + Test("1234\n9ABC\n5678", Keys("1234", _.ShiftEnter, "5678", + _.UpArrow, _.LeftArrow, _.Home, _.Home, CheckThat(() => AssertCursorLeftTopIs(0, 0)), + _.CtrlShiftEnter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), "9ABC")); // Test case - insert multiple blank lines @@ -340,7 +340,7 @@ public void TestMultilineHomeBugFixed() { TestSetup(KeyMode.Cmd); - // Going from second line to first line, press left arrow and then home. + // Going from second line to first line, press left arrow and then home. // That puts cursor in column 1 instead of 0. Bug? This could have something // to do with BeginningOfLine testing against i > 1 in multiline edit instead // of i > 0. @@ -352,7 +352,10 @@ public void TestIgnore() { TestSetup(KeyMode.Emacs); - Test("ab", Keys("a", _.VolumeDown, _.VolumeMute, _.VolumeUp, "b")); + var volumeUp = new ConsoleKeyInfo('\0', ConsoleKey.VolumeUp, false, false, false); + var volumeDown = new ConsoleKeyInfo('\0', ConsoleKey.VolumeDown, false, false, false); + var volumeMute = new ConsoleKeyInfo('\0', ConsoleKey.VolumeMute, false, false, false); + Test("ab", Keys("a", volumeDown, volumeMute, volumeUp, "b")); } } } diff --git a/UnitTestPSReadLine/CompletionTest.cs b/UnitTestPSReadLine/CompletionTest.cs index 2cfeddcce..41a6f44e9 100644 --- a/UnitTestPSReadLine/CompletionTest.cs +++ b/UnitTestPSReadLine/CompletionTest.cs @@ -69,6 +69,12 @@ public void TestPossibleCompletions() const string promptLine1 = "c:\\windows"; const string promptLine2 = "PS> "; + using (var ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + ps.AddScript($@"function prompt {{ ""{promptLine1}`n{promptLine2}"" }}"); + ps.Invoke(); + } + _console.Clear(); Test("psvar", Keys( "psvar", @@ -87,6 +93,12 @@ public void TestPossibleCompletions() TokenClassification.Command, "psvar"))), prompt: promptLine1 + "\n" + promptLine2); + using (var ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + ps.AddCommand("Remove-Item").AddArgument("prompt"); + ps.Invoke(); + } + _console.Clear(); TestMustDing("none", Keys( "none", @@ -130,12 +142,7 @@ public void TestShowTooltips() PSConsoleReadLine.GetOptions().ShowToolTips = true; _console.Clear(); - Test("Get-Tooltips", Keys( - "Get-Tooltips", _.CtrlSpace, - CheckThat(() => AssertScreenIs(2, - TokenClassification.Command, "Get-Tooltips", NextLine, - TokenClassification.None, - "item1 - useful description goes here")))); + // TODO: } [TestMethod] diff --git a/UnitTestPSReadLine/DigitArgumentTest.cs b/UnitTestPSReadLine/DigitArgumentTest.cs index c81ef8dec..d2139eb8d 100644 --- a/UnitTestPSReadLine/DigitArgumentTest.cs +++ b/UnitTestPSReadLine/DigitArgumentTest.cs @@ -14,8 +14,8 @@ public void TestDigitArgumentValues() { int argValue = 0; TestSetup(KeyMode.Emacs, - new KeyHandler("Ctrl+Z", (key, arg) => argValue = (int)arg)); - + new KeyHandler("Ctrl+z", (key, arg) => argValue = (int)arg)); + Test("", Keys( _.Alt1, _.CtrlZ, CheckThat(() => Assert.AreEqual(1, argValue)), _.Alt2, _.CtrlZ, CheckThat(() => Assert.AreEqual(2, argValue)), @@ -48,7 +48,7 @@ public void TestDigitArgumentValues() public void TestDigitArgumentPrompt() { TestSetup(KeyMode.Emacs); - + Test("", Keys( _.Alt1, CheckThat(() => AssertScreenIs(2, NextLine, "digit-argument: 1")), _.CtrlG, diff --git a/UnitTestPSReadLine/HistoryTest.cs b/UnitTestPSReadLine/HistoryTest.cs index 8f7e27f98..2025898b3 100644 --- a/UnitTestPSReadLine/HistoryTest.cs +++ b/UnitTestPSReadLine/HistoryTest.cs @@ -69,8 +69,7 @@ public void TestSearchHistory() SetHistory(); Test(" ", Keys(' ', _.UpArrow, _.DownArrow)); - var options = PSConsoleReadLine.GetOptions(); - var emphasisColors = Tuple.Create(options.EmphasisForegroundColor, options.EmphasisBackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadlineOptions.DefaultEmphasisColor, _console.BackgroundColor); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( @@ -110,8 +109,7 @@ public void TestHistorySearchCursorMovesToEnd() new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); PSConsoleReadLine.SetOptions(new SetPSReadlineOption {HistorySearchCursorMovesToEnd = true}); - var options = PSConsoleReadLine.GetOptions(); - var emphasisColors = Tuple.Create(options.EmphasisForegroundColor, options.EmphasisBackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadlineOptions.DefaultEmphasisColor, _console.BackgroundColor); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( @@ -180,8 +178,7 @@ public void TestInteractiveHistorySearch() SetHistory("echo aaa"); Test("echo aaa", Keys(_.CtrlR, 'a')); - var options = PSConsoleReadLine.GetOptions(); - var emphasisColors = Tuple.Create(options.EmphasisForegroundColor, options.EmphasisBackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadlineOptions.DefaultEmphasisColor, _console.BackgroundColor); var statusColors = Tuple.Create(_console.ForegroundColor, _console.BackgroundColor); // Test entering multiple characters and the line is updated with new matches diff --git a/UnitTestPSReadLine/KeyInfoConverterTest.cs b/UnitTestPSReadLine/KeyInfoConverterTest.cs index 99d69e2df..64fdb4a32 100644 --- a/UnitTestPSReadLine/KeyInfoConverterTest.cs +++ b/UnitTestPSReadLine/KeyInfoConverterTest.cs @@ -8,169 +8,156 @@ namespace UnitTestPSReadLine [TestClass] public class KeyInfoConverterTest { - [TestMethod] - public void TestKeyInfoConverterSimpleCharLiteral() - { - var result = ConsoleKeyChordConverter.Convert("x"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, 'x'); - Assert.AreEqual(key.Key, ConsoleKey.X); - Assert.AreEqual(key.Modifiers, (ConsoleModifiers)0); - } - - [TestMethod] - public void TestKeyInfoConverterSimpleCharLiteralWithModifiers() - { - var result = ConsoleKeyChordConverter.Convert("alt+shift+x"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, 'X'); - Assert.AreEqual(key.Key, ConsoleKey.X); - Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift | ConsoleModifiers.Alt); - } - - [TestMethod] - public void TestKeyInfoConverterSymbolLiteral() - { - var result = ConsoleKeyChordConverter.Convert("}"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, '}'); - Assert.AreEqual(key.Key, ConsoleKey.Oem6); - Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift); - } - - [TestMethod] - public void TestKeyInfoConverterShiftedSymbolLiteral() - { - // } => shift+] / shift+oem6 - var result = ConsoleKeyChordConverter.Convert("shift+]"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, '}'); - Assert.AreEqual(key.Key, ConsoleKey.Oem6); - Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift); - } - - [TestMethod] - public void TestKeyInfoConverterWellKnownConsoleKey() - { - // oem6 - var result = ConsoleKeyChordConverter.Convert("shift+oem6"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, '}'); - Assert.AreEqual(key.Key, ConsoleKey.Oem6); - Assert.AreEqual(key.Modifiers, ConsoleModifiers.Shift); - } + private const ConsoleModifiers NoModifiers = 0; [TestMethod] - public void TestKeyInfoConverterSequence() - { - // oem6 - var result = ConsoleKeyChordConverter.Convert("Escape,X"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 2); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, (char)27); - Assert.AreEqual(key.Key, ConsoleKey.Escape); - Assert.AreEqual(key.Modifiers, (ConsoleModifiers)0); - - key = result[1]; - - Assert.AreEqual(key.KeyChar, 'x'); - Assert.AreEqual(key.Key, ConsoleKey.X); - Assert.AreEqual(key.Modifiers, (ConsoleModifiers)0); - } - - [TestMethod] - public void TestKeyInfoConverterDigits() - { - var result = ConsoleKeyChordConverter.Convert("1"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - var key = result[0]; - - Assert.AreEqual(key.KeyChar, '1'); - Assert.AreEqual(key.Key, ConsoleKey.D1); - Assert.AreEqual(key.Modifiers, (ConsoleModifiers)0); - - result = ConsoleKeyChordConverter.Convert("Ctrl+7"); - Assert.IsNotNull(result); - Assert.AreEqual(result.Length, 1); - - key = result[0]; - - Assert.AreEqual(key.KeyChar, (char)0); - Assert.AreEqual(key.Key, ConsoleKey.D7); - Assert.AreEqual(key.Modifiers, ConsoleModifiers.Control); - } - - [TestMethod] - [ExcludeFromCodeCoverage] - [ExpectedException(typeof(ArgumentException))] - public void TestKeyInfoConverterInvalidKey() - { - ConsoleKeyChordConverter.Convert("escrape"); - } - - [TestMethod] - [ExcludeFromCodeCoverage] - [ExpectedException(typeof(ArgumentException))] - public void TestKeyInfoConverterInvalidModifierTypo() - { - ConsoleKeyChordConverter.Convert("alt+shuft+x"); - } - - [TestMethod] - [ExcludeFromCodeCoverage] - [ExpectedException(typeof(ArgumentException))] - public void TestKeyInfoConverterInvalidModifierInapplicable() + public void TestKeyInfoConverterSimpleCharLiteral() { - ConsoleKeyChordConverter.Convert("shift+}"); + void TestOne(char keyChar, ConsoleKey key) + { + var r = ConsoleKeyChordConverter.Convert(keyChar.ToString()); + Assert.IsNotNull(r); + Assert.AreEqual(1, r.Length); + Assert.AreEqual(keyChar, r[0].KeyChar); + if (key != 0) Assert.AreEqual(key, r[0].Key); + Assert.AreEqual(NoModifiers, r[0].Modifiers); + } + + for (char c = 'a'; c <= 'z'; c++) + { + TestOne(c, ConsoleKey.A + (c - 'a')); + } + + for (char c = 'A'; c <= 'Z'; c++) + { + TestOne(c, ConsoleKey.A + (c - 'A')); + } + + for (char c = '0'; c <= '9'; c++) + { + TestOne(c, ConsoleKey.D0 + (c - '0')); + } + + foreach (char c in "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?") + { + // Symbols often have different ConsoleKey's depending + // on the keyboard layout, so we don't verify those + // (which is fine, because we go out of the way to map + // those ConsoleKey's to the symbol before looking for + // key handlers.) + TestOne(c, 0); + } } [TestMethod] - [ExcludeFromCodeCoverage] - [ExpectedException(typeof (ArgumentException))] - public void TestKeyInfoConverterInvalidSubsequence1() + public void TestKeyInfoConverterSimpleConsoleKey() { - ConsoleKeyChordConverter.Convert("x,"); + var cases = new [] { + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", + "Delete", "DownArrow", "End", "Enter", "Home", "LeftArrow", "PageUp", "PageDown", + "RightArrow", "Tab", "UpArrow", + }; + + var mods = new[] + { + Tuple.Create("Control", ConsoleModifiers.Control), + Tuple.Create("Ctrl", ConsoleModifiers.Control), + Tuple.Create("Alt", ConsoleModifiers.Alt), + Tuple.Create("Shift", ConsoleModifiers.Shift), + Tuple.Create("Ctrl+Shift", ConsoleModifiers.Shift | ConsoleModifiers.Control), + Tuple.Create("Control+Shift", ConsoleModifiers.Shift | ConsoleModifiers.Control), + Tuple.Create("Shift+Ctrl", ConsoleModifiers.Shift | ConsoleModifiers.Control), + Tuple.Create("Shift+Control", ConsoleModifiers.Shift | ConsoleModifiers.Control), + Tuple.Create("Ctrl+Alt", ConsoleModifiers.Alt | ConsoleModifiers.Control), + Tuple.Create("Control+Alt", ConsoleModifiers.Alt | ConsoleModifiers.Control), + Tuple.Create("Alt+Ctrl", ConsoleModifiers.Alt | ConsoleModifiers.Control), + Tuple.Create("Alt+Control", ConsoleModifiers.Alt | ConsoleModifiers.Control), + Tuple.Create("Shift+Alt", ConsoleModifiers.Alt | ConsoleModifiers.Shift), + Tuple.Create("Shift+Alt", ConsoleModifiers.Alt | ConsoleModifiers.Shift), + Tuple.Create("Alt+Shift", ConsoleModifiers.Alt | ConsoleModifiers.Shift), + Tuple.Create("Alt+Shift", ConsoleModifiers.Alt | ConsoleModifiers.Shift), + Tuple.Create("Ctrl+Shift+Alt", ConsoleModifiers.Alt | ConsoleModifiers.Shift | ConsoleModifiers.Control), + Tuple.Create("Control+Shift+Alt", ConsoleModifiers.Alt | ConsoleModifiers.Shift | ConsoleModifiers.Control), + }; + + void TestOne(string s) + { + void VerifyOne(string input, ConsoleModifiers m) + { + var r = ConsoleKeyChordConverter.Convert(input); + Assert.IsNotNull(r); + Assert.AreEqual(1, r.Length); + Assert.AreEqual(Enum.Parse(typeof(ConsoleKey), s), r[0].Key); + Assert.AreEqual(m, r[0].Modifiers); + } + + VerifyOne(s, NoModifiers); + VerifyOne(s.ToLowerInvariant(), NoModifiers); + VerifyOne(s.ToUpperInvariant(), NoModifiers); + + foreach (var c in mods) + { + VerifyOne(c.Item1 + "+" + s, c.Item2); + VerifyOne(c.Item1.ToLowerInvariant() + "+" + s, c.Item2); + VerifyOne(c.Item1.ToUpperInvariant() + "+" + s, c.Item2); + VerifyOne(c.Item1.Replace('+', '-') + "-" + s, c.Item2); + } + } + + foreach (var c in cases) + { + TestOne(c); + } } [TestMethod] - [ExcludeFromCodeCoverage] - [ExpectedException(typeof (ArgumentException))] - public void TestKeyInfoConverterInvalidSubsequence2() + public void TestKeyInfoConverterErrors() { - ConsoleKeyChordConverter.Convert(",x"); + void TestOne(string s) + { + Exception ex = null; + try + { + ConsoleKeyChordConverter.Convert(s); + } + catch (Exception e) + { + ex = e; + } + + if (!(ex is ArgumentException)) + { + Assert.Fail($"Input `${s}` did not throw an ArgumentException"); + } + } + + var cases = new [] { + "escrape", + "alt+shft+x", + "alt+ctrl+sift+x", + "alt+control+shifr+x", + "alt+alt+x", + "shift+shift+x", + "ctrl+ctrl+x", + "control+control+x", + "control+alt+control+x", + "control+shift+alt+control+x", + "shift+", + "+x", + "x+", + "x,", + ",x", + "Ctrl+10", + "Ctrl+1a", + "Ctrl+ab", + }; + + foreach (var c in cases) + { + TestOne(c); + } } - [TestMethod] - [ExcludeFromCodeCoverage] - [ExpectedException(typeof(ArgumentException))] - public void TestKeyInfoConverterInvalidDigits() - { - ConsoleKeyChordConverter.Convert("Ctrl+10"); - } } } diff --git a/UnitTestPSReadLine/KillYankTest.cs b/UnitTestPSReadLine/KillYankTest.cs index 89bc9d034..21cb9e76d 100644 --- a/UnitTestPSReadLine/KillYankTest.cs +++ b/UnitTestPSReadLine/KillYankTest.cs @@ -49,7 +49,7 @@ public void TestUnixWordRubout() [TestMethod] public void TestKillRegion() { - TestSetup(KeyMode.Emacs, new KeyHandler("Ctrl+Z", PSConsoleReadLine.KillRegion)); + TestSetup(KeyMode.Emacs, new KeyHandler("Ctrl+z", PSConsoleReadLine.KillRegion)); Test("echo foobar", Keys("bar", _.CtrlAt, "echo foo", _.CtrlZ, _.Home, _.CtrlY)); } @@ -175,7 +175,7 @@ public void TestKillAppend() public void TestShellKillWord() { TestSetup(KeyMode.Emacs, - new KeyHandler("Alt+D", PSConsoleReadLine.ShellKillWord)); + new KeyHandler("Alt+d", PSConsoleReadLine.ShellKillWord)); Test("echo defabc", Keys( _.AltD, // Test on empty input @@ -207,7 +207,7 @@ public void TestShellBackwardKillWord() public void TestExchangePointAndMark() { TestSetup(KeyMode.Emacs, - new KeyHandler("Ctrl+Z", PSConsoleReadLine.ExchangePointAndMark)); + new KeyHandler("Ctrl+z", PSConsoleReadLine.ExchangePointAndMark)); var exchangePointAndMark = _.CtrlZ; var setMark = _.CtrlAt; @@ -238,17 +238,17 @@ public void TestYankLastNth() TestSetup(KeyMode.Emacs); SetHistory(); - TestMustDing("", Keys(_.AltCtrlY)); + TestMustDing("", Keys(_.CtrlAltY)); SetHistory("echo a b c"); - TestMustDing("", Keys(_.Alt9, _.AltCtrlY)); + TestMustDing("", Keys(_.Alt9, _.CtrlAltY)); SetHistory("echo a b c"); - TestMustDing("", Keys(_.AltMinus, _.Alt9, _.AltCtrlY)); + TestMustDing("", Keys(_.AltMinus, _.Alt9, _.CtrlAltY)); // Test no argument, gets the first argument (not the command). SetHistory("echo aa bb"); - Test("aa", Keys(_.AltCtrlY)); + Test("aa", Keys(_.CtrlAltY)); // Test various arguments: // * 0 - is the command @@ -257,10 +257,10 @@ public void TestYankLastNth() // * -2 - the second to last argument SetHistory("echo aa bb cc 'zz zz $(1 2 3)'"); Test("echo bb 'zz zz $(1 2 3)' cc", Keys( - _.Alt0, _.AltCtrlY, ' ', - _.Alt2, _.AltCtrlY, ' ', + _.Alt0, _.CtrlAltY, ' ', + _.Alt2, _.CtrlAltY, ' ', _.AltMinus, _.Escape, _.CtrlY, ' ', - _.AltMinus, _.Alt2, _.AltCtrlY)); + _.AltMinus, _.Alt2, _.CtrlAltY)); } [TestMethod] @@ -293,10 +293,10 @@ public void TestYankLastArg() _.AltPeriod, _.AltPeriod, _.AltMinus, _.AltPeriod, _.AltPeriod)); // Somewhat silly test to make sure invalid args are handled reasonably. - TestSetup(KeyMode.Emacs, new[] {new KeyHandler("Ctrl+Z", (key,arg) => PSConsoleReadLine.YankLastArg(null, "zz"))}); + TestSetup(KeyMode.Emacs, new[] {new KeyHandler("Ctrl+z", (key,arg) => PSConsoleReadLine.YankLastArg(null, "zz"))}); SetHistory("echo a", "echo a"); - TestMustDing("", Keys(_.CtrlZ)); - TestMustDing("a", Keys(_.AltPeriod, _.CtrlZ)); + TestMustDing("", Keys(_.CtrlZ)); + TestMustDing("a", Keys(_.AltPeriod, _.CtrlZ)); } [TestMethod] @@ -309,7 +309,7 @@ public void TestPaste() ExecuteOnSTAThread(() => Clipboard.SetText("pastetest2")); Test("echo pastetest2", Keys( - "echo foobar", _.ShiftCtrlLeftArrow, _.CtrlV)); + "echo foobar", _.CtrlShiftLeftArrow, _.CtrlV)); } [TestMethod] @@ -319,7 +319,7 @@ public void TestCut() ExecuteOnSTAThread(() => Clipboard.SetText("")); Test("", Keys( - "cuttest1", _.ShiftCtrlLeftArrow, _.CtrlX)); + "cuttest1", _.CtrlShiftLeftArrow, _.CtrlX)); AssertClipboardTextIs("cuttest1"); } @@ -335,7 +335,7 @@ public void TestCopy() ExecuteOnSTAThread(() => Clipboard.SetText("")); Test("echo copytest2", Keys( - "echo copytest2", _.ShiftCtrlLeftArrow, _.CtrlShiftC)); + "echo copytest2", _.CtrlShiftLeftArrow, _.CtrlShiftC)); AssertClipboardTextIs("copytest2"); } @@ -346,7 +346,7 @@ public void TestCopyOrCancelLine() ExecuteOnSTAThread(() => Clipboard.SetText("")); Test("echo copytest2", Keys( - "echo copytest2", _.ShiftCtrlLeftArrow, _.CtrlC)); + "echo copytest2", _.CtrlShiftLeftArrow, _.CtrlC)); AssertClipboardTextIs("copytest2"); Test("", Keys("oops", _.CtrlC, @@ -388,7 +388,7 @@ public void TestSelectBackwardWord() TestSetup(KeyMode.Cmd); Test("echo bar", Keys( - "echo foo bar", _.CtrlLeftArrow, _.ShiftCtrlLeftArrow, + "echo foo bar", _.CtrlLeftArrow, _.CtrlShiftLeftArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "echo", TokenClassification.None, " ", @@ -402,7 +402,7 @@ public void TestSelectNextWord() TestSetup(KeyMode.Cmd); Test("echo bar", Keys( - "foo echo bar", _.Home, _.ShiftCtrlRightArrow, + "foo echo bar", _.Home, _.CtrlShiftRightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, Inverted, "foo", TokenClassification.None, Inverted, " ", Inverted, @@ -426,7 +426,7 @@ public void TestSelectForwardWord() [TestMethod] public void TestSelectShellForwardWord() { - TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Z", PSConsoleReadLine.SelectShellForwardWord)); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+z", PSConsoleReadLine.SelectShellForwardWord)); Test(" echo bar", Keys( "a\\b\\c echo bar", _.Home, _.CtrlZ, @@ -439,7 +439,7 @@ public void TestSelectShellForwardWord() [TestMethod] public void TestSelectShellNextWord() { - TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Z", PSConsoleReadLine.SelectShellNextWord)); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+z", PSConsoleReadLine.SelectShellNextWord)); Test("echo bar", Keys( "a\\b\\c echo bar", _.Home, _.CtrlZ, @@ -453,7 +453,7 @@ public void TestSelectShellNextWord() [TestMethod] public void TestSelectShellBackwardWord() { - TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Z", PSConsoleReadLine.SelectShellBackwardWord)); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+z", PSConsoleReadLine.SelectShellBackwardWord)); Test("echo bar ", Keys( "echo bar 'a b c'", _.CtrlZ, diff --git a/UnitTestPSReadLine/MovementTest.cs b/UnitTestPSReadLine/MovementTest.cs index 4c9f1a08f..bc42fdb50 100644 --- a/UnitTestPSReadLine/MovementTest.cs +++ b/UnitTestPSReadLine/MovementTest.cs @@ -221,16 +221,16 @@ public void TestCharacterSearchBackward() Test("cmd1 | cmd2 | cmd3", Keys( "cmd1 | cmd2 | cmd3", - _.AltCtrlRBracket, '|', CheckThat(() => AssertCursorLeftIs(12)), - _.AltCtrlRBracket, '|', CheckThat(() => AssertCursorLeftIs(5)), + _.CtrlAltRBracket, '|', CheckThat(() => AssertCursorLeftIs(12)), + _.CtrlAltRBracket, '|', CheckThat(() => AssertCursorLeftIs(5)), _.Home, - _.AltMinus, _.Alt2, _.AltCtrlRBracket, '|', CheckThat(() => AssertCursorLeftIs(12)), + _.AltMinus, _.Alt2, _.CtrlAltRBracket, '|', CheckThat(() => AssertCursorLeftIs(12)), _.End, - _.Alt2, _.AltCtrlRBracket, '|', CheckThat(() => AssertCursorLeftIs(5)))); + _.Alt2, _.CtrlAltRBracket, '|', CheckThat(() => AssertCursorLeftIs(5)))); TestMustDing("cmd1 | cmd2 | cmd3", Keys( "cmd1 | cmd2 | cmd3", - _.AltCtrlRBracket, 'z')); + _.CtrlAltRBracket, 'z')); int i = 0; TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+z", diff --git a/UnitTestPSReadLine/OptionsTest.cs b/UnitTestPSReadLine/OptionsTest.cs index 9f573f7ba..601c9cdf6 100644 --- a/UnitTestPSReadLine/OptionsTest.cs +++ b/UnitTestPSReadLine/OptionsTest.cs @@ -39,8 +39,7 @@ public void TestContinuationPrompt() var continuationPrompt = "::::: "; PSConsoleReadLine.SetOptions(new SetPSReadlineOption{ ContinuationPrompt = continuationPrompt, - ContinuationPromptForegroundColor = ConsoleColor.Magenta, - ContinuationPromptBackgroundColor = ConsoleColor.DarkYellow, + ContinuationPromptColor = MakeCombinedColor(ConsoleColor.Magenta, ConsoleColor.DarkYellow), }); Test("", Keys( "{\n}", @@ -90,8 +89,7 @@ public void TestUselessStuffForBetterCoverage() var useless = ((object)options.AddToHistoryHandler ?? options).GetHashCode() + options.EditMode.GetHashCode() + ((object)options.ContinuationPrompt ?? options).GetHashCode() - + options.ContinuationPromptBackgroundColor.GetHashCode() - + options.ContinuationPromptForegroundColor.GetHashCode() + + (options.ContinuationPromptColor ?? options).GetHashCode() + options.HistoryNoDuplicates.GetHashCode() + options.HistorySearchCursorMovesToEnd.GetHashCode() + options.MaximumHistoryCount.GetHashCode() @@ -102,8 +100,7 @@ public void TestUselessStuffForBetterCoverage() + options.ExtraPromptLineCount.GetHashCode() + options.ShowToolTips.GetHashCode() + options.CompletionQueryItems.GetHashCode() - + options.EmphasisBackgroundColor.GetHashCode() - + options.EmphasisForegroundColor.GetHashCode() + + (options.EmphasisColor ?? options).GetHashCode() + options.HistorySearchCaseSensitive.GetHashCode() + getKeyHandlerCommand.Bound.GetHashCode() + getKeyHandlerCommand.Unbound.GetHashCode(); diff --git a/UnitTestPSReadLine/RenderTest.cs b/UnitTestPSReadLine/RenderTest.cs index 32153dc6f..7cd9ac54b 100644 --- a/UnitTestPSReadLine/RenderTest.cs +++ b/UnitTestPSReadLine/RenderTest.cs @@ -104,8 +104,7 @@ public void TestRender() CheckThat(() => AssertScreenIs(1, TokenClassification.None, promptLine.Substring(0, promptLine.IndexOf('>')), - Tuple.Create(ConsoleColor.Red, _console.BackgroundColor), ">", - TokenClassification.None, " ", + Tuple.Create(ConsoleColor.Red, ConsoleColor.DarkRed), "> ", TokenClassification.String, "\"")), '"'), prompt: promptLine); } @@ -160,7 +159,7 @@ public void TestLongLine() [TestMethod] public void TestInvokePrompt() { - TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Z", PSConsoleReadLine.InvokePrompt)); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+z", PSConsoleReadLine.InvokePrompt)); // Test dumb prompt that doesn't return anything. using (var ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) diff --git a/UnitTestPSReadLine/ScreenCaptureTest.cs b/UnitTestPSReadLine/ScreenCaptureTest.cs index 49c1a0468..6d62c79bc 100644 --- a/UnitTestPSReadLine/ScreenCaptureTest.cs +++ b/UnitTestPSReadLine/ScreenCaptureTest.cs @@ -11,11 +11,13 @@ namespace UnitTestPSReadLine public partial class UnitTest { - [TestMethod] + // Disabled because the test is not portable without some mocking that + // probably not worth the effort. + //[TestMethod] public void TestCaptureScreen() { TestSetup(KeyMode.Cmd, - new KeyHandler("Ctrl+Z", PSConsoleReadLine.CaptureScreen)); + new KeyHandler("Ctrl+z", PSConsoleReadLine.CaptureScreen)); var line = new [] {"echo alpha", "echo beta", "echo phi", "echo rho"}; Test(line[0], Keys(line[0], _.CtrlZ, _.Enter, _.Enter)); @@ -33,8 +35,8 @@ public void TestCaptureScreen() // Make sure we know where we are on the screen. AssertCursorTopIs(4); - var shiftUpArrow = new ConsoleKeyInfo((char)0, ConsoleKey.UpArrow, true, false, false); - var shiftDownArrow = new ConsoleKeyInfo((char)0, ConsoleKey.DownArrow, true, false, false); + var shiftUpArrow = new ConsoleKeyInfo('\0', ConsoleKey.UpArrow, true, false, false); + var shiftDownArrow = new ConsoleKeyInfo('\0', ConsoleKey.DownArrow, true, false, false); Test("", Keys( // Basic up/down arrows diff --git a/UnitTestPSReadLine/UnitTestPSReadLine.csproj b/UnitTestPSReadLine/UnitTestPSReadLine.csproj index 49564574e..a69e73af0 100644 --- a/UnitTestPSReadLine/UnitTestPSReadLine.csproj +++ b/UnitTestPSReadLine/UnitTestPSReadLine.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -8,7 +8,7 @@ Properties UnitTestPSReadLine UnitTestPSReadLine - v4.5 + v4.6.1 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -16,6 +16,7 @@ $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages False UnitTest + true @@ -38,9 +39,10 @@ - ..\PSReadLine\packages\Microsoft.PowerShell.3.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll + ..\PSReadLine\packages\Microsoft.PowerShell.5.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll True + diff --git a/UnitTestPSReadLine/UnitTestReadLine.cs b/UnitTestPSReadLine/UnitTestReadLine.cs index 23d3d6fd5..a4cc9cc8a 100644 --- a/UnitTestPSReadLine/UnitTestReadLine.cs +++ b/UnitTestPSReadLine/UnitTestReadLine.cs @@ -6,6 +6,7 @@ using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Reflection; +using System.Text; using System.Threading; using System.Windows; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -37,6 +38,58 @@ public bool RunspaceIsRemote(Runspace runspace) } } + internal struct CHAR_INFO + { + public ushort UnicodeChar; + public ushort Attributes; + + public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background) + { + UnicodeChar = c; + Attributes = (ushort)(((int)background << 4) | (int)foreground); + } + + public ConsoleColor ForegroundColor + { + get => (ConsoleColor)(Attributes & 0xf); + set => Attributes = (ushort)((Attributes & 0xfff0) | ((int)value & 0xf)); + } + + public ConsoleColor BackgroundColor + { + get => (ConsoleColor)((Attributes & 0xf0) >> 4); + set => Attributes = (ushort)((Attributes & 0xff0f) | (((int)value & 0xf) << 4)); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append((char)UnicodeChar); + if (ForegroundColor != Console.ForegroundColor) + sb.AppendFormat(" fg: {0}", ForegroundColor); + if (BackgroundColor != Console.BackgroundColor) + sb.AppendFormat(" bg: {0}", BackgroundColor); + return sb.ToString(); + } + + public override bool Equals(object obj) + { + if (!(obj is CHAR_INFO)) + { + return false; + } + + var other = (CHAR_INFO)obj; + return this.UnicodeChar == other.UnicodeChar && this.Attributes == other.Attributes; + } + + public override int GetHashCode() + { + return UnicodeChar.GetHashCode() + Attributes.GetHashCode(); + } + } + + internal class TestConsole : IConsole { internal int index; @@ -51,7 +104,7 @@ internal class TestConsole : IConsole internal TestConsole() { BackgroundColor = UnitTest.BackgroundColors[0]; - ForegroundColor = UnitTest.ForegroundColors[0]; + ForegroundColor = UnitTest.Colors[0]; CursorLeft = 0; CursorTop = 0; _bufferWidth = _windowWidth = 60; @@ -102,6 +155,7 @@ public ConsoleKeyInfo ReadKey() public int CursorTop { get; set; } public int CursorSize { get; set; } + public bool CursorVisible { get; set; } public int BufferWidth { @@ -128,8 +182,22 @@ public int WindowHeight } public int WindowTop { get; set; } - public ConsoleColor BackgroundColor { get; set; } - public ConsoleColor ForegroundColor { get; set; } + + public ConsoleColor BackgroundColor + { + get => _backgroundColor; + set => _backgroundColor = Negative ? (ConsoleColor)((int)value ^ 7) : value; + } + private ConsoleColor _backgroundColor; + + public ConsoleColor ForegroundColor + { + get => _foregroundColor; + set => _foregroundColor = Negative ? (ConsoleColor)((int)value ^ 7) : value; + } + private ConsoleColor _foregroundColor; + + private bool Negative; public void SetWindowPosition(int left, int top) { @@ -155,6 +223,16 @@ public void Write(string s) var writePos = CursorTop * BufferWidth + CursorLeft; for (int i = 0; i < s.Length; i++) { + if (s[i] == (char) 0x1b) + { + // Escape sequence - limited support here, and assumed to be well formed. + var endSequence = s.IndexOf("m", i, StringComparison.Ordinal); + var escapeSequence = s.Substring(i, endSequence - i + 1); + EscapeSequenceActions[escapeSequence](this); + i = endSequence; + continue; + } + if (s[i] == '\b') { CursorLeft -= 1; @@ -186,33 +264,10 @@ public void Write(string s) } } - public void WriteBufferLines(CHAR_INFO[] bufferToWrite, ref int top) - { - WriteBufferLines(bufferToWrite, ref top, true); - } - - public void WriteBufferLines(CHAR_INFO[] bufferToWrite, ref int top, bool ensureBottomLineVisible) - { - var startPos = top * BufferWidth; - for (int i = 0; i < bufferToWrite.Length; i++) - { - buffer[startPos + i] = bufferToWrite[i]; - } - } - public void ScrollBuffer(int lines) { } - public uint GetConsoleInputMode() - { - return 0; - } - - public void SetConsoleInputMode(uint mode) - { - } - public void Clear() { SetCursorPosition(0, 0); @@ -235,10 +290,69 @@ public CHAR_INFO[] ReadBufferLines(int top, int count) return result; } - public bool IsHandleRedirected(bool stdin) - { - return false; - } + private int _savedX, _savedY; + + public void SaveCursor() + { + _savedX = CursorLeft; + _savedY = CursorTop; + } + + public void RestoreCursor() + { + SetCursorPosition(_savedX, _savedY); + } + + private static readonly ConsoleColor DefaultForeground = UnitTest.Colors[0]; + private static readonly ConsoleColor DefaultBackground = UnitTest.BackgroundColors[0]; + + private static void ToggleNegative(TestConsole c, bool b) + { + c.Negative = false; + c.ForegroundColor = (ConsoleColor)((int)c.ForegroundColor ^ 7); + c.BackgroundColor = (ConsoleColor)((int)c.BackgroundColor ^ 7); + c.Negative = b; + } + private static readonly Dictionary> EscapeSequenceActions = new Dictionary> { + {"\x1b[7m", c => ToggleNegative(c, true) }, + {"\x1b[27m", c => ToggleNegative(c, false) }, + {"\x1b[40m", c => c.BackgroundColor = ConsoleColor.Black}, + {"\x1b[44m", c => c.BackgroundColor = ConsoleColor.DarkBlue }, + {"\x1b[42m", c => c.BackgroundColor = ConsoleColor.DarkGreen}, + {"\x1b[46m", c => c.BackgroundColor = ConsoleColor.DarkCyan}, + {"\x1b[41m", c => c.BackgroundColor = ConsoleColor.DarkRed}, + {"\x1b[45m", c => c.BackgroundColor = ConsoleColor.DarkMagenta}, + {"\x1b[43m", c => c.BackgroundColor = ConsoleColor.DarkYellow}, + {"\x1b[47m", c => c.BackgroundColor = ConsoleColor.Gray}, + {"\x1b[100m", c => c.BackgroundColor = ConsoleColor.DarkGray}, + {"\x1b[104m", c => c.BackgroundColor = ConsoleColor.Blue}, + {"\x1b[102m", c => c.BackgroundColor = ConsoleColor.Green}, + {"\x1b[106m", c => c.BackgroundColor = ConsoleColor.Cyan}, + {"\x1b[101m", c => c.BackgroundColor = ConsoleColor.Red}, + {"\x1b[105m", c => c.BackgroundColor = ConsoleColor.Magenta}, + {"\x1b[103m", c => c.BackgroundColor = ConsoleColor.Yellow}, + {"\x1b[107m", c => c.BackgroundColor = ConsoleColor.White}, + {"\x1b[30m", c => c.ForegroundColor = ConsoleColor.Black}, + {"\x1b[34m", c => c.ForegroundColor = ConsoleColor.DarkBlue}, + {"\x1b[32m", c => c.ForegroundColor = ConsoleColor.DarkGreen}, + {"\x1b[36m", c => c.ForegroundColor = ConsoleColor.DarkCyan}, + {"\x1b[31m", c => c.ForegroundColor = ConsoleColor.DarkRed}, + {"\x1b[35m", c => c.ForegroundColor = ConsoleColor.DarkMagenta}, + {"\x1b[33m", c => c.ForegroundColor = ConsoleColor.DarkYellow}, + {"\x1b[37m", c => c.ForegroundColor = ConsoleColor.Gray}, + {"\x1b[90m", c => c.ForegroundColor = ConsoleColor.DarkGray}, + {"\x1b[94m", c => c.ForegroundColor = ConsoleColor.Blue}, + {"\x1b[92m", c => c.ForegroundColor = ConsoleColor.Green}, + {"\x1b[96m", c => c.ForegroundColor = ConsoleColor.Cyan}, + {"\x1b[91m", c => c.ForegroundColor = ConsoleColor.Red}, + {"\x1b[95m", c => c.ForegroundColor = ConsoleColor.Magenta}, + {"\x1b[93m", c => c.ForegroundColor = ConsoleColor.Yellow}, + {"\x1b[97m", c => c.ForegroundColor = ConsoleColor.White}, + {"\x1b[0m", c => { + c.ForegroundColor = DefaultForeground; + c.BackgroundColor = DefaultBackground; + }} + }; } [TestClass] @@ -298,7 +412,7 @@ private enum KeyMode // These colors are random - we just use these colors instead of the defaults // so the tests aren't sensitive to tweaks to the default colors. - internal static readonly ConsoleColor[] ForegroundColors = new [] + internal static readonly ConsoleColor[] Colors = new [] { /*None*/ ConsoleColor.DarkRed, /*Comment*/ ConsoleColor.Blue, @@ -473,7 +587,7 @@ private CHAR_INFO[] CreateCharInfoBuffer(int lines, params object[] items) } if (item is TokenClassification) { - fg = ForegroundColors[(int)(TokenClassification)item]; + fg = Colors[(int)(TokenClassification)item]; bg = BackgroundColors[(int)(TokenClassification)item]; continue; } @@ -598,29 +712,55 @@ private void AssertScreenIs(int lines, params object[] items) private void SetPrompt(string prompt) { + var options = new SetPSReadlineOption {ExtraPromptLineCount = 0}; if (string.IsNullOrEmpty(prompt)) { - var options = new SetPSReadlineOption {ExtraPromptLineCount = 0}; + options.PromptText = ""; PSConsoleReadLine.SetOptions(options); return; } + int i; + for (i = prompt.Length - 1; i >= 0; i--) + { + if (!char.IsWhiteSpace(prompt[i])) break; + } + + options.PromptText = prompt.Substring(i); + var lineCount = 1 + prompt.Count(c => c == '\n'); if (lineCount > 1) { - var options = new SetPSReadlineOption {ExtraPromptLineCount = lineCount - 1}; - PSConsoleReadLine.SetOptions(options); + options.ExtraPromptLineCount = lineCount - 1; } + PSConsoleReadLine.SetOptions(options); _console.Write(prompt); } [ExcludeFromCodeCoverage] - private void Test(string expectedResult, object[] items, bool resetCursor = true, string prompt = null, bool mustDing = false) + private void Test(string expectedResult, object[] items) + { + Test(expectedResult, items, resetCursor: true, prompt: null, mustDing: false); + } + + [ExcludeFromCodeCoverage] + private void Test(string expectedResult, object[] items, string prompt) + { + Test(expectedResult, items, resetCursor: true, prompt: prompt, mustDing: false); + } + + [ExcludeFromCodeCoverage] + private void Test(string expectedResult, object[] items, bool resetCursor) + { + Test(expectedResult, items, resetCursor: resetCursor, prompt: null, mustDing: false); + } + + [ExcludeFromCodeCoverage] + private void Test(string expectedResult, object[] items, bool resetCursor, string prompt, bool mustDing) { if (resetCursor) { - _console.CursorLeft = 0; - _console.CursorTop = 0; + _console.Clear(); } SetPrompt(prompt); @@ -647,14 +787,17 @@ private void Test(string expectedResult, object[] items, bool resetCursor = true } } - private void TestMustDing(string expectedResult, object[] items, bool resetCursor = true, string prompt = null) + private void TestMustDing(string expectedResult, object[] items) { - Test(expectedResult, items, resetCursor, prompt, true); + Test(expectedResult, items, resetCursor: true, prompt: null, mustDing: true); } private TestConsole _console; private MockedMethods _mockedMethods; + private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) + => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true); + private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) { _console = new TestConsole(); @@ -677,14 +820,11 @@ private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) BellStyle = PSConsoleReadlineOptions.DefaultBellStyle, CompletionQueryItems = PSConsoleReadlineOptions.DefaultCompletionQueryItems, ContinuationPrompt = PSConsoleReadlineOptions.DefaultContinuationPrompt, - ContinuationPromptBackgroundColor = _console.BackgroundColor, - ContinuationPromptForegroundColor = _console.ForegroundColor, + ContinuationPromptColor = MakeCombinedColor(_console.ForegroundColor, _console.BackgroundColor), DingDuration = 1, // Make tests virtually silent when they ding DingTone = 37, // Make tests virtually silent when they ding - EmphasisBackgroundColor = _console.BackgroundColor, - EmphasisForegroundColor = PSConsoleReadlineOptions.DefaultEmphasisForegroundColor, - ErrorBackgroundColor = ConsoleColor.DarkRed, - ErrorForegroundColor = ConsoleColor.Red, + EmphasisColor = MakeCombinedColor(PSConsoleReadlineOptions.DefaultEmphasisColor, _console.BackgroundColor), + ErrorColor = MakeCombinedColor(ConsoleColor.Red, ConsoleColor.DarkRed), ExtraPromptLineCount = PSConsoleReadlineOptions.DefaultExtraPromptLineCount, HistoryNoDuplicates = PSConsoleReadlineOptions.DefaultHistoryNoDuplicates, HistorySaveStyle = HistorySaveStyle.SaveNothing, @@ -695,6 +835,7 @@ private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) ResetTokenColors = true, ShowToolTips = PSConsoleReadlineOptions.DefaultShowToolTips, WordDelimiters = PSConsoleReadlineOptions.DefaultWordDelimiters, + PromptText = "", }; switch (keyMode) @@ -721,8 +862,8 @@ private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) foreach (var val in typeof(TokenClassification).GetEnumValues()) { colorOptions.TokenKind = (TokenClassification)val; - colorOptions.ForegroundColor = ForegroundColors[(int)val]; - colorOptions.BackgroundColor = BackgroundColors[(int)val]; + colorOptions.Color = MakeCombinedColor(Colors[(int) val], BackgroundColors[(int) val]); + PSConsoleReadLine.SetOptions(colorOptions); } } diff --git a/UnitTestPSReadLine/WordsTest.cs b/UnitTestPSReadLine/WordsTest.cs index 93b258d97..ddc062829 100644 --- a/UnitTestPSReadLine/WordsTest.cs +++ b/UnitTestPSReadLine/WordsTest.cs @@ -139,7 +139,7 @@ public void TestShellNextWord() public void TestShellForwardWord() { TestSetup(KeyMode.Emacs, - new KeyHandler("Alt+F", PSConsoleReadLine.ShellForwardWord)); + new KeyHandler("Alt+f", PSConsoleReadLine.ShellForwardWord)); string input = "aaa bbb ccc"; Test(input, Keys( diff --git a/UnitTestPSReadLine/packages.config b/UnitTestPSReadLine/packages.config index cae6c6db8..b4ef22ebc 100644 --- a/UnitTestPSReadLine/packages.config +++ b/UnitTestPSReadLine/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/docs/Get-PSReadlineKeyHandler.md b/docs/Get-PSReadlineKeyHandler.md index b5f0b27fa..f80d5077e 100644 --- a/docs/Get-PSReadlineKeyHandler.md +++ b/docs/Get-PSReadlineKeyHandler.md @@ -19,9 +19,9 @@ If neither -Bound nor -Unbound is specified, returns all bound keys and unbound If -Bound is specified and -Unbound is not specified, only bound keys are returned. -If -Unound is specified and -Bound is not specified, only unbound keys are returned. +If -Unbound is specified and -Bound is not specified, only unbound keys are returned. -If both -Bound and -Unound are specified, returns all bound keys and unbound functions. +If both -Bound and -Unbound are specified, returns all bound keys and unbound functions. ## EXAMPLES ## PARAMETERS