/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Text; 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 ToolTip; internal readonly string SuggestionText; internal readonly int InputMatchIndex; private string _listItemTextRegular; private string _listItemTextSelected; internal SuggestionEntry(string suggestion, int matchIndex) : this(source: HistorySource, predictorId: Guid.Empty, predictorSession: null, suggestion, tooltip: null, matchIndex) { } internal SuggestionEntry(string source, Guid predictorId, uint? predictorSession, string suggestion, string tooltip, int matchIndex) { Source = source; PredictorId = predictorId; PredictorSession = predictorSession; SuggestionText = suggestion; ToolTip = tooltip; InputMatchIndex = matchIndex; _listItemTextRegular = _listItemTextSelected = null; } /// /// Helper method to get the rounded-up result of a division. /// private static int DivideAndRoundUp(int dividend, int divisor) { return (dividend + divisor - 1) / divisor; } /// /// Generate the list item text to be rendered for the list view. /// /// /// The list item text is in this format: /// {> --------------------ITEM TEXT-------------------- [SOURCE]} /// The leading character '>' and the 'SOURCE' portion are rendered with the configured metadata color (ListPredictionColor). /// When the 'ITEM TEXT' portion contains the user input, the matching part will be rendered with the configured emphasis color. /// When the current suggestion entry is selected in the list view, the whole line is rendered with the configured highlighting color (ListPredictionSelectedColor). /// /// The width of the list item. /// The user input. /// The highlighting sequences for a selected list item. internal string GetListItemText(int width, string input, string selectionHighlighting) { 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; int sourceWidth = LengthInBufferCells(Source); if (sourceWidth > PredictionListView.SourceMaxWidth) { sourceWidth = PredictionListView.SourceMaxWidth; sourceStrLen = SubstringLengthByCells(Source, sourceWidth - ellipsisLength); } // Calculate the remaining width after deducting the ' [SOURCE]' portion and the leading '> ' part. // 5 is the length of the decoration characters: "> ", " [", and ']'. int textWidth = width - sourceWidth - 5; string textMetadataColor = _singleton._options._listPredictionColor; StringBuilder line = new StringBuilder(capacity: width) .Append(selectionHighlighting) .Append(textMetadataColor) .Append('>') .EndColorSection(selectionHighlighting) .Append(' '); int textLengthInCells = LengthInBufferCells(SuggestionText); if (textLengthInCells <= textWidth) { // Things are easy when the suggestion text can fit in the text width. switch (InputMatchIndex) { case -1: line.Append(SuggestionText); break; default: { int start = InputMatchIndex + input.Length; int length = SuggestionText.Length - start; line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, start, length); break; } } // Do padding as necessary. int spacesNeeded = textWidth - textLengthInCells; if (spacesNeeded > 0) { line.Append(Spaces(spacesNeeded)); } } else { // Things are more complicated when the suggestion text doesn't fit in the text width. switch (InputMatchIndex) { case -1: { // The suggestion text doesn't contain the user input. int length = SubstringLengthByCells(SuggestionText, textWidth - ellipsisLength); line.Append(SuggestionText, 0, length) .Append(Ellipsis); break; } case 0: { // The suggestion text starts with the user input, so we can divide the suggestion text into // the user input (left portion) and the prediction text (right portion). int inputLenInCells = LengthInBufferCells(input); if (inputLenInCells < textWidth / 2) { // If the user input portion takes less than half of the text width, // then we just truncate the suggestion text at the end. int length = SubstringLengthByCells(SuggestionText, textWidth - ellipsisLength); line.Append(_singleton._options.EmphasisColor) .Append(SuggestionText, 0, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, length - input.Length) .Append(Ellipsis); } else { // We want to reserve 5 cells at least to display the trailing characters of the user input (the left portion). int rightLenInCells = LengthInBufferCells(SuggestionText, input.Length, SuggestionText.Length); if (rightLenInCells <= textWidth - ellipsisLength - 5) { // If the prediction text (the right portion) can fit in the rest width, the list item // will be rendered as '...LLLLLRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR' int remainingLenInCells = textWidth - ellipsisLength - rightLenInCells; int length = SubstringLengthByCellsFromEnd(SuggestionText, input.Length - 1, remainingLenInCells); line.Append(_singleton._options.EmphasisColor) .Append(Ellipsis) .Append(SuggestionText, input.Length - length, length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, SuggestionText.Length - input.Length); } else { // If the prediction text (the right portion) cannot fit in the rest width, the list // item is rendered as '...LLLLLRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR...' int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, input.Length - 1, 5); int startIndex = input.Length - leftStrLen; int totalStrLen = SubstringLengthByCells(SuggestionText, startIndex, textWidth - ellipsisLength * 2); line.Append(_singleton._options.EmphasisColor) .Append(Ellipsis) .Append(SuggestionText, startIndex, leftStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, totalStrLen - leftStrLen) .Append(Ellipsis); } } break; } default: { // The user input is contained in the middle or at the end of the suggestion text, so we can // divide the suggestion text into three portions: // - prediction text on the left of the user input (left portion); // - user input (mid portion); // - prediction text on the right of the user input (right portion). int leftMidLenInCells = LengthInBufferCells(SuggestionText, 0, InputMatchIndex + input.Length); int rightStartindex = InputMatchIndex + input.Length; // Round up the 2/3 of the text width and use that as the threshold. int threshold = DivideAndRoundUp(textWidth * 2, 3); if (leftMidLenInCells <= threshold) { // If the (left+mid) portions take up to 2/3 of the text width, we just truncate the suggestion text at the end. int rightStrLen = SubstringLengthByCells(SuggestionText, rightStartindex, textWidth - leftMidLenInCells - ellipsisLength); line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) .Append(Ellipsis); break; } int midRightLenInCells = LengthInBufferCells(SuggestionText, InputMatchIndex, SuggestionText.Length); if (midRightLenInCells <= threshold) { // 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) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); break; } int midLenInCells = LengthInBufferCells(SuggestionText, InputMatchIndex, InputMatchIndex + input.Length); // Round up the 1/3 of the text width and use that as the threshold. threshold = DivideAndRoundUp(textWidth, 3); if (midLenInCells <= threshold) { // Otherwise, if the mid portion takes up to 1/3 of the text width, we truncate the suggestion text // at both the beginning and the end. int leftCellLen = (textWidth - midLenInCells) / 2; int rigthCellLen = textWidth - midLenInCells - leftCellLen; int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, leftCellLen - ellipsisLength); int rightStrLen = SubstringLengthByCells(SuggestionText, rightStartindex, rigthCellLen - ellipsisLength); 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); break; } int leftPlusRightLenInCells = leftMidLenInCells + midRightLenInCells - midLenInCells * 2; if (leftPlusRightLenInCells <= textWidth - 7) { // Otherwise, the mid portion is relatively too long. In this case, if the (left+right) portions are not // too long -- namely we can reserve 7 cells at least for the mid portion, including '...' -- then let's // render the list item text as: 'LLLLLLLLLLLLLLLLLLMM...MMRRRRRRRRRRRRRRRRRR' int midRemainingLenInCells = textWidth - leftPlusRightLenInCells - ellipsisLength; int midLeftCellLen = midRemainingLenInCells / 2; int midRightCellLen = midRemainingLenInCells - midLeftCellLen; int midLeftStrLen = SubstringLengthByCells(SuggestionText, InputMatchIndex, midLeftCellLen); int midRightStrLen = SubstringLengthByCellsFromEnd(SuggestionText, rightStartindex - 1, midRightCellLen); line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); break; } // If we reach here, then we know that the mid portion takes longer than 1/3 of the text width, // and the (left+right) portions takes pretty long too, very likely close to the text width. int leftLenInCells = leftMidLenInCells - midLenInCells; int rightLenInCells = midRightLenInCells - midLenInCells; if (leftLenInCells <= threshold) { // If the left portion is less than or equal to 1/3 of the text width, then we display the whole left portion, // reserve 1/3 of the text width to the mid portion, and the rest width is for the right portion. So the list // item text looks like: 'LLLLLLLLLMMMMMMMM...MMMMMMMMRRRRRRRRRRRRRRRRRRRRR...' int midRemainingLenInCells = textWidth / 3 - ellipsisLength; int midLeftCellLen = midRemainingLenInCells / 2; int midRightCellLen = midRemainingLenInCells - midLeftCellLen; int midLeftStrLen = SubstringLengthByCells(SuggestionText, InputMatchIndex, midLeftCellLen); int midRightStrLen = SubstringLengthByCellsFromEnd(SuggestionText, rightStartindex - 1, midRightCellLen); int rightStrLen = SubstringLengthByCells(SuggestionText, rightStartindex, midRemainingLenInCells); line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) .Append(Ellipsis); break; } if (rightLenInCells <= threshold) { // Similarly, if the right portion is less than or equal to 1/3 of the text width, then we display the whole // right portion, reserve 1/3 of the text width to the mid portion, and the rest width is for the left portion. // So the list item text looks like: '...LLLLLLLLLLLLLLLLMMMMMMMM...MMMMMMMMRRRRRRRRRRRRRR' int midRemainingLenInCells = textWidth / 3 - ellipsisLength; int midLeftCellLen = midRemainingLenInCells / 2; int midRightCellLen = midRemainingLenInCells - midLeftCellLen; int midLeftStrLen = SubstringLengthByCells(SuggestionText, InputMatchIndex, midLeftCellLen); int midRightStrLen = SubstringLengthByCellsFromEnd(SuggestionText, rightStartindex - 1, midRightCellLen); int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, midRemainingLenInCells); line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); break; } { // All left, mid, and right portions take longer than 1/3 of the text width. We assign 1/3 of the text width // to each of them equally in this case, so the list item text looks like: // '...LLLLLLLLLLLLLLMMMMMMMM...MMMMMMMMRRRRRRRRRRRRRRRR...' int midRemainingLenInCells = textWidth / 3 - ellipsisLength; int midLeftCellLen = midRemainingLenInCells / 2; int midRightCellLen = midRemainingLenInCells - midLeftCellLen; int midLeftStrLen = SubstringLengthByCells(SuggestionText, InputMatchIndex, midLeftCellLen); int midRightStrLen = SubstringLengthByCellsFromEnd(SuggestionText, rightStartindex - 1, midRightCellLen); int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, midRemainingLenInCells); int rightStrLen = SubstringLengthByCells(SuggestionText, rightStartindex, midRemainingLenInCells); int spacesNeeded = textWidth - midRemainingLenInCells * 3 - ellipsisLength * 3; string spaces = spacesNeeded > 0 ? Spaces(spacesNeeded) : string.Empty; line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) .Append(Ellipsis) .Append(spaces); break; } } } } line.Append(' ') .Append('[') .Append(textMetadataColor); if (sourceStrLen == Source.Length) { line.Append(Source); } else { line.Append(Source, 0, sourceStrLen) .Append(Ellipsis); } line.EndColorSection(selectionHighlighting) .Append(']'); 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; } } } }