Skip to content

Commit a7af25a

Browse files
authored
Stop dispatching on ConsoleKeyInfo (#831)
There are likely issues introduced by this PR, but I'm merging for a new Beta so this code gets more testing. * Introduce PSKeyInfo type alias * Explicit conversions b/w ConsoleKeyInfo and PSKeyInfo * Change tests to use actual ConsoleKeyInfo data * Add fr-FR keyboard layout tests * Add KeyInfo-ru-RU-windows.json (#839) All impossible without Shift shortcuts labeled as "Investigate: true" Alt+Spacebar tracked by windows itself * Add KeyInfo for Polish (Programmers) layout on Windows and Linux (#870) On Windows *Alt+Spacebar* is hijacked by Windows On Linux any *Alt+F1-12* combination was not detected
1 parent c664364 commit a7af25a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+13433
-3099
lines changed

PSReadLine.build.ps1

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ Synopsis: Run the unit tests
178178
#>
179179
task RunTests BuildMainModule, {
180180
$env:PSREADLINE_TESTRUN = 1
181-
$runner = $script:dotnet
182181

183182
# need to copy implemented assemblies so test code can host powershell otherwise we have to build for a specific runtime
184183
if ($PSVersionTable.PSEdition -eq "Core")
@@ -198,7 +197,116 @@ task RunTests BuildMainModule, {
198197
}
199198

200199
Push-Location test
201-
exec { & $runner test --no-build -c $configuration -f $target }
200+
if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
201+
{
202+
Add-Type -Language CSharpVersion3 @'
203+
using System;
204+
using System.Collections.Generic;
205+
using System.Globalization;
206+
using System.Runtime.InteropServices;
207+
using System.Threading;
208+
209+
public class KeyboardLayoutHelper
210+
{
211+
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
212+
static extern IntPtr LoadKeyboardLayout(string pwszKLID, uint Flags);
213+
214+
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
215+
static extern IntPtr GetKeyboardLayout(uint idThread);
216+
217+
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
218+
static extern int GetKeyboardLayoutList(int nBuff, [Out] IntPtr[] lpList);
219+
220+
// Used when setting the layout.
221+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
222+
public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
223+
224+
// Used for getting the layout.
225+
[DllImport("user32.dll", SetLastError = true)]
226+
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
227+
228+
// Used in both getting and setting the layout
229+
[DllImport("user32.dll", SetLastError = true)]
230+
static extern IntPtr GetForegroundWindow();
231+
232+
const int WM_INPUTLANGCHANGEREQUEST = 0x0050;
233+
234+
private static string GetLayoutNameFromHKL(IntPtr hkl)
235+
{
236+
var lcid = (int)((uint)hkl & 0xffff);
237+
return (new CultureInfo(lcid)).Name;
238+
}
239+
240+
public static IEnumerable<string> GetKeyboardLayouts()
241+
{
242+
int cnt = GetKeyboardLayoutList(0, null);
243+
var list = new IntPtr[cnt];
244+
GetKeyboardLayoutList(list.Length, list);
245+
246+
foreach (var layout in list)
247+
{
248+
yield return GetLayoutNameFromHKL(layout);
249+
}
250+
}
251+
252+
public static string GetCurrentKeyboardLayout()
253+
{
254+
uint processId;
255+
IntPtr layout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), out processId));
256+
return GetLayoutNameFromHKL(layout);
257+
}
258+
259+
public static IntPtr SetKeyboardLayout(string lang)
260+
{
261+
var layoutId = (new CultureInfo(lang)).KeyboardLayoutId;
262+
var layout = LoadKeyboardLayout(layoutId.ToString("x8"), 0x80);
263+
// Hacky, but tests are probably running in a console app and the layout change
264+
// is ignored, so post the layout change to the foreground window.
265+
PostMessage(GetForegroundWindow(), WM_INPUTLANGCHANGEREQUEST, 0, layoutId);
266+
// Wait a bit until the layout has been changed.
267+
do {
268+
Thread.Sleep(100);
269+
} while (GetCurrentKeyboardLayout() != lang);
270+
return layout;
271+
}
272+
}
273+
'@
274+
275+
# Remember the current keyboard layout, changes are system wide and restoring
276+
# is the nice thing to do.
277+
$savedLayout = [KeyboardLayoutHelper]::GetCurrentKeyboardLayout()
278+
279+
# We want to run tests in as many layouts as possible. We have key info
280+
# data for layouts that might not be installed, and tests would fail
281+
# if we don't set the system wide layout to match the key data we'll use.
282+
$layouts = [KeyboardLayoutHelper]::GetKeyboardLayouts()
283+
Write-Output "Available layouts:", $layouts
284+
foreach ($layout in $layouts)
285+
{
286+
if (Test-Path "KeyInfo-${layout}-windows.json")
287+
{
288+
Write-Output "Testing $layout"
289+
$null = [KeyboardLayoutHelper]::SetKeyboardLayout($layout)
290+
$os,$es = @(New-TemporaryFile; New-TemporaryFile)
291+
$filter = "FullyQualifiedName~Test.$($layout -replace '-','_')_Windows"
292+
exec {
293+
# We have to use Start-Process so it creates a new window, because the keyboard
294+
# layout change won't be picked up by any processes running in the current conhost.
295+
$dnArgs = 'test', '--no-build', '-c', $configuration, '-f', $target, '--filter', $filter, '--logger', 'trx'
296+
$p = Start-Process -FilePath $script:dotnet -Wait -PassThru -RedirectStandardOutput $os -RedirectStandardError $es -ArgumentList $dnArgs
297+
Get-Content $os,$es
298+
Remove-Item $os,$es
299+
#$global:LASTEXITCODE = $p.ExitCode
300+
}
301+
}
302+
}
303+
# Restore the original keyboard layout
304+
$null = [KeyboardLayoutHelper]::SetKeyboardLayout($savedLayout)
305+
}
306+
else
307+
{
308+
exec { & $script:dotnet test --no-build -c $configuration -f $target --filter "FullyQualifiedName~Test.en_US_Linux" --logger trx }
309+
}
202310
Pop-Location
203311

