1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
|
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace ICSharpCode.AvalonEdit.Indentation.CSharp
{
sealed class IndentationSettings
{
public string IndentString = "\t";
/// <summary>Leave empty lines empty.</summary>
public bool LeaveEmptyLines = true;
}
sealed class IndentationReformatter
{
/// <summary>
/// An indentation block. Tracks the state of the indentation.
/// </summary>
struct Block
{
/// <summary>
/// The indentation outside of the block.
/// </summary>
public string OuterIndent;
/// <summary>
/// The indentation inside the block.
/// </summary>
public string InnerIndent;
/// <summary>
/// The last word that was seen inside this block.
/// Because parenthesis open a sub-block and thus don't change their parent's LastWord,
/// this property can be used to identify the type of block statement (if, while, switch)
/// at the position of the '{'.
/// </summary>
public string LastWord;
/// <summary>
/// The type of bracket that opened this block (, [ or {
/// </summary>
public char Bracket;
/// <summary>
/// Gets whether there's currently a line continuation going on inside this block.
/// </summary>
public bool Continuation;
/// <summary>
/// Gets whether there's currently a 'one-line-block' going on. 'one-line-blocks' occur
/// with if statements that don't use '{}'. They are not represented by a Block instance on
/// the stack, but are instead handled similar to line continuations.
/// This property is an integer because there might be multiple nested one-line-blocks.
/// As soon as there is a finished statement, OneLineBlock is reset to 0.
/// </summary>
public int OneLineBlock;
/// <summary>
/// The previous value of one-line-block before it was reset.
/// Used to restore the indentation of 'else' to the correct level.
/// </summary>
public int PreviousOneLineBlock;
public void ResetOneLineBlock()
{
PreviousOneLineBlock = OneLineBlock;
OneLineBlock = 0;
}
/// <summary>
/// Gets the line number where this block started.
/// </summary>
public int StartLine;
public void Indent(IndentationSettings set)
{
Indent(set.IndentString);
}
public void Indent(string indentationString)
{
OuterIndent = InnerIndent;
InnerIndent += indentationString;
Continuation = false;
ResetOneLineBlock();
LastWord = "";
}
public override string ToString()
{
return string.Format(
CultureInfo.InvariantCulture,
"[Block StartLine={0}, LastWord='{1}', Continuation={2}, OneLineBlock={3}, PreviousOneLineBlock={4}]",
this.StartLine, this.LastWord, this.Continuation, this.OneLineBlock, this.PreviousOneLineBlock);
}
}
StringBuilder wordBuilder;
Stack<Block> blocks; // blocks contains all blocks outside of the current
Block block; // block is the current block
bool inString;
bool inChar;
bool verbatim;
bool escape;
bool lineComment;
bool blockComment;
char lastRealChar; // last non-comment char
public void Reformat(IDocumentAccessor doc, IndentationSettings set)
{
Init();
while (doc.MoveNext()) {
Step(doc, set);
}
}
public void Init()
{
wordBuilder = new StringBuilder();
blocks = new Stack<Block>();
block = new Block();
block.InnerIndent = "";
block.OuterIndent = "";
block.Bracket = '{';
block.Continuation = false;
block.LastWord = "";
block.OneLineBlock = 0;
block.PreviousOneLineBlock = 0;
block.StartLine = 0;
inString = false;
inChar = false;
verbatim = false;
escape = false;
lineComment = false;
blockComment = false;
lastRealChar = ' '; // last non-comment char
}
public void Step(IDocumentAccessor doc, IndentationSettings set)
{
string line = doc.Text;
if (set.LeaveEmptyLines && line.Length == 0) return; // leave empty lines empty
line = line.TrimStart();
StringBuilder indent = new StringBuilder();
if (line.Length == 0) {
// Special treatment for empty lines:
if (blockComment || (inString && verbatim))
return;
indent.Append(block.InnerIndent);
indent.Append(Repeat(set.IndentString, block.OneLineBlock));
if (block.Continuation)
indent.Append(set.IndentString);
if (doc.Text != indent.ToString())
doc.Text = indent.ToString();
return;
}
if (TrimEnd(doc))
line = doc.Text.TrimStart();
Block oldBlock = block;
bool startInComment = blockComment;
bool startInString = (inString && verbatim);
#region Parse char by char
lineComment = false;
inChar = false;
escape = false;
if (!verbatim) inString = false;
lastRealChar = '\n';
char lastchar = ' ';
char c = ' ';
char nextchar = line[0];
for (int i = 0; i < line.Length; i++) {
if (lineComment) break; // cancel parsing current line
lastchar = c;
c = nextchar;
if (i + 1 < line.Length)
nextchar = line[i + 1];
else
nextchar = '\n';
if (escape) {
escape = false;
continue;
}
#region Check for comment/string chars
switch (c) {
case '/':
if (blockComment && lastchar == '*')
blockComment = false;
if (!inString && !inChar) {
if (!blockComment && nextchar == '/')
lineComment = true;
if (!lineComment && nextchar == '*')
blockComment = true;
}
break;
case '#':
if (!(inChar || blockComment || inString))
lineComment = true;
break;
case '"':
if (!(inChar || lineComment || blockComment)) {
inString = !inString;
if (!inString && verbatim) {
if (nextchar == '"') {
escape = true; // skip escaped quote
inString = true;
} else {
verbatim = false;
}
} else if (inString && lastchar == '@') {
verbatim = true;
}
}
break;
case '\'':
if (!(inString || lineComment || blockComment)) {
inChar = !inChar;
}
break;
case '\\':
if ((inString && !verbatim) || inChar)
escape = true; // skip next character
break;
}
#endregion
if (lineComment || blockComment || inString || inChar) {
if (wordBuilder.Length > 0)
block.LastWord = wordBuilder.ToString();
wordBuilder.Length = 0;
continue;
}
if (!Char.IsWhiteSpace(c) && c != '[' && c != '/') {
if (block.Bracket == '{')
block.Continuation = true;
}
if (Char.IsLetterOrDigit(c)) {
wordBuilder.Append(c);
} else {
if (wordBuilder.Length > 0)
block.LastWord = wordBuilder.ToString();
wordBuilder.Length = 0;
}
#region Push/Pop the blocks
switch (c) {
case '{':
block.ResetOneLineBlock();
blocks.Push(block);
block.StartLine = doc.LineNumber;
if (block.LastWord == "switch") {
block.Indent(set.IndentString + set.IndentString);
/* oldBlock refers to the previous line, not the previous block
* The block we want is not available anymore because it was never pushed.
* } else if (oldBlock.OneLineBlock) {
// Inside a one-line-block is another statement
// with a full block: indent the inner full block
// by one additional level
block.Indent(set, set.IndentString + set.IndentString);
block.OuterIndent += set.IndentString;
// Indent current line if it starts with the '{' character
if (i == 0) {
oldBlock.InnerIndent += set.IndentString;
}*/
} else {
block.Indent(set);
}
block.Bracket = '{';
break;
case '}':
while (block.Bracket != '{') {
if (blocks.Count == 0) break;
block = blocks.Pop();
}
if (blocks.Count == 0) break;
block = blocks.Pop();
block.Continuation = false;
block.ResetOneLineBlock();
break;
case '(':
case '[':
blocks.Push(block);
if (block.StartLine == doc.LineNumber)
block.InnerIndent = block.OuterIndent;
else
block.StartLine = doc.LineNumber;
block.Indent(Repeat(set.IndentString, oldBlock.OneLineBlock) +
(oldBlock.Continuation ? set.IndentString : "") +
(i == line.Length - 1 ? set.IndentString : new String(' ', i + 1)));
block.Bracket = c;
break;
case ')':
if (blocks.Count == 0) break;
if (block.Bracket == '(') {
block = blocks.Pop();
if (IsSingleStatementKeyword(block.LastWord))
block.Continuation = false;
}
break;
case ']':
if (blocks.Count == 0) break;
if (block.Bracket == '[')
block = blocks.Pop();
break;
case ';':
case ',':
block.Continuation = false;
block.ResetOneLineBlock();
break;
case ':':
if (block.LastWord == "case"
|| line.StartsWith("case ", StringComparison.Ordinal)
|| line.StartsWith(block.LastWord + ":", StringComparison.Ordinal))
{
block.Continuation = false;
block.ResetOneLineBlock();
}
break;
}
if (!Char.IsWhiteSpace(c)) {
// register this char as last char
lastRealChar = c;
}
#endregion
}
#endregion
if (wordBuilder.Length > 0)
block.LastWord = wordBuilder.ToString();
wordBuilder.Length = 0;
if (startInString) return;
if (startInComment && line[0] != '*') return;
if (doc.Text.StartsWith("//\t", StringComparison.Ordinal) || doc.Text == "//")
return;
if (line[0] == '}') {
indent.Append(oldBlock.OuterIndent);
oldBlock.ResetOneLineBlock();
oldBlock.Continuation = false;
} else {
indent.Append(oldBlock.InnerIndent);
}
if (indent.Length > 0 && oldBlock.Bracket == '(' && line[0] == ')') {
indent.Remove(indent.Length - 1, 1);
} else if (indent.Length > 0 && oldBlock.Bracket == '[' && line[0] == ']') {
indent.Remove(indent.Length - 1, 1);
}
if (line[0] == ':') {
oldBlock.Continuation = true;
} else if (lastRealChar == ':' && indent.Length >= set.IndentString.Length) {
if (block.LastWord == "case" || line.StartsWith("case ", StringComparison.Ordinal) || line.StartsWith(block.LastWord + ":", StringComparison.Ordinal))
indent.Remove(indent.Length - set.IndentString.Length, set.IndentString.Length);
} else if (lastRealChar == ')') {
if (IsSingleStatementKeyword(block.LastWord)) {
block.OneLineBlock++;
}
} else if (lastRealChar == 'e' && block.LastWord == "else") {
block.OneLineBlock = Math.Max(1, block.PreviousOneLineBlock);
block.Continuation = false;
oldBlock.OneLineBlock = block.OneLineBlock - 1;
}
if (doc.IsReadOnly) {
// We can't change the current line, but we should accept the existing
// indentation if possible (=if the current statement is not a multiline
// statement).
if (!oldBlock.Continuation && oldBlock.OneLineBlock == 0 &&
oldBlock.StartLine == block.StartLine &&
block.StartLine < doc.LineNumber && lastRealChar != ':')
{
// use indent StringBuilder to get the indentation of the current line
indent.Length = 0;
line = doc.Text; // get untrimmed line
for (int i = 0; i < line.Length; ++i) {
if (!Char.IsWhiteSpace(line[i]))
break;
indent.Append(line[i]);
}
// /* */ multiline comments have an extra space - do not count it
// for the block's indentation.
if (startInComment && indent.Length > 0 && indent[indent.Length - 1] == ' ') {
indent.Length -= 1;
}
block.InnerIndent = indent.ToString();
}
return;
}
if (line[0] != '{') {
if (line[0] != ')' && oldBlock.Continuation && oldBlock.Bracket == '{')
indent.Append(set.IndentString);
indent.Append(Repeat(set.IndentString, oldBlock.OneLineBlock));
}
// this is only for blockcomment lines starting with *,
// all others keep their old indentation
if (startInComment)
indent.Append(' ');
if (indent.Length != (doc.Text.Length - line.Length) ||
!doc.Text.StartsWith(indent.ToString(), StringComparison.Ordinal) ||
Char.IsWhiteSpace(doc.Text[indent.Length]))
{
doc.Text = indent.ToString() + line;
}
}
static string Repeat(string text, int count)
{
if (count == 0)
return string.Empty;
if (count == 1)
return text;
StringBuilder b = new StringBuilder(text.Length * count);
for (int i = 0; i < count; i++)
b.Append(text);
return b.ToString();
}
static bool IsSingleStatementKeyword(string keyword)
{
switch (keyword) {
case "if":
case "for":
case "while":
case "do":
case "foreach":
case "using":
case "lock":
return true;
default:
return false;
}
}
static bool TrimEnd(IDocumentAccessor doc)
{
string line = doc.Text;
if (!Char.IsWhiteSpace(line[line.Length - 1])) return false;
// one space after an empty comment is allowed
if (line.EndsWith("// ", StringComparison.Ordinal) || line.EndsWith("* ", StringComparison.Ordinal))
return false;
doc.Text = line.TrimEnd();
return true;
}
}
}
|