From 75b086402adf77e3045b5334cb66be48804c3ecb Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 19 Jul 2016 14:09:13 -0700 Subject: [PATCH 1/6] Remove GetCharFromConsoleKey The ConsoleKey enum does not necessarily cover all possible chars. Moreover, the times we need to obtain this info, we already have it. --- .../ConsoleKeyChordConverter.cs | 52 ++----------------- .../ConsoleLib.cs | 3 +- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs b/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs index eb108b41e..8993fff99 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs @@ -56,6 +56,7 @@ namespace Microsoft.PowerShell Stack tokens = null; ConsoleModifiers modifiers = 0; ConsoleKey key = 0; + char keyChar = '\u0000'; bool valid = !String.IsNullOrEmpty(sequence); @@ -81,6 +82,9 @@ namespace Microsoft.PowerShell // key should be first token to be popped if (key == 0) { + // the keyChar is this token + keyChar = token[0]; + // Enum.TryParse accepts arbitrary integers. We shouldn't, // but single digits need to map to the correct key, e.g. // ConsoleKey.D1 @@ -152,8 +156,6 @@ namespace Microsoft.PowerShell throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, PSReadLineResources.InvalidSequence, sequence)); } - char keyChar = GetCharFromConsoleKey(key, modifiers); - return new ConsoleKeyInfo(keyChar, key, shift: ((modifiers & ConsoleModifiers.Shift) != 0), alt: ((modifiers & ConsoleModifiers.Alt) != 0), @@ -209,51 +211,5 @@ namespace Microsoft.PowerShell return valid; } - - internal static char GetCharFromConsoleKey(ConsoleKey key, ConsoleModifiers modifiers) - { - // default for unprintables and unhandled - char keyChar = '\u0000'; -#if UNIX - Type keyType = typeof (Keys); - FieldInfo[] keyFields = keyType.GetFields(); - - foreach (FieldInfo field in keyFields) - { - if (field.FieldType == typeof(ConsoleKeyInfo)) - { - ConsoleKeyInfo info = (ConsoleKeyInfo)field.GetValue(null); - if (info.Key == key && info.Modifiers == modifiers) - { - return info.KeyChar; - } - } - } -#else - // 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); - - // 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); - - // TODO: support diacriticals (charCount == 2) - if (charCount == 1) - { - keyChar = chars[0]; - } -#endif - return keyChar; - } } } diff --git a/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs b/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs index 17ef3de2a..efb78b1d0 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs @@ -404,8 +404,7 @@ namespace Microsoft.PowerShell.Internal sb.Append(key.Key); } #else - char c = ConsoleKeyChordConverter.GetCharFromConsoleKey(key.Key, - (mods & ConsoleModifiers.Shift) != 0 ? ConsoleModifiers.Shift : 0); + char c = key.KeyChar; if (char.IsControl(c) || char.IsWhiteSpace(c)) { if (key.Modifiers.HasFlag(ConsoleModifiers.Shift)) From 1facd95f1ebb496061e899cb29e82d0e5d053a53 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Tue, 19 Jul 2016 16:28:00 -0700 Subject: [PATCH 2/6] Implement TryParseCharLiteral for Linux --- .../ConsoleKeyChordConverter.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs b/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs index 8993fff99..cb5444a1e 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs @@ -166,6 +166,27 @@ namespace Microsoft.PowerShell { bool valid = false; +#if UNIX + bool isShift; + bool isCtrl; + key = GetKeyFromCharValue(literal, out isShift, out isCtrl); + + // Failure to get a key for the char just means that the key is not + // special (in the ConsoleKey enum), so we return a default key. + // Thus this never fails. + valid = true; + failReason = null; + + if (isShift) + { + modifiers |= ConsoleModifiers.Shift; + } + if (isCtrl) + { + modifiers |= ConsoleModifiers.Control; + } + // alt is not possible to get +#else // shift state will be in MSB short virtualKey = NativeMethods.VkKeyScan(literal); int hresult = Marshal.GetLastWin32Error(); @@ -208,8 +229,83 @@ namespace Microsoft.PowerShell Exception e = Marshal.GetExceptionForHR(hresult); failReason = e.Message; } +#endif return valid; } + +#if UNIX + // this is borrowed from the CoreFX internal System.IO.StdInReader class + // https://github.com/dotnet/corefx/blob/5b2ae6aa485773cd5569f56f446698633c9ad945/src/System.Console/src/System/IO/StdInReader.cs#L222 + private static ConsoleKey GetKeyFromCharValue(char x, out bool isShift, out bool isCtrl) + { + isShift = false; + isCtrl = false; + + switch (x) + { + case '\b': + return ConsoleKey.Backspace; + + case '\t': + return ConsoleKey.Tab; + + case '\n': + return ConsoleKey.Enter; + + case (char)(0x1B): + return ConsoleKey.Escape; + + case '*': + return ConsoleKey.Multiply; + + case '+': + return ConsoleKey.Add; + + case '-': + return ConsoleKey.Subtract; + + case '/': + return ConsoleKey.Divide; + + case (char)(0x7F): + return ConsoleKey.Delete; + + case ' ': + return ConsoleKey.Spacebar; + + default: + // 1. Ctrl A to Ctrl Z. + if (x >= 1 && x <= 26) + { + isCtrl = true; + return ConsoleKey.A + x - 1; + } + + // 2. Numbers from 0 to 9. + if (x >= '0' && x <= '9') + { + return ConsoleKey.D0 + x - '0'; + } + + //3. A to Z + if (x >= 'A' && x <= 'Z') + { + isShift = true; + return ConsoleKey.A + (x - 'A'); + } + + // 4. a to z. + if (x >= 'a' && x <= 'z') + { + return ConsoleKey.A + (x - 'a'); + } + + break; + } + + return default(ConsoleKey); + } +#endif } } From d5b339fb5d733bad6b7af784ec4dc50339810b37 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 20 Jul 2016 10:52:47 -0700 Subject: [PATCH 3/6] Revert changes made to GetGestureString Given the use of key.KeyChar, the original implementation is now portable. --- .../ConsoleLib.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs b/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs index efb78b1d0..1b3f648b0 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs @@ -386,24 +386,6 @@ namespace Microsoft.PowerShell.Internal sb.Append("Alt"); } -#if UNIX - if (sb.Length > 0) - sb.Append("+"); - // TODO: find better way to map these characters to something more friendly - if ((key.Key >= ConsoleKey.D0 && key.Key <= ConsoleKey.D9) - || (key.Key >= ConsoleKey.Oem1 && key.Key <= ConsoleKey.Oem8)) - { - sb.Append(key.KeyChar); - } - else - { - if (key.Modifiers.HasFlag(ConsoleModifiers.Shift)) - { - sb.Append("Shift+"); - } - sb.Append(key.Key); - } -#else char c = key.KeyChar; if (char.IsControl(c) || char.IsWhiteSpace(c)) { @@ -423,7 +405,6 @@ namespace Microsoft.PowerShell.Internal sb.Append("+"); sb.Append(c); } -#endif return sb.ToString(); } } From 554ef0b7802526e6245cdfb8ea65ae3ec298aafd Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 20 Jul 2016 11:30:22 -0700 Subject: [PATCH 4/6] Use GetCharFromConsoleKey on Windows Due to virtual key codes on Windows, simple chords like Ctrl+] are not recognized as such. Instead, the ConsoleKey is Oem6, and the KeyChar is an control code. --- .../ConsoleKeyChordConverter.cs | 33 +++++++++++++++++-- .../ConsoleLib.cs | 6 ++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs b/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs index cb5444a1e..a6248e8be 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ConsoleKeyChordConverter.cs @@ -6,9 +6,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -#if UNIX -using System.Reflection; -#endif using System.Runtime.InteropServices; using Microsoft.PowerShell.Internal; @@ -306,6 +303,36 @@ namespace Microsoft.PowerShell return default(ConsoleKey); } +#else + 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); + + // 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); + + // TODO: support diacriticals (charCount == 2) + if (charCount == 1) + { + keyChar = chars[0]; + } + return keyChar; + } #endif } } diff --git a/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs b/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs index 1b3f648b0..f9d1b7cc7 100644 --- a/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs +++ b/src/Microsoft.PowerShell.PSReadLine/ConsoleLib.cs @@ -386,7 +386,13 @@ namespace Microsoft.PowerShell.Internal sb.Append("Alt"); } +#if UNIX char c = key.KeyChar; +#else + // Windows cannot use KeyChar as some chords (like Ctrl+[) show up as control characters. + char c = ConsoleKeyChordConverter.GetCharFromConsoleKey(key.Key, + (mods & ConsoleModifiers.Shift) != 0 ? ConsoleModifiers.Shift : 0); +#endif if (char.IsControl(c) || char.IsWhiteSpace(c)) { if (key.Modifiers.HasFlag(ConsoleModifiers.Shift)) From 59624e9b2f00d22573d6938baacef46f2347e108 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Wed, 20 Jul 2016 14:44:14 -0700 Subject: [PATCH 5/6] Fix comparison for default ConsoleKey --- src/Microsoft.PowerShell.PSReadLine/KeyBindings.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.PowerShell.PSReadLine/KeyBindings.cs b/src/Microsoft.PowerShell.PSReadLine/KeyBindings.cs index f4843b3c4..d527dd1bd 100644 --- a/src/Microsoft.PowerShell.PSReadLine/KeyBindings.cs +++ b/src/Microsoft.PowerShell.PSReadLine/KeyBindings.cs @@ -93,9 +93,13 @@ namespace Microsoft.PowerShell { // Because a comparison of two ConsoleKeyInfo objects is a comparison of the // combination of the ConsoleKey and Modifiers, we must combine their hashes. - // This is based on Tuple.GetHashCode - int h1 = obj.Key.GetHashCode(); + // 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.Key == default(ConsoleKey) + ? obj.KeyChar.GetHashCode() + : obj.Key.GetHashCode(); int h2 = obj.Modifiers.GetHashCode(); + // This is based on Tuple.GetHashCode return unchecked(((h1 << 5) + h1) ^ h2); } } From 9cc6673b2783877b11c3cf3051801fd46dfeb196 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 28 Jul 2016 21:57:10 -0700 Subject: [PATCH 6/6] Add regression tests for custom key bindings Also tests capitalized key bindings, hence the need to compare case sensitively. --- .../Modules/PSReadLine/PSReadLine.Tests.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/powershell/Modules/PSReadLine/PSReadLine.Tests.ps1 b/test/powershell/Modules/PSReadLine/PSReadLine.Tests.ps1 index aa0842b8a..d882c79e5 100644 --- a/test/powershell/Modules/PSReadLine/PSReadLine.Tests.ps1 +++ b/test/powershell/Modules/PSReadLine/PSReadLine.Tests.ps1 @@ -20,7 +20,7 @@ Describe "PSReadLine" -tags "CI" { It "Should use Windows Bindings on Windows" -skip:(-not $IsWindows) { (Get-PSReadLineOption).EditMode | Should Be Windows - (Get-PSReadlineKeyHandler | where { $_.Key -eq "Ctrl+A" }).Function | Should Be SelectAll + (Get-PSReadlineKeyHandler | where { $_.Key -eq "Ctrl+a" }).Function | Should Be SelectAll } It "Should set the edit mode" { @@ -31,6 +31,17 @@ Describe "PSReadLine" -tags "CI" { (Get-PSReadlineKeyHandler | where { $_.Key -eq "Ctrl+A" }).Function | Should Be BeginningOfLine } + It "Should allow custom bindings for plain keys" { + Set-PSReadlineKeyHandler -Key '"' -Function SelfInsert + (Get-PSReadLineKeyHandler | where { $_.Key -eq '"' }).Function | Should Be SelfInsert + } + + It "Should report Capitalized bindings correctly" { + Set-PSReadlineOption -EditMode Emacs + (Get-PSReadLineKeyHandler | where { $_.Key -ceq "Alt+b" }).Function | Should Be BackwardWord + (Get-PSReadLineKeyHandler | where { $_.Key -ceq "Alt+B" }).Function | Should Be SelectBackwardWord + } + AfterAll { Remove-Module PSReadLine