Jelajahi Sumber

Rewrite TextFormatter.ReplaceCRLFWithSpace

Almost identical to the StripCRLF implementation.
Tonttu 4 bulan lalu
induk
melakukan
662e745395

+ 89 - 0
Benchmarks/Text/TextFormatter/ReplaceCRLFWithSpace.cs

@@ -0,0 +1,89 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.TextFormatter.ReplaceCRLFWithSpace"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+public class ReplaceCRLFWithSpace
+{
+
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Previous (string str)
+    {
+        return ToRuneListReplaceImplementation (str);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Current (string str)
+    {
+        return Tui.TextFormatter.ReplaceCRLFWithSpace (str);
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate rune list.
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    private static string ToRuneListReplaceImplementation (string str)
+    {
+        var runes = str.ToRuneList ();
+        for (int i = 0; i < runes.Count; i++)
+        {
+            switch (runes [i].Value)
+            {
+                case '\n':
+                    runes [i] = (Rune)' ';
+                    break;
+
+                case '\r':
+                    if ((i + 1) < runes.Count && runes [i + 1].Value == '\n')
+                    {
+                        runes [i] = (Rune)' ';
+                        runes.RemoveAt (i + 1);
+                        i++;
+                    }
+                    else
+                    {
+                        runes [i] = (Rune)' ';
+                    }
+                    break;
+            }
+        }
+        return Tui.StringExtensions.ToString (runes);
+    }
+
+    public IEnumerable<object> DataSource ()
+    {
+        // Extreme newline scenario
+        yield return "E\r\nx\r\nt\r\nr\r\ne\r\nm\r\ne\r\nn\r\ne\r\nw\r\nl\r\ni\r\nn\r\ne\r\ns\r\nc\r\ne\r\nn\r\na\r\nr\r\ni\r\no\r\n";
+        // Long text with few line endings
+        yield return
+            """
+			Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
+			Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
+			Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
+			Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
+			Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
+			"""
+            // Consistent line endings between systems for more consistent performance evaluation.
+            .ReplaceLineEndings ("\r\n");
+        // Long text without line endings
+        yield return
+            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla. " +
+            "Curabitur mollis ex nisl, vitae mattis nisl consequat at. Aliquam dolor lectus, tincidunt ac nunc eu, elementum molestie lectus. Donec lacinia eget dolor a scelerisque. " +
+            "Aenean elementum molestie rhoncus. Duis id ornare lorem. Nam eget porta sapien. Etiam rhoncus dignissim leo, ac suscipit magna finibus eu. Curabitur hendrerit elit erat, sit amet suscipit felis condimentum ut. " +
+            "Nullam semper tempor mi, nec semper quam fringilla eu. Aenean sit amet pretium augue, in posuere ante. Aenean convallis porttitor purus, et posuere velit dictum eu.";
+    }
+}

+ 159 - 133
Terminal.Gui/Text/TextFormatter.cs

@@ -442,7 +442,7 @@ public class TextFormatter
             }
             }
         }
         }
     }
     }
-    
+
     /// <summary>
     /// <summary>
     ///     Determines if the viewport width will be used or only the text width will be used,
     ///     Determines if the viewport width will be used or only the text width will be used,
     ///     If <see langword="true"/> all the viewport area will be filled with whitespaces and the same background color
     ///     If <see langword="true"/> all the viewport area will be filled with whitespaces and the same background color
