// // CSharpIndentEngine.cs // // Author: // Matej Miklečić // // Copyright (c) 2013 Matej Miklečić (matej.miklecic@gmail.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 ICSharpCode.NRefactory.Editor; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ICSharpCode.NRefactory.CSharp { /// /// Indentation engine based on a state machine. /// Supports only pushing new chars to the end. /// /// /// Represents the context for transitions between . /// Delegates the responsibility for pushing a new char to the current /// state and changes between states depending on the pushed chars. /// public class CSharpIndentEngine : IStateMachineIndentEngine { #region Properties /// /// Formatting options. /// internal readonly CSharpFormattingOptions formattingOptions; /// /// Text editor options. /// internal readonly TextEditorOptions textEditorOptions; /// /// A readonly reference to the document that's parsed /// by the engine. /// internal readonly IDocument document; /// /// Represents the new line character. /// internal readonly char newLineChar; /// /// The current indentation state. /// internal IndentState currentState; /// /// Stores conditional symbols of #define directives. /// internal HashSet conditionalSymbols; /// /// Stores custom conditional symbols. /// internal HashSet customConditionalSymbols; /// /// Stores the results of evaluations of the preprocessor if/elif directives /// in the current block (between #if and #endif). /// internal CloneableStack ifDirectiveEvalResults = new CloneableStack (); /// /// Stores the indentation levels of the if directives in the current block. /// internal CloneableStack ifDirectiveIndents = new CloneableStack(); /// /// Stores the last sequence of characters that can form a /// valid keyword or variable name. /// internal StringBuilder wordToken; /// /// Stores the previous sequence of chars that formed a /// valid keyword or variable name. /// internal string previousKeyword; #endregion #region IDocumentIndentEngine /// public IDocument Document { get { return document; } } /// public string ThisLineIndent { get { // OPTION: IndentBlankLines // remove the indentation of this line if isLineStart is true // if (!textEditorOptions.IndentBlankLines && isLineStart) // { // return string.Empty; // } return currentState.ThisLineIndent.IndentString; } } /// public string NextLineIndent { get { return currentState.NextLineIndent.IndentString; } } /// public string CurrentIndent { get { return currentIndent.ToString(); } } /// /// /// This is set depending on the current and /// can change its value until the char is /// pushed. If this is true, that doesn't necessarily mean that the /// current line has an incorrect indent (this can be determined /// only at the end of the current line). /// public bool NeedsReindent { get { // return true if it's the first column of the line and it has an indent if (Location.Column == 1) { return ThisLineIndent.Length > 0; } // ignore incorrect indentations when there's only ws on this line if (isLineStart) { return false; } return ThisLineIndent != CurrentIndent.ToString(); } } /// public int Offset { get { return offset; } } /// public TextLocation Location { get { return new TextLocation(line, column); } } /// public bool EnableCustomIndentLevels { get; set; } #endregion #region Fields /// /// Represents the number of pushed chars. /// internal int offset = 0; /// /// The current line number. /// internal int line = 1; /// /// The current column number. /// /// /// One char can take up multiple columns (e.g. \t). /// internal int column = 1; /// /// True if is true for all /// chars at the current line. /// internal bool isLineStart = true; /// /// True if was true before the current /// . /// internal bool isLineStartBeforeWordToken = true; /// /// Current char that's being pushed. /// internal char currentChar = '\0'; /// /// Last non-whitespace char that has been pushed. /// internal char previousChar = '\0'; /// /// Previous new line char /// internal char previousNewline = '\0'; /// /// Current indent level on this line. /// internal StringBuilder currentIndent = new StringBuilder(); /// /// True if this line began in . /// internal bool lineBeganInsideVerbatimString = false; /// /// True if this line began in . /// internal bool lineBeganInsideMultiLineComment = false; #endregion #region Constructors /// /// Creates a new CSharpIndentEngine instance. /// /// /// An instance of which is being parsed. /// /// /// C# formatting options. /// /// /// Text editor options for indentation. /// public CSharpIndentEngine(IDocument document, TextEditorOptions textEditorOptions, CSharpFormattingOptions formattingOptions) { this.formattingOptions = formattingOptions; this.textEditorOptions = textEditorOptions; this.document = document; this.currentState = new GlobalBodyState(this); this.conditionalSymbols = new HashSet(); this.customConditionalSymbols = new HashSet(); this.wordToken = new StringBuilder(); this.previousKeyword = string.Empty; this.newLineChar = textEditorOptions.EolMarker[0]; } /// /// Creates a new CSharpIndentEngine instance from the given prototype. /// /// /// An CSharpIndentEngine instance. /// public CSharpIndentEngine(CSharpIndentEngine prototype) { this.formattingOptions = prototype.formattingOptions; this.textEditorOptions = prototype.textEditorOptions; this.document = prototype.document; this.newLineChar = prototype.newLineChar; this.currentState = prototype.currentState.Clone(this); this.conditionalSymbols = new HashSet(prototype.conditionalSymbols); this.customConditionalSymbols = new HashSet(prototype.customConditionalSymbols); this.wordToken = new StringBuilder(prototype.wordToken.ToString()); this.previousKeyword = string.Copy(prototype.previousKeyword); this.offset = prototype.offset; this.line = prototype.line; this.column = prototype.column; this.isLineStart = prototype.isLineStart; this.isLineStartBeforeWordToken = prototype.isLineStartBeforeWordToken; this.currentChar = prototype.currentChar; this.previousChar = prototype.previousChar; this.previousNewline = prototype.previousNewline; this.currentIndent = new StringBuilder(prototype.CurrentIndent.ToString()); this.lineBeganInsideMultiLineComment = prototype.lineBeganInsideMultiLineComment; this.lineBeganInsideVerbatimString = prototype.lineBeganInsideVerbatimString; this.ifDirectiveEvalResults = prototype.ifDirectiveEvalResults.Clone(); this.ifDirectiveIndents = prototype.ifDirectiveIndents.Clone(); this.EnableCustomIndentLevels = prototype.EnableCustomIndentLevels; } #endregion #region IClonable object ICloneable.Clone() { return Clone(); } /// IDocumentIndentEngine IDocumentIndentEngine.Clone() { return Clone(); } public IStateMachineIndentEngine Clone() { return new CSharpIndentEngine(this); } #endregion #region Methods /// public void Push(char ch) { // append this char to the wordbuf if it can form a valid keyword, otherwise check // if the last sequence of chars form a valid keyword and reset the wordbuf. if ((wordToken.Length == 0 ? char.IsLetter(ch) : char.IsLetterOrDigit(ch)) || ch == '_') { wordToken.Append(ch); } else if (wordToken.Length > 0) { currentState.CheckKeyword(wordToken.ToString()); previousKeyword = wordToken.ToString(); wordToken.Length = 0; isLineStartBeforeWordToken = false; } var isNewLine = NewLine.IsNewLine(ch); if (!isNewLine) { currentState.Push(currentChar = ch); offset++; previousNewline = '\0'; // ignore whitespace and newline chars var isWhitespace = currentChar == ' ' || currentChar == '\t'; if (!isWhitespace) { previousChar = currentChar; isLineStart = false; } if (isLineStart) { currentIndent.Append(ch); } if (ch == '\t') { var nextTabStop = (column - 1 + textEditorOptions.IndentSize) / textEditorOptions.IndentSize; column = 1 + nextTabStop * textEditorOptions.IndentSize; } else { column++; } } else { if (ch == NewLine.LF && previousNewline == NewLine.CR) { offset++; return; } currentState.Push(currentChar = newLineChar); offset++; previousNewline = ch; // there can be more than one chars that determine the EOL, // the engine uses only one of them defined with newLineChar if (currentChar != newLineChar) { return; } currentIndent.Length = 0; isLineStart = true; isLineStartBeforeWordToken = true; column = 1; line++; lineBeganInsideMultiLineComment = IsInsideMultiLineComment; lineBeganInsideVerbatimString = IsInsideVerbatimString; } } /// public void Reset() { currentState = new GlobalBodyState(this); conditionalSymbols.Clear(); ifDirectiveEvalResults.Clear(); ifDirectiveIndents.Clear(); offset = 0; line = 1; column = 1; isLineStart = true; currentChar = '\0'; previousChar = '\0'; currentIndent.Length = 0; lineBeganInsideMultiLineComment = false; lineBeganInsideVerbatimString = false; } /// public void Update(int offset) { if (Offset > offset) { Reset(); } while (Offset < offset) { Push(Document.GetCharAt(Offset)); } } /// /// Defines the conditional symbol. /// /// The symbol to define. public void DefineSymbol(string defineSymbol) { if (!customConditionalSymbols.Contains(defineSymbol)) customConditionalSymbols.Add(defineSymbol); } /// /// Removes the symbol. /// /// The symbol to undefine. public void RemoveSymbol(string undefineSymbol) { if (customConditionalSymbols.Contains(undefineSymbol)) customConditionalSymbols.Remove(undefineSymbol); } #endregion #region IStateMachineIndentEngine public bool IsInsidePreprocessorDirective { get { return currentState is PreProcessorState; } } public bool IsInsidePreprocessorComment { get { return currentState is PreProcessorCommentState; } } public bool IsInsideStringLiteral { get { return currentState is StringLiteralState; } } public bool IsInsideVerbatimString { get { return currentState is VerbatimStringState; } } public bool IsInsideCharacter { get { return currentState is CharacterState; } } public bool IsInsideString { get { return IsInsideStringLiteral || IsInsideVerbatimString || IsInsideCharacter; } } public bool IsInsideLineComment { get { return currentState is LineCommentState; } } public bool IsInsideMultiLineComment { get { return currentState is MultiLineCommentState; } } public bool IsInsideDocLineComment { get { return currentState is DocCommentState; } } public bool IsInsideComment { get { return IsInsideLineComment || IsInsideMultiLineComment || IsInsideDocLineComment; } } public bool IsInsideOrdinaryComment { get { return IsInsideLineComment || IsInsideMultiLineComment; } } public bool IsInsideOrdinaryCommentOrString { get { return IsInsideOrdinaryComment || IsInsideString; } } public bool LineBeganInsideVerbatimString { get { return lineBeganInsideVerbatimString; } } public bool LineBeganInsideMultiLineComment { get { return lineBeganInsideMultiLineComment; } } #endregion } }