204312
Remove-Item env:PSREADLINE_TESTRUN

PSReadLine/BasicEditing.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null)
2323
return;
2424
}
2525

26+
var keyChar = key.Value.KeyChar;
27+
if (keyChar == '\0')
28+
return;
29+
2630
if (arg is int count)
2731
{
2832
if (count <= 0)
2933
return;
3034
if (count > 1)
3135
{
32-
var toInsert = new string(key.Value.KeyChar, count);
36+
var toInsert = new string(keyChar, count);
3337
if (_singleton._visualSelectionCommandCount > 0)
3438
{
3539
_singleton.GetRegion(out var start, out var length);
@@ -46,13 +50,12 @@ public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null)
4650
if (_singleton._visualSelectionCommandCount > 0)
4751
{
4852
_singleton.GetRegion(out var start, out var length);
49-
Replace(start, length, new string(key.Value.KeyChar, 1));
53+
Replace(start, length, new string(keyChar, 1));
5054
}
5155
else
5256
{
53-
Insert(key.Value.KeyChar);
57+
Insert(keyChar);
5458
}
55-
5659
}
5760

5861
/// <summary>

PSReadLine/Completion.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public partial class PSConsoleReadLine
2424
private CommandCompletion _tabCompletions;
2525
private Runspace _runspace;
2626

27-
private static readonly Dictionary<CompletionResultType, ConsoleKeyInfo []> KeysEndingCompletion =
28-
new Dictionary<CompletionResultType, ConsoleKeyInfo []>
27+
private static readonly Dictionary<CompletionResultType, PSKeyInfo []> KeysEndingCompletion =
28+
new Dictionary<CompletionResultType, PSKeyInfo []>
2929
{
3030
{ CompletionResultType.Variable, new[] { Keys.Period } },
3131
{ CompletionResultType.Namespace, new[] { Keys.Period } },
@@ -733,12 +733,12 @@ private int FindUserCompletionTextPosition(CompletionResult match, string userCo
733733
: match.CompletionText.IndexOf(userCompletionText, StringComparison.OrdinalIgnoreCase);
734734
}
735735

736-
private bool IsDoneWithCompletions(CompletionResult currentCompletion, ConsoleKeyInfo nextKey)
736+
private bool IsDoneWithCompletions(CompletionResult currentCompletion, PSKeyInfo nextKey)
737737
{
738-
return nextKey.EqualsNormalized(Keys.Space)
739-
|| nextKey.EqualsNormalized(Keys.Enter)
738+
return nextKey == Keys.Space
739+
|| nextKey == Keys.Enter
740740
|| KeysEndingCompletion.TryGetValue(currentCompletion.ResultType, out var doneKeys)
741-
&& doneKeys.Contains(nextKey, ConsoleKeyInfoComparer.Instance);
741+
&& doneKeys.Contains(nextKey);
742742
}
743743

744744
private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSelect)
@@ -886,13 +886,13 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions)
886886
}
887887