@@ -942,67 +942,67 @@ public class TextFormatter
             {
             {
                 // Horizontal Alignment
                 // Horizontal Alignment
                 case Alignment.End when isVertical:
                 case Alignment.End when isVertical:
-                {
-                    int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
-                    x = screen.Right - runesWidth;
+                    {
+                        int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
+                        x = screen.Right - runesWidth;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.End:
                 case Alignment.End:
-                {
-                    int runesWidth = StringExtensions.ToString (runes).GetColumns ();
-                    x = screen.Right - runesWidth;
+                    {
+                        int runesWidth = StringExtensions.ToString (runes).GetColumns ();
+                        x = screen.Right - runesWidth;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Start when isVertical:
                 case Alignment.Start when isVertical:
-                {
-                    int runesWidth = line > 0
+                    {
+                        int runesWidth = line > 0
                                          ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
                                          ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
                                          : 0;
                                          : 0;
-                    x = screen.Left + runesWidth;
+                        x = screen.Left + runesWidth;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Start:
                 case Alignment.Start:
                     x = screen.Left;
                     x = screen.Left;
 
 
                     break;
                     break;
                 case Alignment.Fill when isVertical:
                 case Alignment.Fill when isVertical:
-                {
-                    int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
-                    int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
-                    int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
-                    int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
-                    var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
-
-                    x = line == 0
-                            ? screen.Left
-                            : line < linesFormatted.Count - 1
-                                ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
-                                : screen.Right - lastLineWidth;
+                    {
+                        int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
+                        int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
+                        int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
+                        int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
+                        var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
+
+                        x = line == 0
+                                ? screen.Left
+                                : line < linesFormatted.Count - 1
+                                    ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
+                                    : screen.Right - lastLineWidth;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Fill:
                 case Alignment.Fill:
                     x = screen.Left;
                     x = screen.Left;
 
 
                     break;
                     break;
                 case Alignment.Center when isVertical:
                 case Alignment.Center when isVertical:
-                {
-                    int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
-                    int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
-                    x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
+                    {
+                        int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
+                        int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
+                        x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Center:
                 case Alignment.Center:
-                {
-                    int runesWidth = StringExtensions.ToString (runes).GetColumns ();
-                    x = screen.Left + (screen.Width - runesWidth) / 2;
+                    {
+                        int runesWidth = StringExtensions.ToString (runes).GetColumns ();
+                        x = screen.Left + (screen.Width - runesWidth) / 2;
 
 
-                    break;
-                }
+                        break;
+                    }
                 default:
                 default:
                     Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
                     Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
 
 
@@ -1033,28 +1033,28 @@ public class TextFormatter
 
 
                     break;
                     break;
                 case Alignment.Fill:
                 case Alignment.Fill:
-                {
-                    var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
+                    {
+                        var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
 
 
-                    y = line == 0 ? screen.Top :
-                        line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
+                        y = line == 0 ? screen.Top :
+                            line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Center when isVertical:
                 case Alignment.Center when isVertical:
-                {
-                    int s = (screen.Height - runes.Length) / 2;
-                    y = screen.Top + s;
+                    {
+                        int s = (screen.Height - runes.Length) / 2;
+                        y = screen.Top + s;
 
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Center:
                 case Alignment.Center:
-                {
-                    int s = (screen.Height - linesFormatted.Count) / 2;
-                    y = screen.Top + line + s;
+                    {
+                        int s = (screen.Height - linesFormatted.Count) / 2;
+                        y = screen.Top + line + s;
 
 
-                    break;
-                }
+                        break;
+                    }
                 default:
                 default:
                     Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
                     Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
 
 
@@ -1144,48 +1144,48 @@ public class TextFormatter
     public static bool IsHorizontalDirection (TextDirection textDirection)
     public static bool IsHorizontalDirection (TextDirection textDirection)
     {
     {
         return textDirection switch
         return textDirection switch
-               {
-                   TextDirection.LeftRight_TopBottom => true,
-                   TextDirection.LeftRight_BottomTop => true,
-                   TextDirection.RightLeft_TopBottom => true,
-                   TextDirection.RightLeft_BottomTop => true,
-                   _ => false
-               };
+        {
+            TextDirection.LeftRight_TopBottom => true,
+            TextDirection.LeftRight_BottomTop => true,
+            TextDirection.RightLeft_TopBottom => true,
+            TextDirection.RightLeft_BottomTop => true,
+            _ => false
+        };
     }
     }
 
 
     /// <summary>Check if it is a vertical direction</summary>
     /// <summary>Check if it is a vertical direction</summary>
     public static bool IsVerticalDirection (TextDirection textDirection)
     public static bool IsVerticalDirection (TextDirection textDirection)
     {
     {
         return textDirection switch
         return textDirection switch
-               {
-                   TextDirection.TopBottom_LeftRight => true,
-                   TextDirection.TopBottom_RightLeft => true,
-                   TextDirection.BottomTop_LeftRight => true,
-                   TextDirection.BottomTop_RightLeft => true,
-                   _ => false
-               };
+        {
+            TextDirection.TopBottom_LeftRight => true,
+            TextDirection.TopBottom_RightLeft => true,
+            TextDirection.BottomTop_LeftRight => true,
+            TextDirection.BottomTop_RightLeft => true,
+            _ => false
+        };
     }
     }
 
 
     /// <summary>Check if it is Left to Right direction</summary>
     /// <summary>Check if it is Left to Right direction</summary>
     public static bool IsLeftToRight (TextDirection textDirection)
     public static bool IsLeftToRight (TextDirection textDirection)
     {
     {
         return textDirection switch
         return textDirection switch
-               {
-                   TextDirection.LeftRight_TopBottom => true,
-                   TextDirection.LeftRight_BottomTop => true,
-                   _ => false
-               };
+        {
+            TextDirection.LeftRight_TopBottom => true,
+            TextDirection.LeftRight_BottomTop => true,
+            _ => false
+        };
     }
     }
 
 
     /// <summary>Check if it is Top to Bottom direction</summary>
     /// <summary>Check if it is Top to Bottom direction</summary>
     public static bool IsTopToBottom (TextDirection textDirection)
     public static bool IsTopToBottom (TextDirection textDirection)
     {
     {
         return textDirection switch
         return textDirection switch
-               {
-                   TextDirection.TopBottom_LeftRight => true,
-                   TextDirection.TopBottom_RightLeft => true,
-                   _ => false
-               };
+        {
+            TextDirection.TopBottom_LeftRight => true,
+            TextDirection.TopBottom_RightLeft => true,
+            _ => false
+        };
     }
     }
 
 
     // TODO: Move to StringExtensions?
     // TODO: Move to StringExtensions?
@@ -1259,34 +1259,60 @@ public class TextFormatter
     // TODO: Move to StringExtensions?
     // TODO: Move to StringExtensions?
     internal static string ReplaceCRLFWithSpace (string str)
     internal static string ReplaceCRLFWithSpace (string str)
     {
     {
-        List<Rune> runes = str.ToRuneList ();
+        ReadOnlySpan<char> remaining = str.AsSpan ();
+        int firstNewlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+        // Early exit to avoid StringBuilder allocation if there are no newline characters.
+        if (firstNewlineCharIndex < 0)
+        {
+            return str;
+        }
 
 
-        for (var i = 0; i < runes.Count; i++)
+        StringBuilder stringBuilder = new();
+        ReadOnlySpan<char> firstSegment = remaining[..firstNewlineCharIndex];
+        stringBuilder.Append (firstSegment);
+
+        // The first newline is not yet skipped because the newline type has not been evaluated.
+        // This means there will be 1 extra iteration because the same newline index is checked again in the loop.
+        remaining = remaining [firstNewlineCharIndex..];
+
+        while (remaining.Length > 0)
         {
         {
-            switch (runes [i].Value)
+            int newlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+            if (newlineCharIndex == -1)
             {
             {
-                case '\n':
-                    runes [i] = (Rune)' ';
-
-                    break;
+                break;
+            }
 
 
-                case '\r':
-                    if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
-                    {
-                        runes [i] = (Rune)' ';
-                        runes.RemoveAt (i + 1);
-                        i++;
-                    }
-                    else
-                    {
-                        runes [i] = (Rune)' ';
-                    }
+            ReadOnlySpan<char> segment = remaining[..newlineCharIndex];
+            stringBuilder.Append (segment);
 
 
-                    break;
+            int stride = segment.Length;
+            // Replace newlines
+            char newlineChar = remaining [newlineCharIndex];
+            if (newlineChar == '\n')
+            {
+                stride++;
+                stringBuilder.Append (' ');
+            }
+            else // '\r'
+            {
+                int nextCharIndex = newlineCharIndex + 1;
+                bool crlf = nextCharIndex < remaining.Length && remaining [nextCharIndex] == '\n';
+                if (crlf)
+                {
+                    stride += 2;
+                    stringBuilder.Append (' ');
+                }
+                else
+                {
+                    stride++;
+                    stringBuilder.Append (' ');
+                }
             }
             }
+            remaining = remaining [stride..];
         }
         }
-
-        return StringExtensions.ToString (runes);
+        stringBuilder.Append (remaining);
+        return stringBuilder.ToString ();
     }
     }
 
 
     // TODO: Move to StringExtensions?
     // TODO: Move to StringExtensions?
@@ -1598,21 +1624,21 @@ public class TextFormatter
                     case ' ':
                     case ' ':
                         return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
                         return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
                     case '\t':
                     case '\t':
-                    {
-                        length += tabWidth + 1;
-
-                        if (length == tabWidth && tabWidth > cWidth)
                         {
                         {
-                            return to + 1;
-                        }
+                            length += tabWidth + 1;
 
 
-                        if (length > cWidth && tabWidth > cWidth)
-                        {
-                            return to;
-                        }
+                            if (length == tabWidth && tabWidth > cWidth)
+                            {
+                                return to + 1;
+                            }
 
 
-                        return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
-                    }
+                            if (length > cWidth && tabWidth > cWidth)
+                            {
+                                return to;
+                            }
+
+                            return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
+                        }
                     default:
                     default:
                         to++;
                         to++;
 
 
@@ -1621,11 +1647,11 @@ public class TextFormatter
             }
             }
 
 
             return cLength switch
             return cLength switch
-                   {
-                       > 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from,
-                       > 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from,
-                       _ => to
-                   };
+            {
+                > 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from,
+                > 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from,
+                _ => to
+            };
         }
         }
 
 
         if (start < text.GetRuneCount ())
         if (start < text.GetRuneCount ())
@@ -2061,13 +2087,13 @@ public class TextFormatter
     private static string PerformCorrectFormatDirection (TextDirection textDirection, string line)
     private static string PerformCorrectFormatDirection (TextDirection textDirection, string line)
     {
     {
         return textDirection switch
         return textDirection switch
-               {
-                   TextDirection.RightLeft_BottomTop
-                       or TextDirection.RightLeft_TopBottom
-                       or TextDirection.BottomTop_LeftRight
-                       or TextDirection.BottomTop_RightLeft => StringExtensions.ToString (line.EnumerateRunes ().Reverse ()),
-                   _ => line
-               };
+        {
+            TextDirection.RightLeft_BottomTop
+                or TextDirection.RightLeft_TopBottom
+                or TextDirection.BottomTop_LeftRight
+                or TextDirection.BottomTop_RightLeft => StringExtensions.ToString (line.EnumerateRunes ().Reverse ()),
+            _ => line
+        };
     }
     }
 
 
     private static List<Rune> PerformCorrectFormatDirection (TextDirection textDirection, List<Rune> runes)
     private static List<Rune> PerformCorrectFormatDirection (TextDirection textDirection, List<Rune> runes)
@@ -2078,13 +2104,13 @@ public class TextFormatter
     private static List<string> PerformCorrectFormatDirection (TextDirection textDirection, List<string> lines)
     private static List<string> PerformCorrectFormatDirection (TextDirection textDirection, List<string> lines)
     {
     {
         return textDirection switch
         return textDirection switch
-               {
-                   TextDirection.TopBottom_RightLeft
-                       or TextDirection.LeftRight_BottomTop
-                       or TextDirection.RightLeft_BottomTop
-                       or TextDirection.BottomTop_RightLeft => lines.ToArray ().Reverse ().ToList (),
-                   _ => lines
-               };
+        {
+            TextDirection.TopBottom_RightLeft
+                or TextDirection.LeftRight_BottomTop
+                or TextDirection.RightLeft_BottomTop
+                or TextDirection.BottomTop_RightLeft => lines.ToArray ().Reverse ().ToList (),
+            _ => lines
+        };
     }
     }
 
 
     /// <summary>
     /// <summary>