MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System;5using System.Collections.Generic;6using System.Collections.ObjectModel;7using System.Collections.Specialized;8using System.Globalization;9using System.Management.Automation;10using System.Management.Automation.Internal;11using System.Text;12 13namespace Microsoft.PowerShell.Commands.Internal.Format14{15 /// <summary>16 /// Writer class to handle Complex Object formatting.17 /// </summary>18 internal sealed class ComplexWriter19 {20 /// <summary>21 /// Initialization method to be called before any other operation.22 /// </summary>23 /// <param name="lineOutput">LineOutput interfaces to write to.</param>24 /// <param name="numberOfTextColumns">Number of columns used to write out.</param>25 internal void Initialize(LineOutput lineOutput, int numberOfTextColumns)26 {27 _lo = lineOutput;28 _textColumns = numberOfTextColumns;29 }30 31 /// <summary>32 /// Writes a string.33 /// </summary>34 /// <param name="s"></param>35 internal void WriteString(string s)36 {37 _indentationManager.Clear();38 39 AddToBuffer(s);40 41 WriteToScreen();42 }43 44 /// <summary>45 /// It interprets a list of format value tokens and outputs it.46 /// </summary>47 /// <param name="formatValueList">List of FormatValue tokens to interpret.</param>48 internal void WriteObject(List<FormatValue> formatValueList)49 {50 // we always start with no indentation51 _indentationManager.Clear();52 53 foreach (FormatEntry fe in formatValueList)54 {55 // operate on each directive inside the list,56 // carrying the indentation from invocation to invocation57 GenerateFormatEntryDisplay(fe, 0);58 }59 // make sure that, if we have pending text in the buffer it gets flushed60 WriteToScreen();61 }62 63 /// <summary>64 /// Operate on a single entry.65 /// </summary>66 /// <param name="fe">Entry to process.</param>67 /// <param name="currentDepth">Current depth of recursion.</param>68 private void GenerateFormatEntryDisplay(FormatEntry fe, int currentDepth)69 {70 foreach (object obj in fe.formatValueList)71 {72 if (obj is FormatEntry feChild)73 {74 if (currentDepth < maxRecursionDepth)75 {76 if (feChild.frameInfo != null)77 {78 // if we have frame information, we need to push it on the79 // indentation stack80 using (_indentationManager.StackFrame(feChild.frameInfo))81 {82 GenerateFormatEntryDisplay(feChild, currentDepth + 1);83 }84 }85 else86 {87 // no need here of activating an indentation stack frame88 GenerateFormatEntryDisplay(feChild, currentDepth + 1);89 }90 }91 92 continue;93 }94 95 if (obj is FormatNewLine)96 {97 this.WriteToScreen();98 continue;99 }100 101 if (obj is FormatTextField ftf)102 {103 this.AddToBuffer(ftf.text);104 continue;105 }106 107 if (obj is FormatPropertyField fpf)108 {109 this.AddToBuffer(fpf.propertyValue);110 }111 }112 }113 114 /// <summary>115 /// Add a string to the current buffer, waiting for a FlushBuffer()116 /// </summary>117 /// <param name="s">String to add to buffer.</param>118 private void AddToBuffer(string s)119 {120 _stringBuffer.Append(s);121 }122 123 /// <summary>124 /// Write to the output interface.125 /// </summary>126 private void WriteToScreen()127 {128 int leftIndentation = _indentationManager.LeftIndentation;129 int rightIndentation = _indentationManager.RightIndentation;130 int firstLineIndentation = _indentationManager.FirstLineIndentation;131 132 // VALIDITY CHECKS:133 134 // check the useful ("active") width135 int usefulWidth = _textColumns - rightIndentation - leftIndentation;136 if (usefulWidth <= 0)137 {138 // fatal error, there is nothing to write to the device139 // just clear the buffer and return140 _stringBuffer = new StringBuilder();141 }142 143 // check indentation or hanging is not larger than the active width144 int indentationAbsoluteValue = (firstLineIndentation > 0) ? firstLineIndentation : -firstLineIndentation;145 if (indentationAbsoluteValue >= usefulWidth)146 {147 // value too big, we reset it to zero148 firstLineIndentation = 0;149 }150 151 // compute the first line indentation or hanging152 int firstLineWidth = _textColumns - rightIndentation - leftIndentation;153 int followingLinesWidth = firstLineWidth;154 155 if (firstLineIndentation >= 0)156 {157 // the first line has an indentation158 firstLineWidth -= firstLineIndentation;159 }160 else161 {162 // the first line is hanging163 followingLinesWidth += firstLineIndentation;164 }165 166 // error checking on invalid values167 168 // generate the lines using the computed widths169 StringCollection sc = StringManipulationHelper.GenerateLines(_lo.DisplayCells, _stringBuffer.ToString(),170 firstLineWidth, followingLinesWidth);171 172 // compute padding173 int firstLinePadding = leftIndentation;174 int followingLinesPadding = leftIndentation;175 if (firstLineIndentation >= 0)176 {177 // the first line has an indentation178 firstLinePadding += firstLineIndentation;179 }180 else181 {182 // the first line is hanging183 followingLinesPadding -= firstLineIndentation;184 }185 186 // now write the lines on the screen187 bool firstLine = true;188 foreach (string s in sc)189 {190 if (firstLine)191 {192 firstLine = false;193 _lo.WriteLine(StringManipulationHelper.PadLeft(s, firstLinePadding));194 }195 else196 {197 _lo.WriteLine(StringManipulationHelper.PadLeft(s, followingLinesPadding));198 }199 }200 201 _stringBuffer = new StringBuilder();202 }203 204 /// <summary>205 /// Helper object to manage the frame-based indentation and margins.206 /// </summary>207 private readonly IndentationManager _indentationManager = new IndentationManager();208 209 /// <summary>210 /// Buffer to accumulate partially constructed text.211 /// </summary>212 private StringBuilder _stringBuffer = new StringBuilder();213 214 /// <summary>215 /// Interface to write to.216 /// </summary>217 private LineOutput _lo;218 219 /// <summary>220 /// Number of columns for the output device.221 /// </summary>222 private int _textColumns;223 224 private const int maxRecursionDepth = 50;225 }226 227 internal sealed class IndentationManager228 {229 private sealed class IndentationStackFrame : IDisposable230 {231 internal IndentationStackFrame(IndentationManager mgr)232 {233 _mgr = mgr;234 }235 236 public void Dispose()237 {238 _mgr?.RemoveStackFrame();239 }240 241 private readonly IndentationManager _mgr;242 }243 244 internal void Clear()245 {246 _frameInfoStack.Clear();247 }248 249 internal IDisposable StackFrame(FrameInfo frameInfo)250 {251 IndentationStackFrame frame = new IndentationStackFrame(this);252 _frameInfoStack.Push(frameInfo);253 return frame;254 }255 256 private void RemoveStackFrame()257 {258 _frameInfoStack.Pop();259 }260 261 internal int RightIndentation262 {263 get264 {265 return ComputeRightIndentation();266 }267 }268 269 internal int LeftIndentation270 {271 get272 {273 return ComputeLeftIndentation();274 }275 }276 277 internal int FirstLineIndentation278 {279 get280 {281 if (_frameInfoStack.Count == 0)282 return 0;283 return _frameInfoStack.Peek().firstLine;284 }285 }286 287 private int ComputeRightIndentation()288 {289 int val = 0;290 foreach (FrameInfo fi in _frameInfoStack)291 {292 val += fi.rightIndentation;293 }294 295 return val;296 }297 298 private int ComputeLeftIndentation()299 {300 int val = 0;301 foreach (FrameInfo fi in _frameInfoStack)302 {303 val += fi.leftIndentation;304 }305 306 return val;307 }308 309 private readonly Stack<FrameInfo> _frameInfoStack = new Stack<FrameInfo>();310 }311 312 /// <summary>313 /// Result of GetWords.314 /// </summary>315 internal struct GetWordsResult316 {317 internal string Word;318 internal string Delim;319 internal bool VtResetAdded;320 }321 322 /// <summary>323 /// Collection of helper functions for string formatting.324 /// </summary>325 internal sealed class StringManipulationHelper326 {327 private const char SoftHyphen = '\u00AD';328 private const char HardHyphen = '\u2011';329 private const char NonBreakingSpace = '\u00A0';330 331 private static readonly Collection<string> s_cultureCollection = new Collection<string>();332 333 static StringManipulationHelper()334 {335 s_cultureCollection.Add("en"); // English336 s_cultureCollection.Add("fr"); // French337 s_cultureCollection.Add("de"); // German338 s_cultureCollection.Add("it"); // Italian339 s_cultureCollection.Add("pt"); // Portuguese340 s_cultureCollection.Add("es"); // Spanish341 }342 343 /// <summary>344 /// Breaks a string into a collection of words345 /// TODO: we might be able to improve this function in the future346 /// so that we do not break paths etc.347 /// </summary>348 /// <param name="s">Input string.</param>349 /// <returns>A collection of words.</returns>350 private static IEnumerable<GetWordsResult> GetWords(string s)351 {352 StringBuilder sb = new StringBuilder();353 StringBuilder vtSeqs = null;354 Dictionary<int, int> vtRanges = null;355 356 var valueStrDec = new ValueStringDecorated(s);357 if (valueStrDec.IsDecorated)358 {359 vtSeqs = new StringBuilder();360 vtRanges = valueStrDec.EscapeSequenceRanges;361 }362 363 bool wordHasVtSeqs = false;364 for (int i = 0; i < s.Length; i++)365 {366 if (vtRanges?.TryGetValue(i, out int len) == true)367 {368 var vtSpan = s.AsSpan(i, len);369 sb.Append(vtSpan);370 371 if (vtSpan.SequenceEqual(PSStyle.Instance.Reset))372 {373 // The Reset sequence will void all previous VT sequences.374 vtSeqs.Clear();375 wordHasVtSeqs = false;376 }377 else378 {379 vtSeqs.Append(vtSpan);380 wordHasVtSeqs = true;381 }382 383 i += len - 1;384 continue;385 }386 387 string delimiter = null;388 if (s[i] is ' ' or '\t' or SoftHyphen)389 {390 // Soft hyphen = \u00AD - Should break, and add a hyphen if needed.391 // If not needed for a break, hyphen should be absent.392 delimiter = new string(s[i], 1);393 }394 else if (s[i] is HardHyphen or NonBreakingSpace)395 {396 // Non-breaking space = \u00A0 - ideally shouldn't wrap.397 // Hard hyphen = \u2011 - Should not break.398 delimiter = string.Empty;399 }400 401 if (delimiter is not null)402 {403 bool vtResetAdded = false;404 if (wordHasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset))405 {406 vtResetAdded = true;407 sb.Append(PSStyle.Instance.Reset);408 }409 410 var result = new GetWordsResult()411 {412 Word = sb.ToString(),413 Delim = delimiter,414 VtResetAdded = vtResetAdded415 };416 417 sb.Clear().Append(vtSeqs);418 yield return result;419 }420 else421 {422 sb.Append(s[i]);423 }424 }425 426 if (wordHasVtSeqs)427 {428 if (sb.Length == vtSeqs.Length)429 {430 // This indicates 'sb' only contains all VT sequences, which may happen when the string ends with a word delimiter.431 // For a word that contains VT sequence only, it's the same as an empty string to the formatting system,432 // because nothing will actually be rendered.433 // So, we use an empty string in this case to avoid unneeded string allocations.434 sb.Clear();435 }436 else if (!sb.EndsWith(PSStyle.Instance.Reset))437 {438 sb.Append(PSStyle.Instance.Reset);439 }440 }441 442 yield return new GetWordsResult() { Word = sb.ToString(), Delim = string.Empty };443 }444 445 internal static StringCollection GenerateLines(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)446 {447 if (s_cultureCollection.Contains(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))448 {449 return GenerateLinesWithWordWrap(displayCells, val, firstLineLen, followingLinesLen);450 }451 else452 {453 return GenerateLinesWithoutWordWrap(displayCells, val, firstLineLen, followingLinesLen);454 }455 }456 457 private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)458 {459 StringCollection retVal = new StringCollection();460 461 if (string.IsNullOrEmpty(val))462 {463 // if null or empty, just add and we are done464 retVal.Add(val);465 return retVal;466 }467 468 // break string on newlines and process each line separately469 List<string> lines = SplitLines(val);470 471 for (int k = 0; k < lines.Count; k++)472 {473 string currentLine = lines[k];474 475 if (currentLine == null || displayCells.Length(currentLine) <= firstLineLen)476 {477 // we do not need to split further, just add478 retVal.Add(currentLine);479 continue;480 }481 482 // the string does not fit, so we have to wrap around on multiple lines483 // for each of these lines in the string, the first line will have484 // a (potentially) different length (indentation or hanging)485 486 // for each line, start a new state487 SplitLinesAccumulator accumulator = new SplitLinesAccumulator(retVal, firstLineLen, followingLinesLen);488 489 int offset = 0; // offset into the line we are splitting490 491 while (offset < currentLine.Length)492 {493 // acquire the current active display line length (it can very from call to call)494 int currentDisplayLen = accumulator.ActiveLen;495 496 // determine if the current tail would fit or not497 498 // for the remaining part of the string, determine its display cell count499 int currentCellsToFit = displayCells.Length(currentLine, offset);500 501 // determine if we fit into the line502 int excessCells = currentCellsToFit - currentDisplayLen;503 504 if (excessCells > 0)505 {506 // we are not at the end of the string, select a sub string507 // that would fit in the remaining display length508 int charactersToAdd = displayCells.TruncateTail(currentLine, offset, currentDisplayLen);509 510 if (charactersToAdd <= 0)511 {512 // corner case: we have a two cell character and the current513 // display length is one.514 // add a single cell arbitrary character instead of the original515 // one and keep going516 charactersToAdd = 1;517 accumulator.AddLine("?");518 }519 else520 {521 // of the given length, add it to the accumulator522 accumulator.AddLine(currentLine.VtSubstring(offset, charactersToAdd));523 }524 525 // increase the offset by the # of characters added526 offset += charactersToAdd;527 }528 else529 {530 // we reached the last (partial) line, we add it all531 accumulator.AddLine(currentLine.VtSubstring(offset));532 break;533 }534 }535 }536 537 return retVal;538 }539 540 private sealed class SplitLinesAccumulator541 {542 internal SplitLinesAccumulator(StringCollection retVal, int firstLineLen, int followingLinesLen)543 {544 _retVal = retVal;545 _firstLineLen = firstLineLen;546 _followingLinesLen = followingLinesLen;547 }548 549 internal void AddLine(string s)550 {551 if (!_addedFirstLine)552 {553 _addedFirstLine = true;554 }555 556 _retVal.Add(s);557 }558 559 internal int ActiveLen560 {561 get562 {563 if (_addedFirstLine)564 return _followingLinesLen;565 return _firstLineLen;566 }567 }568 569 private readonly StringCollection _retVal;570 private bool _addedFirstLine;571 private readonly int _firstLineLen;572 private readonly int _followingLinesLen;573 }574 575 private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCells, string val, int firstLineLen, int followingLinesLen)576 {577 StringCollection retVal = new StringCollection();578 579 if (string.IsNullOrEmpty(val))580 {581 // if null or empty, just add and we are done582 retVal.Add(val);583 return retVal;584 }585 586 // break string on newlines and process each line separately587 List<string> lines = SplitLines(val);588 589 for (int k = 0; k < lines.Count; k++)590 {591 if (lines[k] == null || displayCells.Length(lines[k]) <= firstLineLen)592 {593 // we do not need to split further, just add594 retVal.Add(lines[k]);595 continue;596 }597 598 int spacesLeft = firstLineLen;599 int lineWidth = firstLineLen;600 bool firstLine = true;601 StringBuilder singleLine = new StringBuilder();602 string resetStr = PSStyle.Instance.Reset;603 604 foreach (GetWordsResult word in GetWords(lines[k]))605 {606 string wordToAdd = word.Word;607 string suffix = null;608 609 // Handle soft hyphen610 if (word.Delim.Length == 1 && word.Delim[0] is SoftHyphen)611 {612 int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(SoftHyphen);613 614 // Add hyphen only if necessary615 if (wordWidthWithHyphen == spacesLeft)616 {617 suffix = "-";618 }619 }620 else if (!string.IsNullOrEmpty(word.Delim))621 {622 suffix = word.Delim;623 }624 625 if (suffix is not null)626 {627 wordToAdd = word.VtResetAdded628 ? wordToAdd.Insert(wordToAdd.Length - resetStr.Length, suffix)629 : wordToAdd + suffix;630 }631 632 int wordWidth = displayCells.Length(wordToAdd);633 634 // Handle zero width635 if (lineWidth == 0)636 {637 if (firstLine)638 {639 firstLine = false;640 lineWidth = followingLinesLen;641 }642 643 if (lineWidth == 0)644 {645 break;646 }647 648 spacesLeft = lineWidth;649 }650 651 // Word is wider than a single line652 if (wordWidth > lineWidth)653 {654 Dictionary<int, int> vtRanges = null;655 StringBuilder vtSeqs = null;656 657 var valueStrDec = new ValueStringDecorated(wordToAdd);658 if (valueStrDec.IsDecorated)659 {660 vtSeqs = new StringBuilder();661 vtRanges = valueStrDec.EscapeSequenceRanges;662 }663 664 bool hasEscSeqs = false;665 for (int i = 0; i < wordToAdd.Length; i++)666 {667 if (vtRanges?.TryGetValue(i, out int len) == true)668 {669 var vtSpan = wordToAdd.AsSpan(i, len);670 singleLine.Append(vtSpan);671 vtSeqs.Append(vtSpan);672 673 hasEscSeqs = true;674 i += len - 1;675 continue;676 }677 678 char charToAdd = wordToAdd[i];679 int charWidth = displayCells.Length(charToAdd);680 681 // Corner case: we have a two cell character and the current display length is one.682 // Add a single cell arbitrary character instead of the original one and keep going.683 if (charWidth > lineWidth)684 {685 charToAdd = '?';686 charWidth = 1;687 }688 689 if (charWidth > spacesLeft)690 {691 if (hasEscSeqs && !singleLine.EndsWith(resetStr))692 {693 singleLine.Append(resetStr);694 }695 696 retVal.Add(singleLine.ToString());697 singleLine.Clear().Append(vtSeqs).Append(charToAdd);698 699 if (firstLine)700 {701 firstLine = false;702 lineWidth = followingLinesLen;703 }704 705 spacesLeft = lineWidth - charWidth;706 }707 else708 {709 singleLine.Append(charToAdd);710 spacesLeft -= charWidth;711 }712 }713 }714 else715 {716 if (wordWidth > spacesLeft)717 {718 retVal.Add(singleLine.ToString());719 singleLine.Clear().Append(wordToAdd);720 721 if (firstLine)722 {723 firstLine = false;724 lineWidth = followingLinesLen;725 }726 727 spacesLeft = lineWidth - wordWidth;728 }729 else730 {731 singleLine.Append(wordToAdd);732 spacesLeft -= wordWidth;733 }734 }735 }736 737 retVal.Add(singleLine.ToString());738 }739 740 return retVal;741 }742 743 /// <summary>744 /// Split a multiline string into an array of strings745 /// by honoring both \n and \r\n.746 /// </summary>747 /// <param name="s">String to split.</param>748 /// <returns>String array with the values.</returns>749 internal static List<string> SplitLines(string s)750 {751 if (string.IsNullOrEmpty(s) || !s.Contains('\n'))752 {753 return new List<string>(capacity: 1) { s?.Replace("\r", string.Empty) };754 }755 756 StringBuilder sb = new StringBuilder();757 List<string> list = new List<string>();758 759 StringBuilder vtSeqs = null;760 Dictionary<int, int> vtRanges = null;761 762 var valueStrDec = new ValueStringDecorated(s);763 if (valueStrDec.IsDecorated)764 {765 vtSeqs = new StringBuilder();766 vtRanges = valueStrDec.EscapeSequenceRanges;767 }768 769 bool hasVtSeqs = false;770 for (int i = 0; i < s.Length; i++)771 {772 if (vtRanges?.TryGetValue(i, out int len) == true)773 {774 var vtSpan = s.AsSpan(i, len);775 sb.Append(vtSpan);776 777 if (vtSpan.SequenceEqual(PSStyle.Instance.Reset))778 {779 // The Reset sequence will void all previous VT sequences.780 vtSeqs.Clear();781 hasVtSeqs = false;782 }783 else784 {785 vtSeqs.Append(vtSpan);786 hasVtSeqs = true;787 }788 789 i += len - 1;790 continue;791 }792 793 char c = s[i];794 if (c == '\n')795 {796 if (hasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset))797 {798 sb.Append(PSStyle.Instance.Reset);799 }800 801 list.Add(sb.ToString());802 sb.Clear().Append(vtSeqs);803 }804 else if (c != '\r')805 {806 sb.Append(c);807 }808 }809 810 if (hasVtSeqs)811 {812 if (sb.Length == vtSeqs.Length)813 {814 // This indicates 'sb' only contains all VT sequences, which may happen when the string ends with '\n'.815 // For a sub-string that contains VT sequence only, it's the same as an empty string to the formatting816 // system, because nothing will actually be rendered.817 // So, we use an empty string in this case to avoid unneeded string allocations.818 sb.Clear();819 }820 else if (!sb.EndsWith(PSStyle.Instance.Reset))821 {822 sb.Append(PSStyle.Instance.Reset);823 }824 }825 826 list.Add(sb.ToString());827 return list;828 }829 830 internal static string TruncateAtNewLine(string s)831 {832 if (string.IsNullOrEmpty(s))833 {834 return string.Empty;835 }836 837 int lineBreak = s.AsSpan().IndexOfAny('\n', '\r');838 839 if (lineBreak < 0)840 {841 return s;842 }843 844 return s.Substring(0, lineBreak) + PSObjectHelper.Ellipsis;845 }846 847 internal static string PadLeft(string val, int count)848 {849 return StringUtil.Padding(count) + val;850 }851 }852}853 