888888
var nextKey = ReadKey();
889-
if (nextKey.EqualsNormalized(Keys.RightArrow)) { menu.MoveRight(); }
890-
else if (nextKey.EqualsNormalized(Keys.LeftArrow)) { menu.MoveLeft(); }
891-
else if (nextKey.EqualsNormalized(Keys.DownArrow)) { menu.MoveDown(); }
892-
else if (nextKey.EqualsNormalized(Keys.UpArrow)) { menu.MoveUp(); }
893-
else if (nextKey.EqualsNormalized(Keys.PageDown)) { menu.MovePageDown(); }
894-
else if (nextKey.EqualsNormalized(Keys.PageUp)) { menu.MovePageUp(); }
895-
else if (nextKey.EqualsNormalized(Keys.Tab))
889+
if (nextKey == Keys.RightArrow) { menu.MoveRight(); }
890+
else if (nextKey == Keys.LeftArrow) { menu.MoveLeft(); }
891+
else if (nextKey == Keys.DownArrow) { menu.MoveDown(); }
892+
else if (nextKey == Keys.UpArrow) { menu.MoveUp(); }
893+
else if (nextKey == Keys.PageDown) { menu.MovePageDown(); }
894+
else if (nextKey == Keys.PageUp) { menu.MovePageUp(); }
895+
else if (nextKey == Keys.Tab)
896896
{
897897
// Search for possible unambiguous common prefix.
898898
string unAmbiguousText = GetUnambiguousPrefix(menu.MenuItems, out ambiguous);
@@ -915,19 +915,19 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions)
915915
menu.MoveN(1);
916916
}
917917
}
918-
else if (nextKey.EqualsNormalized(Keys.ShiftTab))
918+
else if (nextKey == Keys.ShiftTab)
919919
{
920920
menu.MoveN(-1);
921921
}
922-
else if (nextKey.EqualsNormalized(Keys.CtrlG)
923-
|| nextKey.EqualsNormalized(Keys.Escape))
922+
else if (nextKey == Keys.CtrlG
923+
|| nextKey == Keys.Escape)
924924
{
925925
undo = true;
926926
processingKeys = false;
927927
_visualSelectionCommandCount = 0;
928928
_mark = savedUserMark;
929929
}
930-
else if (nextKey.EqualsNormalized(Keys.Backspace))
930+
else if (nextKey == Keys.Backspace)
931931
{
932932
// TODO: Shift + Backspace does not fail here?
933933
if (menuStack.Count > 1)
@@ -970,7 +970,7 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions)
970970
{
971971
processingKeys = false;
972972
ExchangePointAndMark(); // cursor to the end of Completion
973-
if (!nextKey.EqualsNormalized(Keys.Enter))
973+
if (nextKey != Keys.Enter)
974974
{
975975
if (currentMenuItem.ResultType == CompletionResultType.ProviderContainer)
976976
{

PSReadLine/ConsoleKeyChordConverter.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,16 @@ static bool MapKeyChar(string input, ref ConsoleModifiers mods, out ConsoleKey k
255255
keyChar = (char)(keyChar - 'A' + 1);
256256
break;
257257
case '_':
258-
keyChar = Keys.CtrlUnderbar.KeyChar;
259-
mods |= Keys.CtrlUnderbar.Modifiers;
258+
keyChar = '\x1f';
259+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
260+
mods |= ConsoleModifiers.Shift;
261+
}
260262
break;
261263
case '^':
262-
keyChar = Keys.CtrlCaret.KeyChar;
263-
mods |= Keys.CtrlCaret.Modifiers;
264+
keyChar = '\x1e';
265+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
266+
mods |= ConsoleModifiers.Shift;
267+
}
264268
break;
265269
}
266270
}
@@ -286,7 +290,7 @@ static bool MapKeyChar(string input, ref ConsoleModifiers mods, out ConsoleKey k
286290

287291
key = 0;
288292
keyChar = '\0';
289-
return false;
293+
return Enum.TryParse(input, out key) && !int.TryParse(input, out var asInt);
290294
}
291295
}
292296
}

PSReadLine/History.cs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -688,8 +688,8 @@ private void InteractiveHistorySearchLoop(int direction)
688688
UpdateHistoryDuringInteractiveSearch(toMatch.ToString(), +1, ref searchFromPoint);
689689
}
690690
else if (function == BackwardDeleteChar
691-
|| key.EqualsNormalized(Keys.Backspace)
692-
|| key.EqualsNormalized(Keys.CtrlH))
691+
|| key == Keys.Backspace
692+
|| key == Keys.CtrlH)
693693
{
694694
if (toMatch.Length > 0)
695695
{
@@ -733,7 +733,7 @@ private void InteractiveHistorySearchLoop(int direction)
733733
Ding();
734734
}
735735
}
736-
else if (key.EqualsNormalized(Keys.Escape))
736+
else if (key == Keys.Escape)
737737
{
738738
// End search
739739
break;
@@ -744,15 +744,16 @@ private void InteractiveHistorySearchLoop(int direction)
744744
EndOfHistory();
745745
break;
746746
}
747-
else if (EndInteractiveHistorySearch(key))
748-
{
749-
PrependQueuedKeys(key);
750-
break;
751-
}
752747
else
753748
{
754-
toMatch.Append(key.KeyChar);
755-
_statusBuffer.Insert(_statusBuffer.Length - 1, key.KeyChar);
749+
char toAppend = key.KeyChar;
750+
if (char.IsControl(toAppend))
751+
{
752+
PrependQueuedKeys(key);
753+
break;
754+
}
755+
toMatch.Append(toAppend);
756+
_statusBuffer.Insert(_statusBuffer.Length - 1, toAppend);
756757

757758
var toMatchStr = toMatch.ToString();
758759
var startIndex = _buffer.ToString().IndexOf(toMatchStr, Options.HistoryStringComparison);
@@ -772,12 +773,6 @@ private void InteractiveHistorySearchLoop(int direction)
772773
}
773774
}
774775

775-
private static bool EndInteractiveHistorySearch(ConsoleKeyInfo key)
776-
{
777-
return char.IsControl(key.KeyChar)
778-
|| (key.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0;
779-
}
780-
781776
private void InteractiveHistorySearch(int direction)
782777
{
783778
SaveCurrentLine();

0 commit comments

Comments
 (0)