Skip to content

Commit f1b2043

Browse files
authored
Fix the menu completion to better handle the backspace key (#3574)
1 parent 372212e commit f1b2043

File tree

2 files changed

+113
-5
lines changed

2 files changed

+113
-5
lines changed

PSReadLine/Completion.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -983,14 +983,25 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions)
983983
// TODO: Shift + Backspace does not fail here?
984984
if (menuStack.Count > 1)
985985
{
986-
var newMenu = menuStack.Pop();
987-
988-
newMenu.DrawMenu(menu, menuSelect: true);
989986
previousSelection = -1;
987+
userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1);
990988

991-
menu = newMenu;
989+
Menu newMenu = menuStack.Peek();
990+
int pos = FindUserCompletionTextPosition(newMenu.CurrentMenuItem, userCompletionText);
991+
if (pos >= 0)
992+
{
993+
newMenu = menuStack.Pop();
994+
newMenu.DrawMenu(menu, menuSelect: true);
992995

993-
userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1);
996+
menu = newMenu;
997+
}
998+
// else {
999+
// We should not pop the stack yet. The updated user completion text contains characters
1000+
// that are not included in the selected item of the menu at the top of stack. This may
1001+
// happen when the user pressed a 'Tab' before this 'Backspace', which updated the user
1002+
// completion text to include the unambiguous common prefix of the available completion
1003+
// candidates. In this case, we should stay in the current menu.
1004+
// }
9941005
}
9951006
else if (menuStack.Count == 1)
9961007
{

test/CompletionTest.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,95 @@ public void DirectoryCompletion()
13241324
_.Ctrl_c, InputAcceptedNow));
13251325
}
13261326

1327+
[SkippableFact]
1328+
public void MenuCompletions_Backspace()
1329+
{
1330+
TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete));
1331+
1332+
_console.Clear();
1333+
char separator = Path.DirectorySeparatorChar;
1334+
1335+
Test("cd stro", Keys(
1336+
"cd stron", _.Ctrl_Spacebar,
1337+
CheckThat(() => AssertScreenIs(3,
1338+
TokenClassification.Command, "cd",
1339+
TokenClassification.None, $" .{separator}strong",
1340+
TokenClassification.Selection, separator,
1341+
NextLine,
1342+
TokenClassification.Selection, "strong ",
1343+
TokenClassification.None, "stronghold strongholp",
1344+
NextLine,
1345+
NextLine)),
1346+
_.h, CheckThat(() => AssertScreenIs(3,
1347+
TokenClassification.Command, "cd",
1348+
TokenClassification.None, $" .{separator}strongh",
1349+
TokenClassification.Selection, $"old{separator}",
1350+
NextLine,
1351+
TokenClassification.Selection, "stronghold ",
1352+
TokenClassification.None, "strongholp",
1353+
NextLine,
1354+
NextLine)),
1355+
// Tab will update the user completion text to include the unambiguous common prefix.
1356+
_.Tab, CheckThat(() => AssertScreenIs(3,
1357+
TokenClassification.Command, "cd",
1358+
TokenClassification.None, $" .{separator}stronghol",
1359+
TokenClassification.Selection, $"d{separator}",
1360+
NextLine,
1361+
TokenClassification.Selection, "stronghold ",
1362+
TokenClassification.None, "strongholp",
1363+
NextLine,
1364+
NextLine)),
1365+
_.Backspace, CheckThat(() => AssertScreenIs(3,
1366+
TokenClassification.Command, "cd",
1367+
TokenClassification.None, $" .{separator}strongho",
1368+
TokenClassification.Selection, $"ld{separator}",
1369+
NextLine,
1370+
TokenClassification.Selection, "stronghold ",
1371+
TokenClassification.None, "strongholp",
1372+
NextLine,
1373+
NextLine)),
1374+
_.Backspace, CheckThat(() => AssertScreenIs(3,
1375+
TokenClassification.Command, "cd",
1376+
TokenClassification.None, $" .{separator}strongh",
1377+
TokenClassification.Selection, $"old{separator}",
1378+
NextLine,
1379+
TokenClassification.Selection, "stronghold ",
1380+
TokenClassification.None, "strongholp",
1381+
NextLine,
1382+
NextLine)),
1383+
_.Backspace, CheckThat(() => AssertScreenIs(3,
1384+
TokenClassification.Command, "cd",
1385+
TokenClassification.None, $" .{separator}strong",
1386+
TokenClassification.Selection, separator,
1387+
NextLine,
1388+
TokenClassification.Selection, "strong ",
1389+
TokenClassification.None, "stronghold strongholp",
1390+
NextLine,
1391+
NextLine)),
1392+
_.h, CheckThat(() => AssertScreenIs(3,
1393+
TokenClassification.Command, "cd",
1394+
TokenClassification.None, $" .{separator}strongh",
1395+
TokenClassification.Selection, $"old{separator}",
1396+
NextLine,
1397+
TokenClassification.Selection, "stronghold ",
1398+
TokenClassification.None, "strongholp",
1399+
NextLine,
1400+
NextLine)),
1401+
_.Backspace, CheckThat(() => AssertScreenIs(3,
1402+
TokenClassification.Command, "cd",
1403+
TokenClassification.None, $" .{separator}strong",
1404+
TokenClassification.Selection, separator,
1405+
NextLine,
1406+
TokenClassification.Selection, "strong ",
1407+
TokenClassification.None, "stronghold strongholp",
1408+
NextLine,
1409+
NextLine)),
1410+
_.Backspace,
1411+
_.Backspace, CheckThat(() => AssertLineIs("cd stro")),
1412+
_.Enter
1413+
));
1414+
}
1415+
13271416
internal static CommandCompletion MockedCompleteInput(string input, int cursor, Hashtable options, PowerShell powerShell)
13281417
{
13291418
var ctor = typeof (CommandCompletion).GetConstructor(
@@ -1424,6 +1513,14 @@ internal static CommandCompletion MockedCompleteInput(string input, int cursor,
14241513
break;
14251514
case "none":
14261515
break;
1516+
case "cd stron":
1517+
replacementIndex = 3;
1518+
replacementLength = 5;
1519+
char separator = Path.DirectorySeparatorChar;
1520+
completions.Add(new CompletionResult($".{separator}strong", "strong", CompletionResultType.ProviderContainer, $".{separator}strong"));
1521+
completions.Add(new CompletionResult($".{separator}stronghold", "stronghold", CompletionResultType.ProviderContainer, $".{separator}stronghold"));
1522+
completions.Add(new CompletionResult($".{separator}strongholp", "strongholp", CompletionResultType.ProviderContainer, $".{separator}strongholp"));
1523+
break;
14271524

14281525
default:
14291526
if (input.EndsWith("Get-Mo", StringComparison.OrdinalIgnoreCase))

0 commit comments

Comments
 (0)