diff --git a/.vsts-ci/releaseBuild.yml b/.vsts-ci/releaseBuild.yml index 765e2b69e..225796fab 100644 --- a/.vsts-ci/releaseBuild.yml +++ b/.vsts-ci/releaseBuild.yml @@ -76,6 +76,7 @@ stages: *.ps1xml **\*.dll !System.Runtime.InteropServices.RuntimeInformation.dll + !Microsoft.PowerShell.Pager.dll useMinimatch: true # Replace the *.psm1, *.ps1, *.psd1, *.dll files with the signed ones diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index 6d01503c1..4bca152fc 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -18,7 +18,7 @@ - + diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index b07930b58..620019ffd 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -142,6 +142,7 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { $binPath = "PSReadLine/bin/$Configuration/$Framework/publish" Copy-Item $binPath/Microsoft.PowerShell.PSReadLine2.dll $targetDir + Copy-Item $binPath/Microsoft.PowerShell.Pager.dll $targetDir if (Test-Path $binPath/System.Runtime.InteropServices.RuntimeInformation.dll) { Copy-Item $binPath/System.Runtime.InteropServices.RuntimeInformation.dll $targetDir diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 0a5ce8334..c7d9902af 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -422,17 +422,15 @@ private static string HandleNewlinesForPossibleCompletions(string s) return s; } - private class Menu + private class Menu : DisplayBlockBase { - internal PSConsoleReadLine Singleton; - internal int Top; - internal int PreviousTop; internal int ColumnWidth; internal int BufferLines; internal int Rows; internal int Columns; internal int ToolTipLines; + internal Collection MenuItems; internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; internal int CurrentSelection; @@ -701,39 +699,6 @@ public void MoveN(int n) CurrentSelection += MenuItems.Count; } } - - private void MoveCursorDown(int cnt) - { - IConsole console = Singleton._console; - while (cnt-- > 0) - { - console.Write("\n"); - } - } - - private void AdjustForPossibleScroll(int cnt) - { - IConsole console = Singleton._console; - var scrollCnt = console.CursorTop + cnt + 1 - console.BufferHeight; - if (scrollCnt > 0) - { - Top -= scrollCnt; - _singleton._initialY -= scrollCnt; - _savedCursorTop -= scrollCnt; - } - } - - private int _savedCursorLeft; - private int _savedCursorTop; - - public void SaveCursor() - { - IConsole console = Singleton._console; - _savedCursorLeft = console.CursorLeft; - _savedCursorTop = console.CursorTop; - } - - public void RestoreCursor() => Singleton._console.SetCursorPosition(_savedCursorLeft, _savedCursorTop); } private Menu CreateCompletionMenu(Collection matches) diff --git a/PSReadLine/DisplayBlockBase.cs b/PSReadLine/DisplayBlockBase.cs new file mode 100644 index 000000000..7f3d5a0a0 --- /dev/null +++ b/PSReadLine/DisplayBlockBase.cs @@ -0,0 +1,51 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using Microsoft.PowerShell.Internal; + +namespace Microsoft.PowerShell +{ + public partial class PSConsoleReadLine + { + private class DisplayBlockBase + { + internal PSConsoleReadLine Singleton; + internal int Top; + + protected void MoveCursorDown(int cnt) + { + IConsole console = Singleton._console; + while (cnt-- > 0) + { + console.Write("\n"); + } + } + + protected void AdjustForPossibleScroll(int cnt) + { + IConsole console = Singleton._console; + var scrollCnt = console.CursorTop + cnt + 1 - console.BufferHeight; + if (scrollCnt > 0) + { + Top -= scrollCnt; + _singleton._initialY -= scrollCnt; + _savedCursorTop -= scrollCnt; + } + } + + private int _savedCursorLeft; + private int _savedCursorTop; + + public void SaveCursor() + { + IConsole console = Singleton._console; + _savedCursorLeft = console.CursorLeft; + _savedCursorTop = console.CursorTop; + } + + public void RestoreCursor() => Singleton._console.SetCursorPosition(_savedCursorLeft, _savedCursorTop); + } + } +} diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs new file mode 100644 index 000000000..d57eadaeb --- /dev/null +++ b/PSReadLine/DynamicHelp.cs @@ -0,0 +1,289 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; + +namespace Microsoft.PowerShell +{ + public partial class PSConsoleReadLine + { + private Microsoft.PowerShell.Pager _pager; + + /// + /// Attempt to show help content. + /// Show the full help for the command on the alternate screen buffer. + /// + public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null) + { + if (_singleton._console is PlatformWindows.LegacyWin32Console) + { + Collection helpBlock = new Collection() + { + string.Empty, + PSReadLineResources.FullHelpNotSupportedInLegacyConsole + }; + + _singleton.WriteDynamicHelpBlock(helpBlock); + + return; + } + + _singleton.DynamicHelpImpl(isFullHelp: true); + } + + /// + /// Attempt to show help content. + /// Show the short help of the parameter next to the cursor. + /// + public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null) + { + _singleton.DynamicHelpImpl(isFullHelp: false); + } + + [ExcludeFromCodeCoverage] + object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + { + if (string.IsNullOrEmpty(commandName)) + { + return null; + } + + System.Management.Automation.PowerShell ps = null; + + try + { + if (!_mockableMethods.RunspaceIsRemote(_runspace)) + { + ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + } + else + { + ps = System.Management.Automation.PowerShell.Create(); + ps.Runspace = _runspace; + } + + if (isFullHelp) + { + return ps + .AddCommand($"Microsoft.PowerShell.Core\\Get-Help") + .AddParameter("Name", commandName) + .AddParameter("Full", value: true) + .AddCommand($"Microsoft.PowerShell.Utility\\Out-String") + .Invoke() + .FirstOrDefault(); + } + + if (string.IsNullOrEmpty(parameterName)) + { + return null; + } + + return ps + .AddCommand("Microsoft.PowerShell.Core\\Get-Help") + .AddParameter("Name", commandName) + .AddParameter("Parameter", parameterName) + .Invoke() + .FirstOrDefault(); + + } + catch (Exception) + { + return null; + } + finally + { + ps?.Dispose(); + + // GetDynamicHelpContent could scroll the screen, e.g. via Write-Progress. For example, + // Get-Help for unknown command under the CloudShell Azure drive will show the progress bar while searching for command. + // We need to update the _initialY in case the current cursor postion has changed. + if (_singleton._initialY > _console.CursorTop) + { + _singleton._initialY = _console.CursorTop; + } + } + } + + void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo) + { + _pager.Write(content, regexPatternToScrollTo); + } + + private void WriteDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + { + var helpContent = _mockableMethods.GetDynamicHelpContent(commandName, parameterName, isFullHelp); + + if (helpContent is string fullHelp && fullHelp.Length > 0) + { + string regexPatternToScrollTo = null; + + if (!string.IsNullOrEmpty(parameterName)) + { + regexPatternToScrollTo = $"-{parameterName} [<|\\[]"; + } + + _mockableMethods.RenderFullHelp(fullHelp, regexPatternToScrollTo); + } + else if (helpContent is PSObject paramHelp) + { + WriteParameterHelp(paramHelp); + } + } + + private void DynamicHelpImpl(bool isFullHelp) + { + if (isFullHelp) + { + _pager ??= new Pager(); + } + + int cursor = _singleton._current; + string commandName = null; + string parameterName = null; + + foreach(var token in _singleton._tokens) + { + var extent = token.Extent; + + if (extent.StartOffset > cursor) + { + break; + } + + if (token.TokenFlags == TokenFlags.CommandName) + { + commandName = token.Text; + } + + if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) + { + if (token.Kind == TokenKind.Parameter) + { + parameterName = ((ParameterToken)token).ParameterName; + break; + } + } + } + + WriteDynamicHelpContent(commandName, parameterName, isFullHelp); + } + + private void WriteDynamicHelpBlock(Collection helpBlock) + { + var dynHelp = new MultilineDisplayBlock + { + Singleton = this, + ItemsToDisplay = helpBlock + }; + + dynHelp.DrawMultilineBlock(); + ReadKey(); + dynHelp.Clear(); + } + + private void WriteParameterHelp(dynamic helpContent) + { + Collection helpBlock; + + if (string.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) + { + helpBlock = new Collection() + { + String.Empty, + PSReadLineResources.NeedsUpdateHelp + }; + } + else + { + string syntax = $"-{helpContent.name} <{helpContent.type.name}>"; + string desc = "DESC: " + helpContent.Description[0].Text; + + // trim new line characters as some help content has it at the end of the first list on the description. + desc = desc.Trim('\r', '\n'); + + string details = $"Required: {helpContent.required}, Position: {helpContent.position}, Default Value: {helpContent.defaultValue}, Pipeline Input: {helpContent.pipelineInput}, WildCard: {helpContent.globbing}"; + + helpBlock = new Collection + { + string.Empty, + syntax, + string.Empty, + desc, + details + }; + } + + WriteDynamicHelpBlock(helpBlock); + } + + private class MultilineDisplayBlock : DisplayBlockBase + { + internal Collection ItemsToDisplay; + + private int multilineItems = 0; + + public void DrawMultilineBlock() + { + IConsole console = Singleton._console; + + multilineItems = 0; + + this.SaveCursor(); + + // Move cursor to the start of the first line after our input. + var bufferEndPoint = Singleton.ConvertOffsetToPoint(Singleton._buffer.Length); + console.SetCursorPosition(bufferEndPoint.X, bufferEndPoint.Y); + // Top must be initialized before calling AdjustForPossibleScroll, otherwise + // on the last line of the buffer, the scroll operation causes Top to point + // past the buffer, which in turn causes the menu to be printed twice. + this.Top = bufferEndPoint.Y + 1; + AdjustForPossibleScroll(1); + MoveCursorDown(1); + + var bufferWidth = console.BufferWidth; + + var items = this.ItemsToDisplay; + + for (var index = 0; index < items.Count; index++) + { + var itemLength = LengthInBufferCells(items[index]); + + if (itemLength > bufferWidth) + { + multilineItems += itemLength / bufferWidth; + + if (itemLength % bufferWidth == 0) + { + multilineItems--; + } + } + + console.Write(items[index]); + + // Explicit newline so consoles see each row as distinct lines, but skip the + // last line so we don't scroll. + if (index != (items.Count - 1)) + { + AdjustForPossibleScroll(1); + MoveCursorDown(1); + } + } + + this.RestoreCursor(); + } + + public void Clear() + { + _singleton.WriteBlankLines(Top, ItemsToDisplay.Count + multilineItems); + } + } + } +} diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index edd049a5e..8d0bf7be6 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -234,6 +234,8 @@ void SetDefaultWindowsBindings() { Keys.AltD, MakeKeyHandler(KillWord, "KillWord") }, { Keys.CtrlAt, MakeKeyHandler(MenuComplete, "MenuComplete") }, { Keys.CtrlW, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, + { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -328,6 +330,8 @@ void SetDefaultEmacsBindings() { Keys.AltPeriod, MakeKeyHandler(YankLastArg, "YankLastArg") }, { Keys.AltUnderbar, MakeKeyHandler(YankLastArg, "YankLastArg") }, { Keys.CtrlAltY, MakeKeyHandler(YankNthArg, "YankNthArg") }, + { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -561,6 +565,8 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(NextSuggestion): case nameof(PreviousSuggestion): case nameof(SwitchPredictionView): + case nameof(ShowCommandHelp): + case nameof(ShowParameterHelp): return KeyHandlerGroup.Miscellaneous; case nameof(CharacterSearch): diff --git a/PSReadLine/KeyBindings.vi.cs b/PSReadLine/KeyBindings.vi.cs index e77729550..63629db82 100644 --- a/PSReadLine/KeyBindings.vi.cs +++ b/PSReadLine/KeyBindings.vi.cs @@ -88,6 +88,8 @@ private void SetDefaultViBindings() { Keys.CtrlAltQuestion, MakeKeyHandler(ShowKeyBindings, "ShowKeyBindings") }, { Keys.CtrlR, MakeKeyHandler(ViSearchHistoryBackward,"ViSearchHistoryBackward") }, { Keys.CtrlS, MakeKeyHandler(SearchForward, "SearchForward") }, + { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -208,6 +210,8 @@ private void SetDefaultViBindings() { Keys.Period, MakeKeyHandler(RepeatLastCommand, "RepeatLastCommand") }, { Keys.Semicolon, MakeKeyHandler(RepeatLastCharSearch, "RepeatLastCharSearch") }, { Keys.Comma, MakeKeyHandler(RepeatLastCharSearchBackwards, "RepeatLastCharSearchBackwards") }, + { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -216,7 +220,6 @@ private void SetDefaultViBindings() _viCmdKeyMap.Add(Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord")); } - _viChordDTable = new Dictionary { { Keys.D, MakeKeyHandler( DeleteLine, "DeleteLine") }, diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 281e926b6..2dfd2a7e7 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -21,7 +21,11 @@ - + + + + + diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index d26a5e4b4..b91a55260 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2067,5 +2067,47 @@ internal static string YankPopDescription { return ResourceManager.GetString("YankPopDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to No help content available. Please use Update-Help to download the latest help content. + /// + internal static string NeedsUpdateHelp { + get { + return ResourceManager.GetString("NeedsUpdateHelp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displaying help content is not supported on this console as it does not support alternate screen buffer. + /// + internal static string FullHelpNotSupportedInLegacyConsole + { + get + { + return ResourceManager.GetString("FullHelpNotSupportedInLegacyConsole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows help for the command at the cursor in an alternate screen buffer. + /// + internal static string ShowCommandHelpDescription + { + get + { + return ResourceManager.GetString("ShowCommandHelpDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows help for the parameter at the cursor. + /// + internal static string ShowParameterHelpDescription + { + get + { + return ResourceManager.GetString("ShowParameterHelpDescription", resourceCulture); + } + } } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 5cf93c530..236c8da7e 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -829,4 +829,16 @@ Or not saving history with: The prediction 'ListView' is temporarily disabled because the current window size of the console is too small. To use the 'ListView', please make sure the 'WindowWidth' is not less than '{0}' and the 'WindowHeight' is not less than '{1}'. + + No help content available. Please use Update-Help to download the latest help content. + + + Displaying help content is not supported on this console as it does not support alternate screen buffer. + + + Shows help for the command at the cursor in an alternate screen buffer. + + + Shows help for the parameter at the cursor. + diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index e7a140a23..7a932661f 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -29,6 +29,8 @@ public interface IPSConsoleReadLineMockableMethods Task> PredictInput(Ast ast, Token[] tokens); void OnCommandLineAccepted(IReadOnlyList history); void OnSuggestionAccepted(Guid predictorId, string suggestionText); + object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); + void RenderFullHelp(string content, string regexPatternToScrollTo); } [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")] diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index ac024e8d4..95cafac7d 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs new file mode 100644 index 000000000..5470f9307 --- /dev/null +++ b/test/DynamicHelpTest.cs @@ -0,0 +1,292 @@ +using System; +using System.Management.Automation; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + private static readonly string fullHelp = @" + +NAME + Get-Date + +SYNOPSIS + Gets the current date and time. + +PARAMETERS + -AsUTC + Converts the date value to the equivalent time in UTC. + + This parameter was introduced in PowerShell 7.1. + + Required? false + Position? named + Default value False + Accept pipeline input? False + Accept wildcard characters? false +"; + + internal static object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + { + if (string.IsNullOrEmpty(commandName)) + { + return null; + } + + if (isFullHelp) + { + return fullHelp; + } + + if (string.IsNullOrEmpty(parameterName)) + { + return null; + } + + PSObject paramHelp = new PSObject(); + + if (string.Equals(commandName, "Get-FakeHelp", StringComparison.OrdinalIgnoreCase) && string.Equals(parameterName, "Fake", StringComparison.OrdinalIgnoreCase)) + { + PSObject[] descDetails = new PSObject[1]; + descDetails[0] = new PSObject(); + descDetails[0].Members.Add(new PSNoteProperty("Text", null)); + } + else if (string.Equals(commandName, "Get-Date", StringComparison.OrdinalIgnoreCase) && string.Equals(parameterName, "Date", StringComparison.OrdinalIgnoreCase)) + { + return GetParameterHelpObject(description: "Specifies a date and time."); + } + else if (string.Equals(commandName, "Get-MultiLineHelp", StringComparison.OrdinalIgnoreCase)) + { + if(string.Equals(parameterName, "OneAndHalf", StringComparison.OrdinalIgnoreCase)) + { + string multiLineDesc = "Some very long description that is over the buffer width of 60 characters but shorter than 120."; + return GetParameterHelpObject(multiLineDesc); + } + else if(string.Equals(parameterName, "ExactlyTwo", StringComparison.OrdinalIgnoreCase)) + { + string multiLineDesc = "Some very long description that is over the buffer width of 60 characters and exactly the length of 120 characters"; + return GetParameterHelpObject(multiLineDesc); + } + } + + return paramHelp; + } + + private static PSObject GetParameterHelpObject(string description) + { + PSObject paramHelp = new PSObject(); + PSObject[] descDetails = new PSObject[1]; + descDetails[0] = new PSObject(); + descDetails[0].Members.Add(new PSNoteProperty("Text", description)); + + var np = new PSNoteProperty("name", "System.Datetime"); + np.Value = "System.Datetime"; + + var typeName = new PSObject(np); + + paramHelp.Members.Add(new PSNoteProperty("Description", descDetails)); + paramHelp.Members.Add(new PSNoteProperty("Name", "Date")); + paramHelp.Members.Add(new PSNoteProperty("type", typeName)); + paramHelp.Members.Add(new PSNoteProperty("required", "false")); + paramHelp.Members.Add(new PSNoteProperty("position", "0")); + paramHelp.Members.Add(new PSNoteProperty("defaultValue", "None")); + paramHelp.Members.Add(new PSNoteProperty("pipelineInput", "True (ByPropertyName, ByValue)")); + paramHelp.Members.Add(new PSNoteProperty("globbing", "false")); + + return paramHelp; + } + + [SkippableFact] + public void DynHelp_GetFullHelp() + { + TestSetup(KeyMode.Cmd); + Test("Get-Date", Keys( + "Get-Date", _.F1, + CheckThat(() => Assert.Equal(fullHelp, _mockedMethods.helpContentRendered)), + _.Enter + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelp_And_Clear() + { + TestSetup(KeyMode.Cmd); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-Date -Date", Keys( + "Get-Date -Date", _.Alt_h, + CheckThat(() => AssertScreenIs(9, + TokenClassification.Command, "Get-Date", + TokenClassification.None, " ", + TokenClassification.Parameter, "-Date", + NextLine, + emptyLine, + TokenClassification.None, $"-Date ", + NextLine, + emptyLine, + TokenClassification.None, "DESC: Specifies a date and time.", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + NextLine, + "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.Escape, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-Date", + TokenClassification.None, " ", + TokenClassification.Parameter, "-Date")), + _.Enter + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelpMultiLine_And_Clear() + { + TestSetup(KeyMode.Cmd); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-MultiLineHelp -OneAndHalf", Keys( + "Get-MultiLineHelp -OneAndHalf", _.Alt_h, + CheckThat(() => AssertScreenIs(9, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf", + NextLine, + emptyLine, + TokenClassification.None, $"-Date ", + NextLine, + emptyLine, + 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 ", + NextLine, + "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.LeftArrow, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf")), + _.Enter + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelpTwoLines_And_Clear() + { + TestSetup(KeyMode.Cmd); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-MultiLineHelp -ExactlyTwo", Keys( + "Get-MultiLineHelp -ExactlyTwo", _.Alt_h, + CheckThat(() => AssertScreenIs(9, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-ExactlyTwo", + NextLine, + emptyLine, + TokenClassification.None, $"-Date ", + NextLine, + emptyLine, + TokenClassification.None, "DESC: Some very long description that is over the buffer wid", + TokenClassification.None, "th of 60 characters and exactly the length of 120 characters", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + NextLine, + "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.RightArrow, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-ExactlyTwo")), + _.Enter + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelpTwoLines_And_Clear_Emacs() + { + TestSetup(KeyMode.Emacs); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-MultiLineHelp -ExactlyTwo", Keys( + "Get-MultiLineHelp -ExactlyTwo", _.Alt_h, + CheckThat(() => AssertScreenIs(9, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-ExactlyTwo", + NextLine, + emptyLine, + TokenClassification.None, $"-Date ", + NextLine, + emptyLine, + TokenClassification.None, "DESC: Some very long description that is over the buffer wid", + TokenClassification.None, "th of 60 characters and exactly the length of 120 characters", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + NextLine, + "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.RightArrow, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-ExactlyTwo")), + _.Enter + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelpTwoLines_And_Clear_Vi() + { + TestSetup(KeyMode.Vi); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-MultiLineHelp -ExactlyTwo", Keys( + "Get-MultiLineHelp -ExactlyTwo", _.Alt_h, + CheckThat(() => AssertScreenIs(9, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-ExactlyTwo", + NextLine, + emptyLine, + TokenClassification.None, $"-Date ", + NextLine, + emptyLine, + TokenClassification.None, "DESC: Some very long description that is over the buffer wid", + TokenClassification.None, "th of 60 characters and exactly the length of 120 characters", + NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", + NextLine, + "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.RightArrow, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-ExactlyTwo")), + _.Enter + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelpErrorMessage() + { + TestSetup(KeyMode.Cmd); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-FakeHelp -Fake", Keys( + "Get-FakeHelp -Fake", _.Alt_h, + CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "Get-FakeHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-Fake", + NextLine, + emptyLine, + TokenClassification.None, "No help content available. Please use Update-Help to downloa", + NextLine, + "d the latest help content.")), + _.RightArrow, + _.Enter + )); + } + } +} diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index 1edff47a1..4c1a993d1 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -24,7 +24,7 @@ - + diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 7e9cb8803..21d05ea4e 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -25,6 +25,7 @@ internal class MockedMethods : IPSConsoleReadLineMockableMethods internal IReadOnlyList commandHistory; internal Guid acceptedPredictorId; internal string acceptedSuggestion; + internal string helpContentRendered; internal void ClearPredictionFields() { @@ -67,6 +68,16 @@ public void OnSuggestionAccepted(Guid predictorId, string suggestionText) acceptedPredictorId = predictorId; acceptedSuggestion = suggestionText; } + + public object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + { + return ReadLine.GetDynamicHelpContent(commandName, parameterName, isFullHelp); + } + + public void RenderFullHelp(string content, string regexPatternToScrollTo) + { + helpContentRendered = content; + } } public enum TokenClassification