Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
{
// we are not at the end of the string, select a sub string
// that would fit in the remaining display length
int charactersToAdd = displayCells.GetHeadSplitLength(currentLine, offset, currentDisplayLen);
int charactersToAdd = displayCells.TruncateTail(currentLine, offset, currentDisplayLen);

if (charactersToAdd <= 0)
{
Expand All @@ -465,7 +465,7 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
else
{
// of the given length, add it to the accumulator
accumulator.AddLine(currentLine.Substring(offset, charactersToAdd));
accumulator.AddLine(currentLine.VtSubstring(offset, charactersToAdd));
}

// increase the offset by the # of characters added
Expand All @@ -474,7 +474,7 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
else
{
// we reached the last (partial) line, we add it all
accumulator.AddLine(currentLine.Substring(offset));
accumulator.AddLine(currentLine.VtSubstring(offset));
break;
}
}
Expand Down Expand Up @@ -553,7 +553,7 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
// Handle soft hyphen
if (word.Delim == s_softHyphen.ToString())
{
int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen.ToString());
int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen);

// Add hyphen only if necessary
if (wordWidthWithHyphen == spacesLeft)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,112 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
{
/// <summary>
/// Base class providing support for string manipulation.
/// This class is a tear off class provided by the LineOutput class
///
/// Assumptions (in addition to the assumptions made for LineOutput):
/// - characters map to one or more character cells
///
/// NOTE: we provide a base class that is valid for devices that have a
/// 1:1 mapping between a UNICODE character and a display cell.
/// This class is a tear off class provided by the LineOutput class.
/// </summary>
internal class DisplayCells
{
internal virtual int Length(string str)
/// <summary>
/// Calculate the buffer cell length of the given string.
/// </summary>
/// <param name="str">String that may contain VT escape sequences.</param>
/// <returns>Number of buffer cells the string needs to take.</returns>
internal int Length(string str)
{
return Length(str, 0);
}

/// <summary>
/// Calculate the buffer cell length of the given string.
/// </summary>
/// <param name="str">String that may contain VT escape sequences.</param>
/// <param name="offset">
/// When the string doesn't contain VT sequences, it's the starting index.
/// When the string contains VT sequences, it means starting from the 'n-th' char that doesn't belong to a escape sequence.</param>
/// <returns>Number of buffer cells the string needs to take.</returns>
internal virtual int Length(string str, int offset)
{
int length = 0;
if (string.IsNullOrEmpty(str))
{
return 0;
}

foreach (char c in str)
var valueStrDec = new ValueStringDecorated(str);
if (valueStrDec.IsDecorated)
{
length += LengthInBufferCells(c);
str = valueStrDec.ToString(OutputRendering.PlainText);
}

return length - offset;
}
int length = 0;
for (; offset < str.Length; offset++)
{
length += CharLengthInBufferCells(str[offset]);
}

internal virtual int Length(char character) { return 1; }
return length;
}

internal virtual int GetHeadSplitLength(string str, int displayCells)
/// <summary>
/// Calculate the buffer cell length of the given character.
/// </summary>
/// <param name="character"></param>
/// <returns>Number of buffer cells the character needs to take.</returns>
internal virtual int Length(char character)
{
return GetHeadSplitLength(str, 0, displayCells);
return CharLengthInBufferCells(character);
}

internal virtual int GetHeadSplitLength(string str, int offset, int displayCells)
/// <summary>
/// Truncate from the tail of the string.
/// </summary>
/// <param name="str">String that may contain VT escape sequences.</param>
/// <param name="displayCells">Number of buffer cells to fit in.</param>
/// <returns>Number of non-escape-sequence characters from head of the string that can fit in the space.</returns>
internal int TruncateTail(string str, int displayCells)
{
int len = str.Length - offset;
return (len < displayCells) ? len : displayCells;
return TruncateTail(str, offset: 0, displayCells);
}

internal virtual int GetTailSplitLength(string str, int displayCells)
/// <summary>
/// Truncate from the tail of the string.
/// </summary>
/// <param name="str">String that may contain VT escape sequences.</param>
/// <param name="offset">
/// When the string doesn't contain VT sequences, it's the starting index.
/// When the string contains VT sequences, it means starting from the 'n-th' char that doesn't belong to a escape sequence.</param>
/// <param name="displayCells">Number of buffer cells to fit in.</param>
/// <returns>Number of non-escape-sequence characters from head of the string that can fit in the space.</returns>
internal int TruncateTail(string str, int offset, int displayCells)
{
return GetTailSplitLength(str, 0, displayCells);
var valueStrDec = new ValueStringDecorated(str);
if (valueStrDec.IsDecorated)
{
str = valueStrDec.ToString(OutputRendering.PlainText);
}

return GetFitLength(str, offset, displayCells, startFromHead: true);
}

internal virtual int GetTailSplitLength(string str, int offset, int displayCells)
/// <summary>
/// Truncate from the head of the string.
/// </summary>
/// <param name="str">String that may contain VT escape sequences.</param>
/// <param name="displayCells">Number of buffer cells to fit in.</param>
/// <returns>Number of non-escape-sequence characters from head of the string that should be skipped.</returns>
internal int TruncateHead(string str, int displayCells)
{
int len = str.Length - offset;
return (len < displayCells) ? len : displayCells;
var valueStrDec = new ValueStringDecorated(str);
if (valueStrDec.IsDecorated)
{
str = valueStrDec.ToString(OutputRendering.PlainText);
}

int tailCount = GetFitLength(str, offset: 0, displayCells, startFromHead: false);
return str.Length - tailCount;
}

#region Helpers

protected static int LengthInBufferCells(char c)
protected static int CharLengthInBufferCells(char c)
{
// The following is based on http://www.cl.cam.ac.uk/~mgk25/c/wcwidth.c
// which is derived from https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
Expand All @@ -94,25 +146,26 @@ protected static int LengthInBufferCells(char c)
/// Given a string and a number of display cells, it computes how many
/// characters would fit starting from the beginning or end of the string.
/// </summary>
/// <param name="str">String to be displayed.</param>
/// <param name="str">String to be displayed, which doesn't contain any VT sequences.</param>
/// <param name="offset">Offset inside the string.</param>
/// <param name="displayCells">Number of display cells.</param>
/// <param name="head">If true compute from the head (i.e. k++) else from the tail (i.e. k--).</param>
/// <param name="startFromHead">If true compute from the head (i.e. k++) else from the tail (i.e. k--).</param>
/// <returns>Number of characters that would fit.</returns>
protected int GetSplitLengthInternalHelper(string str, int offset, int displayCells, bool head)
protected int GetFitLength(string str, int offset, int displayCells, bool startFromHead)
{
int filledDisplayCellsCount = 0; // number of cells that are filled in
int charactersAdded = 0; // number of characters that fit
int currCharDisplayLen; // scratch variable

int k = (head) ? offset : str.Length - 1;
int kFinal = (head) ? str.Length - 1 : offset;
int k = startFromHead ? offset : str.Length - 1;
int kFinal = startFromHead ? str.Length - 1 : offset;
while (true)
{
if ((head && (k > kFinal)) || ((!head) && (k < kFinal)))
if ((startFromHead && k > kFinal) || (!startFromHead && k < kFinal))
{
break;
}

// compute the cell number for the current character
currCharDisplayLen = this.Length(str[k]);

Expand All @@ -121,6 +174,7 @@ protected int GetSplitLengthInternalHelper(string str, int offset, int displayCe
// if we added this character it would not fit, we cannot continue
break;
}

// keep adding, we fit
filledDisplayCellsCount += currCharDisplayLen;
charactersAdded++;
Expand All @@ -132,13 +186,13 @@ protected int GetSplitLengthInternalHelper(string str, int offset, int displayCe
break;
}

k = (head) ? (k + 1) : (k - 1);
k = startFromHead ? (k + 1) : (k - 1);
}

return charactersAdded;
}
#endregion

#endregion
}

/// <summary>
Expand Down Expand Up @@ -354,11 +408,11 @@ private void WriteLineInternal(string val, int cols)
{
// the string is still too long to fit, write the first cols characters
// and go back for more wraparound
int splitLen = _displayCells.GetHeadSplitLength(s, cols);
WriteLineInternal(s.Substring(0, splitLen), cols);
int headCount = _displayCells.TruncateTail(s, cols);
WriteLineInternal(s.VtSubstring(0, headCount), cols);

// chop off the first fieldWidth characters, already printed
s = s.Substring(splitLen);
s = s.VtSubstring(headCount);
if (_displayCells.Length(s) <= cols)
{
// if we fit, print the tail of the string and we are done
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,20 @@ internal void Initialize(string[] propertyNames, int screenColumnWidth, DisplayC

for (int k = 0; k < propertyNames.Length; k++)
{
string propertyName = propertyNames[k];
if (propertyNameCellCounts[k] < _propertyLabelsDisplayLength)
{
// shorter than the max, add padding
_propertyLabels[k] = propertyNames[k] + StringUtil.Padding(_propertyLabelsDisplayLength - propertyNameCellCounts[k]);
_propertyLabels[k] = propertyName + StringUtil.Padding(_propertyLabelsDisplayLength - propertyNameCellCounts[k]);
}
else if (propertyNameCellCounts[k] > _propertyLabelsDisplayLength)
{
// longer than the max, clip
_propertyLabels[k] = propertyNames[k].Substring(0, dc.GetHeadSplitLength(propertyNames[k], _propertyLabelsDisplayLength));
_propertyLabels[k] = propertyName.VtSubstring(0, dc.TruncateTail(propertyName, _propertyLabelsDisplayLength));
}
else
{
_propertyLabels[k] = propertyNames[k];
_propertyLabels[k] = propertyName;
}

_propertyLabels[k] += Separator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ private sealed class ScreenInfo

private ScreenInfo _si;

private const char ESC = '\u001b';

private List<string> _header;

internal static int ComputeWideViewBestItemsPerRowFit(int stringLen, int screenColumns)
Expand Down Expand Up @@ -457,8 +455,10 @@ private string GenerateRow(string[] values, ReadOnlySpan<int> alignment, Display
}
}

sb.Append(GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc, addPadding));
if (values[k] is not null && values[k].Contains(ESC))
string rowField = GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc, addPadding);
sb.Append(rowField);

if (rowField is not null && rowField.Contains(ValueStringDecorated.ESC) && !rowField.AsSpan().TrimEnd().EndsWith(PSStyle.Instance.Reset))
{
// Reset the console output if the content of this column contains ESC
sb.Append(PSStyle.Instance.Reset);
Expand All @@ -472,9 +472,7 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
{
// make sure the string does not have any embedded <CR> in it
string s = StringManipulationHelper.TruncateAtNewLine(val);

string currentValue = s;
int currentValueDisplayLength = dc.Length(currentValue);
int currentValueDisplayLength = dc.Length(s);

if (currentValueDisplayLength < width)
{
Expand Down Expand Up @@ -531,18 +529,10 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
case TextAlignment.Right:
{
// get from "abcdef" to "...f"
int tailCount = dc.GetTailSplitLength(s, truncationDisplayLength);
s = s.Substring(s.Length - tailCount);
s = PSObjectHelper.Ellipsis + s;
}

break;

case TextAlignment.Center:
{
// get from "abcdef" to "a..."
s = s.Substring(0, dc.GetHeadSplitLength(s, truncationDisplayLength));
s += PSObjectHelper.Ellipsis;
s = s.VtSubstring(
startOffset: dc.TruncateHead(s, truncationDisplayLength),
prependStr: PSObjectHelper.EllipsisStr,
appendStr: null);
}

break;
Expand All @@ -551,8 +541,11 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
{
// left align is the default
// get from "abcdef" to "a..."
s = s.Substring(0, dc.GetHeadSplitLength(s, truncationDisplayLength));
s += PSObjectHelper.Ellipsis;
s = s.VtSubstring(
startOffset: 0,
length: dc.TruncateTail(s, truncationDisplayLength),
prependStr: null,
appendStr: PSObjectHelper.EllipsisStr);
}

break;
Expand All @@ -561,23 +554,12 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
else
{
// not enough space for the ellipsis, just truncate at the width
int len = width;

switch (alignment)
{
case TextAlignment.Right:
{
// get from "abcdef" to "f"
int tailCount = dc.GetTailSplitLength(s, len);
s = s.Substring(s.Length - tailCount, tailCount);
}

break;

case TextAlignment.Center:
{
// get from "abcdef" to "a"
s = s.Substring(0, dc.GetHeadSplitLength(s, len));
s = s.VtSubstring(startOffset: dc.TruncateHead(s, width));
}

break;
Expand All @@ -586,7 +568,7 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
{
// left align is the default
// get from "abcdef" to "a"
s = s.Substring(0, dc.GetHeadSplitLength(s, len));
s = s.VtSubstring(startOffset: 0, length: dc.TruncateTail(s, width));
}

break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal static class PSObjectHelper
#endregion tracer

internal const char Ellipsis = '\u2026';
internal const string EllipsisStr = "\u2026";

internal static string PSObjectIsOfExactType(Collection<string> typeNames)
{
Expand Down
Loading