// // CSharpIndentEngine.cs // // Author: // Mike Krüger // // Copyright (c) 2012 Xamarin Inc. (http://xamarin.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using ICSharpCode.NRefactory.Editor; using System.Text; using System.Collections.Generic; using System.Linq; using System.Globalization; namespace ICSharpCode.NRefactory.CSharp { public class CSharpIndentEngine { readonly IDocument document; readonly CSharpFormattingOptions options; readonly TextEditorOptions textEditorOptions; readonly StringBuilder wordBuf = new StringBuilder(); readonly StringBuilder currentIndent = new StringBuilder(); Indent thisLineindent; Indent indent; Indent indentDelta; public IList ConditionalSymbols { get; set; } public TextLocation Location { get { return new TextLocation(line, col); } } public int Offset { get { return offset; } } public string ThisLineIndent { get { return thisLineindent.IndentString; } } public string NewLineIndent { get { return indent.IndentString + indentDelta.IndentString; } } public bool NeedsReindent { get { return ThisLineIndent != currentIndent.ToString(); } } public CSharpIndentEngine(IDocument document, TextEditorOptions textEditorOptions, CSharpFormattingOptions formattingOptions) { this.document = document; this.options = formattingOptions; this.textEditorOptions = textEditorOptions; this.indent = new Indent(textEditorOptions); this.indentDelta = new Indent(textEditorOptions); this.thisLineindent = new Indent(textEditorOptions); } CSharpIndentEngine(CSharpIndentEngine prototype) { this.document = prototype.document; this.options = prototype.options; this.textEditorOptions = prototype.textEditorOptions; this.indent = prototype.indent.Clone(); this.indentDelta = prototype.indentDelta.Clone(); this.thisLineindent = prototype.thisLineindent.Clone(); this.offset = prototype.offset; this.inside = prototype.inside; this.IsLineStart = prototype.IsLineStart; this.pc = prototype.pc; this.parenStack = new Stack(prototype.parenStack.Reverse()); this.currentBody = prototype.currentBody; this.nextBody = prototype.nextBody; this.addContinuation = prototype.addContinuation; this.line = prototype.line; this.col = prototype.col; this.popNextParenBlock = prototype.popNextParenBlock; } public CSharpIndentEngine Clone() { return new CSharpIndentEngine(this); } int offset; Inside inside = Inside.Empty; bool IsLineStart = true; char pc; Stack parenStack = new Stack(); Body currentBody; Body nextBody; bool addContinuation; int line, col; bool popNextParenBlock; bool readPreprocessorExpression; public void Reset() { offset = 0; thisLineindent.Reset(); indent.Reset(); pc = '\0'; IsLineStart = true; addContinuation = false; popNextParenBlock = false; parenStack.Clear(); inside = Inside.Empty; nextBody = currentBody = Body.None; line = col = 1; } public void UpdateToOffset(int toOffset) { if (toOffset < offset) Reset(); for (int i = offset; i < toOffset; i++) Push(document.GetCharAt(i)); } public bool IsInStringOrChar { get { return inside.HasFlag(Inside.StringOrChar); } } public bool IsInComment { get { return inside.HasFlag(Inside.Comment); } } public bool IsInPreProcessorComment { get { return inside.HasFlag(Inside.PreProcessorComment); } } public bool IsInPreProcessorDirective { get { return inside.HasFlag(Inside.PreProcessor); } } public bool IsInVerbatimString { get { return inside.HasFlag(Inside.VerbatimString); } } public bool IsInsideDocLineComment { get { return inside.HasFlag(Inside.DocComment); } } public bool IsInsideMultiLineComment { get { return inside.HasFlag(Inside.MultiLineComment); } } public bool IsInsideStringLiteral { get { return inside.HasFlag(Inside.StringLiteral); } } [Flags] public enum Inside { Empty = 0, PreProcessor = (1 << 0), PreProcessorComment = (1 << 12), MultiLineComment = (1 << 1), LineComment = (1 << 2), DocComment = (1 << 11), Comment = (MultiLineComment | LineComment | DocComment), VerbatimString = (1 << 3), StringLiteral = (1 << 4), CharLiteral = (1 << 5), String = (VerbatimString | StringLiteral), StringOrChar = (String | CharLiteral), Attribute = (1 << 6), ParenList = (1 << 7), FoldedStatement = (1 << 8), Block = (1 << 9), Case = (1 << 10), FoldedOrBlock = (FoldedStatement | Block), FoldedBlockOrCase = (FoldedStatement | Block | Case) } #region Pre processor evaluation (from cs-tokenizer.cs) static bool is_identifier_start_character(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || Char.IsLetter((char)c); } static bool is_identifier_part_character(char c) { if (c >= 'a' && c <= 'z') return true; if (c >= 'A' && c <= 'Z') return true; if (c == '_' || (c >= '0' && c <= '9')) return true; if (c < 0x80) return false; return Char.IsLetter(c) || Char.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation; } bool eval_val(string s) { if (s == "true") return true; if (s == "false") return false; return ConditionalSymbols != null && ConditionalSymbols.Contains(s); } bool pp_primary(ref string s) { s = s.Trim(); int len = s.Length; if (len > 0) { char c = s [0]; if (c == '(') { s = s.Substring(1); bool val = pp_expr(ref s, false); if (s.Length > 0 && s [0] == ')') { s = s.Substring(1); return val; } return false; } if (is_identifier_start_character(c)) { int j = 1; while (j < len) { c = s [j]; if (is_identifier_part_character(c)) { j++; continue; } bool v = eval_val(s.Substring(0, j)); s = s.Substring(j); return v; } bool vv = eval_val(s); s = ""; return vv; } } return false; } bool pp_unary(ref string s) { s = s.Trim(); int len = s.Length; if (len > 0) { if (s [0] == '!') { if (len > 1 && s [1] == '=') { return false; } s = s.Substring(1); return ! pp_primary(ref s); } else return pp_primary(ref s); } else { return false; } } bool pp_eq(ref string s) { bool va = pp_unary(ref s); s = s.Trim(); int len = s.Length; if (len > 0) { if (s [0] == '=') { if (len > 2 && s [1] == '=') { s = s.Substring(2); return va == pp_unary(ref s); } else { return false; } } else if (s [0] == '!' && len > 1 && s [1] == '=') { s = s.Substring(2); return va != pp_unary(ref s); } } return va; } bool pp_and(ref string s) { bool va = pp_eq(ref s); s = s.Trim(); int len = s.Length; if (len > 0) { if (s [0] == '&') { if (len > 2 && s [1] == '&') { s = s.Substring(2); return (va & pp_and(ref s)); } else { return false; } } } return va; } // // Evaluates an expression for `#if' or `#elif' // bool pp_expr(ref string s, bool isTerm) { bool va = pp_and(ref s); s = s.Trim(); int len = s.Length; if (len > 0) { char c = s [0]; if (c == '|') { if (len > 2 && s [1] == '|') { s = s.Substring(2); return va | pp_expr(ref s, isTerm); } else { return false; } } if (isTerm) { return false; } } return va; } bool eval(string s) { bool v = pp_expr(ref s, true); s = s.Trim(); if (s.Length != 0) { return false; } return v; } #endregion public void Push(char ch) { if (readPreprocessorExpression) { wordBuf.Append(ch); } if (inside.HasFlag(Inside.VerbatimString) && pc == '"' && ch != '"') { inside &= ~Inside.StringLiteral; } switch (ch) { case '#': if (IsLineStart) inside = Inside.PreProcessor; break; case '/': if (IsInStringOrChar || IsInPreProcessorComment) break; if (pc == '/') { if (inside.HasFlag(Inside.Comment)) { inside |= Inside.DocComment; } else { inside |= Inside.Comment; } } break; case '*': if (IsInStringOrChar || IsInComment || IsInPreProcessorComment) break; if (pc == '/') inside |= Inside.MultiLineComment; break; case ' ': currentIndent.Append(' '); break; case '\t': var nextTabStop = (col - 1 + textEditorOptions.IndentSize) / textEditorOptions.IndentSize; col = 1 + nextTabStop * textEditorOptions.IndentSize; currentIndent.Append('\t'); offset++; return; case '\r': if (readPreprocessorExpression) { if (!eval(wordBuf.ToString())) inside |= Inside.PreProcessorComment; } inside &= ~(Inside.Comment | Inside.String | Inside.CharLiteral | Inside.PreProcessor); CheckKeyword(wordBuf.ToString()); wordBuf.Length = 0; indent.Push(indentDelta); indentDelta = new Indent(textEditorOptions); if (addContinuation) { indent.Push(IndentType.Continuation); } thisLineindent = indent.Clone(); addContinuation = false; IsLineStart = true; readPreprocessorExpression = false; col = 1; line++; currentIndent.Length = 0; break; case '\n': if (pc == '\r') break; goto case '\r'; case '"': if (IsInComment || IsInPreProcessorComment) break; if (inside.HasFlag(Inside.StringLiteral)) { if (pc != '\\') inside &= ~Inside.StringLiteral; break; } if (pc == '@') { inside |= Inside.VerbatimString; } else { inside |= Inside.StringLiteral; } break; case '<': case '[': case '(': if (IsInComment || IsInStringOrChar || IsInPreProcessorComment) break; parenStack.Push(new TextLocation(line, col)); popNextParenBlock = true; indent.Push(IndentType.Block); break; case '>': case ']': case ')': if (IsInComment || IsInStringOrChar || IsInPreProcessorComment) break; if (popNextParenBlock && parenStack.Count > 0) parenStack.Pop(); if (indent.Count > 0) indent.Pop(); indent.ExtraSpaces = 0; break; case ',': if (IsInComment || IsInStringOrChar || IsInPreProcessorComment) break; if (parenStack.Count > 0 && parenStack.Peek().Line == line) { if (indent.Count > 0) indent.Pop(); popNextParenBlock = false; indent.ExtraSpaces = parenStack.Peek().Column - 1 - thisLineindent.CurIndent; } break; case '{': if (IsInComment || IsInStringOrChar || IsInPreProcessorComment) break; currentBody = nextBody; if (indent.Count > 0 && indent.Peek() == IndentType.Continuation) indent.Pop(); addContinuation = false; AddIndentation(currentBody); break; case '}': if (IsInComment || IsInStringOrChar || IsInPreProcessorComment) break; if (indentDelta.CurIndent > 0) { indentDelta.Pop(); if (indentDelta.Count > 0 && indentDelta.Peek() == IndentType.Continuation) indentDelta.Pop(); } else { if (thisLineindent.Count > 0) thisLineindent.Pop(); if (indent.Count > 0) indent.Pop(); } break; case ';': if (IsInComment || IsInStringOrChar || IsInPreProcessorComment) break; if (indent.Count > 0 && indent.Peek() == IndentType.Continuation) indent.Pop(); break; case '\'': if (IsInComment || inside.HasFlag(Inside.StringLiteral) || IsInPreProcessorComment) break; if (inside.HasFlag(Inside.CharLiteral)) { if (pc != '\\') inside &= ~Inside.CharLiteral; } else { inside &= Inside.CharLiteral; } break; } if (!IsInComment && !IsInStringOrChar && !readPreprocessorExpression) { if ((wordBuf.Length == 0 ? char.IsLetter(ch) : char.IsLetterOrDigit(ch)) || ch == '_') { wordBuf.Append(ch); } else { if (inside.HasFlag(Inside.PreProcessor)) { if (wordBuf.ToString() == "endif") { inside &= ~Inside.PreProcessorComment; } else if (wordBuf.ToString() == "if") { readPreprocessorExpression = true; } else if (wordBuf.ToString() == "elif") { inside &= ~Inside.PreProcessorComment; readPreprocessorExpression = true; } } else { CheckKeyword(wordBuf.ToString()); } wordBuf.Length = 0; } } if (addContinuation) { indent.Push(IndentType.Continuation); addContinuation = false; } IsLineStart &= ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; pc = ch; if (ch != '\n' && ch != '\r') col++; offset++; } void AddIndentation(BraceStyle braceStyle) { switch (braceStyle) { case BraceStyle.DoNotChange: case BraceStyle.EndOfLine: case BraceStyle.EndOfLineWithoutSpace: case BraceStyle.NextLine: case BraceStyle.NextLineShifted: case BraceStyle.BannerStyle: indentDelta.Push(IndentType.Block); break; case BraceStyle.NextLineShifted2: indentDelta.Push(IndentType.DoubleBlock); break; } } void AddIndentation(Body body) { switch (body) { case Body.None: indentDelta.Push(IndentType.Block); break; case Body.Namespace: AddIndentation(options.NamespaceBraceStyle); break; case Body.Class: AddIndentation(options.ClassBraceStyle); break; case Body.Struct: AddIndentation(options.StructBraceStyle); break; case Body.Interface: AddIndentation(options.InterfaceBraceStyle); break; case Body.Enum: AddIndentation(options.EnumBraceStyle); break; case Body.Switch: if (options.IndentSwitchBody) indentDelta.Push(IndentType.Empty); break; default: throw new ArgumentOutOfRangeException(); } } enum Body { None, Namespace, Class, Struct, Interface, Enum, Switch } void CheckKeyword(string keyword) { switch (currentBody) { case Body.None: if (keyword == "namespace") { nextBody = Body.Namespace; return; } goto case Body.Namespace; case Body.Namespace: if (keyword == "class") { nextBody = Body.Class; return; } if (keyword == "enum") { nextBody = Body.Enum; return; } if (keyword == "struct") { nextBody = Body.Struct; return; } if (keyword == "interface") { nextBody = Body.Interface; return; } break; case Body.Class: case Body.Enum: case Body.Struct: case Body.Interface: if (keyword == "switch") nextBody = Body.Switch; if (keyword == "do" || keyword == "if" || keyword == "for" || keyword == "foreach" || keyword == "while") { addContinuation = true; } break; } } } }