From 7a4b39a159a8e6bbd9e019c2e81c36636f250011 Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Thu, 22 Oct 2026 18:38:27 +0800 Subject: [PATCH 1/2] test: pin the behaviour of StructuredStringBuilder - Cover indentation, continuing a line, AppendItem, Empty and editing lines --- .../StructuredStringBuilderTests.cs | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/CSharpExt.UnitTests/StructuredStrings/StructuredStringBuilderTests.cs b/CSharpExt.UnitTests/StructuredStrings/StructuredStringBuilderTests.cs index f87c6dfa..bbfcc64d 100644 --- a/CSharpExt.UnitTests/StructuredStrings/StructuredStringBuilderTests.cs +++ b/CSharpExt.UnitTests/StructuredStrings/StructuredStringBuilderTests.cs @@ -12,6 +12,14 @@ public void AppendWithEmbeddedNewLine() Assert.Equal(new string[] { "A", "B" }, sb); } + [Fact] + public void AppendWithTrailingNewLine() + { + var sb = new StructuredStringBuilder(); + sb.AppendLine($"A{Environment.NewLine}"); + Assert.Equal(new string[] { "A", "" }, sb); + } + [Fact] public void AppendNull() { @@ -27,4 +35,76 @@ public void AppendEmpty() sb.AppendLine(String.Empty); Assert.Equal(new string[] { "" }, sb); } -} \ No newline at end of file + + [Fact] + public void AppendContinuesTheLine() + { + var sb = new StructuredStringBuilder(); + sb.Append("A"); + sb.Append("B", endWithNewLine: true); + sb.Append("C"); + Assert.Equal(new string[] { "AB" }, sb); + Assert.Equal($"AB{Environment.NewLine}C", sb.ToString()); + } + + [Fact] + public void AppendLineIndentsByDepth() + { + var sb = new StructuredStringBuilder(); + sb.AppendLine("A"); + using (sb.Brace()) + { + sb.AppendLine("B"); + using (sb.IncreaseDepth()) + { + sb.AppendLine("C"); + } + } + Assert.Equal(new string[] { "A", "[", " B", " C", "]" }, sb); + } + + [Fact] + public void AppendLineWithExtraLine() + { + var sb = new StructuredStringBuilder(); + sb.AppendLine("A", extraLine: true); + sb.AppendLine("B"); + Assert.Equal(new string[] { "A", "", "B" }, sb); + } + + [Fact] + public void AppendItemNamesTheValue() + { + var sb = new StructuredStringBuilder(); + sb.AppendItem(5, "Count"); + sb.AppendItem(6); + sb.AppendItem((string?)null, "Missing"); + Assert.Equal(new string[] { "Count => 5", "6" }, sb); + } + + [Fact] + public void Empty() + { + var sb = new StructuredStringBuilder(); + Assert.True(sb.Empty); + sb.Append(" "); + Assert.True(sb.Empty); + sb.AppendLine(); + Assert.False(sb.Empty); + } + + [Fact] + public void EditLines() + { + var sb = new StructuredStringBuilder(); + sb.AppendLine("A"); + sb.AppendLine("B"); + sb.Insert(1, "X"); + sb[0] = "Y"; + Assert.Equal("X", sb[1]); + sb.RemoveAt(2); + Assert.Equal(2, sb.Count); + Assert.Equal(new string[] { "Y", "X" }, sb); + Assert.Throws(() => sb[2]); + } +} From b5e790d6bbcc354bcf49669b43365cf0b147027a Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Thu, 22 Oct 2026 18:38:28 +0800 Subject: [PATCH 2/2] perf: build StructuredStringBuilder lines in place - Keep the line being written in a StringBuilder rather than concatenating it on every Append - Split on new lines with ordinal IndexOf instead of string.Split and a closure - Compare new lines ordinally rather than with the current culture - Indent in place instead of allocating a Line scope and an indentation string --- Noggog.CSharpExt/StructuredStrings/Line.cs | 2 +- .../StructuredStringBuilder.cs | 103 +++++++----------- 2 files changed, 42 insertions(+), 63 deletions(-) diff --git a/Noggog.CSharpExt/StructuredStrings/Line.cs b/Noggog.CSharpExt/StructuredStrings/Line.cs index 3903757e..152c74b5 100644 --- a/Noggog.CSharpExt/StructuredStrings/Line.cs +++ b/Noggog.CSharpExt/StructuredStrings/Line.cs @@ -9,7 +9,7 @@ public Line(StructuredStringBuilder sb, bool appendNewLine = true) { _sb = sb; _appendNewLine = appendNewLine; - _sb.Append(_sb.DepthStr); + _sb.AppendIndentation(); } public void Dispose() diff --git a/Noggog.CSharpExt/StructuredStrings/StructuredStringBuilder.cs b/Noggog.CSharpExt/StructuredStrings/StructuredStringBuilder.cs index a4a464c9..29f2dc9a 100644 --- a/Noggog.CSharpExt/StructuredStrings/StructuredStringBuilder.cs +++ b/Noggog.CSharpExt/StructuredStrings/StructuredStringBuilder.cs @@ -10,28 +10,13 @@ namespace Noggog.StructuredStrings; public class StructuredStringBuilder : IEnumerable { - private static readonly string[] NewLineArr = new[] { Environment.NewLine }; - public int Depth; - private readonly List _strings = new(); - public string DepthStr - { - get - { - StringBuilder sb = new StringBuilder(); - sb.Append(' ', Depth * _depthCount); - return sb.ToString(); - } - } - public bool Empty - { - get - { - if (_strings.Count > 1) return false; - if (_strings.Count == 0) return true; - return string.IsNullOrWhiteSpace(_strings[0]); - } - } + // The line being written is kept apart from the finished ones, so that appending to it + // copies nothing until it ends. + private readonly List _lines = new(); + private readonly StringBuilder _line = new(); + public string DepthStr => new string(' ', Depth * _depthCount); + public bool Empty => _lines.Count == 0 && string.IsNullOrWhiteSpace(_line.ToString()); #if NETSTANDARD2_0 #else @@ -41,26 +26,19 @@ public bool Empty #endif private readonly int _depthCount; - public int Count => _strings.Count - 1; + public int Count => _lines.Count; public string this[int index] { get { CheckIndex(index); - return _strings[index]; + return _lines[index]; } set { CheckIndex(index); - if (index == _strings.Count - 1) - { - AppendLine(value); - } - else - { - _strings[index] = value; - } + _lines[index] = value; } } @@ -72,18 +50,17 @@ public StructuredStringBuilder() public StructuredStringBuilder(int depthCount) { _depthCount = depthCount; - AppendLine(); } public void Insert(int index, string str) { CheckIndex(index); - _strings.Insert(index, str); + _lines.Insert(index, str); } private void CheckIndex(int index) { - if (index >= _strings.Count - 1 || index < 0) + if (index >= _lines.Count || index < 0) { throw new IndexOutOfRangeException(); } @@ -96,37 +73,37 @@ public void Append(string? str, bool endWithNewLine = false) #else _lineAppended.OnNext(str); #endif - if (str.StartsWith(Environment.NewLine)) + if (str.StartsWith(Environment.NewLine, StringComparison.Ordinal)) { - _strings.Add(""); + EndLine(); return; } - string[] split = str.Split(NewLineArr, StringSplitOptions.None); - split.First( - (s, first) => - { - if (_strings.Count == 0) - { - _strings.Add(s); - } - else - { - if (first) - { - _strings[_strings.Count - 1] = _strings[_strings.Count - 1] + s; - } - else - { - _strings.Add(s); - } - } - }); + var start = 0; + int newLine; + while ((newLine = str.IndexOf(Environment.NewLine, start, StringComparison.Ordinal)) >= 0) + { + _line.Append(str, start, newLine - start); + EndLine(); + start = newLine + Environment.NewLine.Length; + } + _line.Append(str, start, str.Length - start); if (endWithNewLine) { Append(Environment.NewLine, endWithNewLine: false); } } + private void EndLine() + { + _lines.Add(_line.ToString()); + _line.Clear(); + } + + internal void AppendIndentation() + { + _line.Append(' ', Depth * _depthCount); + } + public void AppendLine() { Append(Environment.NewLine); @@ -158,9 +135,11 @@ public void AppendLines(IEnumerable strs, string delimeter) public void AppendLine(string? str, bool extraLine = false, bool appendNewLine = true) { - using (new Line(this, appendNewLine)) + AppendIndentation(); + Append(str); + if (appendNewLine) { - Append(str); + AppendLine(); } if (extraLine) @@ -191,12 +170,12 @@ public void AppendItem(IPrintable? item, string? name = null) public void RemoveAt(int index) { CheckIndex(index); - _strings.RemoveAt(index); + _lines.RemoveAt(index); } public string GetString() { - return string.Join(Environment.NewLine, _strings); + return string.Join(Environment.NewLine, _lines.Append(_line.ToString())); } public override string ToString() @@ -206,9 +185,9 @@ public override string ToString() public IEnumerator GetEnumerator() { - for (int i = 0; i < _strings.Count - 1; i++) + for (int i = 0; i < _lines.Count; i++) { - yield return _strings[i]; + yield return _lines[i]; } }