Skip to content

Commit c40066d

Browse files
authored
Fix the word wrapping in formatting to handle escape sequences properly (PowerShell#17316)
1 parent 41c7cd7 commit c40066d

File tree

8 files changed

+266
-92
lines changed

8 files changed

+266
-92
lines changed

Settings.StyleCop

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
<Value>op</Value>
163163
<Value>my</Value>
164164
<Value>sb</Value>
165+
<Value>vt</Value>
165166
</CollectionProperty>
166167
</AnalyzerSettings>
167168
</Analyzer>

src/System.Management.Automation/FormatAndOutput/common/ComplexWriter.cs

Lines changed: 169 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
using System.Collections.ObjectModel;
77
using System.Collections.Specialized;
88
using System.Globalization;
9+
using System.Management.Automation;
910
using System.Management.Automation.Internal;
1011
using System.Text;
12+
using System.Text.RegularExpressions;
1113

1214
namespace Microsoft.PowerShell.Commands.Internal.Format
1315
{
@@ -146,7 +148,7 @@ private void WriteToScreen()
146148
int indentationAbsoluteValue = (firstLineIndentation > 0) ? firstLineIndentation : -firstLineIndentation;
147149
if (indentationAbsoluteValue >= usefulWidth)
148150
{
149-
// valu too big, we reset it to zero
151+
// value too big, we reset it to zero
150152
firstLineIndentation = 0;
151153
}
152154

@@ -353,27 +355,58 @@ static StringManipulationHelper()
353355
private static IEnumerable<GetWordsResult> GetWords(string s)
354356
{
355357
StringBuilder sb = new StringBuilder();
356-
GetWordsResult result = new GetWordsResult();
358+
StringBuilder vtSeqs = null;
359+
Dictionary<int, int> vtRanges = null;
357360

361+
var valueStrDec = new ValueStringDecorated(s);
362+
if (valueStrDec.IsDecorated)
363+
{
364+
vtSeqs = new StringBuilder();
365+
vtRanges = valueStrDec.EscapeSequenceRanges;
366+
}
367+
368+
bool wordHasVtSeqs = false;
358369
for (int i = 0; i < s.Length; i++)
359370
{
360-
// Soft hyphen = \u00AD - Should break, and add a hyphen if needed. If not needed for a break, hyphen should be absent
361-
if (s[i] == ' ' || s[i] == '\t' || s[i] == s_softHyphen)
371+
if (vtRanges?.TryGetValue(i, out int len) == true)
362372
{
363-
result.Word = sb.ToString();
364-
sb.Clear();
365-
result.Delim = new string(s[i], 1);
373+
var vtSpan = s.AsSpan(i, len);
374+
sb.Append(vtSpan);
375+
vtSeqs.Append(vtSpan);
366376

367-
yield return result;
377+
wordHasVtSeqs = true;
378+
i += len - 1;
379+
continue;
380+
}
381+
382+
string delimiter = null;
383+
if (s[i] == ' ' || s[i] == '\t' || s[i] == s_softHyphen)
384+
{
385+
// Soft hyphen = \u00AD - Should break, and add a hyphen if needed.
386+
// If not needed for a break, hyphen should be absent.
387+
delimiter = new string(s[i], 1);
368388
}
369-
// Non-breaking space = \u00A0 - ideally shouldn't wrap
370-
// Hard hyphen = \u2011 - Should not break
371389
else if (s[i] == s_hardHyphen || s[i] == s_nonBreakingSpace)
372390
{
373-
result.Word = sb.ToString();
374-
sb.Clear();
375-
result.Delim = string.Empty;
391+
// Non-breaking space = \u00A0 - ideally shouldn't wrap.
392+
// Hard hyphen = \u2011 - Should not break.
393+
delimiter = string.Empty;
394+
}
395+
396+
if (delimiter is not null)
397+
{
398+
if (wordHasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset))
399+
{
400+
sb.Append(PSStyle.Instance.Reset);
401+
}
376402

403+
var result = new GetWordsResult()
404+
{
405+
Word = sb.ToString(),
406+
Delim = delimiter
407+
};
408+
409+
sb.Clear().Append(vtSeqs);
377410
yield return result;
378411
}
379412
else
@@ -382,10 +415,23 @@ private static IEnumerable<GetWordsResult> GetWords(string s)
382415
}
383416
}
384417

385-
result.Word = sb.ToString();
386-
result.Delim = string.Empty;
418+
if (wordHasVtSeqs)
419+
{
420+
if (sb.Length == vtSeqs.Length)
421+
{
422+
// This indicates 'sb' only contains all VT sequences, which may happen when the string ends with a word delimiter.
423+
// For a word that contains VT sequence only, it's the same as an empty string to the formatting system,
424+
// because nothing will actually be rendered.
425+
// So, we use an empty string in this case to avoid unneeded string allocations.
426+
sb.Clear();
427+
}
428+
else if (!sb.EndsWith(PSStyle.Instance.Reset))
429+
{
430+
sb.Append(PSStyle.Instance.Reset);
431+
}
432+
}
387433

388-
yield return result;
434+
yield return new GetWordsResult() { Word = sb.ToString(), Delim = string.Empty };
389435
}
390436

