1+ using System . Runtime . CompilerServices ;
2+ using System . Text ;
3+
4+ namespace StaticCs ;
5+
6+ /// <summary>
7+ /// A string builder with automatic indentation support for multi-line text.
8+ /// Manages indentation levels with <see cref="Indent"/> and <see cref="Dedent"/> methods,
9+ /// and automatically applies the current indentation to appended content and interpolated strings.
10+ /// </summary>
11+ public sealed class IndentingBuilder : IComparable < IndentingBuilder > , IEquatable < IndentingBuilder >
12+ {
13+ public static readonly Encoding UTF8Encoding = new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ;
14+
15+ private string _currentIndentWhitespace = "" ;
16+ private StringBuilder _stringBuilder ;
17+
18+ public IndentingBuilder ( string s )
19+ {
20+ _stringBuilder = new StringBuilder ( s ) ;
21+ }
22+
23+ public IndentingBuilder ( SourceBuilderStringHandler s )
24+ {
25+ _currentIndentWhitespace = "" ;
26+ _stringBuilder = s . _stringBuilder ;
27+ }
28+
29+ public IndentingBuilder ( )
30+ {
31+ _stringBuilder = new StringBuilder ( ) ;
32+ }
33+
34+ /// <summary>
35+ /// Removes trailing whitespace from every line and replace all newlines with
36+ /// Environment.NewLine.
37+ /// </summary>
38+ private void Normalize ( )
39+ {
40+ _stringBuilder . Replace ( "\r \n " , "\n " ) ;
41+
42+ // Remove trailing whitespace from every line
43+ int wsStart ;
44+ for ( int i = 0 ; i < _stringBuilder . Length ; i ++ )
45+ {
46+ if ( _stringBuilder [ i ] is '\n ' )
47+ {
48+ wsStart = i - 1 ;
49+ while ( wsStart >= 0 && ( _stringBuilder [ wsStart ] is ' ' or '\t ' ) )
50+ {
51+ wsStart -- ;
52+ }
53+ wsStart ++ ; // Move back to first whitespace
54+ if ( wsStart < i )
55+ {
56+ int len = i - wsStart ;
57+ _stringBuilder . Remove ( wsStart , len ) ;
58+ i -= len ;
59+ }
60+ }
61+ }
62+
63+ _stringBuilder . Replace ( "\n " , Environment . NewLine ) ;
64+ }
65+
66+ public override string ToString ( )
67+ {
68+ Normalize ( ) ;
69+ return _stringBuilder . ToString ( ) ;
70+ }
71+
72+ public void Append (
73+ [ InterpolatedStringHandlerArgument ( "" ) ]
74+ SourceBuilderStringHandler s )
75+ {
76+ // No work needed, the handler has already added the text to the string builder
77+ }
78+
79+ public void Append ( string s )
80+ {
81+ _stringBuilder . Append ( _currentIndentWhitespace ) ;
82+ Append ( _stringBuilder , _currentIndentWhitespace , s ) ;
83+ }
84+
85+ public void Append ( IndentingBuilder srcBuilder )
86+ {
87+ Append ( srcBuilder . ToString ( ) ) ;
88+ }
89+
90+ private static void Append (
91+ StringBuilder builder ,
92+ string currentIndentWhitespace ,
93+ string str )
94+ {
95+ int start = 0 ;
96+ int nl ;
97+ while ( start < str . Length )
98+ {
99+ nl = str . IndexOf ( '\n ' , start ) ;
100+ if ( nl == - 1 )
101+ {
102+ nl = str . Length ;
103+ }
104+ // Skip blank lines
105+ while ( nl < str . Length && ( str [ nl ] == '\n ' || str [ nl ] == '\r ' ) )
106+ {
107+ nl ++ ;
108+ }
109+ if ( start > 0 )
110+ {
111+ builder . Append ( currentIndentWhitespace ) ;
112+ }
113+ builder . Append ( str , start , nl - start ) ;
114+ start = nl ;
115+ }
116+ }
117+
118+ public void AppendLine (
119+ [ InterpolatedStringHandlerArgument ( "" ) ]
120+ SourceBuilderStringHandler s )
121+ {
122+ Append ( s ) ;
123+ _stringBuilder . AppendLine ( ) ;
124+ }
125+
126+ public void AppendLine ( string s )
127+ {
128+ Append ( s ) ;
129+ _stringBuilder . AppendLine ( ) ;
130+ }
131+
132+ public int CompareTo ( IndentingBuilder ? other )
133+ {
134+ if ( other is null ) return 1 ;
135+
136+ var lenCmp = _stringBuilder . Length . CompareTo ( other . _stringBuilder . Length ) ;
137+ if ( lenCmp != 0 )
138+ {
139+ return lenCmp ;
140+ }
141+ for ( int i = 0 ; i < _stringBuilder . Length ; i ++ )
142+ {
143+ var cCmp = _stringBuilder [ i ] . CompareTo ( other . _stringBuilder [ i ] ) ;
144+ if ( cCmp != 0 )
145+ {
146+ return cCmp ;
147+ }
148+ }
149+ return 0 ;
150+ }
151+
152+ public void Indent ( )
153+ {
154+ _currentIndentWhitespace += " " ;
155+ }
156+
157+ public void Dedent ( )
158+ {
159+ _currentIndentWhitespace = _currentIndentWhitespace [ ..^ 4 ] ;
160+ }
161+
162+ public bool Equals ( IndentingBuilder ? other )
163+ {
164+ return _stringBuilder . Equals ( other ? . _stringBuilder ) ;
165+ }
166+
167+ public void AppendLine ( IndentingBuilder deserialize )
168+ {
169+ Append ( deserialize ) ;
170+ _stringBuilder . AppendLine ( ) ;
171+ }
172+
173+ [ InterpolatedStringHandler ]
174+ public ref struct SourceBuilderStringHandler
175+ {
176+ internal readonly StringBuilder _stringBuilder ;
177+ private readonly string _originalIndentWhitespace ;
178+ private string _currentIndentWhitespace ;
179+ private bool _isFirst = true ;
180+
181+ public SourceBuilderStringHandler ( int literalLength , int formattedCount )
182+ {
183+ _stringBuilder = new StringBuilder ( literalLength ) ;
184+ _originalIndentWhitespace = "" ;
185+ _currentIndentWhitespace = "" ;
186+ }
187+
188+ public SourceBuilderStringHandler (
189+ int literalLength ,
190+ int formattedCount ,
191+ IndentingBuilder sourceBuilder )
192+ {
193+ _stringBuilder = sourceBuilder . _stringBuilder ;
194+ _originalIndentWhitespace = sourceBuilder . _currentIndentWhitespace ;
195+ _currentIndentWhitespace = sourceBuilder . _currentIndentWhitespace ;
196+ }
197+
198+ public void AppendLiteral ( string s )
199+ {
200+ if ( _isFirst )
201+ {
202+ _stringBuilder . Append ( _currentIndentWhitespace ) ;
203+ _isFirst = false ;
204+ }
205+ Append ( _stringBuilder , _currentIndentWhitespace , s ) ;
206+
207+ int last = s . LastIndexOf ( '\n ' ) ;
208+ if ( last == - 1 )
209+ {
210+ return ;
211+ }
212+
213+ var remaining = s . AsSpan ( last + 1 ) ;
214+ foreach ( var c in remaining )
215+ {
216+ if ( c is not ( ' ' or '\t ' ) )
217+ {
218+ return ;
219+ }
220+ }
221+
222+ _currentIndentWhitespace += remaining . ToString ( ) ;
223+ }
224+
225+ public void AppendFormatted < T > ( T value )
226+ {
227+ if ( _isFirst )
228+ {
229+ _stringBuilder . Append ( _currentIndentWhitespace ) ;
230+ _isFirst = false ;
231+ }
232+ var str = value ? . ToString ( ) ;
233+ if ( str is null )
234+ {
235+ _stringBuilder . Append ( str ) ;
236+ return ;
237+ }
238+
239+ Append ( _stringBuilder , _currentIndentWhitespace , str ) ;
240+ _currentIndentWhitespace = _originalIndentWhitespace ;
241+ }
242+ }
243+ }
0 commit comments