From 73fbf7898f66ca8410eda26f0283a64f117ab8e3 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Sat, 23 May 2020 15:10:59 -0700 Subject: [PATCH 01/20] Full help and parameter help working --- PSReadLine/Completion.cs | 451 ++++++++++++++++++++++++++++++++++++++ PSReadLine/KeyBindings.cs | 3 + 2 files changed, 454 insertions(+) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 0a5ce8334..28eafcdcb 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -11,6 +11,7 @@ using System.Globalization; using System.Linq; using System.Management.Automation; +using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; using Microsoft.PowerShell.PSReadLine; @@ -135,6 +136,16 @@ public static void MenuComplete(ConsoleKeyInfo? key = null, object arg = null) _singleton.CompleteImpl(true); } + + /// + /// Some summary + /// + public static void DynamicHelpBlock(ConsoleKeyInfo? key = null, object arg = null) + { + bool isF1 = key?.Key == ConsoleKey.F1; + _singleton.DynamicHelpBlockImpl(isF1); + } + private bool IsConsistentQuoting(Collection matches) { int quotedCompletions = matches.Count(match => IsQuoted(match.CompletionText)); @@ -200,6 +211,183 @@ private string GetUnambiguousPrefix(Collection matches, out bo return replacementText; } + private void DynamicHelpBlockImpl(bool isFullHelp) + { + if (InViInsertMode()) // must close out the current edit group before engaging menu completion + { + ViCommandMode(); + ViInsertWithAppend(); + } + + int cursor = _singleton._current; + string commandName = null; + string parameterName = null; + + foreach(var token in _singleton._tokens) + { + if (token.TokenFlags == TokenFlags.CommandName) + { + commandName = token.Extent.Text; + } + + var extent = token.Extent; + if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) + { + if (token.Kind == TokenKind.Parameter) + { + parameterName = ((ParameterToken)token).ParameterName; + + } + // Possible corner case here when cursor is at the end + } + } + + if (!String.IsNullOrEmpty(commandName) && isFullHelp) + { + System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + var fullHelp = ps.AddScript($"(Get-Help -Name {commandName} -Full | Out-String)").Invoke().FirstOrDefault(); + if (!String.Equals(fullHelp, String.Empty, StringComparison.CurrentCultureIgnoreCase)) + { + WriteToAlternateScreenBuffer(fullHelp); + } + } + else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) + { + System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + PSObject paramHelp = ps.AddScript($"(Get-Help -Name {commandName} -Parameter {parameterName})").Invoke().FirstOrDefault(); + + if (paramHelp != null) + { + WriteParameterHelp(paramHelp); + } + } + } + + private void WriteParameterHelp(dynamic helpContent) + { + char c = (char)0x1b; + + string syntax = $"{c}[7m-{helpContent.name} <{helpContent.type.name}>{c}[0m"; + string desc = "DESC: " + helpContent.Description[0].Text; + string details = $"Required: {helpContent.required}, Position: {helpContent.position}, Default Value: {helpContent.defaultValue}, Pipeline Input: {helpContent.pipelineInput}, WildCard: {helpContent.globbing}"; + + Collection helpBlock = new Collection { + String.Empty, + syntax, + String.Empty, + desc, + details + }; + + var bufferWidth = _console.BufferWidth; + var colWidth = Math.Min(helpBlock.Max(s => LengthInBufferCells(s)) + 2, bufferWidth); + int columns = 1; + + var dynHelp = new DynamicHelp + { + Singleton = this, + ColumnWidth = colWidth, + Columns = columns, + Rows = (helpBlock.Count + columns - 1) / columns, + HelpItems = helpBlock + }; + + dynHelp.SaveCursor(); + dynHelp.DrawHelpBlock(dynHelp); + + Console.ReadKey(true); + + dynHelp.Clear(); + dynHelp.RestoreCursor(); + } + + private void WriteToAlternateScreenBuffer(string helpContent) + { + char c = (char) 0x1b; + string startAltBuffer = $"{c}[?1049h"; + string endAltBuffer = $"{c}[?1049l"; + string reverseColorStart = $"{c}[7m"; + string reverseColorEnd = $"{c}[0m"; + + var contentAsArray = helpContent.Split(Environment.NewLine); + + int startLine = 0; + int bufferHeight = Math.Min(Console.WindowHeight,Console.BufferHeight); + bool moved = true; + + string pagerMessage = String.Concat( + reverseColorStart, + "Up:", + reverseColorEnd, + "↑ ", + reverseColorStart, + "Down:", + reverseColorEnd, + "↓ ", + reverseColorStart, + "Quit:", + reverseColorEnd, + "Q :"); + + Console.Write(startAltBuffer); + + do + { + if (moved) { + Console.Clear(); + int endLine = startLine + bufferHeight - GetMultilineOffset(contentAsArray, startLine); + string content = String.Join(Environment.NewLine, contentAsArray.Skip(startLine).Take(endLine)); + Console.WriteLine(content); + Console.Write(pagerMessage); + } + + var pressed = Console.ReadKey(intercept: true); + + if (pressed.Key == ConsoleKey.UpArrow) { + if (startLine > 0) { + startLine--; + moved = true; + } + } + else if (pressed.Key == ConsoleKey.DownArrow) { + if ((startLine + bufferHeight) < contentAsArray.Count()) { + startLine++; + moved = true; + } + } + else if (pressed.Key == ConsoleKey.Q) { + break; + } + else { + moved = false; + } + + } while (true); + + Console.Write(endAltBuffer); + } + + private int GetMultilineOffset(string[] contentAsArray, int startLine) + { + int bufferHeight = Math.Min(Console.WindowHeight,Console.BufferHeight); + int contentTotalLines = contentAsArray.Count(); + int endLine = startLine + bufferHeight; + int bufferWidth = Console.BufferWidth; + + int offset = 0; + + for(int i = startLine; i < endLine; i++) + { + int lineLength = contentAsArray[i].Length; + if (lineLength > bufferWidth) + { + offset += lineLength / bufferWidth; + } + } + + return offset; + } + private void CompleteImpl(bool menuSelect) { if (InViInsertMode()) // must close out the current edit group before engaging menu completion @@ -422,6 +610,253 @@ private static string HandleNewlinesForPossibleCompletions(string s) return s; } + private class DynamicHelp + { + 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 HelpItems; + //internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; + internal int CurrentSelection; + + public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) + { + IConsole console = Singleton._console; + + // 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 columnWidth = this.ColumnWidth; + + var items = this.HelpItems; + for (var row = 0; row < this.Rows; row++) + { + var cells = 0; + for (var col = 0; col < this.Columns; col++) + { + var index = row + (this.Rows * col); + if (index >= items.Count) + { + break; + } + console.Write(GetHelpItem(items[index], columnWidth)); + cells += columnWidth; + } + + // Make sure we always write out exactly 1 buffer width to erase anything + // from a previous menu. + if (cells < bufferWidth) + { + // 'BlankRestOfLine' erases rest of the current line, but the cursor is not moved. + console.BlankRestOfLine(); + } + + // Explicit newline so consoles see each row as distinct lines, but skip the + // last line so we don't scroll. + if (row != (this.Rows - 1) || !menuSelect) { + AdjustForPossibleScroll(1); + MoveCursorDown(1); + } + } + + 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 (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(previousMenu.Rows + previousMenu.ToolTipLines - Rows); + } + } + + // if the menu has moved, we need to clear the lines under it + if (bufferEndPoint.Y < PreviousTop) + { + console.BlankRestOfLine(); + Singleton.WriteBlankLines(PreviousTop - bufferEndPoint.Y); + } + + PreviousTop = bufferEndPoint.Y; + + if (menuSelect) + { + RestoreCursor(); + console.CursorVisible = true; + } + } + + public void Clear() + { + WriteBlankLines(Top, Rows + ToolTipLines); + } + + /* public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips, string toolTipColor) + { + var console = Singleton._console; + var menuItem = MenuItems[selectedItem]; + var listItem = menuItem.ListItemText; + + string toolTip = null; + if (showTooltips) + { + toolTip = menuItem.ToolTip.Trim(); + + // Don't bother showing the tooltip if it doesn't add information. + showTooltips = !string.IsNullOrWhiteSpace(toolTip) + && !string.Equals(toolTip, listItem, StringComparison.OrdinalIgnoreCase) + && !string.Equals(toolTip, menuItem.CompletionText, StringComparison.OrdinalIgnoreCase); + } + + // We'll use one blank line to set the tooltip apart from the menu, + // and there will be at least 1 line in the tooltip, possibly more. + var toolTipLines = 2; + if (showTooltips) + { + // Determine if showing the tooltip would scroll the top of our buffer off the screen. + + int lineLength = 0; + for (var i = 0; i < toolTip.Length; i++) + { + char c = toolTip[i]; + if (c == '\r' && i < toolTip.Length && toolTip[i+1] == '\n') + { + // Skip the newline, but handle LF, CRLF, and CR. + i += 1; + } + + if (c == '\r' || c == '\n') + { + toolTipLines += 1; + lineLength = 0; + } + else + { + lineLength += 1; + if (lineLength == console.BufferWidth) + { + toolTipLines += 1; + lineLength = 0; + } + } + } + + // The +1 is for the blank line between the menu and tooltips. + if (BufferLines + Rows + toolTipLines + 1 > console.WindowHeight) + { + showTooltips = false; + } + } + + SaveCursor(); + + var row = Top + selectedItem % Rows; + var col = ColumnWidth * (selectedItem / Rows); + + console.SetCursorPosition(col, row); + + if (select) console.Write(Singleton.Options._selectionColor); + console.Write(GetMenuItem(listItem, ColumnWidth)); + if (select) console.Write("\x1b[0m"); + + ToolTipLines = 0; + if (showTooltips) + { + Debug.Assert(select, "On unselect, caller must clear the tooltip"); + console.SetCursorPosition(0, Top + Rows - 1); + // Move down 2 so we have 1 blank line between the menu and buffer. + AdjustForPossibleScroll(toolTipLines); + MoveCursorDown(2); + console.Write(toolTipColor); + console.Write(toolTip); + ToolTipLines = toolTipLines; + + console.Write("\x1b[0m"); + } + + RestoreCursor(); + }*/ + + /* public void MoveRight() => CurrentSelection = Math.Min(CurrentSelection + Rows, HelpItems.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, HelpItems.Count - 1); + public void MovePageDown() => CurrentSelection = Math.Min(CurrentSelection + Rows - (CurrentSelection % Rows) - 1, + HelpItems.Count - 1); + public void MovePageUp() => CurrentSelection = Math.Max(CurrentSelection - (CurrentSelection % Rows), 0); + + public void MoveN(int n) + { + CurrentSelection = (CurrentSelection + n) % MenuItems.Count; + if (CurrentSelection < 0) + { + 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; + } + } + + public void WriteBlankLines(int top, int count) + { + SaveCursor(); + Singleton._console.SetCursorPosition(0, top); + Singleton.WriteBlankLines(count); + RestoreCursor(); + } + + 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 class Menu { internal PSConsoleReadLine Singleton; @@ -837,6 +1272,22 @@ private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSel } } + private static string GetHelpItem(string item, int columnWidth) + { + item = HandleNewlinesForPossibleCompletions(item); + var spacesNeeded = columnWidth - LengthInBufferCells(item); + if (spacesNeeded > 0) + { + item = item + Spaces(spacesNeeded); + } + else if (spacesNeeded < 0) + { + item = SubstringByCells(item, columnWidth - 3) + "..."; + } + + return item; + } + private static string GetMenuItem(string item, int columnWidth) { item = HandleNewlinesForPossibleCompletions(item); diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index edd049a5e..bc5d66464 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -251,6 +251,9 @@ void SetDefaultWindowsBindings() _dispatchTable.Add(Keys.PageDown, MakeKeyHandler(ScrollDisplayDown, "ScrollDisplayDown")); _dispatchTable.Add(Keys.CtrlPageUp, MakeKeyHandler(ScrollDisplayUpLine, "ScrollDisplayUpLine")); _dispatchTable.Add(Keys.CtrlPageDown, MakeKeyHandler(ScrollDisplayDownLine, "ScrollDisplayDownLine")); + + _dispatchTable.Add(Keys.AltH, MakeKeyHandler(DynamicHelpBlock, "DynamicHelpBlock", "Long desc")); + _dispatchTable.Add(Keys.F1, MakeKeyHandler(DynamicHelpBlock, "DynamicHelpBlock")); } _chordDispatchTable = new Dictionary>(); From dbc1e53f68dd4f6de427c7a43073bc08dddec43f Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 26 May 2020 16:59:28 -0700 Subject: [PATCH 02/20] Scroll to parameter name for full help --- PSReadLine/Completion.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 28eafcdcb..54c1d7858 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -245,10 +245,11 @@ private void DynamicHelpBlockImpl(bool isFullHelp) if (!String.IsNullOrEmpty(commandName) && isFullHelp) { System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + var fullHelp = ps.AddScript($"(Get-Help -Name {commandName} -Full | Out-String)").Invoke().FirstOrDefault(); if (!String.Equals(fullHelp, String.Empty, StringComparison.CurrentCultureIgnoreCase)) { - WriteToAlternateScreenBuffer(fullHelp); + WriteToAlternateScreenBuffer(fullHelp, String.IsNullOrEmpty(parameterName) ? null : parameterName); } } else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) @@ -301,7 +302,7 @@ private void WriteParameterHelp(dynamic helpContent) dynHelp.RestoreCursor(); } - private void WriteToAlternateScreenBuffer(string helpContent) + private void WriteToAlternateScreenBuffer(string helpContent, string parameterName) { char c = (char) 0x1b; string startAltBuffer = $"{c}[?1049h"; @@ -315,6 +316,22 @@ private void WriteToAlternateScreenBuffer(string helpContent) int bufferHeight = Math.Min(Console.WindowHeight,Console.BufferHeight); bool moved = true; + if (!String.IsNullOrEmpty(parameterName)) + { + string upper = parameterName[0].ToString().ToUpperInvariant(); + string lower = parameterName[0].ToString().ToLowerInvariant(); + string remainingString = parameterName.Substring(1); + string pattern = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; + + for(int i = 0; i < contentAsArray.Length; i++) + { + if (System.Text.RegularExpressions.Regex.IsMatch(contentAsArray[i], pattern)) + { + startLine = i; + } + } + } + string pagerMessage = String.Concat( reverseColorStart, "Up:", @@ -335,8 +352,8 @@ private void WriteToAlternateScreenBuffer(string helpContent) { if (moved) { Console.Clear(); - int endLine = startLine + bufferHeight - GetMultilineOffset(contentAsArray, startLine); - string content = String.Join(Environment.NewLine, contentAsArray.Skip(startLine).Take(endLine)); + int offset = GetMultilineOffset(contentAsArray, startLine); + string content = String.Join(Environment.NewLine, contentAsArray.Skip(startLine).Take(bufferHeight - offset)); Console.WriteLine(content); Console.Write(pagerMessage); } From 7cdd0cea5f778e5e9de719e2fdaf40f0fafdacc3 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 17 Nov 2020 11:53:51 -0800 Subject: [PATCH 03/20] Remove abstract class and bug fixes --- PSReadLine.build.ps1 | 1 + PSReadLine/Completion.cs | 451 --------------------- PSReadLine/DynamicHelp.cs | 426 +++++++++++++++++++ PSReadLine/KeyBindings.cs | 4 +- PSReadLine/PSReadLine.csproj | 1 + PSReadLine/PSReadLineResources.Designer.cs | 9 + 6 files changed, 439 insertions(+), 453 deletions(-) create mode 100644 PSReadLine/DynamicHelp.cs 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 54c1d7858..b2105e1cc 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -136,16 +136,6 @@ public static void MenuComplete(ConsoleKeyInfo? key = null, object arg = null) _singleton.CompleteImpl(true); } - - /// - /// Some summary - /// - public static void DynamicHelpBlock(ConsoleKeyInfo? key = null, object arg = null) - { - bool isF1 = key?.Key == ConsoleKey.F1; - _singleton.DynamicHelpBlockImpl(isF1); - } - private bool IsConsistentQuoting(Collection matches) { int quotedCompletions = matches.Count(match => IsQuoted(match.CompletionText)); @@ -211,200 +201,6 @@ private string GetUnambiguousPrefix(Collection matches, out bo return replacementText; } - private void DynamicHelpBlockImpl(bool isFullHelp) - { - if (InViInsertMode()) // must close out the current edit group before engaging menu completion - { - ViCommandMode(); - ViInsertWithAppend(); - } - - int cursor = _singleton._current; - string commandName = null; - string parameterName = null; - - foreach(var token in _singleton._tokens) - { - if (token.TokenFlags == TokenFlags.CommandName) - { - commandName = token.Extent.Text; - } - - var extent = token.Extent; - if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) - { - if (token.Kind == TokenKind.Parameter) - { - parameterName = ((ParameterToken)token).ParameterName; - - } - // Possible corner case here when cursor is at the end - } - } - - if (!String.IsNullOrEmpty(commandName) && isFullHelp) - { - System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - - var fullHelp = ps.AddScript($"(Get-Help -Name {commandName} -Full | Out-String)").Invoke().FirstOrDefault(); - if (!String.Equals(fullHelp, String.Empty, StringComparison.CurrentCultureIgnoreCase)) - { - WriteToAlternateScreenBuffer(fullHelp, String.IsNullOrEmpty(parameterName) ? null : parameterName); - } - } - else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) - { - System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - PSObject paramHelp = ps.AddScript($"(Get-Help -Name {commandName} -Parameter {parameterName})").Invoke().FirstOrDefault(); - - if (paramHelp != null) - { - WriteParameterHelp(paramHelp); - } - } - } - - private void WriteParameterHelp(dynamic helpContent) - { - char c = (char)0x1b; - - string syntax = $"{c}[7m-{helpContent.name} <{helpContent.type.name}>{c}[0m"; - string desc = "DESC: " + helpContent.Description[0].Text; - string details = $"Required: {helpContent.required}, Position: {helpContent.position}, Default Value: {helpContent.defaultValue}, Pipeline Input: {helpContent.pipelineInput}, WildCard: {helpContent.globbing}"; - - Collection helpBlock = new Collection { - String.Empty, - syntax, - String.Empty, - desc, - details - }; - - var bufferWidth = _console.BufferWidth; - var colWidth = Math.Min(helpBlock.Max(s => LengthInBufferCells(s)) + 2, bufferWidth); - int columns = 1; - - var dynHelp = new DynamicHelp - { - Singleton = this, - ColumnWidth = colWidth, - Columns = columns, - Rows = (helpBlock.Count + columns - 1) / columns, - HelpItems = helpBlock - }; - - dynHelp.SaveCursor(); - dynHelp.DrawHelpBlock(dynHelp); - - Console.ReadKey(true); - - dynHelp.Clear(); - dynHelp.RestoreCursor(); - } - - private void WriteToAlternateScreenBuffer(string helpContent, string parameterName) - { - char c = (char) 0x1b; - string startAltBuffer = $"{c}[?1049h"; - string endAltBuffer = $"{c}[?1049l"; - string reverseColorStart = $"{c}[7m"; - string reverseColorEnd = $"{c}[0m"; - - var contentAsArray = helpContent.Split(Environment.NewLine); - - int startLine = 0; - int bufferHeight = Math.Min(Console.WindowHeight,Console.BufferHeight); - bool moved = true; - - if (!String.IsNullOrEmpty(parameterName)) - { - string upper = parameterName[0].ToString().ToUpperInvariant(); - string lower = parameterName[0].ToString().ToLowerInvariant(); - string remainingString = parameterName.Substring(1); - string pattern = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; - - for(int i = 0; i < contentAsArray.Length; i++) - { - if (System.Text.RegularExpressions.Regex.IsMatch(contentAsArray[i], pattern)) - { - startLine = i; - } - } - } - - string pagerMessage = String.Concat( - reverseColorStart, - "Up:", - reverseColorEnd, - "↑ ", - reverseColorStart, - "Down:", - reverseColorEnd, - "↓ ", - reverseColorStart, - "Quit:", - reverseColorEnd, - "Q :"); - - Console.Write(startAltBuffer); - - do - { - if (moved) { - Console.Clear(); - int offset = GetMultilineOffset(contentAsArray, startLine); - string content = String.Join(Environment.NewLine, contentAsArray.Skip(startLine).Take(bufferHeight - offset)); - Console.WriteLine(content); - Console.Write(pagerMessage); - } - - var pressed = Console.ReadKey(intercept: true); - - if (pressed.Key == ConsoleKey.UpArrow) { - if (startLine > 0) { - startLine--; - moved = true; - } - } - else if (pressed.Key == ConsoleKey.DownArrow) { - if ((startLine + bufferHeight) < contentAsArray.Count()) { - startLine++; - moved = true; - } - } - else if (pressed.Key == ConsoleKey.Q) { - break; - } - else { - moved = false; - } - - } while (true); - - Console.Write(endAltBuffer); - } - - private int GetMultilineOffset(string[] contentAsArray, int startLine) - { - int bufferHeight = Math.Min(Console.WindowHeight,Console.BufferHeight); - int contentTotalLines = contentAsArray.Count(); - int endLine = startLine + bufferHeight; - int bufferWidth = Console.BufferWidth; - - int offset = 0; - - for(int i = startLine; i < endLine; i++) - { - int lineLength = contentAsArray[i].Length; - if (lineLength > bufferWidth) - { - offset += lineLength / bufferWidth; - } - } - - return offset; - } - private void CompleteImpl(bool menuSelect) { if (InViInsertMode()) // must close out the current edit group before engaging menu completion @@ -627,253 +423,6 @@ private static string HandleNewlinesForPossibleCompletions(string s) return s; } - private class DynamicHelp - { - 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 HelpItems; - //internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; - internal int CurrentSelection; - - public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) - { - IConsole console = Singleton._console; - - // 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 columnWidth = this.ColumnWidth; - - var items = this.HelpItems; - for (var row = 0; row < this.Rows; row++) - { - var cells = 0; - for (var col = 0; col < this.Columns; col++) - { - var index = row + (this.Rows * col); - if (index >= items.Count) - { - break; - } - console.Write(GetHelpItem(items[index], columnWidth)); - cells += columnWidth; - } - - // Make sure we always write out exactly 1 buffer width to erase anything - // from a previous menu. - if (cells < bufferWidth) - { - // 'BlankRestOfLine' erases rest of the current line, but the cursor is not moved. - console.BlankRestOfLine(); - } - - // Explicit newline so consoles see each row as distinct lines, but skip the - // last line so we don't scroll. - if (row != (this.Rows - 1) || !menuSelect) { - AdjustForPossibleScroll(1); - MoveCursorDown(1); - } - } - - 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 (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(previousMenu.Rows + previousMenu.ToolTipLines - Rows); - } - } - - // if the menu has moved, we need to clear the lines under it - if (bufferEndPoint.Y < PreviousTop) - { - console.BlankRestOfLine(); - Singleton.WriteBlankLines(PreviousTop - bufferEndPoint.Y); - } - - PreviousTop = bufferEndPoint.Y; - - if (menuSelect) - { - RestoreCursor(); - console.CursorVisible = true; - } - } - - public void Clear() - { - WriteBlankLines(Top, Rows + ToolTipLines); - } - - /* public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips, string toolTipColor) - { - var console = Singleton._console; - var menuItem = MenuItems[selectedItem]; - var listItem = menuItem.ListItemText; - - string toolTip = null; - if (showTooltips) - { - toolTip = menuItem.ToolTip.Trim(); - - // Don't bother showing the tooltip if it doesn't add information. - showTooltips = !string.IsNullOrWhiteSpace(toolTip) - && !string.Equals(toolTip, listItem, StringComparison.OrdinalIgnoreCase) - && !string.Equals(toolTip, menuItem.CompletionText, StringComparison.OrdinalIgnoreCase); - } - - // We'll use one blank line to set the tooltip apart from the menu, - // and there will be at least 1 line in the tooltip, possibly more. - var toolTipLines = 2; - if (showTooltips) - { - // Determine if showing the tooltip would scroll the top of our buffer off the screen. - - int lineLength = 0; - for (var i = 0; i < toolTip.Length; i++) - { - char c = toolTip[i]; - if (c == '\r' && i < toolTip.Length && toolTip[i+1] == '\n') - { - // Skip the newline, but handle LF, CRLF, and CR. - i += 1; - } - - if (c == '\r' || c == '\n') - { - toolTipLines += 1; - lineLength = 0; - } - else - { - lineLength += 1; - if (lineLength == console.BufferWidth) - { - toolTipLines += 1; - lineLength = 0; - } - } - } - - // The +1 is for the blank line between the menu and tooltips. - if (BufferLines + Rows + toolTipLines + 1 > console.WindowHeight) - { - showTooltips = false; - } - } - - SaveCursor(); - - var row = Top + selectedItem % Rows; - var col = ColumnWidth * (selectedItem / Rows); - - console.SetCursorPosition(col, row); - - if (select) console.Write(Singleton.Options._selectionColor); - console.Write(GetMenuItem(listItem, ColumnWidth)); - if (select) console.Write("\x1b[0m"); - - ToolTipLines = 0; - if (showTooltips) - { - Debug.Assert(select, "On unselect, caller must clear the tooltip"); - console.SetCursorPosition(0, Top + Rows - 1); - // Move down 2 so we have 1 blank line between the menu and buffer. - AdjustForPossibleScroll(toolTipLines); - MoveCursorDown(2); - console.Write(toolTipColor); - console.Write(toolTip); - ToolTipLines = toolTipLines; - - console.Write("\x1b[0m"); - } - - RestoreCursor(); - }*/ - - /* public void MoveRight() => CurrentSelection = Math.Min(CurrentSelection + Rows, HelpItems.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, HelpItems.Count - 1); - public void MovePageDown() => CurrentSelection = Math.Min(CurrentSelection + Rows - (CurrentSelection % Rows) - 1, - HelpItems.Count - 1); - public void MovePageUp() => CurrentSelection = Math.Max(CurrentSelection - (CurrentSelection % Rows), 0); - - public void MoveN(int n) - { - CurrentSelection = (CurrentSelection + n) % MenuItems.Count; - if (CurrentSelection < 0) - { - 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; - } - } - - public void WriteBlankLines(int top, int count) - { - SaveCursor(); - Singleton._console.SetCursorPosition(0, top); - Singleton.WriteBlankLines(count); - RestoreCursor(); - } - - 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 class Menu { internal PSConsoleReadLine Singleton; diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs new file mode 100644 index 000000000..6fc2f3c6a --- /dev/null +++ b/PSReadLine/DynamicHelp.cs @@ -0,0 +1,426 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; + +namespace Microsoft.PowerShell +{ + public partial class PSConsoleReadLine + { + private Pager _pager; + private static System.Management.Automation.PowerShell _ps; + + /// + /// Attempt to show help content. + /// Show the full help for the command on the alternate screen buffer. + /// + public static void DynamicHelpFullHelp(ConsoleKeyInfo? key = null, object arg = null) + { + _singleton.DynamicHelpImpl(isFullHelp: true); + } + + /// + /// Attempt to show help content. + /// Show the short help of the parameter next to the cursor. + /// + public static void DynamicHelpParameter(ConsoleKeyInfo? key = null, object arg = null) + { + _singleton.DynamicHelpImpl(isFullHelp: false); + } + + private void DynamicHelpImpl(bool isFullHelp) + { + _pager ??= new Pager(); + + if (InViInsertMode()) // must close out the current edit group before engaging menu completion + { + ViCommandMode(); + ViInsertWithAppend(); + } + + int cursor = _singleton._current; + string commandName = null; + string parameterName = null; + + foreach(var token in _singleton._tokens) + { + if (token.TokenFlags == TokenFlags.CommandName) + { + commandName = token.Extent.Text; + } + + var extent = token.Extent; + if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) + { + if (token.Kind == TokenKind.Parameter) + { + parameterName = ((ParameterToken)token).ParameterName; + + } + // Possible corner case here when cursor is at the end + } + } + + if (!String.IsNullOrEmpty(commandName) && isFullHelp) + { + _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + _ps.Commands.Clear(); + + var fullHelp = _ps + .AddCommand($"Microsoft.PowerShell.Core\\Get-Help") + .AddParameter("Name", commandName) + .AddParameter("Full", value: true) + .AddCommand($"Microsoft.PowerShell.Utility\\Out-String") + .Invoke() + .FirstOrDefault(); + + if (!String.Equals(fullHelp, String.Empty, StringComparison.OrdinalIgnoreCase)) + { + string regexPatternToScrollTo = null; + + if (!String.IsNullOrEmpty(parameterName)) + { + string upper = parameterName[0].ToString().ToUpperInvariant(); + string lower = parameterName[0].ToString().ToLowerInvariant(); + string remainingString = parameterName.Substring(1); + regexPatternToScrollTo = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; + } + + _pager.Write(fullHelp, regexPatternToScrollTo); + } + } + else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) + { + _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + _ps.Commands.Clear(); + + PSObject paramHelp = _ps + .AddCommand("Microsoft.PowerShell.Core\\Get-Help") + .AddParameter("Name", commandName) + .AddParameter("Parameter", parameterName) + .Invoke() + .FirstOrDefault(); + + WriteParameterHelp(paramHelp); + } + } + + private void WriteDynamicHelpBlock(Collection helpBlock) + { + var bufferWidth = _console.BufferWidth; + var colWidth = Math.Min(helpBlock.Max(s => LengthInBufferCells(s)) + 2, bufferWidth); + int columns = 1; + + var dynHelp = new DynamicHelp + { + Singleton = this, + ColumnWidth = colWidth, + Columns = columns, + Rows = (helpBlock.Count + columns - 1) / columns, + HelpItems = helpBlock + }; + + dynHelp.SaveCursor(); + dynHelp.DrawHelpBlock(dynHelp); + + Console.ReadKey(intercept: true); + + dynHelp.Clear(); + dynHelp.RestoreCursor(); + } + + private void WriteParameterHelp(dynamic helpContent) + { + Collection helpBlock; + + if (helpContent == null || String.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) + { + helpBlock = new Collection() + { + String.Empty, + PSReadLineResources.NeedsUpdateHelp + }; + } + else + { + char c = (char)0x1b; + + string syntax = $"{c}[7m-{helpContent.name} <{helpContent.type.name}>{c}[0m"; + string desc = "DESC: " + helpContent.Description[0].Text; + 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 DynamicHelp + { + 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 HelpItems; + //internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; + internal int CurrentSelection; + + public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) + { + IConsole console = Singleton._console; + + // 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 columnWidth = this.ColumnWidth; + + var items = this.HelpItems; + for (var row = 0; row < this.Rows; row++) + { + var cells = 0; + for (var col = 0; col < this.Columns; col++) + { + var index = row + (this.Rows * col); + if (index >= items.Count) + { + break; + } + console.Write(GetHelpItem(items[index], columnWidth)); + cells += columnWidth; + } + + // Make sure we always write out exactly 1 buffer width to erase anything + // from a previous menu. + if (cells < bufferWidth) + { + // 'BlankRestOfLine' erases rest of the current line, but the cursor is not moved. + console.BlankRestOfLine(); + } + + // Explicit newline so consoles see each row as distinct lines, but skip the + // last line so we don't scroll. + if (row != (this.Rows - 1) || !menuSelect) + { + AdjustForPossibleScroll(1); + MoveCursorDown(1); + } + } + + 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 (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(previousMenu.Rows + previousMenu.ToolTipLines - Rows); + } + } + + // if the menu has moved, we need to clear the lines under it + if (bufferEndPoint.Y < PreviousTop) + { + console.BlankRestOfLine(); + Singleton.WriteBlankLines(PreviousTop - bufferEndPoint.Y); + } + + PreviousTop = bufferEndPoint.Y; + + if (menuSelect) + { + RestoreCursor(); + console.CursorVisible = true; + } + } + + public void Clear() + { + WriteBlankLines(Top, Rows + ToolTipLines); + } + + /* public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips, string toolTipColor) + { + var console = Singleton._console; + var menuItem = MenuItems[selectedItem]; + var listItem = menuItem.ListItemText; + + string toolTip = null; + if (showTooltips) + { + toolTip = menuItem.ToolTip.Trim(); + + // Don't bother showing the tooltip if it doesn't add information. + showTooltips = !string.IsNullOrWhiteSpace(toolTip) + && !string.Equals(toolTip, listItem, StringComparison.OrdinalIgnoreCase) + && !string.Equals(toolTip, menuItem.CompletionText, StringComparison.OrdinalIgnoreCase); + } + + // We'll use one blank line to set the tooltip apart from the menu, + // and there will be at least 1 line in the tooltip, possibly more. + var toolTipLines = 2; + if (showTooltips) + { + // Determine if showing the tooltip would scroll the top of our buffer off the screen. + + int lineLength = 0; + for (var i = 0; i < toolTip.Length; i++) + { + char c = toolTip[i]; + if (c == '\r' && i < toolTip.Length && toolTip[i+1] == '\n') + { + // Skip the newline, but handle LF, CRLF, and CR. + i += 1; + } + + if (c == '\r' || c == '\n') + { + toolTipLines += 1; + lineLength = 0; + } + else + { + lineLength += 1; + if (lineLength == console.BufferWidth) + { + toolTipLines += 1; + lineLength = 0; + } + } + } + + // The +1 is for the blank line between the menu and tooltips. + if (BufferLines + Rows + toolTipLines + 1 > console.WindowHeight) + { + showTooltips = false; + } + } + + SaveCursor(); + + var row = Top + selectedItem % Rows; + var col = ColumnWidth * (selectedItem / Rows); + + console.SetCursorPosition(col, row); + + if (select) console.Write(Singleton.Options._selectionColor); + console.Write(GetMenuItem(listItem, ColumnWidth)); + if (select) console.Write("\x1b[0m"); + + ToolTipLines = 0; + if (showTooltips) + { + Debug.Assert(select, "On unselect, caller must clear the tooltip"); + console.SetCursorPosition(0, Top + Rows - 1); + // Move down 2 so we have 1 blank line between the menu and buffer. + AdjustForPossibleScroll(toolTipLines); + MoveCursorDown(2); + console.Write(toolTipColor); + console.Write(toolTip); + ToolTipLines = toolTipLines; + + console.Write("\x1b[0m"); + } + + RestoreCursor(); + }*/ + + /* public void MoveRight() => CurrentSelection = Math.Min(CurrentSelection + Rows, HelpItems.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, HelpItems.Count - 1); + public void MovePageDown() => CurrentSelection = Math.Min(CurrentSelection + Rows - (CurrentSelection % Rows) - 1, + HelpItems.Count - 1); + public void MovePageUp() => CurrentSelection = Math.Max(CurrentSelection - (CurrentSelection % Rows), 0); + + public void MoveN(int n) + { + CurrentSelection = (CurrentSelection + n) % MenuItems.Count; + if (CurrentSelection < 0) + { + 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; + } + } + + public void WriteBlankLines(int top, int count) + { + SaveCursor(); + Singleton._console.SetCursorPosition(0, top); + Singleton.WriteBlankLines(count); + RestoreCursor(); + } + + 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/KeyBindings.cs b/PSReadLine/KeyBindings.cs index bc5d66464..5adc9cbe8 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -252,8 +252,8 @@ void SetDefaultWindowsBindings() _dispatchTable.Add(Keys.CtrlPageUp, MakeKeyHandler(ScrollDisplayUpLine, "ScrollDisplayUpLine")); _dispatchTable.Add(Keys.CtrlPageDown, MakeKeyHandler(ScrollDisplayDownLine, "ScrollDisplayDownLine")); - _dispatchTable.Add(Keys.AltH, MakeKeyHandler(DynamicHelpBlock, "DynamicHelpBlock", "Long desc")); - _dispatchTable.Add(Keys.F1, MakeKeyHandler(DynamicHelpBlock, "DynamicHelpBlock")); + _dispatchTable.Add(Keys.AltH, MakeKeyHandler(DynamicHelpParameter, "DynamicHelpParameter")); + _dispatchTable.Add(Keys.F1, MakeKeyHandler(DynamicHelpFullHelp, "DynamicHelpFullHelp")); } _chordDispatchTable = new Dictionary>(); diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 281e926b6..1fc5c2443 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -16,6 +16,7 @@ + diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index d26a5e4b4..a5409a684 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2067,5 +2067,14 @@ internal static string YankPopDescription { return ResourceManager.GetString("YankPopDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to No help content available. Please use Update-Help. + /// + internal static string NeedsUpdateHelp { + get { + return ResourceManager.GetString("NeedsUpdateHelp", resourceCulture); + } + } } } From cb021240424502319687bef261c52561f990d40d Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 17 Nov 2020 14:38:08 -0800 Subject: [PATCH 04/20] Refactor --- PSReadLine/Completion.cs | 17 ------------- PSReadLine/DynamicHelp.cs | 47 ++++++++++++++++++++++++++++++------ PSReadLine/PSReadLine.csproj | 5 +++- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index b2105e1cc..0a5ce8334 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -11,7 +11,6 @@ using System.Globalization; using System.Linq; using System.Management.Automation; -using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; using Microsoft.PowerShell.PSReadLine; @@ -838,22 +837,6 @@ private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSel } } - private static string GetHelpItem(string item, int columnWidth) - { - item = HandleNewlinesForPossibleCompletions(item); - var spacesNeeded = columnWidth - LengthInBufferCells(item); - if (spacesNeeded > 0) - { - item = item + Spaces(spacesNeeded); - } - else if (spacesNeeded < 0) - { - item = SubstringByCells(item, columnWidth - 3) + "..."; - } - - return item; - } - private static string GetMenuItem(string item, int columnWidth) { item = HandleNewlinesForPossibleCompletions(item); diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 6fc2f3c6a..3ab066d6f 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -3,24 +3,20 @@ --********************************************************************/ using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; using Microsoft.PowerShell.PSReadLine; +using Microsoft.PowerShell; namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { - private Pager _pager; + private Microsoft.PowerShell.Pager _pager; private static System.Management.Automation.PowerShell _ps; /// @@ -175,6 +171,22 @@ private void WriteParameterHelp(dynamic helpContent) WriteDynamicHelpBlock(helpBlock); } + private static string GetHelpItem(string item, int columnWidth) + { + item = HandleNewlinesForPossibleCompletions(item); + var spacesNeeded = columnWidth - LengthInBufferCells(item); + if (spacesNeeded > 0) + { + item = item + Spaces(spacesNeeded); + } + else if (spacesNeeded < 0) + { + item = SubstringByCells(item, columnWidth - 3) + "..."; + } + + return item; + } + private class DynamicHelp { internal PSConsoleReadLine Singleton; @@ -239,11 +251,18 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) } } + 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, @@ -252,13 +271,25 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) } 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); } diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 1fc5c2443..799b656cf 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -16,7 +16,6 @@ - @@ -25,6 +24,10 @@ + + + + From 9bef595afa9e177dc5eb32ef783cf524aac08f81 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 19 Nov 2020 07:22:12 -0800 Subject: [PATCH 05/20] Refactor helper methods to parent class --- PSReadLine/Completion.cs | 42 +------ PSReadLine/DynamicHelp.cs | 171 ++-------------------------- PSReadLine/MultilineDisplayBlock.cs | 79 +++++++++++++ 3 files changed, 87 insertions(+), 205 deletions(-) create mode 100644 PSReadLine/MultilineDisplayBlock.cs diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 0a5ce8334..9d0a6b5b4 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -422,16 +422,9 @@ private static string HandleNewlinesForPossibleCompletions(string s) return s; } - private class Menu + private class Menu : MultilineDisplayBlock { - 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]; @@ -701,39 +694,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/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 3ab066d6f..328b4226e 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -171,7 +171,7 @@ private void WriteParameterHelp(dynamic helpContent) WriteDynamicHelpBlock(helpBlock); } - private static string GetHelpItem(string item, int columnWidth) + /* private static string GetHelpItem(string item, int columnWidth) { item = HandleNewlinesForPossibleCompletions(item); var spacesNeeded = columnWidth - LengthInBufferCells(item); @@ -185,22 +185,11 @@ private static string GetHelpItem(string item, int columnWidth) } return item; - } + } */ - private class DynamicHelp + private class DynamicHelp : MultilineDisplayBlock { - 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 HelpItems; - //internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; - internal int CurrentSelection; public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) { @@ -230,7 +219,7 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) { break; } - console.Write(GetHelpItem(items[index], columnWidth)); + console.Write(GetItem(items[index], columnWidth)); cells += columnWidth; } @@ -254,7 +243,7 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) bool extraPreRowsCleared = false; if (previousMenu != null) { - if (Rows < previousMenu.Rows + previousMenu.ToolTipLines) + if (Rows < previousMenu.Rows) { // 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 @@ -270,7 +259,7 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) MoveCursorDown(1); } - Singleton.WriteBlankLines(previousMenu.Rows + previousMenu.ToolTipLines - Rows); + Singleton.WriteBlankLines(previousMenu.Rows - Rows); extraPreRowsCleared = true; } } @@ -304,154 +293,8 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) public void Clear() { - WriteBlankLines(Top, Rows + ToolTipLines); + WriteBlankLines(Top, Rows); } - - /* public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips, string toolTipColor) - { - var console = Singleton._console; - var menuItem = MenuItems[selectedItem]; - var listItem = menuItem.ListItemText; - - string toolTip = null; - if (showTooltips) - { - toolTip = menuItem.ToolTip.Trim(); - - // Don't bother showing the tooltip if it doesn't add information. - showTooltips = !string.IsNullOrWhiteSpace(toolTip) - && !string.Equals(toolTip, listItem, StringComparison.OrdinalIgnoreCase) - && !string.Equals(toolTip, menuItem.CompletionText, StringComparison.OrdinalIgnoreCase); - } - - // We'll use one blank line to set the tooltip apart from the menu, - // and there will be at least 1 line in the tooltip, possibly more. - var toolTipLines = 2; - if (showTooltips) - { - // Determine if showing the tooltip would scroll the top of our buffer off the screen. - - int lineLength = 0; - for (var i = 0; i < toolTip.Length; i++) - { - char c = toolTip[i]; - if (c == '\r' && i < toolTip.Length && toolTip[i+1] == '\n') - { - // Skip the newline, but handle LF, CRLF, and CR. - i += 1; - } - - if (c == '\r' || c == '\n') - { - toolTipLines += 1; - lineLength = 0; - } - else - { - lineLength += 1; - if (lineLength == console.BufferWidth) - { - toolTipLines += 1; - lineLength = 0; - } - } - } - - // The +1 is for the blank line between the menu and tooltips. - if (BufferLines + Rows + toolTipLines + 1 > console.WindowHeight) - { - showTooltips = false; - } - } - - SaveCursor(); - - var row = Top + selectedItem % Rows; - var col = ColumnWidth * (selectedItem / Rows); - - console.SetCursorPosition(col, row); - - if (select) console.Write(Singleton.Options._selectionColor); - console.Write(GetMenuItem(listItem, ColumnWidth)); - if (select) console.Write("\x1b[0m"); - - ToolTipLines = 0; - if (showTooltips) - { - Debug.Assert(select, "On unselect, caller must clear the tooltip"); - console.SetCursorPosition(0, Top + Rows - 1); - // Move down 2 so we have 1 blank line between the menu and buffer. - AdjustForPossibleScroll(toolTipLines); - MoveCursorDown(2); - console.Write(toolTipColor); - console.Write(toolTip); - ToolTipLines = toolTipLines; - - console.Write("\x1b[0m"); - } - - RestoreCursor(); - }*/ - - /* public void MoveRight() => CurrentSelection = Math.Min(CurrentSelection + Rows, HelpItems.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, HelpItems.Count - 1); - public void MovePageDown() => CurrentSelection = Math.Min(CurrentSelection + Rows - (CurrentSelection % Rows) - 1, - HelpItems.Count - 1); - public void MovePageUp() => CurrentSelection = Math.Max(CurrentSelection - (CurrentSelection % Rows), 0); - - public void MoveN(int n) - { - CurrentSelection = (CurrentSelection + n) % MenuItems.Count; - if (CurrentSelection < 0) - { - 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; - } - } - - public void WriteBlankLines(int top, int count) - { - SaveCursor(); - Singleton._console.SetCursorPosition(0, top); - Singleton.WriteBlankLines(count); - RestoreCursor(); - } - - 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/MultilineDisplayBlock.cs b/PSReadLine/MultilineDisplayBlock.cs new file mode 100644 index 000000000..6cf5566e1 --- /dev/null +++ b/PSReadLine/MultilineDisplayBlock.cs @@ -0,0 +1,79 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using Microsoft.PowerShell.Internal; + +namespace Microsoft.PowerShell +{ + public partial class PSConsoleReadLine + { + private class MultilineDisplayBlock + { + internal PSConsoleReadLine Singleton; + internal int Top; + internal int PreviousTop; + internal int ColumnWidth; + internal int Rows; + internal int Columns; + + private protected void MoveCursorDown(int cnt) + { + IConsole console = Singleton._console; + while (cnt-- > 0) + { + console.Write("\n"); + } + } + + private 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); + + private protected void WriteBlankLines(int top, int count) + { + SaveCursor(); + Singleton._console.SetCursorPosition(0, top); + Singleton.WriteBlankLines(count); + RestoreCursor(); + } + + private protected static string GetItem(string item, int columnWidth) + { + item = HandleNewlinesForPossibleCompletions(item); + var spacesNeeded = columnWidth - LengthInBufferCells(item); + if (spacesNeeded > 0) + { + item = item + Spaces(spacesNeeded); + } + else if (spacesNeeded < 0) + { + item = SubstringByCells(item, columnWidth - 3) + "..."; + } + + return item; + } + } + } +} \ No newline at end of file From f9a5920bd7061a6af5e57a0a30dcc1d86dfbf70e Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 19 Nov 2020 10:22:52 -0800 Subject: [PATCH 06/20] Add resource string for missing help content --- PSReadLine/PSReadLineResources.resx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 5cf93c530..da3c56f1f 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -829,4 +829,7 @@ 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. + From 66db4697c6cce5a57aa822c514deea6ed24e635f Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 19 Nov 2020 13:34:31 -0800 Subject: [PATCH 07/20] Address feedback --- PSReadLine/DynamicHelp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 328b4226e..843c31b74 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -55,7 +55,7 @@ private void DynamicHelpImpl(bool isFullHelp) { if (token.TokenFlags == TokenFlags.CommandName) { - commandName = token.Extent.Text; + commandName = token.Text; } var extent = token.Extent; From 3430b9d8720c19ea417ede7feffb99aab7c818ce Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 19 Nov 2020 13:35:58 -0800 Subject: [PATCH 08/20] Added empty line at the end of file --- PSReadLine/MultilineDisplayBlock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/MultilineDisplayBlock.cs b/PSReadLine/MultilineDisplayBlock.cs index 6cf5566e1..f81f7962e 100644 --- a/PSReadLine/MultilineDisplayBlock.cs +++ b/PSReadLine/MultilineDisplayBlock.cs @@ -76,4 +76,4 @@ private protected static string GetItem(string item, int columnWidth) } } } -} \ No newline at end of file +} From 4d9cb1ebd8c6c8a77af099e899dd1afe5e7d104a Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 20 Nov 2020 10:52:03 -0800 Subject: [PATCH 09/20] More refactoring --- PSReadLine/Completion.cs | 2 +- ...ineDisplayBlock.cs => DisplayBlockBase.cs} | 2 +- PSReadLine/DynamicHelp.cs | 96 ++++++++----------- 3 files changed, 44 insertions(+), 56 deletions(-) rename PSReadLine/{MultilineDisplayBlock.cs => DisplayBlockBase.cs} (98%) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 9d0a6b5b4..321a346b6 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -422,7 +422,7 @@ private static string HandleNewlinesForPossibleCompletions(string s) return s; } - private class Menu : MultilineDisplayBlock + private class Menu : DisplayBlockBase { internal int BufferLines; internal int ToolTipLines; diff --git a/PSReadLine/MultilineDisplayBlock.cs b/PSReadLine/DisplayBlockBase.cs similarity index 98% rename from PSReadLine/MultilineDisplayBlock.cs rename to PSReadLine/DisplayBlockBase.cs index f81f7962e..48b548ed6 100644 --- a/PSReadLine/MultilineDisplayBlock.cs +++ b/PSReadLine/DisplayBlockBase.cs @@ -9,7 +9,7 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { - private class MultilineDisplayBlock + private class DisplayBlockBase { internal PSConsoleReadLine Singleton; internal int Top; diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 843c31b74..dcd7dc5eb 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -37,39 +37,8 @@ public static void DynamicHelpParameter(ConsoleKeyInfo? key = null, object arg = _singleton.DynamicHelpImpl(isFullHelp: false); } - private void DynamicHelpImpl(bool isFullHelp) + void IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { - _pager ??= new Pager(); - - if (InViInsertMode()) // must close out the current edit group before engaging menu completion - { - ViCommandMode(); - ViInsertWithAppend(); - } - - int cursor = _singleton._current; - string commandName = null; - string parameterName = null; - - foreach(var token in _singleton._tokens) - { - if (token.TokenFlags == TokenFlags.CommandName) - { - commandName = token.Text; - } - - var extent = token.Extent; - if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) - { - if (token.Kind == TokenKind.Parameter) - { - parameterName = ((ParameterToken)token).ParameterName; - - } - // Possible corner case here when cursor is at the end - } - } - if (!String.IsNullOrEmpty(commandName) && isFullHelp) { _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); @@ -114,13 +83,48 @@ private void DynamicHelpImpl(bool isFullHelp) } } + private void DynamicHelpImpl(bool isFullHelp) + { + _pager ??= new Pager(); + + if (InViInsertMode()) // must close out the current edit group before engaging menu completion + { + ViCommandMode(); + ViInsertWithAppend(); + } + + int cursor = _singleton._current; + string commandName = null; + string parameterName = null; + + foreach(var token in _singleton._tokens) + { + if (token.TokenFlags == TokenFlags.CommandName) + { + commandName = token.Text; + } + + var extent = token.Extent; + if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) + { + if (token.Kind == TokenKind.Parameter) + { + parameterName = ((ParameterToken)token).ParameterName; + break; + } + } + } + + _mockableMethods.GetDynamicHelpContent(commandName, parameterName, isFullHelp); + } + private void WriteDynamicHelpBlock(Collection helpBlock) { var bufferWidth = _console.BufferWidth; var colWidth = Math.Min(helpBlock.Max(s => LengthInBufferCells(s)) + 2, bufferWidth); int columns = 1; - var dynHelp = new DynamicHelp + var dynHelp = new MultilineDisplayBlock { Singleton = this, ColumnWidth = colWidth, @@ -171,27 +175,11 @@ private void WriteParameterHelp(dynamic helpContent) WriteDynamicHelpBlock(helpBlock); } - /* private static string GetHelpItem(string item, int columnWidth) - { - item = HandleNewlinesForPossibleCompletions(item); - var spacesNeeded = columnWidth - LengthInBufferCells(item); - if (spacesNeeded > 0) - { - item = item + Spaces(spacesNeeded); - } - else if (spacesNeeded < 0) - { - item = SubstringByCells(item, columnWidth - 3) + "..."; - } - - return item; - } */ - - private class DynamicHelp : MultilineDisplayBlock + private class MultilineDisplayBlock : DisplayBlockBase { internal Collection HelpItems; - public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) + public void DrawHelpBlock(MultilineDisplayBlock previewBlock, bool menuSelect = true) { IConsole console = Singleton._console; @@ -241,9 +229,9 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) } bool extraPreRowsCleared = false; - if (previousMenu != null) + if (previewBlock != null) { - if (Rows < previousMenu.Rows) + if (Rows < previewBlock.Rows) { // 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 @@ -259,7 +247,7 @@ public void DrawHelpBlock(DynamicHelp previousMenu, bool menuSelect = true) MoveCursorDown(1); } - Singleton.WriteBlankLines(previousMenu.Rows - Rows); + Singleton.WriteBlankLines(previewBlock.Rows - Rows); extraPreRowsCleared = true; } } From bc42a76a590538940968008548f69023c7f7bf0f Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 20 Nov 2020 10:53:25 -0800 Subject: [PATCH 10/20] Add mockable API --- PSReadLine/PublicAPI.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index e7a140a23..527cd3c32 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -29,6 +29,7 @@ public interface IPSConsoleReadLineMockableMethods Task> PredictInput(Ast ast, Token[] tokens); void OnCommandLineAccepted(IReadOnlyList history); void OnSuggestionAccepted(Guid predictorId, string suggestionText); + void GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); } [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")] From 1ddf423e0b0d7234adbd5f2a01387d96054ae2a6 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 24 Nov 2020 18:54:56 -0800 Subject: [PATCH 11/20] Partial tests --- PSReadLine/DynamicHelp.cs | 52 ++++++++++++------ PSReadLine/PSReadLine.csproj | 2 +- PSReadLine/PublicAPI.cs | 3 +- test/DynamicHelpTest.cs | 103 +++++++++++++++++++++++++++++++++++ test/UnitTestReadLine.cs | 10 ++++ 5 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 test/DynamicHelpTest.cs diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index dcd7dc5eb..0462725f7 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -37,22 +37,49 @@ public static void DynamicHelpParameter(ConsoleKeyInfo? key = null, object arg = _singleton.DynamicHelpImpl(isFullHelp: false); } - void IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { if (!String.IsNullOrEmpty(commandName) && isFullHelp) { _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); _ps.Commands.Clear(); - var fullHelp = _ps + return _ps .AddCommand($"Microsoft.PowerShell.Core\\Get-Help") .AddParameter("Name", commandName) .AddParameter("Full", value: true) .AddCommand($"Microsoft.PowerShell.Utility\\Out-String") .Invoke() .FirstOrDefault(); + } + else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName) && !isFullHelp) + { + _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + _ps.Commands.Clear(); + + return _ps + .AddCommand("Microsoft.PowerShell.Core\\Get-Help") + .AddParameter("Name", commandName) + .AddParameter("Parameter", parameterName) + .Invoke() + .FirstOrDefault(); + } + + return null; + } + + void IPSConsoleReadLineMockableMethods.WriteToPager(string content, string regexPatternToScrollTo) + { + _pager.Write(content, regexPatternToScrollTo); + } - if (!String.Equals(fullHelp, String.Empty, StringComparison.OrdinalIgnoreCase)) + private void WriteDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + { + var helpContent = _mockableMethods.GetDynamicHelpContent(commandName, parameterName, isFullHelp); + + if (!String.IsNullOrEmpty(commandName) && isFullHelp) + { + if (helpContent is String fullHelp && !String.IsNullOrEmpty(fullHelp)) { string regexPatternToScrollTo = null; @@ -64,22 +91,15 @@ void IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, regexPatternToScrollTo = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; } - _pager.Write(fullHelp, regexPatternToScrollTo); + _mockableMethods.WriteToPager(fullHelp, regexPatternToScrollTo); } } else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) { - _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - _ps.Commands.Clear(); - - PSObject paramHelp = _ps - .AddCommand("Microsoft.PowerShell.Core\\Get-Help") - .AddParameter("Name", commandName) - .AddParameter("Parameter", parameterName) - .Invoke() - .FirstOrDefault(); - - WriteParameterHelp(paramHelp); + if (helpContent is PSObject paramHelp) + { + WriteParameterHelp(paramHelp); + } } } @@ -115,7 +135,7 @@ private void DynamicHelpImpl(bool isFullHelp) } } - _mockableMethods.GetDynamicHelpContent(commandName, parameterName, isFullHelp); + WriteDynamicHelpContent(commandName, parameterName, isFullHelp); } private void WriteDynamicHelpBlock(Collection helpBlock) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 799b656cf..e151f6ad3 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -25,7 +25,7 @@ - + diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index 527cd3c32..fffc5c177 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -29,7 +29,8 @@ public interface IPSConsoleReadLineMockableMethods Task> PredictInput(Ast ast, Token[] tokens); void OnCommandLineAccepted(IReadOnlyList history); void OnSuggestionAccepted(Guid predictorId, string suggestionText); - void GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); + object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); + void WriteToPager(string content, string regexPatternToScrollTo); } [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")] diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs new file mode 100644 index 000000000..40a6f9cf6 --- /dev/null +++ b/test/DynamicHelpTest.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; +using System.Reflection; +using System.Text; +using Microsoft.PowerShell; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + private static StringBuilder actualPagerContent = new StringBuilder(); + 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 +"; + + private static readonly string paramHelp = @" + +-Date + +DESC: Specifies a date and time. Time is optional and if not specified, returns 00:00:00. +Required: false, Position: 0, Default Value: None, Pipeline Input: True (ByPropertyName, ByValue), WildCard: false +"; + + internal static object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) + { + string descText = @"Specifies a date and time. Time is optional and if not specified, returns 00:00:00."; + + if (!String.IsNullOrEmpty(commandName) && isFullHelp) + { + return fullHelp; + } + else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) + { + PSObject paramHelp = new PSObject(); + + PSObject[] descDetails = new PSObject[1]; + descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); + + paramHelp.Members.Add(new PSNoteProperty("Description", descDetails)); + paramHelp.Members.Add(new PSNoteProperty("Name", "Date")); + paramHelp.Members.Add(new PSNoteProperty("type", new PSNoteProperty("name", "System.Datetime"))); + 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; + } + + return null; + } + + internal static void WriteToPager(string content, string regexPatternToScrollTo) + { + actualPagerContent.Clear(); + actualPagerContent.Append(content); + } + + [SkippableFact] + public void DynHelp_GetFullHelp() + { + TestSetup(KeyMode.Cmd); + Test("Get-Date", Keys( + "Get-Date", + _.F1, + CheckThat(() => AssertScreenIs(18, TokenClassification.String, fullHelp)) + )); + } + + [SkippableFact] + public void DynHelp_GetParameterHelp() + { + TestSetup(KeyMode.Cmd); + Test("Get-Date", Keys( + "Get-Date -Date", + _.Alt_h, + CheckThat(() => AssertScreenIs(6, TokenClassification.String, paramHelp)) + )); + } + } +} \ No newline at end of file diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 7e9cb8803..1e094e700 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -67,6 +67,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 WriteToPager(string content, string regexPatternToScrollTo) + { + + } } public enum TokenClassification From def4ca31ed444105fce674542a0a5d277df19d13 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 24 Nov 2020 18:56:36 -0800 Subject: [PATCH 12/20] Move to PowerShell SDK 7.1.0 --- MockPSConsole/MockPSConsole.csproj | 2 +- PSReadLine/PSReadLine.csproj | 4 ++-- Polyfill/Polyfill.csproj | 2 +- test/PSReadLine.Tests.csproj | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index e151f6ad3..2ef437990 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -21,11 +21,11 @@ - + - + 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/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 @@ - + From 4a09baf9aa5125cd9e645f6c2ec0e63362233713 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 4 Dec 2020 11:13:59 -0800 Subject: [PATCH 13/20] Refactor and add tests --- PSReadLine/Completion.cs | 6 +++ PSReadLine/DisplayBlockBase.cs | 8 +-- PSReadLine/DynamicHelp.cs | 89 +++++++++++++--------------------- PSReadLine/PSReadLine.csproj | 2 +- test/DynamicHelpTest.cs | 73 +++++++++++++++++++--------- test/UnitTestReadLine.cs | 2 +- 6 files changed, 96 insertions(+), 84 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 321a346b6..8754d305b 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -426,10 +426,16 @@ private class Menu : DisplayBlockBase { internal int BufferLines; internal int ToolTipLines; + internal int PreviousTop; + internal int ColumnWidth; + internal int Rows; + internal int Columns; + internal Collection MenuItems; internal CompletionResult CurrentMenuItem => MenuItems[CurrentSelection]; internal int CurrentSelection; + public void DrawMenu(Menu previousMenu, bool menuSelect) { IConsole console = Singleton._console; diff --git a/PSReadLine/DisplayBlockBase.cs b/PSReadLine/DisplayBlockBase.cs index 48b548ed6..f54660850 100644 --- a/PSReadLine/DisplayBlockBase.cs +++ b/PSReadLine/DisplayBlockBase.cs @@ -13,10 +13,10 @@ private class DisplayBlockBase { internal PSConsoleReadLine Singleton; internal int Top; - internal int PreviousTop; - internal int ColumnWidth; - internal int Rows; - internal int Columns; + //internal int PreviousTop; + //internal int ColumnWidth; + //internal int Rows; + //internal int Columns; private protected void MoveCursorDown(int cnt) { diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 0462725f7..160f9901d 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -19,6 +19,8 @@ public partial class PSConsoleReadLine private Microsoft.PowerShell.Pager _pager; private static System.Management.Automation.PowerShell _ps; + public static bool EnableDynHelpTestHook; + /// /// Attempt to show help content. /// Show the full help for the command on the alternate screen buffer. @@ -148,18 +150,15 @@ private void WriteDynamicHelpBlock(Collection helpBlock) { Singleton = this, ColumnWidth = colWidth, - Columns = columns, Rows = (helpBlock.Count + columns - 1) / columns, - HelpItems = helpBlock + ItemsToDisplay = helpBlock }; dynHelp.SaveCursor(); - dynHelp.DrawHelpBlock(dynHelp); - - Console.ReadKey(intercept: true); - - dynHelp.Clear(); + dynHelp.DrawMultilineBlock(dynHelp); dynHelp.RestoreCursor(); + ReadKey(); + dynHelp.Clear(); } private void WriteParameterHelp(dynamic helpContent) @@ -176,9 +175,7 @@ private void WriteParameterHelp(dynamic helpContent) } else { - char c = (char)0x1b; - - string syntax = $"{c}[7m-{helpContent.name} <{helpContent.type.name}>{c}[0m"; + string syntax = $"-{helpContent.name} <{helpContent.type.name}>"; string desc = "DESC: " + helpContent.Description[0].Text; string details = $"Required: {helpContent.required}, Position: {helpContent.position}, Default Value: {helpContent.defaultValue}, Pipeline Input: {helpContent.pipelineInput}, WildCard: {helpContent.globbing}"; @@ -197,12 +194,19 @@ private void WriteParameterHelp(dynamic helpContent) private class MultilineDisplayBlock : DisplayBlockBase { - internal Collection HelpItems; + internal Collection ItemsToDisplay; - public void DrawHelpBlock(MultilineDisplayBlock previewBlock, bool menuSelect = true) + internal int Rows; + internal int ColumnWidth; + + private int multilineItems = 0; + + public void DrawMultilineBlock(MultilineDisplayBlock multilineBlock) { IConsole console = Singleton._console; + multilineItems = 0; + // 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); @@ -216,21 +220,25 @@ public void DrawHelpBlock(MultilineDisplayBlock previewBlock, bool menuSelect = var bufferWidth = console.BufferWidth; var columnWidth = this.ColumnWidth; - var items = this.HelpItems; + var items = this.ItemsToDisplay; for (var row = 0; row < this.Rows; row++) { var cells = 0; - for (var col = 0; col < this.Columns; col++) + if (row >= items.Count) { - var index = row + (this.Rows * col); - if (index >= items.Count) - { - break; - } - console.Write(GetItem(items[index], columnWidth)); - cells += columnWidth; + break; + } + + var itemToWrite = items[row]; + + if(itemToWrite.Length > bufferWidth) + { + multilineItems++; } + console.Write(itemToWrite); + cells += columnWidth; + // Make sure we always write out exactly 1 buffer width to erase anything // from a previous menu. if (cells < bufferWidth) @@ -241,17 +249,16 @@ public void DrawHelpBlock(MultilineDisplayBlock previewBlock, bool menuSelect = // Explicit newline so consoles see each row as distinct lines, but skip the // last line so we don't scroll. - if (row != (this.Rows - 1) || !menuSelect) + if (row != (this.Rows - 1)) { AdjustForPossibleScroll(1); MoveCursorDown(1); } } - bool extraPreRowsCleared = false; - if (previewBlock != null) + if (multilineBlock != null) { - if (Rows < previewBlock.Rows) + if (Rows < multilineBlock.Rows) { // 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 @@ -267,41 +274,15 @@ public void DrawHelpBlock(MultilineDisplayBlock previewBlock, bool menuSelect = MoveCursorDown(1); } - Singleton.WriteBlankLines(previewBlock.Rows - Rows); - extraPreRowsCleared = true; + Singleton.WriteBlankLines(multilineBlock.Rows - Rows); } } - - // if the menu has moved, we need to clear the lines under it - if (bufferEndPoint.Y < PreviousTop) - { - // 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); - } - - PreviousTop = bufferEndPoint.Y; - - if (menuSelect) - { - RestoreCursor(); - console.CursorVisible = true; - } } public void Clear() { - WriteBlankLines(Top, Rows); + // Add 1 for the movement to next line before displaying block + WriteBlankLines(Top, Rows + multilineItems + 1); } } } diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 2ef437990..2dfd2a7e7 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -25,7 +25,7 @@ - + diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index 40a6f9cf6..c756e8e93 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -12,7 +12,8 @@ namespace Test { public partial class ReadLine { - private static StringBuilder actualPagerContent = new StringBuilder(); + private static string actualContent; + private static readonly string fullHelp = @" NAME @@ -33,15 +34,7 @@ Default value False Accept pipeline input? False Accept wildcard characters? false "; - - private static readonly string paramHelp = @" - --Date - -DESC: Specifies a date and time. Time is optional and if not specified, returns 00:00:00. -Required: false, Position: 0, Default Value: None, Pipeline Input: True (ByPropertyName, ByValue), WildCard: false -"; - + internal static object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { string descText = @"Specifies a date and time. Time is optional and if not specified, returns 00:00:00."; @@ -55,11 +48,17 @@ internal static object GetDynamicHelpContent(string commandName, string paramete PSObject paramHelp = new PSObject(); PSObject[] descDetails = new PSObject[1]; + descDetails[0] = new PSObject(); descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); + 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", new PSNoteProperty("name", "System.Datetime"))); + 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")); @@ -74,30 +73,56 @@ internal static object GetDynamicHelpContent(string commandName, string paramete internal static void WriteToPager(string content, string regexPatternToScrollTo) { - actualPagerContent.Clear(); - actualPagerContent.Append(content); + actualContent = content; + PSConsoleReadLine.ReadKey(); } [SkippableFact] public void DynHelp_GetFullHelp() { TestSetup(KeyMode.Cmd); + + _console.Clear(); + Test("Get-Date", Keys( - "Get-Date", - _.F1, - CheckThat(() => AssertScreenIs(18, TokenClassification.String, fullHelp)) - )); + "Get-Date", _.F1, + CheckThat(() => Assert.Equal(fullHelp, actualContent)), + _.Enter, + _.Enter + )); } [SkippableFact] public void DynHelp_GetParameterHelp() { - TestSetup(KeyMode.Cmd); - Test("Get-Date", Keys( - "Get-Date -Date", - _.Alt_h, - CheckThat(() => AssertScreenIs(6, TokenClassification.String, paramHelp)) - )); + PSConsoleReadLine.EnableDynHelpTestHook = true; + + try + { + TestSetup(KeyMode.Cmd, new KeyHandler("Alt+h", PSConsoleReadLine.DynamicHelpParameter)); + + _console.Clear(); + 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. Time is optional and if not", NextLine, " specified, returns 00:00:00.", NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.Enter, + _.Enter + )); + } + finally + { + PSConsoleReadLine.EnableDynHelpTestHook = false; + } } } -} \ No newline at end of file +} diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 1e094e700..0a4a2dcd9 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -75,7 +75,7 @@ public object GetDynamicHelpContent(string commandName, string parameterName, bo public void WriteToPager(string content, string regexPatternToScrollTo) { - + ReadLine.WriteToPager(content, regexPatternToScrollTo); } } From 65ff76594ff713f576934e0f6e720907271b1902 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 7 Dec 2020 09:50:39 -0800 Subject: [PATCH 14/20] Add tests for error message --- PSReadLine/DynamicHelp.cs | 2 +- test/DynamicHelpTest.cs | 82 +++++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 160f9901d..bf7d8695f 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -158,7 +158,7 @@ private void WriteDynamicHelpBlock(Collection helpBlock) dynHelp.DrawMultilineBlock(dynHelp); dynHelp.RestoreCursor(); ReadKey(); - dynHelp.Clear(); + dynHelp.Clear(); } private void WriteParameterHelp(dynamic helpContent) diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index c756e8e93..67853413d 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -12,8 +12,8 @@ namespace Test { public partial class ReadLine { - private static string actualContent; - + private static string actualContent; + private static readonly string fullHelp = @" NAME @@ -34,7 +34,7 @@ Default value False Accept pipeline input? False Accept wildcard characters? false "; - + internal static object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { string descText = @"Specifies a date and time. Time is optional and if not specified, returns 00:00:00."; @@ -47,23 +47,33 @@ internal static object GetDynamicHelpContent(string commandName, string paramete { PSObject paramHelp = new PSObject(); - PSObject[] descDetails = new PSObject[1]; - descDetails[0] = new PSObject(); - descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); - - 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")); + 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)) + { + PSObject[] descDetails = new PSObject[1]; + descDetails[0] = new PSObject(); + descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); + + 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; } @@ -113,7 +123,7 @@ public void DynHelp_GetParameterHelp() emptyLine, TokenClassification.None, $"-Date ", NextLine, emptyLine, - TokenClassification.None, "DESC: Specifies a date and time. Time is optional and if not", NextLine, " specified, returns 00:00:00.", NextLine, + TokenClassification.None, "DESC: Specifies a date and time. Time is optional and if not", NextLine, " specified, returns 00:00:00.", NextLine, TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), _.Enter, _.Enter @@ -124,5 +134,35 @@ public void DynHelp_GetParameterHelp() PSConsoleReadLine.EnableDynHelpTestHook = false; } } + + [SkippableFact] + public void DynHelp_GetParameterHelpErrorMessage() + { + PSConsoleReadLine.EnableDynHelpTestHook = true; + + try + { + TestSetup(KeyMode.Cmd, new KeyHandler("Alt+h", PSConsoleReadLine.DynamicHelpParameter)); + + _console.Clear(); + string emptyLine = new string(' ', _console.BufferWidth); + + Test("Get-FakeHelp -Fake", Keys( + "Get-FakeHelp -Fake", _.Alt_h, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "Get-FakeHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-Fake", NextLine, + emptyLine, + TokenClassification.None, "No help content available. Please use Update-Help.")), + _.Enter, + _.Enter + )); + } + finally + { + PSConsoleReadLine.EnableDynHelpTestHook = false; + } + } } } From 492bcbdde45910ad4830ac0125b0de2acd6fe2ba Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 8 Dec 2020 15:44:03 -0800 Subject: [PATCH 15/20] Address code review comments --- PSReadLine/Completion.cs | 5 ++--- PSReadLine/DisplayBlockBase.cs | 30 +----------------------------- PSReadLine/DynamicHelp.cs | 2 +- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 8754d305b..c7d9902af 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -424,18 +424,17 @@ private static string HandleNewlinesForPossibleCompletions(string s) private class Menu : DisplayBlockBase { - internal int BufferLines; - internal int ToolTipLines; 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; - public void DrawMenu(Menu previousMenu, bool menuSelect) { IConsole console = Singleton._console; diff --git a/PSReadLine/DisplayBlockBase.cs b/PSReadLine/DisplayBlockBase.cs index f54660850..f3f74ea88 100644 --- a/PSReadLine/DisplayBlockBase.cs +++ b/PSReadLine/DisplayBlockBase.cs @@ -13,10 +13,6 @@ private class DisplayBlockBase { internal PSConsoleReadLine Singleton; internal int Top; - //internal int PreviousTop; - //internal int ColumnWidth; - //internal int Rows; - //internal int Columns; private protected void MoveCursorDown(int cnt) { @@ -27,7 +23,7 @@ private protected void MoveCursorDown(int cnt) } } - private protected void AdjustForPossibleScroll(int cnt) + protected void AdjustForPossibleScroll(int cnt) { IConsole console = Singleton._console; var scrollCnt = console.CursorTop + cnt + 1 - console.BufferHeight; @@ -50,30 +46,6 @@ public void SaveCursor() } public void RestoreCursor() => Singleton._console.SetCursorPosition(_savedCursorLeft, _savedCursorTop); - - private protected void WriteBlankLines(int top, int count) - { - SaveCursor(); - Singleton._console.SetCursorPosition(0, top); - Singleton.WriteBlankLines(count); - RestoreCursor(); - } - - private protected static string GetItem(string item, int columnWidth) - { - item = HandleNewlinesForPossibleCompletions(item); - var spacesNeeded = columnWidth - LengthInBufferCells(item); - if (spacesNeeded > 0) - { - item = item + Spaces(spacesNeeded); - } - else if (spacesNeeded < 0) - { - item = SubstringByCells(item, columnWidth - 3) + "..."; - } - - return item; - } } } } diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index bf7d8695f..1beba573d 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -282,7 +282,7 @@ public void DrawMultilineBlock(MultilineDisplayBlock multilineBlock) public void Clear() { // Add 1 for the movement to next line before displaying block - WriteBlankLines(Top, Rows + multilineItems + 1); + _singleton.WriteBlankLines(Top, Rows + multilineItems + 1); } } } From 28acebfcbd906a5e5a3707d867d7d27346d42b17 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 10 Dec 2020 15:53:02 -0800 Subject: [PATCH 16/20] Address code review feedback --- PSReadLine/DynamicHelp.cs | 200 +++++++++++++--------------- PSReadLine/KeyBindings.cs | 7 +- PSReadLine/KeyBindings.vi.cs | 3 +- PSReadLine/PSReadLineResources.resx | 2 +- PSReadLine/PublicAPI.cs | 2 +- test/DynamicHelpTest.cs | 174 +++++++++++------------- test/UnitTestReadLine.cs | 5 +- 7 files changed, 181 insertions(+), 212 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 1beba573d..7671c0ca3 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -17,15 +17,12 @@ namespace Microsoft.PowerShell public partial class PSConsoleReadLine { private Microsoft.PowerShell.Pager _pager; - private static System.Management.Automation.PowerShell _ps; - - public static bool EnableDynHelpTestHook; /// /// Attempt to show help content. /// Show the full help for the command on the alternate screen buffer. /// - public static void DynamicHelpFullHelp(ConsoleKeyInfo? key = null, object arg = null) + public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null) { _singleton.DynamicHelpImpl(isFullHelp: true); } @@ -34,43 +31,71 @@ public static void DynamicHelpFullHelp(ConsoleKeyInfo? key = null, object arg = /// Attempt to show help content. /// Show the short help of the parameter next to the cursor. /// - public static void DynamicHelpParameter(ConsoleKeyInfo? key = null, object arg = null) + public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null) { _singleton.DynamicHelpImpl(isFullHelp: false); } object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { - if (!String.IsNullOrEmpty(commandName) && isFullHelp) + if (string.IsNullOrEmpty(commandName)) { - _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - _ps.Commands.Clear(); - - return _ps - .AddCommand($"Microsoft.PowerShell.Core\\Get-Help") - .AddParameter("Name", commandName) - .AddParameter("Full", value: true) - .AddCommand($"Microsoft.PowerShell.Utility\\Out-String") - .Invoke() - .FirstOrDefault(); + return null; } - else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName) && !isFullHelp) + + try { - _ps ??= System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - _ps.Commands.Clear(); + System.Management.Automation.PowerShell ps; + 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 + return ps .AddCommand("Microsoft.PowerShell.Core\\Get-Help") .AddParameter("Name", commandName) .AddParameter("Parameter", parameterName) .Invoke() .FirstOrDefault(); } - - return null; + catch (Exception) + { + return null; + } + finally + { + // GetDynamicHelpContent could scroll the screen, e.g. via Write-Progress. For example, + // cd under the CloudShell Azure drive will show the progress bar while fetching data. + // 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.WriteToPager(string content, string regexPatternToScrollTo) + void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo) { _pager.Write(content, regexPatternToScrollTo); } @@ -79,40 +104,31 @@ private void WriteDynamicHelpContent(string commandName, string parameterName, b { var helpContent = _mockableMethods.GetDynamicHelpContent(commandName, parameterName, isFullHelp); - if (!String.IsNullOrEmpty(commandName) && isFullHelp) + if (helpContent is string fullHelp && fullHelp.Length > 0) { - if (helpContent is String fullHelp && !String.IsNullOrEmpty(fullHelp)) - { - string regexPatternToScrollTo = null; + string regexPatternToScrollTo = null; - if (!String.IsNullOrEmpty(parameterName)) - { - string upper = parameterName[0].ToString().ToUpperInvariant(); - string lower = parameterName[0].ToString().ToLowerInvariant(); - string remainingString = parameterName.Substring(1); - regexPatternToScrollTo = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; - } - - _mockableMethods.WriteToPager(fullHelp, regexPatternToScrollTo); + if (!string.IsNullOrEmpty(parameterName)) + { + string upper = parameterName[0].ToString().ToUpperInvariant(); + string lower = parameterName[0].ToString().ToLowerInvariant(); + string remainingString = parameterName.Substring(1); + regexPatternToScrollTo = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; } + + _mockableMethods.RenderFullHelp(fullHelp, regexPatternToScrollTo); } - else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) + else if (helpContent is PSObject paramHelp) { - if (helpContent is PSObject paramHelp) - { - WriteParameterHelp(paramHelp); - } + WriteParameterHelp(paramHelp); } } private void DynamicHelpImpl(bool isFullHelp) { - _pager ??= new Pager(); - - if (InViInsertMode()) // must close out the current edit group before engaging menu completion + if (isFullHelp) { - ViCommandMode(); - ViInsertWithAppend(); + _pager ??= new Pager(); } int cursor = _singleton._current; @@ -121,12 +137,18 @@ private void DynamicHelpImpl(bool isFullHelp) foreach(var token in _singleton._tokens) { + var extent = token.Extent; + + if (extent.EndOffset > cursor) + { + break; + } + if (token.TokenFlags == TokenFlags.CommandName) { commandName = token.Text; } - var extent = token.Extent; if (extent.StartOffset <= cursor && extent.EndOffset >= cursor) { if (token.Kind == TokenKind.Parameter) @@ -143,20 +165,14 @@ private void DynamicHelpImpl(bool isFullHelp) private void WriteDynamicHelpBlock(Collection helpBlock) { var bufferWidth = _console.BufferWidth; - var colWidth = Math.Min(helpBlock.Max(s => LengthInBufferCells(s)) + 2, bufferWidth); - int columns = 1; var dynHelp = new MultilineDisplayBlock { Singleton = this, - ColumnWidth = colWidth, - Rows = (helpBlock.Count + columns - 1) / columns, ItemsToDisplay = helpBlock }; - dynHelp.SaveCursor(); - dynHelp.DrawMultilineBlock(dynHelp); - dynHelp.RestoreCursor(); + dynHelp.DrawMultilineBlock(); ReadKey(); dynHelp.Clear(); } @@ -165,7 +181,7 @@ private void WriteParameterHelp(dynamic helpContent) { Collection helpBlock; - if (helpContent == null || String.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) + if (helpContent == null || string.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) { helpBlock = new Collection() { @@ -177,13 +193,17 @@ private void WriteParameterHelp(dynamic helpContent) { 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, + string.Empty, syntax, - String.Empty, + string.Empty, desc, details }; @@ -196,17 +216,16 @@ private class MultilineDisplayBlock : DisplayBlockBase { internal Collection ItemsToDisplay; - internal int Rows; - internal int ColumnWidth; - private int multilineItems = 0; - public void DrawMultilineBlock(MultilineDisplayBlock multilineBlock) + 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); @@ -218,71 +237,40 @@ public void DrawMultilineBlock(MultilineDisplayBlock multilineBlock) MoveCursorDown(1); var bufferWidth = console.BufferWidth; - var columnWidth = this.ColumnWidth; var items = this.ItemsToDisplay; - for (var row = 0; row < this.Rows; row++) + + for (var index = 0; index < items.Count; index++) { - var cells = 0; - if (row >= items.Count) + if (items[index].Length > bufferWidth) { - break; - } + // if the length required is exactly a multiple of buffer width. + multilineItems += items[index].Length / bufferWidth; - var itemToWrite = items[row]; - - if(itemToWrite.Length > bufferWidth) - { - multilineItems++; + /* if the item length is more than the buffer width it needs 1 extra line for the remaining characters. + if (items[index].Length % bufferWidth != 0) + { + multilineItems++; + }*/ } - console.Write(itemToWrite); - cells += columnWidth; - - // Make sure we always write out exactly 1 buffer width to erase anything - // from a previous menu. - if (cells < bufferWidth) - { - // 'BlankRestOfLine' erases rest of the current line, but the cursor is not moved. - console.BlankRestOfLine(); - } + 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 (row != (this.Rows - 1)) + if (index != (items.Count - 1)) { AdjustForPossibleScroll(1); MoveCursorDown(1); } } - if (multilineBlock != null) - { - if (Rows < multilineBlock.Rows) - { - // 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, - // so we are sure there is no need to scroll. - MoveCursorDown(1); - } - - Singleton.WriteBlankLines(multilineBlock.Rows - Rows); - } - } + this.RestoreCursor(); } public void Clear() { - // Add 1 for the movement to next line before displaying block - _singleton.WriteBlankLines(Top, Rows + multilineItems + 1); + _singleton.WriteBlankLines(Top, ItemsToDisplay.Count + multilineItems); } } } diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 5adc9cbe8..7365a364b 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 @@ -251,9 +253,6 @@ void SetDefaultWindowsBindings() _dispatchTable.Add(Keys.PageDown, MakeKeyHandler(ScrollDisplayDown, "ScrollDisplayDown")); _dispatchTable.Add(Keys.CtrlPageUp, MakeKeyHandler(ScrollDisplayUpLine, "ScrollDisplayUpLine")); _dispatchTable.Add(Keys.CtrlPageDown, MakeKeyHandler(ScrollDisplayDownLine, "ScrollDisplayDownLine")); - - _dispatchTable.Add(Keys.AltH, MakeKeyHandler(DynamicHelpParameter, "DynamicHelpParameter")); - _dispatchTable.Add(Keys.F1, MakeKeyHandler(DynamicHelpFullHelp, "DynamicHelpFullHelp")); } _chordDispatchTable = new Dictionary>(); @@ -331,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 diff --git a/PSReadLine/KeyBindings.vi.cs b/PSReadLine/KeyBindings.vi.cs index e77729550..6221d0c89 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 @@ -216,7 +218,6 @@ private void SetDefaultViBindings() _viCmdKeyMap.Add(Keys.CtrlDelete, MakeKeyHandler(KillWord, "KillWord")); } - _viChordDTable = new Dictionary { { Keys.D, MakeKeyHandler( DeleteLine, "DeleteLine") }, diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index da3c56f1f..6438105dc 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -830,6 +830,6 @@ 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. + No help content available. Please use Update-Help to download the latest help content. diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index fffc5c177..7a932661f 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -30,7 +30,7 @@ public interface IPSConsoleReadLineMockableMethods void OnCommandLineAccepted(IReadOnlyList history); void OnSuggestionAccepted(Guid predictorId, string suggestionText); object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); - void WriteToPager(string content, string regexPatternToScrollTo); + void RenderFullHelp(string content, string regexPatternToScrollTo); } [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")] diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index 67853413d..ecc680e70 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -39,65 +39,65 @@ internal static object GetDynamicHelpContent(string commandName, string paramete { string descText = @"Specifies a date and time. Time is optional and if not specified, returns 00:00:00."; - if (!String.IsNullOrEmpty(commandName) && isFullHelp) + if (string.IsNullOrEmpty(commandName)) + { + return null; + } + + if (isFullHelp) { return fullHelp; } - else if (!String.IsNullOrEmpty(commandName) && !String.IsNullOrEmpty(parameterName)) + + if (string.IsNullOrEmpty(parameterName)) { - 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)) - { - PSObject[] descDetails = new PSObject[1]; - descDetails[0] = new PSObject(); - descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); - - 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; + return null; } - return null; - } + if (string.IsNullOrEmpty(parameterName)) + { + return null; + } - internal static void WriteToPager(string content, string regexPatternToScrollTo) - { - actualContent = content; - PSConsoleReadLine.ReadKey(); - } + 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)) + { + PSObject[] descDetails = new PSObject[1]; + descDetails[0] = new PSObject(); + descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); + + 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); - - _console.Clear(); - Test("Get-Date", Keys( "Get-Date", _.F1, - CheckThat(() => Assert.Equal(fullHelp, actualContent)), - _.Enter, + CheckThat(() => Assert.Equal(fullHelp, _mockedMethods.helpContentRendered)), _.Enter )); } @@ -105,64 +105,42 @@ public void DynHelp_GetFullHelp() [SkippableFact] public void DynHelp_GetParameterHelp() { - PSConsoleReadLine.EnableDynHelpTestHook = true; - - try - { - TestSetup(KeyMode.Cmd, new KeyHandler("Alt+h", PSConsoleReadLine.DynamicHelpParameter)); - - _console.Clear(); - 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. Time is optional and if not", NextLine, " specified, returns 00:00:00.", NextLine, - TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), - _.Enter, - _.Enter - )); - } - finally - { - PSConsoleReadLine.EnableDynHelpTestHook = false; - } + 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. Time is optional and if not", NextLine, " specified, returns 00:00:00.", NextLine, + TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), + _.Enter, + _.Enter + )); } [SkippableFact] public void DynHelp_GetParameterHelpErrorMessage() { - PSConsoleReadLine.EnableDynHelpTestHook = true; - - try - { - TestSetup(KeyMode.Cmd, new KeyHandler("Alt+h", PSConsoleReadLine.DynamicHelpParameter)); - - _console.Clear(); - string emptyLine = new string(' ', _console.BufferWidth); - - Test("Get-FakeHelp -Fake", Keys( - "Get-FakeHelp -Fake", _.Alt_h, - CheckThat(() => AssertScreenIs(3, - TokenClassification.Command, "Get-FakeHelp", - TokenClassification.None, " ", - TokenClassification.Parameter, "-Fake", NextLine, - emptyLine, - TokenClassification.None, "No help content available. Please use Update-Help.")), - _.Enter, - _.Enter - )); - } - finally - { - PSConsoleReadLine.EnableDynHelpTestHook = false; - } + 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.")), + _.Enter, + _.Enter + )); } } } diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 0a4a2dcd9..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() { @@ -73,9 +74,9 @@ public object GetDynamicHelpContent(string commandName, string parameterName, bo return ReadLine.GetDynamicHelpContent(commandName, parameterName, isFullHelp); } - public void WriteToPager(string content, string regexPatternToScrollTo) + public void RenderFullHelp(string content, string regexPatternToScrollTo) { - ReadLine.WriteToPager(content, regexPatternToScrollTo); + helpContentRendered = content; } } From 4f69fd6eb40b4f390dc71c75e16eb31d7dc7c779 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 10 Dec 2020 16:37:57 -0800 Subject: [PATCH 17/20] Add error message for legacy console --- PSReadLine/DynamicHelp.cs | 26 +++++++++++++--------- PSReadLine/PSReadLineResources.Designer.cs | 12 ++++++++++ PSReadLine/PSReadLineResources.resx | 3 +++ test/DynamicHelpTest.cs | 2 +- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 7671c0ca3..2d20be8db 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -24,6 +24,19 @@ public partial class PSConsoleReadLine /// public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null) { + if (_singleton._console is PlatformWindows.LegacyWin32Console) + { + Collection helpBlock = new Collection() + { + String.Empty, + PSReadLineResources.LegacyConsoleFullHelpNotSupported + }; + + _singleton.WriteDynamicHelpBlock(helpBlock); + + return; + } + _singleton.DynamicHelpImpl(isFullHelp: true); } @@ -193,10 +206,10 @@ private void WriteParameterHelp(dynamic helpContent) { 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 @@ -242,16 +255,9 @@ public void DrawMultilineBlock() for (var index = 0; index < items.Count; index++) { - if (items[index].Length > bufferWidth) + if (LengthInBufferCells(items[index]) > bufferWidth) { - // if the length required is exactly a multiple of buffer width. multilineItems += items[index].Length / bufferWidth; - - /* if the item length is more than the buffer width it needs 1 extra line for the remaining characters. - if (items[index].Length % bufferWidth != 0) - { - multilineItems++; - }*/ } console.Write(items[index]); diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index a5409a684..14e2f692b 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2076,5 +2076,17 @@ internal static string NeedsUpdateHelp { return ResourceManager.GetString("NeedsUpdateHelp", resourceCulture); } } + + /// + /// Looks up a localized string similar to Displaying help content is not supported on this console. + /// + internal static string LegacyConsoleFullHelpNotSupported + { + get + { + return ResourceManager.GetString("LegacyConsoleFullHelpNotSupported", resourceCulture); + } + } + } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 6438105dc..2120321e0 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -832,4 +832,7 @@ Or not saving history with: No help content available. Please use Update-Help to download the latest help content. + + Displaying help content is not supported on this console. + diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index ecc680e70..740a7c2bb 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -90,7 +90,7 @@ internal static object GetDynamicHelpContent(string commandName, string paramete return paramHelp; } - + [SkippableFact] public void DynHelp_GetFullHelp() { From cb605e06fabd091e9525d423e25b5d531e5dcab4 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 1 Feb 2021 12:27:26 -0800 Subject: [PATCH 18/20] Address more feedback --- .vsts-ci/releaseBuild.yml | 1 + PSReadLine/DynamicHelp.cs | 38 +++--- PSReadLine/KeyBindings.cs | 8 +- PSReadLine/KeyBindings.vi.cs | 4 +- PSReadLine/PSReadLineResources.Designer.cs | 27 +++- PSReadLine/PSReadLineResources.resx | 8 +- test/DynamicHelpTest.cs | 144 ++++++++++++++------- 7 files changed, 159 insertions(+), 71 deletions(-) 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/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 2d20be8db..d57eadaeb 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -4,13 +4,12 @@ using System; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; using Microsoft.PowerShell.PSReadLine; -using Microsoft.PowerShell; namespace Microsoft.PowerShell { @@ -28,8 +27,8 @@ public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null { Collection helpBlock = new Collection() { - String.Empty, - PSReadLineResources.LegacyConsoleFullHelpNotSupported + string.Empty, + PSReadLineResources.FullHelpNotSupportedInLegacyConsole }; _singleton.WriteDynamicHelpBlock(helpBlock); @@ -49,6 +48,7 @@ public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = nu _singleton.DynamicHelpImpl(isFullHelp: false); } + [ExcludeFromCodeCoverage] object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { if (string.IsNullOrEmpty(commandName)) @@ -56,9 +56,10 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam return null; } + System.Management.Automation.PowerShell ps = null; + try { - System.Management.Automation.PowerShell ps; if (!_mockableMethods.RunspaceIsRemote(_runspace)) { ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); @@ -91,6 +92,7 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam .AddParameter("Parameter", parameterName) .Invoke() .FirstOrDefault(); + } catch (Exception) { @@ -98,8 +100,10 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam } finally { + ps?.Dispose(); + // GetDynamicHelpContent could scroll the screen, e.g. via Write-Progress. For example, - // cd under the CloudShell Azure drive will show the progress bar while fetching data. + // 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) { @@ -123,10 +127,7 @@ private void WriteDynamicHelpContent(string commandName, string parameterName, b if (!string.IsNullOrEmpty(parameterName)) { - string upper = parameterName[0].ToString().ToUpperInvariant(); - string lower = parameterName[0].ToString().ToLowerInvariant(); - string remainingString = parameterName.Substring(1); - regexPatternToScrollTo = $"-[{upper}|{lower}]{remainingString} [<|\\[]"; + regexPatternToScrollTo = $"-{parameterName} [<|\\[]"; } _mockableMethods.RenderFullHelp(fullHelp, regexPatternToScrollTo); @@ -152,7 +153,7 @@ private void DynamicHelpImpl(bool isFullHelp) { var extent = token.Extent; - if (extent.EndOffset > cursor) + if (extent.StartOffset > cursor) { break; } @@ -177,8 +178,6 @@ private void DynamicHelpImpl(bool isFullHelp) private void WriteDynamicHelpBlock(Collection helpBlock) { - var bufferWidth = _console.BufferWidth; - var dynHelp = new MultilineDisplayBlock { Singleton = this, @@ -194,7 +193,7 @@ private void WriteParameterHelp(dynamic helpContent) { Collection helpBlock; - if (helpContent == null || string.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) + if (string.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) { helpBlock = new Collection() { @@ -255,9 +254,16 @@ public void DrawMultilineBlock() for (var index = 0; index < items.Count; index++) { - if (LengthInBufferCells(items[index]) > bufferWidth) + var itemLength = LengthInBufferCells(items[index]); + + if (itemLength > bufferWidth) { - multilineItems += items[index].Length / bufferWidth; + multilineItems += itemLength / bufferWidth; + + if (itemLength % bufferWidth == 0) + { + multilineItems--; + } } console.Write(items[index]); diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 7365a364b..8d0bf7be6 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -235,7 +235,7 @@ void SetDefaultWindowsBindings() { Keys.CtrlAt, MakeKeyHandler(MenuComplete, "MenuComplete") }, { Keys.CtrlW, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, - { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") } + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -330,8 +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") } + { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -565,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 6221d0c89..63629db82 100644 --- a/PSReadLine/KeyBindings.vi.cs +++ b/PSReadLine/KeyBindings.vi.cs @@ -89,7 +89,7 @@ private void SetDefaultViBindings() { Keys.CtrlR, MakeKeyHandler(ViSearchHistoryBackward,"ViSearchHistoryBackward") }, { Keys.CtrlS, MakeKeyHandler(SearchForward, "SearchForward") }, { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, - { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") } + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -210,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 diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 14e2f692b..9a809f4f3 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2069,7 +2069,7 @@ internal static string YankPopDescription { } /// - /// Looks up a localized string similar to No help content available. Please use Update-Help. + /// 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 { @@ -2080,13 +2080,34 @@ internal static string NeedsUpdateHelp { /// /// Looks up a localized string similar to Displaying help content is not supported on this console. /// - internal static string LegacyConsoleFullHelpNotSupported + internal static string FullHelpNotSupportedInLegacyConsole { get { - return ResourceManager.GetString("LegacyConsoleFullHelpNotSupported", resourceCulture); + 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 paramter at the cursor.. + /// + internal static string ShowParameterHelpDescription + { + get + { + return ResourceManager.GetString("ShowParameterHelpDescription", resourceCulture); + } + } } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 2120321e0..d5d06db7f 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -832,7 +832,13 @@ Or not saving history with: No help content available. Please use Update-Help to download the latest help content. - + Displaying help content is not supported on this console. + + Shows help for the command at the cursor in an alternate screen buffer. + + + Shows help for the paramter at the cursor. + diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index 740a7c2bb..c54785603 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -1,19 +1,11 @@ using System; -using System.Collections; -using System.Collections.ObjectModel; -using System.IO; using System.Management.Automation; -using System.Reflection; -using System.Text; -using Microsoft.PowerShell; using Xunit; namespace Test { public partial class ReadLine { - private static string actualContent; - private static readonly string fullHelp = @" NAME @@ -37,8 +29,6 @@ Accept wildcard characters? false internal static object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) { - string descText = @"Specifies a date and time. Time is optional and if not specified, returns 00:00:00."; - if (string.IsNullOrEmpty(commandName)) { return null; @@ -54,11 +44,6 @@ internal static object GetDynamicHelpContent(string commandName, string paramete return null; } - if (string.IsNullOrEmpty(parameterName)) - { - return null; - } - PSObject paramHelp = new PSObject(); if (string.Equals(commandName, "Get-FakeHelp", StringComparison.OrdinalIgnoreCase) && string.Equals(parameterName, "Fake", StringComparison.OrdinalIgnoreCase)) @@ -69,24 +54,45 @@ internal static object GetDynamicHelpContent(string commandName, string paramete } else if (string.Equals(commandName, "Get-Date", StringComparison.OrdinalIgnoreCase) && string.Equals(parameterName, "Date", StringComparison.OrdinalIgnoreCase)) { - PSObject[] descDetails = new PSObject[1]; - descDetails[0] = new PSObject(); - descDetails[0].Members.Add(new PSNoteProperty("Text", descText)); + 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 total"; + return GetParameterHelpObject(multiLineDesc); + } + } - var np = new PSNoteProperty("name", "System.Datetime"); - np.Value = "System.Datetime"; + return paramHelp; + } - var typeName = new PSObject(np); + 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)); - 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")); - } + 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; } @@ -103,24 +109,65 @@ public void DynHelp_GetFullHelp() } [SkippableFact] - public void DynHelp_GetParameterHelp() + 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. Time is optional and if not", NextLine, " specified, returns 00:00:00.", NextLine, - TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), + 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")), _.Enter, - _.Enter + _.Enter, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-Date", + TokenClassification.None, " ", + TokenClassification.Parameter, "-Date")) + )); + } + + [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")), + _.Enter, + _.Enter, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Get-MultiLineHelp", + TokenClassification.None, " ", + TokenClassification.Parameter, "-OneAndHalf")) )); } @@ -132,12 +179,15 @@ public void DynHelp_GetParameterHelpErrorMessage() 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.")), + 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.")), _.Enter, _.Enter )); From fc817c6a8c5a43d4aa64acd7bfb7a97e91c31d87 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 10 Feb 2021 13:15:55 -0800 Subject: [PATCH 19/20] Add tests for two full lines and change keys --- test/DynamicHelpTest.cs | 112 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/test/DynamicHelpTest.cs b/test/DynamicHelpTest.cs index c54785603..5470f9307 100644 --- a/test/DynamicHelpTest.cs +++ b/test/DynamicHelpTest.cs @@ -65,7 +65,7 @@ internal static object GetDynamicHelpContent(string commandName, string paramete } 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 total"; + 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); } } @@ -130,12 +130,12 @@ public void DynHelp_GetParameterHelp_And_Clear() TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), - _.Enter, - _.Enter, + _.Escape, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "Get-Date", TokenClassification.None, " ", - TokenClassification.Parameter, "-Date")) + TokenClassification.Parameter, "-Date")), + _.Enter )); } @@ -162,12 +162,108 @@ public void DynHelp_GetParameterHelpMultiLine_And_Clear() TokenClassification.None, "Required: false, Position: 0, Default Value: None, Pipeline ", NextLine, "Input: True (ByPropertyName, ByValue), WildCard: false")), - _.Enter, - _.Enter, + _.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, "-OneAndHalf")) + TokenClassification.Parameter, "-ExactlyTwo")), + _.Enter )); } @@ -188,7 +284,7 @@ public void DynHelp_GetParameterHelpErrorMessage() TokenClassification.None, "No help content available. Please use Update-Help to downloa", NextLine, "d the latest help content.")), - _.Enter, + _.RightArrow, _.Enter )); } From 024043d50b7398e865f7b59c39aed1a5f906cc59 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 10 Feb 2021 15:59:16 -0800 Subject: [PATCH 20/20] Address feedback --- PSReadLine/DisplayBlockBase.cs | 2 +- PSReadLine/PSReadLineResources.Designer.cs | 4 ++-- PSReadLine/PSReadLineResources.resx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PSReadLine/DisplayBlockBase.cs b/PSReadLine/DisplayBlockBase.cs index f3f74ea88..7f3d5a0a0 100644 --- a/PSReadLine/DisplayBlockBase.cs +++ b/PSReadLine/DisplayBlockBase.cs @@ -14,7 +14,7 @@ private class DisplayBlockBase internal PSConsoleReadLine Singleton; internal int Top; - private protected void MoveCursorDown(int cnt) + protected void MoveCursorDown(int cnt) { IConsole console = Singleton._console; while (cnt-- > 0) diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 9a809f4f3..b91a55260 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2078,7 +2078,7 @@ internal static string NeedsUpdateHelp { } /// - /// Looks up a localized string similar to Displaying help content is not supported on this console. + /// 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 { @@ -2100,7 +2100,7 @@ internal static string ShowCommandHelpDescription } /// - /// Looks up a localized string similar to Shows help for the paramter at the cursor.. + /// Looks up a localized string similar to Shows help for the parameter at the cursor. /// internal static string ShowParameterHelpDescription { diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index d5d06db7f..236c8da7e 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -833,12 +833,12 @@ Or not saving history with: No help content available. Please use Update-Help to download the latest help content. - Displaying help content is not supported on this console. + 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 paramter at the cursor. + Shows help for the parameter at the cursor.