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