391437
internal static StringCollection GenerateLines(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)
@@ -412,9 +458,9 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
412458
}
413459

414460
// break string on newlines and process each line separately
415-
string[] lines = SplitLines(val);
461+
List<string> lines = SplitLines(val);
416462

417-
for (int k = 0; k < lines.Length; k++)
463+
for (int k = 0; k < lines.Count; k++)
418464
{
419465
string currentLine = lines[k];
420466

@@ -530,9 +576,9 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
530576
}
531577

532578
// break string on newlines and process each line separately
533-
string[] lines = SplitLines(val);
579+
List<string> lines = SplitLines(val);
534580

535-
for (int k = 0; k < lines.Length; k++)
581+
for (int k = 0; k < lines.Count; k++)
536582
{
537583
if (lines[k] == null || displayCells.Length(lines[k]) <= firstLineLen)
538584
{
@@ -545,28 +591,34 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
545591
int lineWidth = firstLineLen;
546592
bool firstLine = true;
547593
StringBuilder singleLine = new StringBuilder();
594+
string resetStr = PSStyle.Instance.Reset;
548595

549596
foreach (GetWordsResult word in GetWords(lines[k]))
550597
{
551598
string wordToAdd = word.Word;
599+
string suffix = null;
552600

553601
// Handle soft hyphen
554-
if (word.Delim == s_softHyphen.ToString())
602+
if (word.Delim.Length == 1 && word.Delim[0] == s_softHyphen)
555603
{
556604
int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen);
557605

558606
// Add hyphen only if necessary
559607
if (wordWidthWithHyphen == spacesLeft)
560608
{
561-
wordToAdd += "-";
609+
suffix = "-";
562610
}
563611
}
564-
else
612+
else if (!string.IsNullOrEmpty(word.Delim))
565613
{
566-
if (!string.IsNullOrEmpty(word.Delim))
567-
{
568-
wordToAdd += word.Delim;
569-
}
614+
suffix = word.Delim;
615+
}
616+
617+
if (suffix is not null)
618+
{
619+
wordToAdd = wordToAdd.EndsWith(resetStr)
620+
? wordToAdd.Insert(wordToAdd.Length - resetStr.Length, suffix)
621+
: wordToAdd + suffix;
570622
}
571623

572624
int wordWidth = displayCells.Length(wordToAdd);
@@ -591,15 +643,35 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
591643
// Word is wider than a single line
592644
if (wordWidth > lineWidth)
593645
{
594-
foreach (char c in wordToAdd)
646+
Dictionary<int, int> vtRanges = null;
647+
StringBuilder vtSeqs = null;
648+
649+
var valueStrDec = new ValueStringDecorated(wordToAdd);
650+
if (valueStrDec.IsDecorated)
595651
{
596-
char charToAdd = c;
597-
int charWidth = displayCells.Length(c);
652+
vtSeqs = new StringBuilder();
653+
vtRanges = valueStrDec.EscapeSequenceRanges;
654+
}
598655

599-
// corner case: we have a two cell character and the current
600-
// display length is one.
601-
// add a single cell arbitrary character instead of the original
602-
// one and keep going
656+
bool hasEscSeqs = false;
657+
for (int i = 0; i < wordToAdd.Length; i++)
658+
{
659+
if (vtRanges?.TryGetValue(i, out int len) == true)
660+
{
661+
var vtSpan = wordToAdd.AsSpan(i, len);
662+
singleLine.Append(vtSpan);
663+
vtSeqs.Append(vtSpan);
664+
665+
hasEscSeqs = true;
666+
i += len - 1;
667+
continue;
668+
}
669+
670+
char charToAdd = wordToAdd[i];
671+
int charWidth = displayCells.Length(charToAdd);
672+
673+
// Corner case: we have a two cell character and the current display length is one.
674+
// Add a single cell arbitrary character instead of the original one and keep going.
603675
if (charWidth > lineWidth)
604676
{
605677
charToAdd = '?';
@@ -608,9 +680,13 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
608680

609681
if (charWidth > spacesLeft)
610682
{
683+
if (hasEscSeqs && !singleLine.EndsWith(resetStr))
684+
{
685+
singleLine.Append(resetStr);
686+
}
687+
611688
retVal.Add(singleLine.ToString());
612-
singleLine.Clear();
613-
singleLine.Append(charToAdd);
689+
singleLine.Clear().Append(vtSeqs).Append(charToAdd);
614690

615691
if (firstLine)
616692
{
@@ -632,8 +708,7 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
632708
if (wordWidth > spacesLeft)
633709
{
634710
retVal.Add(singleLine.ToString());
635-
singleLine.Clear();
636-
singleLine.Append(wordToAdd);
711+
singleLine.Clear().Append(wordToAdd);
637712

638713
if (firstLine)
639714
{
@@ -663,49 +738,77 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
663738
/// </summary>
664739
/// <param name="s">String to split.</param>
665740
/// <returns>String array with the values.</returns>
666-
internal static string[] SplitLines(string s)
741+
internal static List<string> SplitLines(string s)
667742
{
668-
if (string.IsNullOrEmpty(s))
669-
return new string[1] { s };
743+
if (string.IsNullOrEmpty(s) || !s.Contains('\n'))
744+
{
745+
return new List<string>(capacity: 1) { s?.Replace("\r", string.Empty) };
746+
}
670747

671748
StringBuilder sb = new StringBuilder();
749+
List<string> list = new List<string>();
750+
751+
StringBuilder vtSeqs = null;
752+
Dictionary<int, int> vtRanges = null;
672753

673-
foreach (char c in s)
754+
var valueStrDec = new ValueStringDecorated(s);
755+
if (valueStrDec.IsDecorated)
674756
{
675-
if (c != '\r')
676-
sb.Append(c);
757+
vtSeqs = new StringBuilder();
758+
vtRanges = valueStrDec.EscapeSequenceRanges;
677759
}
678760

679-
return sb.ToString().Split(s_newLineChar);
680-
}
681-
682-
#if false
683-
internal static string StripNewLines (string s)
684-
{
685-
if (string.IsNullOrEmpty (s))
686-
return s;
687-
688-
string[] lines = SplitLines (s);
761+
bool hasVtSeqs = false;
762+
for (int i = 0; i < s.Length; i++)
763+
{
764+
if (vtRanges?.TryGetValue(i, out int len) == true)
765+
{
766+
var vtSpan = s.AsSpan(i, len);
767+
sb.Append(vtSpan);
768+
vtSeqs.Append(vtSpan);
689769

690-
if (lines.Length == 0)
691-
return null;
770+
hasVtSeqs = true;
771+
i += len - 1;
772+
continue;
773+
}
692774

693-
if (lines.Length == 1)
694-
return lines[0];
775+
char c = s[i];
776+
if (c == '\n')
777+
{
778+
if (hasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset))
779+
{
780+
sb.Append(PSStyle.Instance.Reset);
781+
}
695782

696-
StringBuilder sb = new StringBuilder ();
783+
list.Add(sb.ToString());
784+
sb.Clear().Append(vtSeqs);
785+
}
786+
else if (c != '\r')
787+
{
788+
sb.Append(c);
789+
}
790+
}
697791

698-
for (int k = 0; k < lines.Length; k++)
792+
if (hasVtSeqs)
699793
{
700-
if (k == 0)
701-
sb.Append (lines[k]);
702-
else
703-
sb.Append (" " + lines[k]);
794+
if (sb.Length == vtSeqs.Length)
795+
{
796+
// This indicates 'sb' only contains all VT sequences, which may happen when the string ends with '\n'.
797+
// For a sub-string that contains VT sequence only, it's the same as an empty string to the formatting
798+
// system, because nothing will actually be rendered.
799+
// So, we use an empty string in this case to avoid unneeded string allocations.
800+
sb.Clear();
801+
}
802+
else if (!sb.EndsWith(PSStyle.Instance.Reset))
803+
{
804+
sb.Append(PSStyle.Instance.Reset);
805+
}
704806
}
705807

706-
return sb.ToString ();
808+
list.Add(sb.ToString());
809+
return list;
707810
}
708-
#endif
811+
709812
internal static string TruncateAtNewLine(string s)
710813
{
711814
if (string.IsNullOrEmpty(s))

src/System.Management.Automation/FormatAndOutput/common/ILineOutput.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Collections.Generic;
45
using System.Globalization;
56
using System.IO;
67
using System.Management.Automation;
@@ -379,10 +380,10 @@ private void WriteLineInternal(string val, int cols)
379380
}
380381

381382
// check for line breaks
382-
string[] lines = StringManipulationHelper.SplitLines(val);
383+
List<string> lines = StringManipulationHelper.SplitLines(val);
383384

384385
// process the substrings as separate lines
385-
for (int k = 0; k < lines.Length; k++)
386+
for (int k = 0; k < lines.Count; k++)
386387
{
387388
// compute the display length of the string
388389
int displayLength = _displayCells.Length(lines[k]);

0 commit comments

Comments
 (0)