diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index f2721d7..002d54b 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -867,6 +867,7 @@ namespace Microsoft.PowerShell // getting shorter or longer. var endOfCommandLine = ConvertOffsetToPoint(_buffer.Length); var topAdjustment = (endOfCommandLine.Y + 1) - menu.Top; + int oldInitialY = _initialY; if (topAdjustment != 0) { @@ -877,6 +878,14 @@ namespace Microsoft.PowerShell // Render did not clear the rest of the command line which flowed // into the menu, so we must do that here. menu.SaveCursor(); + + if (oldInitialY > _initialY) + { + // Scrolling happened when drawing the menu, so we need to adjust + // this point as it was calculated before drawing the menu. + endOfCommandLine.Y -= oldInitialY - _initialY; + } + _console.SetCursorPosition(endOfCommandLine.X, endOfCommandLine.Y); _console.Write(Spaces(_console.BufferWidth - endOfCommandLine.X)); menu.RestoreCursor(); diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 2d97a7f..62e7800 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -1108,7 +1108,9 @@ namespace Microsoft.PowerShell internal static bool IsRunningCI(IConsole console) { - return console.GetType().FullName == "Test.TestConsole"; + Type consoleType = console.GetType(); + return consoleType.FullName == "Test.TestConsole" + || consoleType.BaseType.FullName == "Test.TestConsole"; } } } diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index 2a209fe..c2f6bad 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -733,6 +733,174 @@ namespace Test )); } + [SkippableFact] + public void MenuCompletions_Navigation7() + { + // Trigger the menu completion from the last line in the screen buffer, which will cause the screen + // to scroll up. Then test 'DownArrow' and 'UpArrow' with the following menu to verify if scrolling + // was handled correctly: + // 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 + + var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 60, height: 10); + TestSetup(basicScrollingConsole, KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + // Write 12 new-lines, so that the next input will be at the last line of the screen buffer. + basicScrollingConsole.Write(new string('\n', 12)); + AssertCursorLeftTopIs(0, 9); + + Test("Get-Many0", Keys( + "Get-Many", + CheckThat(() => AssertCursorLeftTopIs(8, 9)), + _.Ctrl_Spacebar, + // Menu completion will trigger scrolling. + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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")), + + _.DownArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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")), + + _.DownArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "2", 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, + TokenClassification.Selection, "Get-Many2 ", + TokenClassification.None, + "Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + + _.DownArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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")), + + _.DownArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "4", NextLine, + TokenClassification.None, + "Get-Many0 Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 ", + TokenClassification.Selection, "Get-Many4 ", + TokenClassification.None, + "Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + + _.DownArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "5", 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 ", + TokenClassification.Selection, "Get-Many5 ", + TokenClassification.None, + "Get-Many8 Get-Many11 Get-Many14")), + + _.UpArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "4", NextLine, + TokenClassification.None, + "Get-Many0 Get-Many3 Get-Many6 Get-Many9 Get-Many12", NextLine, + "Get-Many1 ", + TokenClassification.Selection, "Get-Many4 ", + TokenClassification.None, + "Get-Many7 Get-Many10 Get-Many13", NextLine, + "Get-Many2 Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + + _.UpArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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")), + + _.UpArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Command, "Get-Many", + TokenClassification.Selection, "2", 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, + TokenClassification.Selection, "Get-Many2 ", + TokenClassification.None, + "Get-Many5 Get-Many8 Get-Many11 Get-Many14")), + + _.UpArrow, _.UpArrow, + CheckThat(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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(() => AssertCursorLeftTopIs(8, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 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), + resetCursor: false); + } + [SkippableFact] public void MenuCompletions_ClearProperly() { @@ -856,6 +1024,269 @@ namespace Test )); } + [SkippableFact] + public void MenuCompletions_HandleScrolling1() + { + // This test case covers the fix to https://github.com/PowerShell/PSReadLine/issues/2928. + var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 133, height: 10); + TestSetup(basicScrollingConsole, KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + // Write 12 new-lines, so that the next input will be at the last line of the screen buffer. + basicScrollingConsole.Write(new string('\n', 12)); + AssertCursorLeftTopIs(0, 9); + + // Input length: 131; BufferWidth: 133. MenuComplete on '[reg' will first get '[regex', which makes the line + // fit exactly the whole buffer width, and thus will cause screen scrolling when the current line is at the + // last line of the screen buffer. + string input = @"$instMods = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' -replace [reg] $env:HOMEPATH ,""`$env:HOMEPATH"""; + + Test(input, Keys( + input, + CheckThat(() => AssertCursorLeftTopIs(131, 9)), + _.Ctrl_LeftArrow, _.Ctrl_LeftArrow, _.Ctrl_LeftArrow, _.Ctrl_LeftArrow, + _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertCursorLeftTopIs(98, 9)), + _.Ctrl_Spacebar, + CheckThat(() => AssertCursorLeftTopIs(98, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Variable, "$instMods", + TokenClassification.None, ' ', + TokenClassification.Operator, '=', + TokenClassification.None, ' ', + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "reg", + TokenClassification.Selection, "ex", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, @"""`$env:HOMEPATH""", + TokenClassification.None, _emptyLine, + TokenClassification.Selection, "Regex ", + TokenClassification.None, "RegionInfo RegisterPSSessionConfigurationCommand", NextLine, + TokenClassification.None, "RegexCompilationInfo RegisterArgumentCompleterCommand RegistryProviderSetItemDynamicParameter")), + + _.RightArrow, + CheckThat(() => AssertCursorLeftTopIs(119, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Variable, "$instMods", + TokenClassification.None, ' ', + TokenClassification.Operator, '=', + TokenClassification.None, ' ', + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "System.Globalization.Reg", + TokenClassification.Selection, "ionInfo", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, @"""`$env:HOMEPATH""", + NextLine, + TokenClassification.None, "Regex ", + TokenClassification.Selection, "RegionInfo ", + TokenClassification.None, "RegisterPSSessionConfigurationCommand", NextLine, + TokenClassification.None, "RegexCompilationInfo RegisterArgumentCompleterCommand RegistryProviderSetItemDynamicParameter")), + + _.Escape, + CheckThat(() => AssertCursorLeftTopIs(98, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.Variable, "$instMods", + TokenClassification.None, ' ', + TokenClassification.Operator, '=', + TokenClassification.None, ' ', + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "reg", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, @"""`$env:HOMEPATH""", + NextLine, + NextLine, + NextLine, + NextLine)), + + _.Enter), + resetCursor: false); + } + + [SkippableFact] + public void MenuCompletions_HandleScrolling2() + { + // This test case covers the fix to https://github.com/PowerShell/PSReadLine/issues/2948. + var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 133, height: 10); + TestSetup(basicScrollingConsole, KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + // Write 12 new-lines, so that the next input will be at the last line of the screen buffer. + basicScrollingConsole.Write(new string('\n', 12)); + AssertCursorLeftTopIs(0, 9); + + // Input length: 69; BufferWidth: 133. + // MenuComplete on '[reg' contains one entry that will make the line fit exactly the whole buffer width, + // and thus will cause screen scrolling. But that entry is not the first one in the menu. + string input = @"'AAAAAAAAAAAAAAAAAAAAA' -replace [reg] $env:HOMEPATH ,'$env:HOMEPATH'"; + + Test(input, Keys( + input, + CheckThat(() => AssertCursorLeftTopIs(69, 9)), + _.Ctrl_LeftArrow, _.Ctrl_LeftArrow, _.Ctrl_LeftArrow, _.Ctrl_LeftArrow, + _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertCursorLeftTopIs(37, 9)), + _.Ctrl_Spacebar, + CheckThat(() => AssertCursorLeftTopIs(37, 7)), + CheckThat(() => AssertScreenIs(top: 12, lines: 3, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "reg", + TokenClassification.Selection, "ex", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + NextLine, + TokenClassification.Selection, "Regex ", + TokenClassification.None, "RegionInfo RegisterPSSessionConfigurationCommand", NextLine, + TokenClassification.None, "RegexCompilationInfo RegisterArgumentCompleterCommand RegistryProviderSetItemDynamicParameter")), + + _.RightArrow, + CheckThat(() => AssertCursorLeftTopIs(58, 7)), + CheckThat(() => AssertScreenIs(top: 12, lines: 3, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "System.Globalization.Reg", + TokenClassification.Selection, "ionInfo", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + NextLine, + TokenClassification.None, "Regex ", + TokenClassification.Selection, "RegionInfo ", + TokenClassification.None, "RegisterPSSessionConfigurationCommand", NextLine, + TokenClassification.None, "RegexCompilationInfo RegisterArgumentCompleterCommand RegistryProviderSetItemDynamicParameter")), + + _.RightArrow, + CheckThat(() => AssertCursorLeftTopIs(67, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "Microsoft.PowerShell.Commands.Reg", + TokenClassification.Selection, "isterPSSessionConfigurationCommand", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + TokenClassification.None, _emptyLine, + TokenClassification.None, "Regex RegionInfo ", + TokenClassification.Selection, "RegisterPSSessionConfigurationCommand ", NextLine, + TokenClassification.None, "RegexCompilationInfo RegisterArgumentCompleterCommand RegistryProviderSetItemDynamicParameter")), + + _.RightArrow, + CheckThat(() => AssertCursorLeftTopIs(49, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "System.Text.Reg", + TokenClassification.Selection, "ularExpressions.RegexCompilationInfo", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + NextLine, + TokenClassification.None, "Regex RegionInfo RegisterPSSessionConfigurationCommand", + NextLine, + TokenClassification.Selection, "RegexCompilationInfo ", + TokenClassification.None, "RegisterArgumentCompleterCommand RegistryProviderSetItemDynamicParameter", + NextLine, + NextLine)), + + _.RightArrow, + CheckThat(() => AssertCursorLeftTopIs(66, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "System.Management.Automation.Reg", + TokenClassification.Selection, "isterArgumentCompleterCommand", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + NextLine, + TokenClassification.None, "Regex RegionInfo RegisterPSSessionConfigurationCommand", + NextLine, + TokenClassification.None, "RegexCompilationInfo ", + TokenClassification.Selection, "RegisterArgumentCompleterCommand ", + TokenClassification.None, "RegistryProviderSetItemDynamicParameter", + NextLine, + NextLine)), + + _.RightArrow, + CheckThat(() => AssertCursorLeftTopIs(67, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "Microsoft.PowerShell.Commands.Reg", + TokenClassification.Selection, "istryProviderSetItemDynamicParameter", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + NextLine, + TokenClassification.None, "Regex RegionInfo RegisterPSSessionConfigurationCommand", + NextLine, + TokenClassification.None, "RegexCompilationInfo RegisterArgumentCompleterCommand ", + TokenClassification.Selection, "RegistryProviderSetItemDynamicParameter ", + NextLine)), + + _.Escape, + CheckThat(() => AssertCursorLeftTopIs(37, 6)), + CheckThat(() => AssertScreenIs(top: 12, lines: 4, + TokenClassification.String, "'AAAAAAAAAAAAAAAAAAAAA'", + TokenClassification.None, ' ', + TokenClassification.Operator, "-replace", + TokenClassification.None, " [", + TokenClassification.Type, "reg", + TokenClassification.None, "] ", + TokenClassification.Variable, "$env:HOMEPATH", + TokenClassification.None, ' ', + TokenClassification.Operator, ',', + TokenClassification.String, "'$env:HOMEPATH'", + NextLine, + NextLine, + NextLine, + NextLine)), + + _.Enter), + resetCursor: false); + } + [SkippableFact] public void ShowTooltips() { @@ -996,10 +1427,58 @@ namespace Test replacementLength = 6; completions.Add(new CompletionResult("Get-MockDynamicParameters")); completions.Add(new CompletionResult("Get-Module")); + break; } + + int index = input.IndexOf("[reg]"); + if (index > 0 && index + 4 == cursor) + { + // cursor is pointing at ']'. + replacementIndex = index + 1; + replacementLength = 3; + completions.Add( + new CompletionResult( + "regex", + "Regex", + CompletionResultType.Type, + "regex")); + completions.Add( + new CompletionResult( + "System.Text.RegularExpressions.RegexCompilationInfo", + "RegexCompilationInfo", + CompletionResultType.Type, + "System.Text.RegularExpressions.RegexCompilationInfo")); + completions.Add( + new CompletionResult( + "System.Globalization.RegionInfo", + "RegionInfo", + CompletionResultType.Type, + "System.Globalization.RegionInfo")); + completions.Add( + new CompletionResult( + "System.Management.Automation.RegisterArgumentCompleterCommand", + "RegisterArgumentCompleterCommand", + CompletionResultType.Type, + "System.Management.Automation.RegisterArgumentCompleterCommand")); + completions.Add( + new CompletionResult( + "Microsoft.PowerShell.Commands.RegisterPSSessionConfigurationCommand", + "RegisterPSSessionConfigurationCommand", + CompletionResultType.Type, + "Microsoft.PowerShell.Commands.RegisterPSSessionConfigurationCommand")); + completions.Add( + new CompletionResult( + "Microsoft.PowerShell.Commands.RegistryProviderSetItemDynamicParameter", + "RegistryProviderSetItemDynamicParameter", + CompletionResultType.Type, + "Microsoft.PowerShell.Commands.RegistryProviderSetItemDynamicParameter")); + break; + } + break; } + //new CommandCompletion(completions, currentMatchIndex, replacementIndex, replacementLength); return (CommandCompletion)ctor.Invoke( new object[] {completions, currentMatchIndex, replacementIndex, replacementLength}); } diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index ac4284a..2e3ae1b 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -159,7 +159,7 @@ PARAMETERS Test("Get-MultiLineHelp -OneAndHalf", Keys( "Get-MultiLineHelp -OneAndHalf", _.Alt_h, - CheckThat(() => AssertScreenIs(9, + CheckThat(() => AssertScreenIs(8, TokenClassification.Command, "Get-MultiLineHelp", TokenClassification.None, " ", TokenClassification.Parameter, "-OneAndHalf", @@ -172,8 +172,7 @@ PARAMETERS TokenClassification.None, "60 characters but shorter than 120.", NextLine, TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", - NextLine, - "Input: True (ByPropertyName, ByValue), WildCard: false")), + TokenClassification.None, "Input: True (ByPropertyName, ByValue), WildCard: false")), _.LeftArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "Get-MultiLineHelp", @@ -183,6 +182,157 @@ PARAMETERS )); } + [SkippableFact] + public void DynHelp_GetParameterHelpMultiLine_And_Clear_WithScrolling1() + { + // This test case covers the fix to https://github.com/PowerShell/PSReadLine/issues/2950. + var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 60, height: 10); + TestSetup(basicScrollingConsole, KeyMode.Cmd); + + // Write 12 new-lines, so that the next input will be at the last line of the screen buffer. + basicScrollingConsole.Write(new string('\n', 12)); + AssertCursorLeftTopIs(0, 9); + + Test("Get-MultiLineHelp -OneAndHalf", Keys( + "Get-MultiLineHelp -OneAndHalf", + CheckThat(() => AssertCursorLeftTopIs(29, 9)), + _.Alt_h, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", + NextLine, + NextLine, + TokenClassification.None, $"-Date ", + NextLine, + NextLine, + TokenClassification.None, "DESC: Some very long description that is over the buffer width of ", + TokenClassification.None, "60 characters but shorter than 120.", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + TokenClassification.None, "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.Alt_h, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine)), + _.Alt_h, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", + NextLine, + NextLine, + TokenClassification.None, $"-Date ", + NextLine, + NextLine, + TokenClassification.None, "DESC: Some very long description that is over the buffer width of ", + TokenClassification.None, "60 characters but shorter than 120.", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + TokenClassification.None, "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.LeftArrow, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine)), + _.Enter), + resetCursor: false); + } + + [SkippableFact] + public void DynHelp_GetParameterHelpMultiLine_And_Clear_WithScrolling2() + { + // This test case covers the new changes in 'RecomputeInitialCoords', to verify that the + // previous cursor position gets updated when scrolling happens. + var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 60, height: 10); + TestSetup(basicScrollingConsole, KeyMode.Cmd); + + // Write 12 new-lines, so that the next input will be at the last line of the screen buffer. + basicScrollingConsole.Write(new string('\n', 12)); + AssertCursorLeftTopIs(0, 9); + + Test("Get-MultiLineHelp -OneAndHalf", Keys( + "Get-MultiLineHelp -OneAndHalf", + CheckThat(() => AssertCursorLeftTopIs(29, 9)), + _.Alt_h, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", + NextLine, + NextLine, + TokenClassification.None, $"-Date ", + NextLine, + NextLine, + TokenClassification.None, "DESC: Some very long description that is over the buffer width of ", + TokenClassification.None, "60 characters but shorter than 120.", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + TokenClassification.None, "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.Escape, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine)), + // Write more characters after clearing the inline help content, verify that the initial coordinates are up-to-date. + "abc", + CheckThat(() => AssertCursorLeftTopIs(32, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalfabc", NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine)), + _.Backspace, _.Backspace, _.Backspace, + CheckThat(() => AssertCursorLeftTopIs(29, 2)), + CheckThat(() => AssertScreenIs(top: 12, lines: 8, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine, + NextLine)), + _.Enter), + resetCursor: false); + } + [SkippableFact] public void DynHelp_GetParameterHelpTwoLines_And_Clear() { diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index c594ace..9c0edd7 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -731,7 +731,7 @@ namespace Test [SkippableFact] public void Inline_TruncateVeryLongSuggestion() { - TestSetup(new TestConsole(width: 10, height: 2, keyboardLayout: _), KeyMode.Cmd); + TestSetup(new TestConsole(keyboardLayout: _, width: 10, height: 2), KeyMode.Cmd); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); // Truncate long suggestion to make sure the user input is not scrolled up-off the console buffer. diff --git a/test/MockConsole.cs b/test/MockConsole.cs index 415d79d..372a6ff 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -57,43 +57,54 @@ namespace Test } } - internal class TestConsole : IConsole { internal int index; internal object[] inputOrValidateItems; internal Exception validationFailure; - private readonly CHAR_INFO[] buffer; - private readonly int _bufferWidth; - private readonly int _bufferHeight; - private readonly int _windowWidth; - private readonly int _windowHeight; - private readonly dynamic _keyboardLayout; - private bool _ignoreNextNewline; + protected readonly CHAR_INFO[] buffer; + protected readonly int _bufferWidth; + protected readonly int _windowWidth; + protected readonly int _bufferHeight; + protected readonly int _windowHeight; + protected dynamic _keyboardLayout; + + private bool _ignoreNextNewline; + private bool _negative; + private ConsoleColor _foregroundColor; + private ConsoleColor _backgroundColor; + + /// + /// Use big enough window/buffer to avoid the need to implement scrolling. + /// internal TestConsole(dynamic keyboardLayout) + : this(width: 60, height: 1000, mimicScrolling: false) { _keyboardLayout = keyboardLayout; - BackgroundColor = ReadLine.BackgroundColors[0]; - ForegroundColor = ReadLine.Colors[0]; - CursorLeft = 0; - CursorTop = 0; - _bufferWidth = _windowWidth = 60; - _bufferHeight = _windowHeight = 1000; // big enough to avoid the need to implement scrolling - buffer = new CHAR_INFO[BufferWidth * BufferHeight]; - ClearBuffer(); } - internal TestConsole(int width, int height, dynamic keyboardLayout) + /// + /// Use specific window width and height without scrolling capability. + /// + internal TestConsole(dynamic keyboardLayout, int width, int height) + : this(width, height, mimicScrolling: false) { _keyboardLayout = keyboardLayout; + } + + protected TestConsole(int width, int height, bool mimicScrolling) + { BackgroundColor = ReadLine.BackgroundColors[0]; ForegroundColor = ReadLine.Colors[0]; CursorLeft = 0; CursorTop = 0; _bufferWidth = _windowWidth = width; - _bufferHeight = _windowHeight = height; // big enough to avoid the need to implement scrolling - buffer = new CHAR_INFO[BufferWidth * BufferHeight]; + _bufferHeight = _windowHeight = height; + + // Use a big enough buffer when we are mimicing scrolling. + int bufferSize = mimicScrolling ? BufferWidth * 1000 : BufferWidth * BufferHeight; + buffer = new CHAR_INFO[bufferSize]; ClearBuffer(); } @@ -104,7 +115,7 @@ namespace Test this.validationFailure = null; } - public ConsoleKeyInfo ReadKey() + public virtual ConsoleKeyInfo ReadKey() { while (index < inputOrValidateItems.Length) { @@ -133,67 +144,63 @@ namespace Test return _keyboardLayout.Ctrl_c; } - public bool KeyAvailable => index < inputOrValidateItems.Length && inputOrValidateItems[index] is ConsoleKeyInfo; + public virtual bool KeyAvailable => index < inputOrValidateItems.Length && inputOrValidateItems[index] is ConsoleKeyInfo; - public int CursorLeft { get; set; } - public int CursorTop { get; set; } + public virtual int CursorLeft { get; set; } + public virtual int CursorTop { get; set; } - public int CursorSize { get; set; } - public bool CursorVisible { get; set; } + public virtual int CursorSize { get; set; } + public virtual bool CursorVisible { get; set; } - public int BufferWidth + public virtual int BufferWidth { get => _bufferWidth; set => throw new NotImplementedException(); } - public int BufferHeight + public virtual int BufferHeight { get => _bufferHeight; set => throw new NotImplementedException(); } - public int WindowWidth + public virtual int WindowWidth { get => _windowWidth; set => throw new NotImplementedException(); } - public int WindowHeight + public virtual int WindowHeight { get => _windowHeight; set => throw new NotImplementedException(); } - public int WindowTop { get; set; } + public virtual int WindowTop { get; set; } - public ConsoleColor BackgroundColor + public virtual ConsoleColor BackgroundColor { get => _backgroundColor; - set => _backgroundColor = Negative ? (ConsoleColor)((int)value ^ 7) : value; + set => _backgroundColor = _negative ? (ConsoleColor)((int)value ^ 7) : value; } - private ConsoleColor _backgroundColor; - public ConsoleColor ForegroundColor + public virtual ConsoleColor ForegroundColor { get => _foregroundColor; - set => _foregroundColor = Negative ? (ConsoleColor)((int)value ^ 7) : value; + set => _foregroundColor = _negative ? (ConsoleColor)((int)value ^ 7) : value; } - private ConsoleColor _foregroundColor; - public Encoding OutputEncoding + public virtual Encoding OutputEncoding { get => Encoding.Default; set { } } - private bool Negative; - - public void SetWindowPosition(int left, int top) + public virtual void SetWindowPosition(int left, int top) { } - public void SetCursorPosition(int left, int top) + public virtual void SetCursorPosition(int left, int top) { if (left != CursorLeft || top != CursorTop) { @@ -204,7 +211,7 @@ namespace Test CursorTop = top; } - public void WriteLine(string s) + public virtual void WriteLine(string s) { // Crappy code here - no checks for a string that's too long, no scrolling. Write(s); @@ -212,8 +219,7 @@ namespace Test CursorTop += 1; } - static readonly char[] endEscapeChars = { 'm', 'J' }; - public void Write(string s) + public virtual void Write(string s) { // Crappy code here - no checks for a string that's too long, no scrolling. var writePos = CursorTop * BufferWidth + CursorLeft; @@ -277,7 +283,7 @@ namespace Test } } - public void BlankRestOfLine() + public virtual void BlankRestOfLine() { var writePos = CursorTop * BufferWidth + CursorLeft; for (int i = 0; i < BufferWidth - CursorLeft; i++) @@ -288,13 +294,13 @@ namespace Test } } - public void Clear() + public virtual void Clear() { SetCursorPosition(0, 0); ClearBuffer(); } - void ClearBuffer() + protected void ClearBuffer() { for (int i = 0; i < buffer.Length; i++) { @@ -310,17 +316,19 @@ namespace Test return result; } + protected static readonly char[] endEscapeChars = { 'm', 'J' }; private static readonly ConsoleColor DefaultForeground = ReadLine.Colors[0]; private static readonly ConsoleColor DefaultBackground = ReadLine.BackgroundColors[0]; private static void ToggleNegative(TestConsole c, bool b) { - c.Negative = false; + c._negative = false; c.ForegroundColor = (ConsoleColor)((int)c.ForegroundColor ^ 7); c.BackgroundColor = (ConsoleColor)((int)c.BackgroundColor ^ 7); - c.Negative = b; + c._negative = b; } - private static readonly Dictionary> EscapeSequenceActions = new Dictionary> { + protected static readonly Dictionary> EscapeSequenceActions = new() + { {"7", c => ToggleNegative(c, true) }, {"27", c => ToggleNegative(c, false) }, {"40", c => c.BackgroundColor = ConsoleColor.Black}, @@ -364,5 +372,144 @@ namespace Test {"2J", c => c.SetCursorPosition(0, 0) } }; } -} + internal class BasicScrollingConsole : TestConsole + { + private int _offset; + + internal BasicScrollingConsole(dynamic keyboardLayout, int width, int height) + : base(width, height, mimicScrolling: true) + { + _keyboardLayout = keyboardLayout; + _offset = 0; + } + + private void AdjustCursorWhenNeeded() + { + // If the last character written out happened to be in the last cell + // of a physical line, we need to adjust the cursor. + if (CursorLeft == BufferWidth) + { + CursorLeft = 0; + CursorTop++; + } + + // After adjusting the cursor, we may need to handle scrolling in case + // that the cursor top went beyond the buffer height. + if (CursorTop == BufferHeight) + { + _offset++; + CursorTop--; + } + } + + public override void SetCursorPosition(int left, int top) + { + if (left < 0 || left >= _bufferWidth) + { + throw new ArgumentOutOfRangeException(nameof(left), $"Value should be >= 0 and < BufferWidth({_bufferWidth}), but it's {left}."); + } + + if (top < 0 || top >= _bufferHeight) + { + throw new ArgumentOutOfRangeException(nameof(top), $"Value should be >= 0 and < BufferHeight({_bufferHeight}), but it's {top}."); + } + + CursorLeft = left; + CursorTop = top; + } + + public override void WriteLine(string s) + { + Write(s); + CursorLeft = 0; + CursorTop += 1; + + AdjustCursorWhenNeeded(); + } + + public override void Write(string s) + { + // Crappy code here - no checks for a string that's too long, basic scrolling handling. + var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft; + for (int i = 0; i < s.Length; i++) + { + if (s[i] == (char)0x1b) + { + // Escape sequence - limited support here, and assumed to be well formed. + if (s[i + 1] != '[') throw new ArgumentException("Unexpected escape sequence", nameof(s)); + + var endSequence = s.IndexOfAny(endEscapeChars, i); + var len = endSequence - i - (s[endSequence] != 'm' ? 1 : 2); + var escapeSequence = s.Substring(i + 2, len); + foreach (var subsequence in escapeSequence.Split(';')) + { + EscapeSequenceActions[subsequence](this); + } + i = endSequence; + continue; + } + + if (s[i] == '\b') + { + CursorLeft -= 1; + if (CursorLeft < 0) + { + CursorTop -= 1; + CursorLeft = BufferWidth - 1; + } + } + else if (s[i] == '\n') + { + CursorTop += 1; + CursorLeft = 0; + + // Explicitly writing a new-line may trigger scrolling. + AdjustCursorWhenNeeded(); + writePos = (_offset + CursorTop) * BufferWidth; + } + else + { + AdjustCursorWhenNeeded(); + CursorLeft += 1; + + // When 'CursorLeft == BufferWidth', it means the current character will take up the last cell in the current physical line. + // Assuming the current physical line is 'Y'. + // In such a case, Windows Terminal will set the cursor position to be the following after writing out the character in the + // last cell of the current physical line: + // CursorLeft: BufferWidth - 1 + // CursorTop: Y + // It doesn't directly set the cursor to be (0, Y+1) in this case, but when there are more visible characters (non-control chars) + // to be written, it will automatically adjust the cursor position to be (2, Y+1) after writing out the next visible character. + // + // This behavior makes handling the new-line character '\n' much easier -- you can simply set the cursor to be (0, top+1), and + // then take care of scrolling if needed. + // So, we mimic that behavior here: we allow 'CursorLeft' to be 'BufferWidth' after 'CursorLeft += 1' above, and we adjust the + // cursor when we are about to write a new visible character. + + buffer[writePos].UnicodeChar = s[i]; + buffer[writePos].BackgroundColor = BackgroundColor; + buffer[writePos].ForegroundColor = ForegroundColor; + writePos += 1; + } + } + } + + public override void BlankRestOfLine() + { + var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft; + for (int i = 0; i < BufferWidth - CursorLeft; i++) + { + buffer[writePos + i].UnicodeChar = ' '; + buffer[writePos + i].BackgroundColor = BackgroundColor; + buffer[writePos + i].ForegroundColor = ForegroundColor; + } + } + + public override void Clear() + { + _offset = 0; + base.Clear(); + } + } +} diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 47f239d..2399fb4 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -429,7 +429,12 @@ namespace Test private void AssertScreenIs(int lines, params object[] items) { - var consoleBuffer = _console.ReadBufferLines(0, lines); + AssertScreenIs(top: 0, lines, items); + } + + private void AssertScreenIs(int top, int lines, params object[] items) + { + var consoleBuffer = _console.ReadBufferLines(top, lines); var expectedBuffer = CreateCharInfoBuffer(lines, items); Assert.Equal(expectedBuffer.Length, consoleBuffer.Length);