internal override int Length(string str, int offset) { if (string.IsNullOrEmpty(str)) { return(0); } if (offset < 0 || offset >= str.Length) { throw PSTraceSource.NewArgumentException(nameof(offset)); } try { var valueStrDec = new ValueStringDecorated(str); if (valueStrDec.IsDecorated) { str = valueStrDec.ToString(OutputRendering.PlainText); } int length = 0; for (; offset < str.Length; offset++) { length += _rawUserInterface.LengthInBufferCells(str[offset]); } return(length); } catch { // thrown when external host rawui is not implemented, in which case // we will fallback to the default value. return(base.Length(str, offset)); } }
/// <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) { var valueStrDec = new ValueStringDecorated(str); if (valueStrDec.IsDecorated) { str = valueStrDec.ToString(OutputRendering.PlainText); } return(GetFitLength(str, offset, displayCells, startFromHead: true)); }
/// <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) { 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); }
/// <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) { if (string.IsNullOrEmpty(str)) { return(0); } var valueStrDec = new ValueStringDecorated(str); if (valueStrDec.IsDecorated) { str = valueStrDec.ToString(OutputRendering.PlainText); } int length = 0; for (; offset < str.Length; offset++) { length += CharLengthInBufferCells(str[offset]); } return(length); }
/// <summary> /// Split a multiline string into an array of strings /// by honoring both \n and \r\n. /// </summary> /// <param name="s">String to split.</param> /// <returns>String array with the values.</returns> internal static List <string> SplitLines(string s) { if (string.IsNullOrEmpty(s) || !s.Contains('\n')) { return(new List <string>(capacity: 1) { s?.Replace("\r", string.Empty) }); } StringBuilder sb = new StringBuilder(); List <string> list = new List <string>(); StringBuilder vtSeqs = null; Dictionary <int, int> vtRanges = null; var valueStrDec = new ValueStringDecorated(s); if (valueStrDec.IsDecorated) { vtSeqs = new StringBuilder(); vtRanges = valueStrDec.EscapeSequenceRanges; } bool hasVtSeqs = false; for (int i = 0; i < s.Length; i++) { if (vtRanges?.TryGetValue(i, out int len) == true) { var vtSpan = s.AsSpan(i, len); sb.Append(vtSpan); vtSeqs.Append(vtSpan); hasVtSeqs = true; i += len - 1; continue; } char c = s[i]; if (c == '\n') { if (hasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset)) { sb.Append(PSStyle.Instance.Reset); } list.Add(sb.ToString()); sb.Clear().Append(vtSeqs); } else if (c != '\r') { sb.Append(c); } } if (hasVtSeqs) { if (sb.Length == vtSeqs.Length) { // This indicates 'sb' only contains all VT sequences, which may happen when the string ends with '\n'. // For a sub-string that contains VT sequence only, it's the same as an empty string to the formatting // system, because nothing will actually be rendered. // So, we use an empty string in this case to avoid unneeded string allocations. sb.Clear(); } else if (!sb.EndsWith(PSStyle.Instance.Reset)) { sb.Append(PSStyle.Instance.Reset); } } list.Add(sb.ToString()); return(list); }
private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen) { StringCollection retVal = new StringCollection(); if (string.IsNullOrEmpty(val)) { // if null or empty, just add and we are done retVal.Add(val); return(retVal); } // break string on newlines and process each line separately List <string> lines = SplitLines(val); for (int k = 0; k < lines.Count; k++) { if (lines[k] == null || displayCells.Length(lines[k]) <= firstLineLen) { // we do not need to split further, just add retVal.Add(lines[k]); continue; } int spacesLeft = firstLineLen; int lineWidth = firstLineLen; bool firstLine = true; StringBuilder singleLine = new StringBuilder(); string resetStr = PSStyle.Instance.Reset; foreach (GetWordsResult word in GetWords(lines[k])) { string wordToAdd = word.Word; string suffix = null; // Handle soft hyphen if (word.Delim.Length == 1 && word.Delim[0] == s_softHyphen) { int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen); // Add hyphen only if necessary if (wordWidthWithHyphen == spacesLeft) { suffix = "-"; } } else if (!string.IsNullOrEmpty(word.Delim)) { suffix = word.Delim; } if (suffix is not null) { wordToAdd = wordToAdd.EndsWith(resetStr) ? wordToAdd.Insert(wordToAdd.Length - resetStr.Length, suffix) : wordToAdd + suffix; } int wordWidth = displayCells.Length(wordToAdd); // Handle zero width if (lineWidth == 0) { if (firstLine) { firstLine = false; lineWidth = followingLinesLen; } if (lineWidth == 0) { break; } spacesLeft = lineWidth; } // Word is wider than a single line if (wordWidth > lineWidth) { Dictionary <int, int> vtRanges = null; StringBuilder vtSeqs = null; var valueStrDec = new ValueStringDecorated(wordToAdd); if (valueStrDec.IsDecorated) { vtSeqs = new StringBuilder(); vtRanges = valueStrDec.EscapeSequenceRanges; } bool hasEscSeqs = false; for (int i = 0; i < wordToAdd.Length; i++) { if (vtRanges?.TryGetValue(i, out int len) == true) { var vtSpan = wordToAdd.AsSpan(i, len); singleLine.Append(vtSpan); vtSeqs.Append(vtSpan); hasEscSeqs = true; i += len - 1; continue; } char charToAdd = wordToAdd[i]; int charWidth = displayCells.Length(charToAdd); // Corner case: we have a two cell character and the current display length is one. // Add a single cell arbitrary character instead of the original one and keep going. if (charWidth > lineWidth) { charToAdd = '?'; charWidth = 1; } if (charWidth > spacesLeft) { if (hasEscSeqs && !singleLine.EndsWith(resetStr)) { singleLine.Append(resetStr); } retVal.Add(singleLine.ToString()); singleLine.Clear().Append(vtSeqs).Append(charToAdd); if (firstLine) { firstLine = false; lineWidth = followingLinesLen; } spacesLeft = lineWidth - charWidth; } else { singleLine.Append(charToAdd); spacesLeft -= charWidth; } } } else { if (wordWidth > spacesLeft) { retVal.Add(singleLine.ToString()); singleLine.Clear().Append(wordToAdd); if (firstLine) { firstLine = false; lineWidth = followingLinesLen; } spacesLeft = lineWidth - wordWidth; } else { singleLine.Append(wordToAdd); spacesLeft -= wordWidth; } } } retVal.Add(singleLine.ToString()); } return(retVal); }
/// <summary> /// Breaks a string into a collection of words /// TODO: we might be able to improve this function in the future /// so that we do not break paths etc. /// </summary> /// <param name="s">Input string.</param> /// <returns>A collection of words.</returns> private static IEnumerable <GetWordsResult> GetWords(string s) { StringBuilder sb = new StringBuilder(); StringBuilder vtSeqs = null; Dictionary <int, int> vtRanges = null; var valueStrDec = new ValueStringDecorated(s); if (valueStrDec.IsDecorated) { vtSeqs = new StringBuilder(); vtRanges = valueStrDec.EscapeSequenceRanges; } bool wordHasVtSeqs = false; for (int i = 0; i < s.Length; i++) { if (vtRanges?.TryGetValue(i, out int len) == true) { var vtSpan = s.AsSpan(i, len); sb.Append(vtSpan); vtSeqs.Append(vtSpan); wordHasVtSeqs = true; i += len - 1; continue; } string delimiter = null; if (s[i] == ' ' || s[i] == '\t' || s[i] == s_softHyphen) { // Soft hyphen = \u00AD - Should break, and add a hyphen if needed. // If not needed for a break, hyphen should be absent. delimiter = new string(s[i], 1); } else if (s[i] == s_hardHyphen || s[i] == s_nonBreakingSpace) { // Non-breaking space = \u00A0 - ideally shouldn't wrap. // Hard hyphen = \u2011 - Should not break. delimiter = string.Empty; } if (delimiter is not null) { if (wordHasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset)) { sb.Append(PSStyle.Instance.Reset); } var result = new GetWordsResult() { Word = sb.ToString(), Delim = delimiter }; sb.Clear().Append(vtSeqs); yield return(result); } else { sb.Append(s[i]); } } if (wordHasVtSeqs) { if (sb.Length == vtSeqs.Length) { // This indicates 'sb' only contains all VT sequences, which may happen when the string ends with a word delimiter. // For a word that contains VT sequence only, it's the same as an empty string to the formatting system, // because nothing will actually be rendered. // So, we use an empty string in this case to avoid unneeded string allocations. sb.Clear(); } else if (!sb.EndsWith(PSStyle.Instance.Reset)) { sb.Append(PSStyle.Instance.Reset); } } yield return(new GetWordsResult() { Word = sb.ToString(), Delim = string.Empty }); }