From 93609aaf20f2841787da56bc136fbf251a7e4c87 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 7 Feb 2023 12:15:42 -0800 Subject: [PATCH 1/2] Fix the menu completion to better handle the backspace key --- PSReadLine/Completion.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 5a8dd9b02..0f46b3e7a 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -983,14 +983,25 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions) // TODO: Shift + Backspace does not fail here? if (menuStack.Count > 1) { - var newMenu = menuStack.Pop(); - - newMenu.DrawMenu(menu, menuSelect: true); previousSelection = -1; + userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); - menu = newMenu; + Menu newMenu = menuStack.Peek(); + int pos = FindUserCompletionTextPosition(newMenu.CurrentMenuItem, userCompletionText); + if (pos >= 0) + { + newMenu = menuStack.Pop(); + newMenu.DrawMenu(menu, menuSelect: true); - userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); + menu = newMenu; + } + // else { + // We should not pop the stack yet. The updated user completion text contains characters + // that are not included in the selected item of the menu at the top of stack. This may + // happen when the user pressed a 'Tab' before this 'Backspace', which updated the user + // completion text with the unambiguous common prefix. In this case, we should stay in + // the current menu. + // } } else if (menuStack.Count == 1) { From d4cc16401836143e6082657a5475f8ac4bea6aa9 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 17 Feb 2023 09:43:43 -0800 Subject: [PATCH 2/2] Add test --- PSReadLine/Completion.cs | 4 +- test/CompletionTest.cs | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 0f46b3e7a..f35c7cd52 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -999,8 +999,8 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions) // We should not pop the stack yet. The updated user completion text contains characters // that are not included in the selected item of the menu at the top of stack. This may // happen when the user pressed a 'Tab' before this 'Backspace', which updated the user - // completion text with the unambiguous common prefix. In this case, we should stay in - // the current menu. + // completion text to include the unambiguous common prefix of the available completion + // candidates. In this case, we should stay in the current menu. // } } else if (menuStack.Count == 1) diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index eeec6a162..e87ddc69c 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -1319,6 +1319,95 @@ public void DirectoryCompletion() _.Ctrl_c, InputAcceptedNow)); } + [SkippableFact] + public void MenuCompletions_Backspace() + { + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + char separator = Path.DirectorySeparatorChar; + + Test("cd stro", Keys( + "cd stron", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strong", + TokenClassification.Selection, separator, + NextLine, + TokenClassification.Selection, "strong ", + TokenClassification.None, "stronghold strongholp", + NextLine, + NextLine)), + _.h, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongh", + TokenClassification.Selection, $"old{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + // Tab will update the user completion text to include the unambiguous common prefix. + _.Tab, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}stronghol", + TokenClassification.Selection, $"d{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongho", + TokenClassification.Selection, $"ld{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongh", + TokenClassification.Selection, $"old{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strong", + TokenClassification.Selection, separator, + NextLine, + TokenClassification.Selection, "strong ", + TokenClassification.None, "stronghold strongholp", + NextLine, + NextLine)), + _.h, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongh", + TokenClassification.Selection, $"old{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strong", + TokenClassification.Selection, separator, + NextLine, + TokenClassification.Selection, "strong ", + TokenClassification.None, "stronghold strongholp", + NextLine, + NextLine)), + _.Backspace, + _.Backspace, CheckThat(() => AssertLineIs("cd stro")), + _.Enter + )); + } + internal static CommandCompletion MockedCompleteInput(string input, int cursor, Hashtable options, PowerShell powerShell) { var ctor = typeof (CommandCompletion).GetConstructor( @@ -1419,6 +1508,14 @@ internal static CommandCompletion MockedCompleteInput(string input, int cursor, break; case "none": break; + case "cd stron": + replacementIndex = 3; + replacementLength = 5; + char separator = Path.DirectorySeparatorChar; + completions.Add(new CompletionResult($".{separator}strong", "strong", CompletionResultType.ProviderContainer, $".{separator}strong")); + completions.Add(new CompletionResult($".{separator}stronghold", "stronghold", CompletionResultType.ProviderContainer, $".{separator}stronghold")); + completions.Add(new CompletionResult($".{separator}strongholp", "strongholp", CompletionResultType.ProviderContainer, $".{separator}strongholp")); + break; default: if (input.EndsWith("Get-Mo", StringComparison.OrdinalIgnoreCase))