Free cookie consent management tool by TermsFeed Policy Generator

source: branches/CodeEditor/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Indentation/CSharp/IndentationReformatter.cs @ 11700

Last change on this file since 11700 was 11700, checked in by jkarder, 9 years ago

#2077: created branch and added first version

File size: 14.2 KB
Line 
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
19using System;
20using System.Collections.Generic;
21using System.Globalization;
22using System.Text;
23
24namespace 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}
Note: See TracBrowser for help on using the repository browser.