From 9f786b3397ac3cfd8eaa95319601ae74eaa1d001 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 13 Nov 2020 11:20:51 -0800 Subject: [PATCH] Fix three issues with the menu completion (#1946) 1. Fix menu completion to work with the predictive list view (fix #1935 -- regression introduced in 2.2.0-beta1) 2. Fix menu completion to clear lines properly when the menu top changes on re-drawing (regression introduced in 2.2.0-beta1) 3. Fix how `RightArrow`, `LeftArrow`, `UpArrow`, and `DownArrow` navigate in the menu view (enhancement) --- PSReadLine/Completion.cs | 143 +++++-- PSReadLine/PlatformWindows.cs | 2 + test/CompletionTest.cs | 702 +++++++++++++++++++++++++++++++++- test/MockConsole.cs | 5 + 4 files changed, 819 insertions(+), 33 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 8ec3b09..5b49cd3 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -491,11 +491,18 @@ namespace Microsoft.PowerShell } } + bool extraPreRowsCleared = false; if (previousMenu != null) { if (Rows < previousMenu.Rows + previousMenu.ToolTipLines) { - // Rest of the current line was erased, but the cursor was not moved to the next line. + // If the last menu row took the whole buffer width, then the cursor could be pushed to the + // beginning of the next line in the legacy console host (NOT in modern terminals such as + // Windows Terminal, VSCode Terminal, or virtual-terminal-enabled console host). In such a + // case, there is no need to move the cursor to the next line. + // + // If that is not the case, namely 'CursorLeft != 0', then the rest of the last menu row was + // erased, but the cursor was not moved to the next line, so we will move the cursor. if (console.CursorLeft != 0) { // There are lines from the previous rendering that need to be cleared, @@ -504,13 +511,25 @@ namespace Microsoft.PowerShell } Singleton.WriteBlankLines(previousMenu.Rows + previousMenu.ToolTipLines - Rows); + extraPreRowsCleared = true; } } // if the menu has moved, we need to clear the lines under it if (bufferEndPoint.Y < PreviousTop) { - console.BlankRestOfLine(); + // In either of the following two cases, we will need to move the cursor to the next line: + // - if extra rows from previous menu were cleared, then we know the current line was erased + // but the cursor was not moved to the next line. + // - if 'CursorLeft != 0', then the rest of the last menu row was erased, but the cursor + // was not moved to the next line. + if (extraPreRowsCleared || console.CursorLeft != 0) + { + // There are lines from the previous rendering that need to be cleared, + // so we are sure there is no need to scroll. + MoveCursorDown(1); + } + Singleton.WriteBlankLines(PreviousTop - bufferEndPoint.Y); } @@ -614,10 +633,62 @@ namespace Microsoft.PowerShell RestoreCursor(); } - 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 MoveRight() + { + int nextInSameRow = CurrentSelection + Rows; + if (nextInSameRow <= MenuItems.Count - 1) + { + CurrentSelection = nextInSameRow; + return; + } + + // Index of the column where 'CurrentSelection' is at, assuming columns start from left at index 0. + int columnIndex = CurrentSelection / Rows; + int leftmostItemInSameRow = CurrentSelection - columnIndex * Rows; + + // Index of the row where 'leftMostItemAtSameRow' is at, assuming rows start from top at index 0. + int rowIndex = leftmostItemInSameRow % Rows; + + // If 'rowIndex == Rows - 1', then 'CurrentSelection' is at the rightmost position in the last row, + // so moving-to-right again should move to the item at index 0. + CurrentSelection = rowIndex == Rows - 1 ? 0 : leftmostItemInSameRow + 1; + } + + public void MoveLeft() + { + int previousInSameRow = CurrentSelection - Rows; + if (previousInSameRow >= 0) + { + CurrentSelection = previousInSameRow; + return; + } + + // Index of the row where 'CurrentSelection' is at, assuming rows start from top at index 0. + int rowIndex = CurrentSelection % Rows; + int leftmostItemInPreviousRow = rowIndex == 0 ? Rows - 1 : rowIndex - 1; + + int lastItemIndex = MenuItems.Count - 1; + // Index of the column where the last item is at, assuming columns start from left at index 0. + int lastItemColumnIndex = lastItemIndex / Rows; + + // Get the rightmost item in the previous row. + CurrentSelection = leftmostItemInPreviousRow + lastItemColumnIndex * Rows; + if (CurrentSelection > lastItemIndex) + { + CurrentSelection = leftmostItemInPreviousRow + (lastItemColumnIndex - 1) * Rows; + } + } + + public void MoveUp() + { + CurrentSelection = CurrentSelection > 0 ? CurrentSelection - 1 : MenuItems.Count - 1; + } + + public void MoveDown() + { + CurrentSelection = CurrentSelection < (MenuItems.Count - 1) ? CurrentSelection + 1 : 0; + } + public void MovePageDown() => CurrentSelection = Math.Min(CurrentSelection + Rows - (CurrentSelection % Rows) - 1, MenuItems.Count - 1); public void MovePageUp() => CurrentSelection = Math.Max(CurrentSelection - (CurrentSelection % Rows), 0); @@ -808,9 +879,6 @@ namespace Microsoft.PowerShell var userInitialCompletionLength = userCompletionText.Length; - completions.CurrentMatchIndex = 0; - menu.DrawMenu(null, menuSelect:true); - bool processingKeys = true; int previousSelection = -1; @@ -832,38 +900,51 @@ namespace Microsoft.PowerShell 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 (topAdjustment != 0) + if (previousSelection == -1) { - menu.Top += topAdjustment; - menu.DrawMenu(null, menuSelect:true); + completions.CurrentMatchIndex = 0; + menu.DrawMenu(null, menuSelect: true); } - if (topAdjustment > 0) + else { - // Render did not clear the rest of the command line which flowed - // into the menu, so we must do that here. - menu.SaveCursor(); - _console.SetCursorPosition(endOfCommandLine.X, endOfCommandLine.Y); - _console.Write(Spaces(_console.BufferWidth - endOfCommandLine.X)); - menu.RestoreCursor(); - } + // 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 (topAdjustment != 0) + { + menu.Top += topAdjustment; + menu.DrawMenu(null, menuSelect: true); + } + if (topAdjustment > 0) + { + // Render did not clear the rest of the command line which flowed + // into the menu, so we must do that here. + menu.SaveCursor(); + _console.SetCursorPosition(endOfCommandLine.X, endOfCommandLine.Y); + _console.Write(Spaces(_console.BufferWidth - endOfCommandLine.X)); + menu.RestoreCursor(); + } - if (previousSelection != -1) - { if (menu.ToolTipLines > 0) { // Erase previous tooltip, taking into account if the menu moved up/down. WriteBlankLines(menu.Top + menu.Rows, -topAdjustment + menu.ToolTipLines); } - menu.UpdateMenuSelection(previousSelection, /*select*/ false, - /*showToolTips*/false, Options._emphasisColor); + + menu.UpdateMenuSelection( + previousSelection, + select: false, + showTooltips: false, + Options._emphasisColor); } - menu.UpdateMenuSelection(menu.CurrentSelection, /*select*/ true, - Options.ShowToolTips, Options._emphasisColor); + + menu.UpdateMenuSelection( + menu.CurrentSelection, + select: true, + Options.ShowToolTips, + Options._emphasisColor); previousSelection = menu.CurrentSelection; } diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index 4fce03f..0e0b1c7 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -601,6 +601,8 @@ static class PlatformWindows var y = CursorTop; for (int i = 0; i < BufferWidth - x; i++) Console.Write(' '); + + // Last step may result in scrolling. if (CursorTop != y+1) y -= 1; SetCursorPosition(x, y); diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index 4967244..f3ceb72 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.ObjectModel; using System.IO; using System.Management.Automation; @@ -147,7 +148,7 @@ namespace Test } [SkippableFact] - public void MenuCompletions() + public void MenuCompletions_FilterByTyping() { TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); @@ -173,6 +174,678 @@ namespace Test )); } + [SkippableFact] + public void MenuCompletions_Navigation1() + { + // Test 'RightArrow' and 'LeftArrow' with the following menu: + // Get-Many0 Get-Many3 Get-Many6 Get-Many9 Get-Many12 + // Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13 + // Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14 + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + Test("Get-Many0", Keys( + "Get-Many", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Many0 ", + TokenClassification.None, + "Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "3", NextLine, + TokenClassification.None, "Get-Many0 ", + TokenClassification.Selection, "Get-Many3 ", + TokenClassification.None, + "Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "6", NextLine, + TokenClassification.None, "Get-Many0 Get-Many3 ", + TokenClassification.Selection, "Get-Many6 ", + TokenClassification.None, "Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "9", NextLine, + TokenClassification.None, "Get-Many0 Get-Many3 Get-Many6 ", + TokenClassification.Selection, "Get-Many9 ", + TokenClassification.None, "Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "12", NextLine, + TokenClassification.None, "Get-Many0 Get-Many3 Get-Many6 Get-Many9 ", + TokenClassification.Selection, "Get-Many12 ", NextLine, + TokenClassification.None, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "1", NextLine, + TokenClassification.None, + "Get-Many0 Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + TokenClassification.Selection, "Get-Many1 ", + TokenClassification.None, + "Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "12", NextLine, + TokenClassification.None, "Get-Many0 Get-Many3 Get-Many6 Get-Many9 ", + TokenClassification.Selection, "Get-Many12 ", NextLine, + TokenClassification.None, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "9", NextLine, + TokenClassification.None, "Get-Many0 Get-Many3 Get-Many6 ", + TokenClassification.Selection, "Get-Many9 ", + TokenClassification.None, "Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "6", NextLine, + TokenClassification.None, "Get-Many0 Get-Many3 ", + TokenClassification.Selection, "Get-Many6 ", + TokenClassification.None, "Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Many0 ", + TokenClassification.None, + "Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "14", NextLine, + TokenClassification.None, + "Get-Many0 Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 ", + TokenClassification.Selection, "Get-Many14 ", NextLine)), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Many0 ", + TokenClassification.None, + "Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_Navigation2() + { + // Test 'RightArrow' with the following menu: + // Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12 + // Get-Less1 Get-Less4 Get-Less7 Get-Less10 + // Get-Less2 Get-Less5 Get-Less8 Get-Less11 + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + Test("Get-Less0", Keys( + "Get-Less", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Less0 ", + TokenClassification.None, + "Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.RightArrow, _.RightArrow, _.RightArrow, _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "12", NextLine, + TokenClassification.None, "Get-Less0 Get-Less3 Get-Less6 Get-Less9 ", + TokenClassification.Selection, "Get-Less12 ", NextLine, + TokenClassification.None, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "1", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + TokenClassification.Selection, "Get-Less1 ", + TokenClassification.None, + "Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.RightArrow, _.RightArrow, _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "10", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 ", + TokenClassification.Selection, "Get-Less10 ", NextLine, + TokenClassification.None, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "2", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + TokenClassification.Selection, "Get-Less2 ", + TokenClassification.None, + "Get-Less5 Get-Less8 Get-Less11")), + _.RightArrow, _.RightArrow, _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "11", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 ", + TokenClassification.Selection, "Get-Less11 ")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Less0 ", + TokenClassification.None, + "Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_Navigation3() + { + // Test 'LeftArrow' with the following menu: + // Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12 + // Get-Less1 Get-Less4 Get-Less7 Get-Less10 + // Get-Less2 Get-Less5 Get-Less8 Get-Less11 + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + Test("Get-Less6", Keys( + "Get-Less", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Less0 ", + TokenClassification.None, + "Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "11", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 ", + TokenClassification.Selection, "Get-Less11 ")), + _.LeftArrow, _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "2", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + TokenClassification.Selection, "Get-Less2 ", + TokenClassification.None, + "Get-Less5 Get-Less8 Get-Less11")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "10", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 ", + TokenClassification.Selection, "Get-Less10 ", NextLine, + TokenClassification.None, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.LeftArrow, _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "1", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + TokenClassification.Selection, "Get-Less1 ", + TokenClassification.None, + "Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "12", NextLine, + TokenClassification.None, "Get-Less0 Get-Less3 Get-Less6 Get-Less9 ", + TokenClassification.Selection, "Get-Less12 ", NextLine, + TokenClassification.None, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.LeftArrow, _.LeftArrow, + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_Navigation4() + { + // Test 'UpArrow' and 'DownArrow' with the following menu: + // Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12 + // Get-Less1 Get-Less4 Get-Less7 Get-Less10 + // Get-Less2 Get-Less5 Get-Less8 Get-Less11 + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + Test("Get-Less0", Keys( + "Get-Less", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Less0 ", + TokenClassification.None, + "Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.DownArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "1", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + TokenClassification.Selection, "Get-Less1 ", + TokenClassification.None, + "Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.DownArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "2", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + TokenClassification.Selection, "Get-Less2 ", + TokenClassification.None, + "Get-Less5 Get-Less8 Get-Less11")), + _.DownArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "3", NextLine, + TokenClassification.None, "Get-Less0 ", + TokenClassification.Selection, "Get-Less3 ", + TokenClassification.None, + "Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.UpArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "2", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + TokenClassification.Selection, "Get-Less2 ", + TokenClassification.None, + "Get-Less5 Get-Less8 Get-Less11")), + _.UpArrow, _.UpArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Less0 ", + TokenClassification.None, + "Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.UpArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "12", NextLine, + TokenClassification.None, "Get-Less0 Get-Less3 Get-Less6 Get-Less9 ", + TokenClassification.Selection, "Get-Less12 ", NextLine, + TokenClassification.None, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.UpArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "11", NextLine, + TokenClassification.None, + "Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 ", + TokenClassification.Selection, "Get-Less11 ")), + _.DownArrow, _.DownArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Less", + TokenClassification.Selection, "0", NextLine, + TokenClassification.Selection, "Get-Less0 ", + TokenClassification.None, + "Get-Less3 Get-Less6 Get-Less9 Get-Less12", NextLine, + "Get-Less1 Get-Less4 Get-Less7 Get-Less10", NextLine, + "Get-Less2 Get-Less5 Get-Less8 Get-Less11")), + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_Navigation5() + { + // Test 'UpArrow', 'DownArrow', 'LeftArrow', and 'RightArrow' with the following menu: + // Get-MockDynamicParameters Get-Module + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + Test("Get-MockDynamicParameters", Keys( + "Get-Mo", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module")), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ")), + _.RightArrow, + CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module")), + _.UpArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ")), + _.UpArrow, + CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module")), + _.DownArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ")), + _.DownArrow, + CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module")), + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_Navigation6() + { + // Test 'UpArrow', 'DownArrow', 'LeftArrow', and 'RightArrow' with the following menu: + // Get-NewDynamicParameters Get-NewStyle + // Get-NewIdea + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + Test("Get-NewDynamicParameters", Keys( + "Get-New", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "DynamicParameters", NextLine, + TokenClassification.Selection, "Get-NewDynamicParameters ", + TokenClassification.None, "Get-NewStyle", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.RightArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Style", NextLine, + TokenClassification.None, "Get-NewDynamicParameters ", + TokenClassification.Selection, "Get-NewStyle ", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.RightArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Idea", NextLine, + TokenClassification.None, "Get-NewDynamicParameters Get-NewStyle", NextLine, + TokenClassification.Selection, "Get-NewIdea ")), + _.RightArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "DynamicParameters", NextLine, + TokenClassification.Selection, "Get-NewDynamicParameters ", + TokenClassification.None, "Get-NewStyle", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Idea", NextLine, + TokenClassification.None, "Get-NewDynamicParameters Get-NewStyle", NextLine, + TokenClassification.Selection, "Get-NewIdea ")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Style", NextLine, + TokenClassification.None, "Get-NewDynamicParameters ", + TokenClassification.Selection, "Get-NewStyle ", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "DynamicParameters", NextLine, + TokenClassification.Selection, "Get-NewDynamicParameters ", + TokenClassification.None, "Get-NewStyle", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.DownArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Idea", NextLine, + TokenClassification.None, "Get-NewDynamicParameters Get-NewStyle", NextLine, + TokenClassification.Selection, "Get-NewIdea ")), + _.DownArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Style", NextLine, + TokenClassification.None, "Get-NewDynamicParameters ", + TokenClassification.Selection, "Get-NewStyle ", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.DownArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "DynamicParameters", NextLine, + TokenClassification.Selection, "Get-NewDynamicParameters ", + TokenClassification.None, "Get-NewStyle", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.UpArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Style", NextLine, + TokenClassification.None, "Get-NewDynamicParameters ", + TokenClassification.Selection, "Get-NewStyle ", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.UpArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "Idea", NextLine, + TokenClassification.None, "Get-NewDynamicParameters Get-NewStyle", NextLine, + TokenClassification.Selection, "Get-NewIdea ")), + _.UpArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-New", + TokenClassification.Selection, "DynamicParameters", NextLine, + TokenClassification.Selection, "Get-NewDynamicParameters ", + TokenClassification.None, "Get-NewStyle", NextLine, + TokenClassification.None, "Get-NewIdea")), + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_ClearProperly() + { + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + int width = _console.BufferWidth; + string placeholderCommand = new string('A', width - 12); // 12 = "Get-Module".Length + 2 + string emptyLine = new string(' ', width); + + Test($"{placeholderCommand};Get-Module", Keys( + placeholderCommand, ';', + "Get-Mo", _.Ctrl_Spacebar, + // At this point, the editing line buffer takes 2 physical lines. + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, placeholderCommand, + TokenClassification.None, ';', + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module ", NextLine, + TokenClassification.None, emptyLine)), + _.RightArrow, + // Navigating to the next item will cause the editing line to fit in + // one physical line, so the new menu is moved up and lines from the + // previous menu need to be properly cleared. + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, placeholderCommand, + TokenClassification.None, ';', + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ", NextLine, + TokenClassification.None, emptyLine)), + _.LeftArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, placeholderCommand, + TokenClassification.None, ';', + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module ", NextLine, + TokenClassification.None, emptyLine)), + _.DownArrow, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, placeholderCommand, + TokenClassification.None, ';', + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ", NextLine, + TokenClassification.None, emptyLine)), + _.Enter, + _.Enter + )); + } + + [SkippableFact] + public void MenuCompletions_WorkWithListView() + { + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + var (listWidth, windowWidth) = CheckWindowSize(); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + string emptyLine = new string(' ', windowWidth); + using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); + + _console.Clear(); + SetHistory("Get-Mocha -AddMilk -AddSugur -ExtraCup", "Get-MoreBook -Kind Fiction -FlatCover"); + + Test("Get-Module", Keys( + "Get-Mo", + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-Mo", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "Get-Mo", + TokenClassification.None, "reBook -Kind Fiction -FlatCover", + TokenClassification.None, new string(' ', listWidth - 48), // 48 is the length of '> Get-MoreBook -Kind Fiction -FlatCover' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "Get-Mo", + TokenClassification.None, "cha -AddMilk -AddSugur -ExtraCup", + TokenClassification.None, new string(' ', listWidth - 49), // 49 is the length of '> Get-Mocha -AddMilk -AddSugur -ExtraCup' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']')), + _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "ckDynamicParameters", NextLine, + TokenClassification.Selection, "Get-MockDynamicParameters ", + TokenClassification.None, "Get-Module", NextLine, + TokenClassification.None, emptyLine, + TokenClassification.None, emptyLine)), + _.RightArrow, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-Mo", + TokenClassification.Selection, "dule", NextLine, + TokenClassification.None, "Get-MockDynamicParameters ", + TokenClassification.Selection, "Get-Module ", NextLine, + TokenClassification.None, emptyLine, + TokenClassification.None, emptyLine)), + _.Enter, + _.Enter + )); + } + [SkippableFact] public void ShowTooltips() { @@ -249,6 +922,21 @@ namespace Test completions.Add(new CompletionResult("Get-Many" + i)); } break; + case "Get-Less": + replacementIndex = 0; + replacementLength = 8; + for (int i = 0; i < 13; i++) + { + completions.Add(new CompletionResult("Get-Less" + i)); + } + break; + case "Get-New": + replacementIndex = 0; + replacementLength = 7; + completions.Add(new CompletionResult("Get-NewDynamicParameters")); + completions.Add(new CompletionResult("Get-NewIdea")); + completions.Add(new CompletionResult("Get-NewStyle")); + break; case "Get-Tooltips": replacementIndex = 0; replacementLength = 12; @@ -290,6 +978,16 @@ namespace Test break; case "none": break; + + default: + if (input.EndsWith("Get-Mo", StringComparison.OrdinalIgnoreCase)) + { + replacementIndex = input.IndexOf("Get-Mo", StringComparison.OrdinalIgnoreCase); + replacementLength = 6; + completions.Add(new CompletionResult("Get-MockDynamicParameters")); + completions.Add(new CompletionResult("Get-Module")); + } + break; } return (CommandCompletion)ctor.Invoke( diff --git a/test/MockConsole.cs b/test/MockConsole.cs index a451d1a..436f030 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -182,6 +182,11 @@ namespace Test public void SetCursorPosition(int left, int top) { + if (left != CursorLeft || top != CursorTop) + { + _ignoreNextNewline = false; + } + CursorLeft = left; CursorTop = top; }