diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 7b3f0c1c..69daaa0c 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -664,7 +664,7 @@ public static void ShowKeyBindings(ConsoleKeyInfo? key = null, object arg = null } // Don't overwrite any of the line - so move to first line after the end of our buffer. - var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + var point = _singleton.EndOfBufferPosition(); console.SetCursorPosition(point.X, point.Y); console.Write("\n"); @@ -721,7 +721,7 @@ public static void WhatIsKey(ConsoleKeyInfo? key = null, object arg = null) var console = _singleton._console; // Don't overwrite any of the line - so move to first line after the end of our buffer. - var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + var point = _singleton.EndOfBufferPosition(); console.SetCursorPosition(point.X, point.Y); console.Write("\n"); diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 925355f7..35dd5a33 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2070,6 +2070,17 @@ internal static string WindowSizeTooSmallForListView } } + /// + /// Looks up a localized string similar to "! terminal size too small to show the list view". + /// + internal static string WindowSizeTooSmallWarning + { + get + { + return ResourceManager.GetString("WindowSizeTooSmallWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy the text from the current kill ring position to the input. /// diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 7dc66be2..28d6e5d4 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -840,6 +840,9 @@ 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}'. + + ! terminal size too small to show the list view + No help content available. Please use Update-Help to download the latest help content. diff --git a/PSReadLine/Prediction.Entry.cs b/PSReadLine/Prediction.Entry.cs index a60d05e2..f576f62a 100644 --- a/PSReadLine/Prediction.Entry.cs +++ b/PSReadLine/Prediction.Entry.cs @@ -9,19 +9,52 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { + /// + /// Represents a prediction source. + /// + private readonly struct SourceInfo + { + internal readonly string SourceName; + internal readonly int EndIndex; + internal readonly int PrevSourceEndIndex; + internal readonly int ItemCount; + + internal SourceInfo(string sourceName, int endIndex, int prevSourceEndIndex) + { + SourceName = sourceName; + int sourceWidth = LengthInBufferCells(SourceName); + if (sourceWidth > PredictionListView.SourceMaxWidth) + { + sourceWidth = PredictionListView.SourceMaxWidth - 1; + int sourceStrLen = SubstringLengthByCells(sourceName, sourceWidth); + SourceName = sourceName.Substring(0, sourceStrLen) + SuggestionEntry.Ellipsis; + } + + EndIndex = endIndex; + PrevSourceEndIndex = prevSourceEndIndex; + ItemCount = EndIndex - PrevSourceEndIndex; + } + } + /// /// This type represents an individual suggestion entry. /// private struct SuggestionEntry { + internal const char Ellipsis = '\u2026'; + internal const string HistorySource = "History"; + internal readonly Guid PredictorId; internal readonly uint? PredictorSession; internal readonly string Source; internal readonly string SuggestionText; internal readonly int InputMatchIndex; + private string _listItemTextRegular; + private string _listItemTextSelected; + internal SuggestionEntry(string suggestion, int matchIndex) - : this(source: "History", predictorId: Guid.Empty, predictorSession: null, suggestion, matchIndex) + : this(source: HistorySource, predictorId: Guid.Empty, predictorSession: null, suggestion, matchIndex) { } @@ -32,6 +65,8 @@ internal SuggestionEntry(string source, Guid predictorId, uint? predictorSession PredictorSession = predictorSession; SuggestionText = suggestion; InputMatchIndex = matchIndex; + + _listItemTextRegular = _listItemTextSelected = null; } /// @@ -57,8 +92,19 @@ private static int DivideAndRoundUp(int dividend, int divisor) /// The highlighting sequences for a selected list item. internal string GetListItemText(int width, string input, string selectionHighlighting) { - const string ellipsis = "..."; - const int ellipsisLength = 3; + const int ellipsisLength = 1; + + if (selectionHighlighting is null) + { + if (_listItemTextRegular is not null) + { + return _listItemTextRegular; + } + } + else if (_listItemTextSelected is not null) + { + return _listItemTextSelected; + } // Calculate the 'SOURCE' portion to be rendered. int sourceStrLen = Source.Length; @@ -119,7 +165,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig // The suggestion text doesn't contain the user input. int length = SubstringLengthByCells(SuggestionText, textWidth - ellipsisLength); line.Append(SuggestionText, 0, length) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -136,7 +182,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig .Append(SuggestionText, 0, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, length - input.Length) - .Append(ellipsis); + .Append(Ellipsis); } else { @@ -149,7 +195,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig int remainingLenInCells = textWidth - ellipsisLength - rightLenInCells; int length = SubstringLengthByCellsFromEnd(SuggestionText, input.Length - 1, remainingLenInCells); line.Append(_singleton._options.EmphasisColor) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, input.Length - length, length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, SuggestionText.Length - input.Length); @@ -162,11 +208,11 @@ internal string GetListItemText(int width, string input, string selectionHighlig int startIndex = input.Length - leftStrLen; int totalStrLen = SubstringLengthByCells(SuggestionText, startIndex, textWidth - ellipsisLength * 2); line.Append(_singleton._options.EmphasisColor) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, startIndex, leftStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, totalStrLen - leftStrLen) - .Append(ellipsis); + .Append(Ellipsis); } } @@ -192,7 +238,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -201,7 +247,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig { // Otherwise, if the (mid+right) portions take up to 2/3 of the text width, we just truncate the suggestion text at the beginning. int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, textWidth - midRightLenInCells - ellipsisLength); - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) @@ -223,13 +269,13 @@ internal string GetListItemText(int width, string input, string selectionHighlig int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, leftCellLen - ellipsisLength); int rightStrLen = SubstringLengthByCells(SuggestionText, rightStartindex, rigthCellLen - ellipsisLength); - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -249,7 +295,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); @@ -277,11 +323,11 @@ internal string GetListItemText(int width, string input, string selectionHighlig line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -298,11 +344,11 @@ internal string GetListItemText(int width, string input, string selectionHighlig int midRightStrLen = SubstringLengthByCellsFromEnd(SuggestionText, rightStartindex - 1, midRightCellLen); int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, midRemainingLenInCells); - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); @@ -324,15 +370,15 @@ internal string GetListItemText(int width, string input, string selectionHighlig int spacesNeeded = textWidth - midRemainingLenInCells * 3 - ellipsisLength * 3; string spaces = spacesNeeded > 0 ? Spaces(spacesNeeded) : string.Empty; - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(spaces); break; } @@ -351,13 +397,29 @@ internal string GetListItemText(int width, string input, string selectionHighlig else { line.Append(Source, 0, sourceStrLen) - .Append(ellipsis); + .Append(Ellipsis); } line.EndColorSection(selectionHighlighting) .Append(']'); - return line.ToString(); + if (selectionHighlighting is not null) + { + // Need to reset at the end if the selection highlighting is being applied. + line.Append(VTColorUtils.AnsiReset); + } + + string textForRendering = line.ToString(); + if (selectionHighlighting is null) + { + _listItemTextRegular = textForRendering; + } + else + { + _listItemTextSelected = textForRendering; + } + + return textForRendering; } } } diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index ddfbd073..91ef8437 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using System.Management.Automation.Subsystem.Prediction; using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -207,19 +208,48 @@ protected List GetPredictionResults() /// private class PredictionListView : PredictionViewBase { - internal const int ListMaxCount = 10; - internal const int ListMaxWidth = 100; + // Item count constants. + internal const int ListMaxCount = 50; + internal const int HistoryMaxCount = 10; + + // List view constants. + internal const int ListViewMaxHeight = 10; + internal const int ListViewMaxWidth = 100; internal const int SourceMaxWidth = 15; - internal const int MinWindowWidth = 54; - internal const int MinWindowHeight = 15; + // Minimal window size. + internal const int MinWindowWidth = 50; + internal const int MinWindowHeight = 5; + // The items to be displayed in the list view. private List _listItems; - private int _listItemWidth; - private int _listItemHeight; + // Information about the sources of those items. + private List _sources; + // The index that is currently selected by user. private int _selectedIndex; + // Indicates to have the list view starts at the selected index. + private bool _renderFromSelected; + // Indicates a navigation update within the list view is pending. private bool _updatePending; + // The max list height to be used for rendering, which is auto-adjusted based on terminal height. + private int _maxViewHeight; + // The actual height of the list view that is currently rendered. + private int _listViewHeight; + // The actual width of the list view that is currently rendered. + private int _listViewWidth; + // An index pointing to the item that is shown in the first slot of the list view. + private int _listViewTop; + // An index pointing to the item right AFTER the one that is shown in the last slot of the list view. + private int _listViewEnd; + + // Indicates if we need to check on the height for each navigation in the list view. + private bool _checkOnHeight; + // To warn about that the terminal size is too small to display the list view. + private bool _warnAboutSize; + // Indicates if a warning message was displayed. + private bool _warningPrinted; + // Caches re-used when aggregating the suggestion results from predictors and history. // Those caches help us avoid allocation on tons of short-lived collections. private List _cacheList1; @@ -273,6 +303,36 @@ internal PredictionListView(PSConsoleReadLine singleton) internal override bool HasPendingUpdate => _updatePending; internal override bool HasActiveSuggestion => _listItems != null; + /// + /// Calculate the max width and height of the list view based on the current terminal size. + /// + private (int maxWidth, int maxHeight, bool checkOnHeight) RefreshMaxViewSize() + { + var console = _singleton._console; + int maxWidth = Math.Min(console.BufferWidth, ListViewMaxWidth); + + (int maxHeight, bool moreCheck) = console.BufferHeight switch + { + > ListViewMaxHeight * 2 => (ListViewMaxHeight, false), + > ListViewMaxHeight => (ListViewMaxHeight / 2, false), + _ => (ListViewMaxHeight / 3, true) + }; + + return (maxWidth, maxHeight, moreCheck); + } + + /// + /// Check if the height becomes too small for the current rendering. + /// + private bool HeightIsTooSmall() + { + int physicalLineCountForBuffer = _singleton.EndOfBufferPosition().Y - _singleton._initialY + 1; + return _singleton._console.BufferHeight < physicalLineCountForBuffer + _maxViewHeight + 1 /* one metadata line */; + } + + /// + /// Get suggestion results. + /// internal override void GetSuggestion(string userInput) { if (_singleton._initialY < 0) @@ -295,14 +355,16 @@ internal override void GetSuggestion(string userInput) if (!WindowSizeMeetsMinRequirement) { - // If the window size is too small for the list view to work, we just disable the list view. - Reset(); + // If the window size is too small to show the list view, we disable the list view and show a warning. + _warnAboutSize = true; return; } _inputText = userInput; + // Reset the list item selection. _selectedIndex = -1; - _listItemWidth = Math.Min(_singleton._console.BufferWidth, ListMaxWidth); + // Refresh the list view width and height in case the terminal was resized. + (_listViewWidth, _maxViewHeight, _checkOnHeight) = RefreshMaxViewSize(); if (inputUnchanged) { @@ -313,6 +375,7 @@ internal override void GetSuggestion(string userInput) } _listItems?.Clear(); + _sources?.Clear(); try { @@ -323,7 +386,11 @@ internal override void GetSuggestion(string userInput) if (UseHistory) { - _listItems = GetHistorySuggestions(userInput, ListMaxCount); + _listItems = GetHistorySuggestions(userInput, HistoryMaxCount); + if (_listItems?.Count > 0) + { + _sources = new List() { new SourceInfo(SuggestionEntry.HistorySource, _listItems.Count - 1, -1) }; + } } } catch @@ -348,6 +415,7 @@ private void AggregateSuggestions() try { _listItems ??= new List(); + _sources ??= new List(); _cacheList1 ??= new List(); // This list holds the total number of suggestions from each of the predictors. _cacheList2 ??= new List(); // This list holds the final number of suggestions that will be rendered for each of the predictors. @@ -412,13 +480,18 @@ private void AggregateSuggestions() break; } - more = _cacheList1[i] > 0; + more |= _cacheList1[i] > 0; } } if (hCount > 0) { - _listItems.RemoveRange(hCount, _listItems.Count - hCount); + if (hCount < _listItems.Count) + { + _listItems.RemoveRange(hCount, _listItems.Count - hCount); + _sources.Clear(); + _sources.Add(new SourceInfo(SuggestionEntry.HistorySource, hCount - 1, prevSourceEndIndex: -1)); + } if (_cachedComparer != _singleton._options.HistoryStringComparer) { @@ -471,22 +544,29 @@ private void AggregateSuggestions() // Get the number of prediction results that were actually put in the list after filtering out the duplicate ones. int count = _cacheList2[index] - num; - if (item.Session.HasValue && count > 0) + if (count > 0) { - // Send feedback only if the mini-session id is specified and we truely have its results in the list to be rendered. - // When the mini-session id is not specified, we consider the predictor doesn't accept feedback. - // - // NOTE: when any duplicate results were skipped, the 'count' passed in here won't be accurate as it still includes - // those skipped ones. This is due to the limitation of the 'OnSuggestionDisplayed' interface method, which didn't - // assume any prediction results from a predictor could be filtered out at the initial design time. We will have to - // change the predictor interface to pass in accurate information, such as: - // void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex, int[] skippedIndices) - // - // However, an interface change has huge impacts. At least, a newer version of PSReadLine will stop working on the - // existing PowerShell 7+ versions. For this particular issue, the chance that it could happen is low and the impact - // of the inaccurate feedback is also low, so we should delay this interface change until another highly-demanded - // change to the interface is required in future (e.g. changes related to supporting OpenAI models). - _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, count + skipCount); + int prevEndIndex = _sources.Count > 0 ? _sources[_sources.Count - 1].EndIndex : -1; + int endIndex = _listItems.Count - 1; + _sources.Add(new SourceInfo(_listItems[endIndex].Source, endIndex, prevEndIndex)); + + if (item.Session.HasValue && count > 0) + { + // Send feedback only if the mini-session id is specified and we truely have its results in the list to be rendered. + // When the mini-session id is not specified, we consider the predictor doesn't accept feedback. + // + // NOTE: when any duplicate results were skipped, the 'count' passed in here won't be accurate as it still includes + // those skipped ones. This is due to the limitation of the 'OnSuggestionDisplayed' interface method, which didn't + // assume any prediction results from a predictor could be filtered out at the initial design time. We will have to + // change the predictor interface to pass in accurate information, such as: + // void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex, int[] skippedIndices) + // + // However, an interface change has huge impacts. At least, a newer version of PSReadLine will stop working on the + // existing PowerShell 7+ versions. For this particular issue, the chance that it could happen is low and the impact + // of the inaccurate feedback is also low, so we should delay this interface change until another highly-demanded + // change to the interface is required in future (e.g. changes related to supporting OpenAI models). + _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, count + skipCount); + } } } } @@ -501,7 +581,10 @@ private void AggregateSuggestions() if (_listItems?.Count > 0) { - _listItemHeight = Math.Min(_listItems.Count, ListMaxCount); + // Initialize the view window position here. + _listViewTop = 0; + _listViewEnd = Math.Min(_listItems.Count, _maxViewHeight); + _listViewHeight = _listViewEnd - _listViewTop; } else { @@ -509,8 +592,24 @@ private void AggregateSuggestions() } } + /// + /// Generate the rendering text for the list view. + /// internal override void RenderSuggestion(List consoleBufferLines, ref int currentLogicalLine) { + if (_warnAboutSize || (_checkOnHeight && HeightIsTooSmall())) + { + _warningPrinted = true; + RenderWarningLine(NextBufferLine(consoleBufferLines, ref currentLogicalLine)); + + Reset(); + return; + } + else + { + _warningPrinted = false; + } + if (_updatePending) { _updatePending = false; @@ -520,36 +619,254 @@ internal override void RenderSuggestion(List consoleBufferLines, AggregateSuggestions(); } - if (_listItems == null) + if (_listItems is null) { return; } - for (int i = 0; i < _listItemHeight; i++) + // Create the metadata line. + RenderMetadataLine(NextBufferLine(consoleBufferLines, ref currentLogicalLine)); + + if (_selectedIndex >= 0) { - currentLogicalLine += 1; - if (currentLogicalLine == consoleBufferLines.Count) + // An item was selected, so update the view window accrodingly. + if (_renderFromSelected) { - consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); + // Render from the selected index if there are enough items left for a page. + // If not, then render all remaining items plus a few from above the selected one, so as to render a full page. + _renderFromSelected = false; + int offset = _maxViewHeight - Math.Min(_listItems.Count - _selectedIndex, _maxViewHeight); + _listViewTop = offset > 0 ? Math.Max(0, _selectedIndex - offset) : _selectedIndex; + _listViewEnd = Math.Min(_listItems.Count, _listViewTop + _maxViewHeight); + } + else + { + // - if the selected item is within the current top/end, then no need to move the list view window. + // - if the selected item is before the current top, then move the top to the selected item. + // - if the selected item is after the current end, then move the end to one beyond the selected item. + if (_selectedIndex < _listViewTop) + { + _listViewTop = _selectedIndex; + _listViewEnd = Math.Min(_listItems.Count, _selectedIndex + _maxViewHeight); + } + else if (_selectedIndex >= _listViewEnd) + { + _listViewEnd = _selectedIndex + 1; + _listViewTop = Math.Max(0, _listViewEnd - _maxViewHeight); + } } - bool itemSelected = i == _selectedIndex; - StringBuilder currentLineBuffer = consoleBufferLines[currentLogicalLine]; + _listViewHeight = _listViewEnd - _listViewTop; + } + for (int i = _listViewTop; i < _listViewEnd; i++) + { + bool itemSelected = i == _selectedIndex; string selectionColor = itemSelected ? _singleton._options._listPredictionSelectedColor : null; - currentLineBuffer.Append( - _listItems[i].GetListItemText( - _listItemWidth, + + NextBufferLine(consoleBufferLines, ref currentLogicalLine) + .Append(_listItems[i].GetListItemText( + _listViewWidth, _inputText, selectionColor)); + } + } + + /// + /// Generate the rendering text for the warning message. + /// + private void RenderWarningLine(StringBuilder buffer) + { + // Add italic text effect to the highlight color. + string highlightStyle = _singleton._options._listPredictionColor + "\x1b[3m"; + + buffer.Append(highlightStyle) + .Append(PSReadLineResources.WindowSizeTooSmallWarning) + .Append(VTColorUtils.AnsiReset); + } - if (itemSelected) + /// + /// Calculate the height of the list when warning was displayed. + /// + private int GetPesudoListHeightForWarningRendering() + { + int bufferWidth = _singleton._console.BufferWidth; + int lengthInCells = LengthInBufferCells(PSReadLineResources.WindowSizeTooSmallWarning); + int pesudoListHeight = lengthInCells / bufferWidth; + + if (lengthInCells % bufferWidth == 0) + { + pesudoListHeight--; + } + + return pesudoListHeight; + } + + /// + /// Generate the rendering text for the metadata line. + /// + private void RenderMetadataLine(StringBuilder buffer) + { + // Add italic text effect to the highlight color. + string highlightStyle = _singleton._options._listPredictionColor + "\x1b[3m"; + string dimmedStyle = PSConsoleReadLineOptions.DefaultInlinePredictionColor; + string activeStyle = null; + + // Render the quick indicator. + buffer.Append(highlightStyle) + .Append('<') + .Append(_selectedIndex > -1 ? _selectedIndex + 1 : "-") + .Append('/') + .Append(_listItems.Count) + .Append('>') + .Append(VTColorUtils.AnsiReset); + + if (_listViewWidth < 60) + { + // We don't render the additional information about sources when the list view width is less than 60. + // Adjust the position of quick indicator a little bit in this case and call it done. + buffer.Insert(0, VTColorUtils.AnsiReset); + buffer.Insert(VTColorUtils.AnsiReset.Length, " ", count: 2); + return; + } + + /// + /// A helper function to avoid appending extra color VT sequences unnecessarily. + /// + static StringBuilder AppendColor(StringBuilder buffer, string colorToUse, ref string activeColor, out int nextCharPos) + { + if (activeColor is null) { - currentLineBuffer.Append(VTColorUtils.AnsiReset); + buffer.Append(colorToUse); } + else if (activeColor != colorToUse) + { + buffer.Append(VTColorUtils.AnsiReset).Append(colorToUse); + } + + activeColor = colorToUse; + nextCharPos = buffer.Length; + return buffer; } + + // The list view width decides how to render the source information: + // - when width >= 80, we render upto 3 sources, + // - when width >= 60, we render upto 2 sources. + // The reason to select '80' and '60' here is because: + // - To render upto 3 sources, the maximum cell length that could be taken by both the total-count part and the extra-info part + // will be 75 (7+68), so we choose '80' as the minimal requirement for rendering 3 sources. + // - To render upto 2 sources, the maximum cell length that could be taken by both the total-count part and the extra-info part + // will be 55 (7+48), so we choose '60' as the minimal requirement for rendering 2 sources. + int maxSourceCount = _listViewWidth >= 80 ? 3 : 2; + int charPosition = buffer.Length; + int totalCountPartLength = buffer.Length - highlightStyle.Length - VTColorUtils.AnsiReset.Length; + int additionalPartLength = 0; + + int selected = -1; + int startFrom = 0; + + // If a list item was selected, calculate which source the list item belongs to and which source + // to start render for the additional information part. + if (_selectedIndex > -1) + { + for (int i = 0; i < _sources.Count; i++) + { + if (_selectedIndex <= _sources[i].EndIndex) + { + selected = i; + break; + } + } + + if (selected == 0) + { + startFrom = 0; + } + else if (selected == _sources.Count - 1) + { + startFrom = Math.Max(0, selected - (maxSourceCount - 1)); + } + else + { + startFrom = maxSourceCount == 3 ? selected - 1 : selected; + } + } + + // Start the extra information about the sources -- add the opening arrow bracket. + AppendColor(buffer, dimmedStyle, ref activeStyle, out _).Append('<'); + additionalPartLength++; + + // Add the prefix, continue to use dimmed color. + if (startFrom > 0) + { + buffer.Append(SuggestionEntry.Ellipsis).Append(' '); + additionalPartLength += 2; + } + + // Add the sources. + for (int i = 0; i < maxSourceCount; i++) + { + int index = startFrom + i; + if (index == _sources.Count) + { + break; + } + + if (i > 0) + { + // Add the separator. + buffer.Append(' '); + additionalPartLength++; + } + + int nextCharPos; + SourceInfo info = _sources[index]; + if (selected == index) + { + AppendColor(buffer, highlightStyle, ref activeStyle, out nextCharPos) + .Append(info.SourceName) + .Append('(') + .Append(_selectedIndex - info.PrevSourceEndIndex) + .Append('/') + .Append(info.ItemCount) + .Append(')'); + } + else + { + AppendColor(buffer, dimmedStyle, ref activeStyle, out nextCharPos) + .Append(info.SourceName) + .Append('(') + .Append(info.ItemCount) + .Append(')'); + } + + // Need to take into account multi-cell characters when calculating length. + additionalPartLength += LengthInBufferCells(buffer, nextCharPos, buffer.Length); + } + + // Add the suffix. + if (startFrom + maxSourceCount < _sources.Count) + { + AppendColor(buffer, dimmedStyle, ref activeStyle, out _) + .Append(' ') + .Append(SuggestionEntry.Ellipsis); + additionalPartLength += 2; + } + + // Add the closing arrow bracket. + AppendColor(buffer, dimmedStyle, ref activeStyle, out _) + .Append('>') + .Append(VTColorUtils.AnsiReset); + additionalPartLength++; + + // Lastly, insert the padding spaces. + int padding = _listViewWidth - additionalPartLength - totalCountPartLength; + buffer.Insert(charPosition, " ", padding); } + /// + /// Trigger the feedback about a suggestion was accepted. + /// internal override void OnSuggestionAccepted() { if (!UsePlugin) @@ -569,24 +886,40 @@ internal override void OnSuggestionAccepted() } } + /// + /// Clear the list view. + /// internal override void Clear(bool cursorAtEol) { - if (_listItems == null) { return; } + if (_listItems == null && !_warningPrinted) + { + return; + } + + int listHeight = _warningPrinted + ? GetPesudoListHeightForWarningRendering() + : _listViewHeight; int top = cursorAtEol ? _singleton._console.CursorTop - : _singleton.ConvertOffsetToPoint(_inputText.Length).Y; + : _singleton.EndOfBufferPosition().Y; - _singleton.WriteBlankLines(top + 1, _listItemHeight); + _warningPrinted = false; + _singleton.WriteBlankLines(top + 1, listHeight + 1 /* plus 1 to include the metadata line */); Reset(); } + /// + /// Reset all the list view states. + /// internal override void Reset() { base.Reset(); + + _sources = null; _listItems = null; - _listItemWidth = _listItemHeight = _selectedIndex = -1; - _updatePending = false; + _maxViewHeight = _listViewTop = _listViewEnd = _listViewWidth = _listViewHeight = _selectedIndex = -1; + _warnAboutSize = _checkOnHeight = _updatePending = _renderFromSelected = false; } /// @@ -595,8 +928,12 @@ internal override void Reset() /// internal void UpdateListSelection(int move) { + // While moving around the list, we want to go back to the original input when we move one down + // after the last item, or move one up before the first item in the list. + // So, we can imagine a virtual list constructed by inserting the original input at the index 0 + // of the real list. int virtualItemIndex = _selectedIndex + 1; - int virtualItemCount = _listItemHeight + 1; + int virtualItemCount = _listItems.Count + 1; _updatePending = true; virtualItemIndex += move; @@ -614,6 +951,107 @@ internal void UpdateListSelection(int move) _selectedIndex = virtualItemIndex % virtualItemCount + virtualItemCount - 1; } } + + /// + /// Page up/down within the list view. + /// Update the index of the selected item based on and . + /// + internal bool UpdateListByPaging(bool pageUp, int num) + { + if (_selectedIndex == -1) + { + return false; + } + + int oldSelectedIndex = _selectedIndex; + int lastItemIndex = _listItems.Count - 1; + + for (int i = 0; i < num; i++) + { + if (pageUp) + { + if (_selectedIndex == 0) + { + break; + } + + // Do one page up. + _selectedIndex = _selectedIndex == _listViewEnd - 1 + ? _listViewTop + : Math.Max(0, _selectedIndex - (_maxViewHeight - 1)); + } + else + { + if (_selectedIndex == lastItemIndex) + { + break; + } + + // Do one page down. + _selectedIndex = _selectedIndex == _listViewTop + ? _listViewEnd - 1 + : Math.Min(lastItemIndex, _selectedIndex + (_maxViewHeight - 1)); + } + } + + if (_selectedIndex != oldSelectedIndex) + { + // The selected item is changed, so we need to update the rendering. + _updatePending = true; + return true; + } + + return false; + } + + /// + /// Loop up/down through the sources rendered in the list view. + /// Update the index of the selected item based on and . + /// + internal bool UpdateListByLoopingSources(bool jumpUp, int num) + { + if (_selectedIndex == -1) + { + return false; + } + + int selectedSource = -1; + for (int i = 0; i < _sources.Count; i++) + { + if (_selectedIndex <= _sources[i].EndIndex) + { + selectedSource = i; + break; + } + } + + int oldSelectedIndex = _selectedIndex; + for (int i = 0; i < num; i++) + { + if (jumpUp) + { + _selectedIndex = selectedSource == 0 + ? _sources[_sources.Count - 1].PrevSourceEndIndex + 1 + : _sources[selectedSource - 1].PrevSourceEndIndex + 1; + } + else + { + _selectedIndex = selectedSource == _sources.Count - 1 + ? 0 + : _sources[selectedSource].EndIndex + 1; + } + } + + if (_selectedIndex != oldSelectedIndex) + { + // The selected item is changed, so we need to update the rendering. + _updatePending = true; + _renderFromSelected = true; + return true; + } + + return false; + } } /// diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index 53f6b843..dfa1f1cc 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -163,6 +163,42 @@ private static bool UpdateListSelection(int numericArg) return false; } + private static bool UpdateListByPaging(bool pageUp, int numericArg) + { + if (_singleton._prediction.ActiveView is PredictionListView listView && listView.HasActiveSuggestion) + { + // Ignore the visual selection. + _singleton._visualSelectionCommandCount = 0; + + if (listView.UpdateListByPaging(pageUp, numericArg)) + { + ReplaceSelection(listView.SelectedItemText); + } + + return true; + } + + return false; + } + + private static bool UpdateListByLoopingSources(bool jumpUp, int numericArg) + { + if (_singleton._prediction.ActiveView is PredictionListView listView && listView.HasActiveSuggestion) + { + // Ignore the visual selection. + _singleton._visualSelectionCommandCount = 0; + + if (listView.UpdateListByLoopingSources(jumpUp, numericArg)) + { + ReplaceSelection(listView.SelectedItemText); + } + + return true; + } + + return false; + } + /// /// Replace current buffer with the selected list item text. /// The replacement is done in a way that allows further selection updates for the same list view diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 0c041e73..8fe8de07 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -3,6 +3,7 @@ --********************************************************************/ using System; +using System.Text; namespace Microsoft.PowerShell { @@ -70,6 +71,26 @@ internal static int LengthInBufferCells(string str, int start, int end) return sum; } + internal static int LengthInBufferCells(StringBuilder sb, int start, int end) + { + var sum = 0; + for (var i = start; i < end; i++) + { + var c = sb[i]; + if (c == 0x1b && (i + 1) < end && sb[i + 1] == '[') + { + // Simple escape sequence skipping + i += 2; + while (i < end && sb[i] != 'm') + i++; + + continue; + } + sum += LengthInBufferCells(c); + } + return sum; + } + internal static int LengthInBufferCells(char c) { if (c < 256) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index ff5649fe..d08b6597 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -294,11 +294,7 @@ void RenderOneChar(char charToRender, bool toEmphasize) _consoleBufferLines[currentLogicalLine].Append(VTColorUtils.AnsiReset); } - currentLogicalLine += 1; - if (currentLogicalLine == _consoleBufferLines.Count) - { - _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); - } + NextBufferLine(_consoleBufferLines, ref currentLogicalLine); // Reset the color for continuation prompt so the color sequence will always be explicitly // specified for continuation prompt in the generated render strings. @@ -458,11 +454,7 @@ void RenderOneChar(char charToRender, bool toEmphasize) if (_statusLinePrompt != null) { - currentLogicalLine += 1; - if (currentLogicalLine > _consoleBufferLines.Count - 1) - { - _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); - } + NextBufferLine(_consoleBufferLines, ref currentLogicalLine); color = _statusIsErrorMessage ? Options._errorColor : defaultColor; UpdateColorsIfNecessary(color); @@ -478,6 +470,20 @@ void RenderOneChar(char charToRender, bool toEmphasize) return currentLogicalLine + 1; } + /// + /// Return the next logical line buffer, and create a new one if we are at the end. + /// + private static StringBuilder NextBufferLine(List consoleBufferLines, ref int current) + { + current += 1; + if (current == consoleBufferLines.Count) + { + consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); + } + + return consoleBufferLines[current]; + } + /// /// Flip the color on the prompt if the error state changed. /// @@ -1168,6 +1174,11 @@ private void MoveCursor(int newCursor) _current = newCursor; } + internal Point EndOfBufferPosition() + { + return ConvertOffsetToPoint(_buffer.Length); + } + internal Point ConvertOffsetToPoint(int offset) { int x = _initialX; @@ -1670,6 +1681,12 @@ private bool PromptYesOrNo(string s) public static void ScrollDisplayUp(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByPaging(pageUp: true, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop - (numericArg * console.WindowHeight); if (newTop < 0) @@ -1685,6 +1702,12 @@ public static void ScrollDisplayUp(ConsoleKeyInfo? key = null, object arg = null public static void ScrollDisplayUpLine(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByLoopingSources(jumpUp: true, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop - numericArg; if (newTop < 0) @@ -1700,6 +1723,12 @@ public static void ScrollDisplayUpLine(ConsoleKeyInfo? key = null, object arg = public static void ScrollDisplayDown(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByPaging(pageUp: false, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop + (numericArg * console.WindowHeight); if (newTop > (console.BufferHeight - console.WindowHeight)) @@ -1715,6 +1744,12 @@ public static void ScrollDisplayDown(ConsoleKeyInfo? key = null, object arg = nu public static void ScrollDisplayDownLine(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByLoopingSources(jumpUp: false, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop + numericArg; if (newTop > (console.BufferHeight - console.WindowHeight)) @@ -1738,7 +1773,7 @@ public static void ScrollDisplayTop(ConsoleKeyInfo? key = null, object arg = nul public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg = null) { // Ideally, we'll put the last input line at the bottom of the window - var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + var point = _singleton.EndOfBufferPosition(); var console = _singleton._console; var newTop = point.Y - console.WindowHeight + 1; diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index eeec6a16..238006c0 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -971,6 +971,7 @@ public void MenuCompletions_WorkWithListView() TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); @@ -979,9 +980,13 @@ public void MenuCompletions_WorkWithListView() Test("Get-Module", Keys( "Get-Mo", - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "Get-Mo", NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "Get-Mo", diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index 9c0edd75..7dedb2c5 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -397,6 +397,7 @@ public void ViDefect2408() private const uint MiniSessionId = 56; private static readonly Guid predictorId_1 = Guid.Parse("b45b5fbe-90fa-486c-9c87-e7940fdd6273"); private static readonly Guid predictorId_2 = Guid.Parse("74a86463-033b-44a3-b386-41ee191c94be"); + private static readonly Guid predictorId_3 = Guid.Parse("19e98622-99e0-41f7-9ee0-a8bed92cde51"); /// /// Mocked implementation of 'PredictInput'. @@ -423,13 +424,22 @@ internal static List MockedPredictInput(Ast ast, Token[] token new PredictiveSuggestion($"SOME NEW TEXT"), }; - return new List + var result = new List { (PredictionResult)ctor.Invoke( new object[] { predictorId_1, "TestPredictor", MiniSessionId, suggestions_1 }), (PredictionResult)ctor.Invoke( new object[] { predictorId_2, "LongNamePredictor", MiniSessionId, suggestions_2 }), }; + + // Return an extra source if it's for testing the metadata line. + if (input == "metadata-line") + { + result.Add((PredictionResult)ctor.Invoke( + new object[] { predictorId_3, "Metadata", MiniSessionId, suggestions_2 })); + } + + return result; } [SkippableFact] diff --git a/test/KeyInfo-en-US-windows.json b/test/KeyInfo-en-US-windows.json index 53243ab2..c0fcc65d 100644 --- a/test/KeyInfo-en-US-windows.json +++ b/test/KeyInfo-en-US-windows.json @@ -831,7 +831,7 @@ "Key": "Ctrl+PageUp", "KeyChar": "\u0000", "ConsoleKey": "PageUp", - "Modifiers": "0" + "Modifiers": "Control" }, { "Key": "Ctrl+q", diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 3788fca1..4931867a 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -8,7 +8,7 @@ public partial class ReadLine { // The source of truth is defined in 'Microsoft.PowerShell.PSConsoleReadLine+PredictionListView'. // Make sure the values are in sync. - private const int MinWindowWidth = 54; + private const int MinWindowWidth = 50; private const int MinWindowHeight = 15; private const int ListMaxWidth = 100; private const int SourceMaxWidth = 15; @@ -19,8 +19,8 @@ private int CheckWindowSize() // This is a precaution check, just in case that things change. int winWidth = _console.WindowWidth; int winHeight = _console.WindowHeight; - Assert.True(winWidth >= 54, $"list-view prediction requires minimum window width {MinWindowWidth}. Make sure the TestConsole's width is set properly."); - Assert.True(winHeight >= 15, $"list-view prediction requires minimum window height {MinWindowHeight}. Make sure the TestConsole's height is set properly."); + Assert.True(winWidth >= MinWindowWidth, $"list-view prediction requires minimum window width {MinWindowWidth}. Make sure the TestConsole's width is set properly."); + Assert.True(winHeight >= MinWindowHeight, $"list-view prediction requires minimum window height {MinWindowHeight}. Make sure the TestConsole's height is set properly."); int listWidth = winWidth > ListMaxWidth ? ListMaxWidth : winWidth; return listWidth; @@ -98,15 +98,22 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + // The font effect sequences of the dimmed color used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Different matches as more input coming SetHistory("echo -bar", "eca -zoo"); Test("ech", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -125,9 +132,13 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() TokenClassification.ListPrediction, "History", TokenClassification.None, ']' )), - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "ec", NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "ec", @@ -146,9 +157,13 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() TokenClassification.ListPrediction, "History", TokenClassification.None, ']' )), - 'h', CheckThat(() => AssertScreenIs(2, + 'h', CheckThat(() => AssertScreenIs(3, TokenClassification.Command, "ech", NextLine, + TokenClassification.ListPrediction, "<-/1>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/1>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "ech", @@ -171,15 +186,20 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Navigate up and down in the list SetHistory("echo -bar", "eca -zoo"); Test("e", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -199,11 +219,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -223,11 +249,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -247,9 +279,13 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.ListPredictionSelected, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -269,11 +305,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -293,11 +335,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.ListPredictionSelected, ']' )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -317,9 +365,13 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -353,15 +405,20 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() new KeyHandler("Ctrl+p", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("Ctrl+l", PSConsoleReadLine.HistorySearchForward)); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Navigate up and down in the list SetHistory("echo -bar", "eca -zoo"); Test("e", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -381,11 +438,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_l, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -405,11 +468,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_l, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -429,9 +498,13 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.ListPredictionSelected, ']' )), _.Ctrl_l, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -451,11 +524,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_p, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -475,11 +554,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.ListPredictionSelected, ']' )), _.Ctrl_p, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -499,9 +584,13 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_p, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -533,15 +622,20 @@ public void List_RenderSuggestion_Escape() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Press 'Escape' without selecting an item. SetHistory("echo -bar", "eca -zoo"); Test("echo -bar", Keys( - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -568,9 +662,13 @@ public void List_RenderSuggestion_Escape() TokenClassification.None, new string(' ', listWidth) )), // Keep typing will trigger the list view again - 'h', CheckThat(() => AssertScreenIs(2, + 'h', CheckThat(() => AssertScreenIs(3, TokenClassification.Command, "ch", NextLine, + TokenClassification.ListPrediction, "<-/1>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/1>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, "ch", @@ -581,11 +679,17 @@ public void List_RenderSuggestion_Escape() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(2, + CheckThat(() => AssertScreenIs(3, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<1/1>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/1>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " e", emphasisColors, "ch", @@ -607,9 +711,13 @@ public void List_RenderSuggestion_Escape() // Press 'Escape' after selecting an item. SetHistory("echo -bar", "eca -zoo"); Test("c", Keys( - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -629,11 +737,17 @@ public void List_RenderSuggestion_Escape() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " e", emphasisColors, 'c', @@ -671,14 +785,19 @@ public void List_RenderSuggestion_DigitArgument() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); SetHistory("echo -bar", "eca -zoo"); Test("c", Keys( - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -698,9 +817,13 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.None, ']' )), _.Alt_2, - CheckThat(() => AssertScreenIs(4, + CheckThat(() => AssertScreenIs(5, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -722,11 +845,17 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.None, "digit-argument: 2" )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -746,11 +875,17 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.ListPredictionSelected, ']' )), _.Alt_2, - CheckThat(() => AssertScreenIs(4, + CheckThat(() => AssertScreenIs(5, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -772,9 +907,13 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.None, "digit-argument: 2" )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -806,14 +945,19 @@ public void List_RenderSuggestion_CtrlZ() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); SetHistory("echo -bar", "eca -zoo"); Test("e", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -833,11 +977,17 @@ public void List_RenderSuggestion_CtrlZ() TokenClassification.None, ']' )), _.UpArrow, _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -858,9 +1008,13 @@ public void List_RenderSuggestion_CtrlZ() )), // No matter how many navigation operations were done, 'Ctrl+z' (undo) reverts back to the initial list view state. _.Ctrl_z, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -881,11 +1035,17 @@ public void List_RenderSuggestion_CtrlZ() )), // After undo, you can continue to navigate in the list. _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -918,14 +1078,19 @@ public void List_RenderSuggestion_Selection() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); SetHistory("echo -bar", "eca -zoo"); Test("eca -zoo", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -946,11 +1111,17 @@ public void List_RenderSuggestion_Selection() )), _.DownArrow, CheckThat(() => AssertCursorLeftIs(8)), - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -972,11 +1143,17 @@ public void List_RenderSuggestion_Selection() // Moving cursor won't trigger a new prediction. _.LeftArrow, _.LeftArrow, CheckThat(() => AssertCursorLeftIs(6)), - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -997,11 +1174,17 @@ public void List_RenderSuggestion_Selection() )), _.Ctrl_LeftArrow, CheckThat(() => AssertCursorLeftIs(5)), - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1022,12 +1205,18 @@ public void List_RenderSuggestion_Selection() )), // Text selection won't trigger a new prediction. _.Shift_LeftArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Selection, '-', TokenClassification.Parameter, "zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1047,10 +1236,16 @@ public void List_RenderSuggestion_Selection() TokenClassification.None, ']' )), _.Ctrl_Shift_LeftArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Selection, "eca -", TokenClassification.Parameter, "zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1083,6 +1278,7 @@ public void List_HistorySource_NoAcceptanceCallback() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'History' source will not trigger 'acceptance' callbacks. @@ -1092,11 +1288,17 @@ public void List_HistorySource_NoAcceptanceCallback() SetHistory("echo -bar", "eca -zoo"); Test("eca -zooa", Keys( 'e', _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1140,6 +1342,7 @@ public void List_PluginSource_Acceptance() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'Plugin' source will make PSReadLine get prediction from the plugin only. @@ -1148,9 +1351,13 @@ public void List_PluginSource_Acceptance() SetHistory("echo -bar", "eca -zoo"); Test("SOME NEW TEX SOME TEXT AFTER", Keys( - "ec", CheckThat(() => AssertScreenIs(5, + "ec", CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "ec", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "ec", @@ -1170,9 +1377,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1183,10 +1390,16 @@ public void List_PluginSource_Acceptance() CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => _mockedMethods.ClearPredictionFields()), _.DownArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " TEXT BEFORE ec", NextLine, + TokenClassification.ListPrediction, "<1/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(1/2) ", + dimmedColors, "LongNamePredic�(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", emphasisColors, "ec", @@ -1206,9 +1419,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1217,9 +1430,15 @@ public void List_PluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when navigating the list. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Shift_Home, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Selection, "SOME TEXT BEFORE ec", NextLine, + TokenClassification.ListPrediction, "<1/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(1/2) ", + dimmedColors, "LongNamePredic�(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", emphasisColors, "ec", @@ -1239,9 +1458,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1250,9 +1469,13 @@ public void List_PluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when selecting the input. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), "j", - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "j", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "j", @@ -1272,9 +1495,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1290,10 +1513,16 @@ public void List_PluginSource_Acceptance() _.DownArrow, _.DownArrow, _.DownArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEXT", NextLine, + TokenClassification.ListPrediction, "<3/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + dimmedColors, "', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "j", @@ -1313,9 +1542,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME NEW TEXT", - TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.ListPredictionSelected, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.ListPredictionSelected, ']', // List view is done, no more list item following. NextLine, @@ -1324,10 +1553,14 @@ public void List_PluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when navigating the input. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Backspace, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1349,9 +1582,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1366,10 +1599,16 @@ public void List_PluginSource_Acceptance() CheckThat(() => _mockedMethods.ClearPredictionFields()), _.UpArrow, _.UpArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX SOME TEXT AFTER", NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(2/2) ", + dimmedColors, "LongNamePredic�(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1391,9 +1630,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1424,6 +1663,7 @@ public void List_HistoryAndPluginSource_Acceptance() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. @@ -1432,9 +1672,13 @@ public void List_HistoryAndPluginSource_Acceptance() SetHistory("echo -bar", "java", "eca -zoo"); Test("SOME NEW TEX SOME TEXT AFTER", Keys( - "ec", CheckThat(() => AssertScreenIs(7, + "ec", CheckThat(() => AssertScreenIs(8, TokenClassification.Command, "ec", NextLine, + TokenClassification.ListPrediction, "<-/5>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/5>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "ec", @@ -1472,9 +1716,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1485,9 +1729,15 @@ public void List_HistoryAndPluginSource_Acceptance() CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => _mockedMethods.ClearPredictionFields()), _.DownArrow, _.Shift_Home, - CheckThat(() => AssertScreenIs(7, + CheckThat(() => AssertScreenIs(8, TokenClassification.Selection, "eca -zoo", NextLine, + TokenClassification.ListPrediction, "<1/5>", + TokenClassification.None, new string(' ', listWidth - 38), // 38 is the length of '<1/5>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2) ", + dimmedColors, "TestPredictor(2) �>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, "ec", @@ -1525,9 +1775,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1535,9 +1785,13 @@ public void List_HistoryAndPluginSource_Acceptance() )), // `OnSuggestionDisplayed` should not be fired when navigating the list. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), - 'j', CheckThat(() => AssertScreenIs(6, + 'j', CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "j", NextLine, + TokenClassification.ListPrediction, "<-/4>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "j", @@ -1566,9 +1820,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1583,10 +1837,16 @@ public void List_HistoryAndPluginSource_Acceptance() CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), CheckThat(() => _mockedMethods.ClearPredictionFields()), _.UpArrow, - CheckThat(() => AssertScreenIs(6, + CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEXT", NextLine, + TokenClassification.ListPrediction, "<4/4>", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '<4/4>' plus '<� TestPredictor(2) LongNamePredic�(1/1)>'. + dimmedColors, "<� TestPredictor(2) ", + TokenClassification.ListPrediction, "LongNamePredic�(1/1)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "j", @@ -1615,9 +1875,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME NEW TEXT", - TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.ListPredictionSelected, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.ListPredictionSelected, ']', // List view is done, no more list item following. NextLine, @@ -1626,10 +1886,14 @@ public void List_HistoryAndPluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when navigating the list. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Backspace, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1651,9 +1915,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1668,10 +1932,16 @@ public void List_HistoryAndPluginSource_Acceptance() CheckThat(() => _mockedMethods.ClearPredictionFields()), _.UpArrow, _.UpArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX SOME TEXT AFTER", NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(2/2) ", + dimmedColors, "LongNamePredic�(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1693,9 +1963,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1726,6 +1996,7 @@ public void List_HistoryAndPluginSource_Deduplication() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. @@ -1736,9 +2007,13 @@ public void List_HistoryAndPluginSource_Deduplication() // which is the default comparison. So, that result will be filtered out due to the de-duplication logic. SetHistory("some TEXT BEFORE de-dup", "de-dup -of"); Test("de-dup", Keys( - "de-dup", CheckThat(() => AssertScreenIs(6, + "de-dup", CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "de-dup", NextLine, + TokenClassification.ListPrediction, "<-/4>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "de-dup", @@ -1767,9 +2042,9 @@ public void List_HistoryAndPluginSource_Deduplication() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1796,9 +2071,13 @@ public void List_HistoryAndPluginSource_Deduplication() // so, that result will be filtered out due to the de-duplication logic. SetHistory("de-dup SOME TEXT AFTER", "some TEXT BEFORE de-dup"); Test("de-dup", Keys( - "de-dup", CheckThat(() => AssertScreenIs(6, + "de-dup", CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "de-dup", NextLine, + TokenClassification.ListPrediction, "<-/4>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "de-dup", @@ -1826,9 +2105,9 @@ public void List_HistoryAndPluginSource_Deduplication() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic�]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic�", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, diff --git a/test/ListScrollableViewTest.cs b/test/ListScrollableViewTest.cs new file mode 100644 index 00000000..11452bb2 --- /dev/null +++ b/test/ListScrollableViewTest.cs @@ -0,0 +1,908 @@ +using System; +using Microsoft.PowerShell; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + [SkippableFact] + public void List_MetaLine_And_Paging_Navigation() + { + int listWidth = 100; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 15), KeyMode.Cmd); + + // The font effect sequences of the dimmed color used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + + // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("metadata-line -zoo"); + Test("SOME TEXT BEFORE metadata-line", Keys( + "metadata-line", CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "metadata-line", + NextLine, + TokenClassification.ListPrediction, "<-/5>", + TokenClassification.None, new string(' ', listWidth - 55), // 55 is the length of '<-/5>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, _.PageDown, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<5/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<5/5>' plus '<… TestPredictor(2) LongNamePredic…(1) Metadata(1/1)>'. + dimmedColors, "<… TestPredictor(2) LongNamePredic…(1) ", + TokenClassification.ListPrediction, "Metadata(1/1)", + dimmedColors, '>', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageUp, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.ListPrediction, "<1/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1) ", + dimmedColors, "TestPredictor(2) LongNamePredic…(1) …>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + TokenClassification.ListPrediction, "<2/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<4/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<4/5>' plus '<… TestPredictor(2) LongNamePredic…(1/1) Metadata(1)>'. + dimmedColors, "<… TestPredictor(2) ", + TokenClassification.ListPrediction, "LongNamePredic…(1/1) ", + dimmedColors, "Metadata(1)>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<5/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<5/5>' plus '<… TestPredictor(2) LongNamePredic…(1) Metadata(1/1)>'. + dimmedColors, "<… TestPredictor(2) LongNamePredic…(1) ", + TokenClassification.ListPrediction, "Metadata(1/1)", + dimmedColors, '>', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.ListPrediction, "<1/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1) ", + dimmedColors, "TestPredictor(2) LongNamePredic…(1) …>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageUp, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<5/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<5/5>' plus '<… TestPredictor(2) LongNamePredic…(1) Metadata(1/1)>'. + dimmedColors, "<… TestPredictor(2) LongNamePredic…(1) ", + TokenClassification.ListPrediction, "Metadata(1/1)", + dimmedColors, '>', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageUp, _.Ctrl_PageUp, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + TokenClassification.ListPrediction, "<2/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + NextLine)) + )); + } + + [SkippableFact] + public void ListView_AdapteTo_ConsoleSize() + { + // Console size is very small (h: 6, w: 50), and thus the list view will adjust to use 3-line height, + // and the metadata line will be reduced to only show the (index/total) info. + int listWidth = 50; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 6), KeyMode.Cmd); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + + // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("metadata-line -zoo"); + Test("metadata-line -zoo", Keys( + "metadata-line", CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<-/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.UpArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<5/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.UpArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<4/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageUp, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<2/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageUp, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<1/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageDown, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<3/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " SOME TEXT AFTER", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageDown, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<5/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<-/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<1/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + NextLine)) + )); + } + + [SkippableFact] + public void ListView_TermSize_Warning() + { + // Console size is very small (h: 6, w: 50), and thus the list view will adjust to use 3-line height, + // and the metadata line will be reduced to only show the (index/total) info. + int listWidth = 40; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 4), KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); + + Test("git", Keys( + _.F2, // Switch to the list view, then test the warning message. + 'g', CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "g", + NextLine, + TokenClassification.ListPrediction, "! terminal size too small to show the li", + NextLine, + TokenClassification.ListPrediction, "st view", + // List view is done, no more list item following. + NextLine, + NextLine + )), + 'i', CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "gi", + NextLine, + TokenClassification.ListPrediction, "! terminal size too small to show the li", + NextLine, + TokenClassification.ListPrediction, "st view", + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Escape should clear the warning as well. + _.Escape, CheckThat(() => AssertScreenIs(3, + NextLine, + NextLine, + NextLine + )), + "git", CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "git", + NextLine, + TokenClassification.ListPrediction, "! terminal size too small to show the li", + NextLine, + TokenClassification.ListPrediction, "st view", + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "git", + NextLine, + NextLine, + NextLine)) + )); + } + } +} diff --git a/test/MockConsole.cs b/test/MockConsole.cs index dc218e77..f827d469 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -235,6 +235,13 @@ public virtual void Write(string s) var escapeSequence = s.Substring(i + 2, len); foreach (var subsequence in escapeSequence.Split(';')) { + if (subsequence is "2" or "3") + { + // Ignore the font effect sequence: 2 - dimmed color; 3 - italics + // They are used in the metadata line of the list view. + continue; + } + EscapeSequenceActions[subsequence](this); } i = endSequence; @@ -444,6 +451,13 @@ public override void Write(string s) var escapeSequence = s.Substring(i + 2, len); foreach (var subsequence in escapeSequence.Split(';')) { + if (subsequence is "2" or "3") + { + // Ignore the font effect sequence: 2 - dimmed color; 3 - italics + // They are used in the metadata line of the list view. + continue; + } + EscapeSequenceActions[subsequence](this); } i = endSequence;