diff --git a/ICSharpCode.NRefactory.CSharp/Formatter/CSharpIndentEngine.cs b/ICSharpCode.NRefactory.CSharp/Formatter/CSharpIndentEngine.cs new file mode 100644 index 0000000000..b6a300817a --- /dev/null +++ b/ICSharpCode.NRefactory.CSharp/Formatter/CSharpIndentEngine.cs @@ -0,0 +1,324 @@ +using System; +using ICSharpCode.NRefactory.Editor; +using System.Text; + +namespace ICSharpCode.NRefactory.CSharp +{ + public class CSharpIndentEngine + { + readonly IDocument document; + readonly CSharpFormattingOptions options; + readonly TextEditorOptions textEditorOptions; + readonly StringBuilder wordBuf = new StringBuilder(); + Indent thisLineindent; + Indent indent; + + public string ThisLineIndent { + get { + return thisLineindent.IndentString; + } + } + + public string NewLineIndent { + get { + return indent.IndentString; + } + } + + public CSharpIndentEngine(IDocument document, TextEditorOptions textEditorOptions, CSharpFormattingOptions formattingOptions) + { + this.document = document; + this.options = formattingOptions; + this.textEditorOptions = textEditorOptions; + this.indent = new Indent(textEditorOptions); + this.thisLineindent = new Indent(textEditorOptions); + } + + int offset; + + void Reset() + { + offset = 0; + thisLineindent.Reset(); + indent.Reset(); + pc = '\0'; + IsLineStart = true; + addContinuation = false; + inside = Inside.Empty; + nextBody = currentBody = Body.None; + } + + public void UpdateToOffset (int toOffset) + { + if (toOffset < offset) + Reset(); + for (int i = offset; i < toOffset; i++) + Push(document.GetCharAt(i)); + } + + Inside inside = Inside.Empty; + bool IsLineStart = true; + + bool IsInStringOrChar { + get { + return inside.HasFlag (Inside.StringOrChar); + } + } + + bool IsInComment { + get { + return inside.HasFlag (Inside.Comment); + } + } + + [Flags] + public enum Inside { + Empty = 0, + + PreProcessor = (1 << 0), + + 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) + } + + char pc; + int parens = 0; + void Push(char ch) + { + if (inside.HasFlag (Inside.VerbatimString) && pc == '"' && ch != '"') { + inside &= ~Inside.String; + } + Console.WriteLine(ch); + switch (ch) { + case '#': + if (IsLineStart) + inside = Inside.PreProcessor; + break; + case '/': + if (IsInStringOrChar) + break; + if (pc == '/') { + if (inside.HasFlag (Inside.Comment)) { + inside |= Inside.DocComment; + } else { + inside |= Inside.Comment; + } + } + break; + case '*': + if (IsInStringOrChar || IsInComment) + break; + if (pc == '/') + inside |= Inside.MultiLineComment; + break; + case '\n': + case '\r': + inside &= ~(Inside.Comment | Inside.String | Inside.CharLiteral | Inside.PreProcessor); + CheckKeyword(wordBuf.ToString()); + wordBuf.Length = 0; + if (addContinuation) { + indent.Push (IndentType.Continuation); + } + thisLineindent = indent.Clone (); + addContinuation = false; + IsLineStart = true; + break; + case '"': + if (IsInComment) + break; + if (inside.HasFlag (Inside.String)) { + if (pc != '\\') + inside &= ~Inside.String; + break; + } + + if (pc =='@') { + inside |= Inside.VerbatimString; + } else { + inside |= Inside.String; + } + break; + case '<': + case '[': + case '(': + if (IsInComment || IsInStringOrChar) + break; + parens++; + indent.Push (IndentType.Block); + break; + case '>': + case ']': + case ')': + if (IsInComment || IsInStringOrChar) + break; + parens--; + indent.Pop (); + break; + case '{': + if (IsInComment || IsInStringOrChar) + break; + currentBody = nextBody; + if (indent.Count > 0 && indent.Peek() == IndentType.Continuation) + indent.Pop(); + addContinuation = false; + AddIndentation (currentBody); + break; + case '}': + if (IsInComment || IsInStringOrChar) + break; + indent.Pop (); + if (indent.Count > 0 && indent.Peek() == IndentType.Continuation) + indent.Pop(); + break; + case ';': + if (indent.Count > 0 && indent.Peek() == IndentType.Continuation) + indent.Pop(); + break; + case '\'': + if (IsInComment || inside.HasFlag (Inside.String)) + break; + if (inside.HasFlag (Inside.CharLiteral)) { + if (pc != '\\') + inside &= ~Inside.CharLiteral; + } else { + inside &= Inside.CharLiteral; + } + break; + } + + if (!IsInComment && !IsInStringOrChar) { + if ((wordBuf.Length == 0 ? char.IsLetter(ch) : char.IsLetterOrDigit(ch)) || ch == '_') { + wordBuf.Append(ch); + } 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; + } + + + void AddIndentation(BraceStyle braceStyle) + { + switch (braceStyle) { + case BraceStyle.DoNotChange: + case BraceStyle.EndOfLine: + case BraceStyle.EndOfLineWithoutSpace: + case BraceStyle.NextLine: + case BraceStyle.NextLineShifted: + case BraceStyle.BannerStyle: + indent.Push (IndentType.Block); + break; + + case BraceStyle.NextLineShifted2: + indent.Push (IndentType.DoubleBlock); + break; + } + } + + void AddIndentation(Body body) + { + switch (body) { + case Body.None: + indent.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) + indent.Push (IndentType.Empty); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + enum Body + { + None, + Namespace, + Class, Struct, Interface, Enum, + Switch + } + + Body currentBody; + Body nextBody; + bool addContinuation; + 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; + } + } + } +} + diff --git a/ICSharpCode.NRefactory.CSharp/Formatter/Indent.cs b/ICSharpCode.NRefactory.CSharp/Formatter/Indent.cs index 1db659b8f0..27f31e7ebc 100644 --- a/ICSharpCode.NRefactory.CSharp/Formatter/Indent.cs +++ b/ICSharpCode.NRefactory.CSharp/Formatter/Indent.cs @@ -30,8 +30,10 @@ namespace ICSharpCode.NRefactory.CSharp { public enum IndentType { Block, + DoubleBlock, Continuation, - Label + Label, + Empty } public class Indent @@ -44,8 +46,28 @@ namespace ICSharpCode.NRefactory.CSharp public Indent(TextEditorOptions options) { this.options = options; + Reset(); } + Indent(TextEditorOptions options, Stack indentStack, int curIndent) : this(options) + { + this.indentStack = indentStack; + this.curIndent = curIndent; + } + + public Indent Clone() + { + var result = new Indent(options, new Stack (indentStack), curIndent); + result.indentString = indentString; + return result; + } + + public void Reset() + { + curIndent = 0; + indentString = ""; + indentStack.Clear(); + } public void Push(IndentType type) { @@ -60,15 +82,30 @@ namespace ICSharpCode.NRefactory.CSharp Update(); } + public int Count { + get { + return indentStack.Count; + } + } + + public IndentType Peek () + { + return indentStack.Peek(); + } + int GetIndent(IndentType indentType) { switch (indentType) { case IndentType.Block: return options.IndentSize; + case IndentType.DoubleBlock: + return options.IndentSize * 2; case IndentType.Continuation: return options.ContinuationIndent; case IndentType.Label: return options.LabelIndent; + case IndentType.Empty: + return 0; default: throw new ArgumentOutOfRangeException(); } diff --git a/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj b/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj index 8d788bbac9..72477e16a5 100644 --- a/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj +++ b/ICSharpCode.NRefactory.CSharp/ICSharpCode.NRefactory.CSharp.csproj @@ -505,6 +505,7 @@ + diff --git a/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj b/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj index 8a81b60746..c36d411fd7 100644 --- a/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj +++ b/ICSharpCode.NRefactory.Tests/ICSharpCode.NRefactory.Tests.csproj @@ -362,6 +362,7 @@ + diff --git a/ICSharpCode.NRefactory.Tests/IndentationTests/IndentationTests.cs b/ICSharpCode.NRefactory.Tests/IndentationTests/IndentationTests.cs new file mode 100644 index 0000000000..8507fa04d4 --- /dev/null +++ b/ICSharpCode.NRefactory.Tests/IndentationTests/IndentationTests.cs @@ -0,0 +1,173 @@ +using System; + +using System; +using System.IO; +using NUnit.Framework; +using ICSharpCode.NRefactory.CSharp; +using ICSharpCode.NRefactory.Editor; +using System.Text; + +namespace ICSharpCode.NRefactory.CSharp.Indentation +{ + [TestFixture] + public class IndentationTests + { + static CSharpIndentEngine CreateEngine (string text) + { + var policy = FormattingOptionsFactory.CreateMono (); + + var sb = new StringBuilder(); + int offset = 0; + for (int i = 0; i < text.Length; i++) { + var ch = text [i]; + if (ch == '$') { + offset = i; + continue; + } + sb.Append (ch); + } + var document = new ReadOnlyDocument(sb.ToString ()); + + var options = new TextEditorOptions(); + + var result = new CSharpIndentEngine(document, options, policy); + result.UpdateToOffset(offset); + return result; + } + + [Test] + public void TestNamespaceIndent () + { + var indent = CreateEngine("namespace Foo {$"); + Assert.AreEqual("", indent.ThisLineIndent); + Assert.AreEqual("\t", indent.NewLineIndent); + } + + [Ignore ("TODO")] + [Test] + public void TestPreProcessorDirectives () + { + var indent = CreateEngine(@" +namespace Foo { + class Foo { +#if NOTTHERE + { +#endif + $"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t", indent.NewLineIndent); + } + + [Test] + public void TestIf () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + if (true)$"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t\t", indent.NewLineIndent); + } + + [Test] + public void TestFor () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + for (;;)$"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t\t", indent.NewLineIndent); + } + + [Test] + public void TestForEach () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + foreach (;;)$"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t\t", indent.NewLineIndent); + } + + + [Test] + public void TestDo () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + do +$"); + Assert.AreEqual("\t\t\t", indent.ThisLineIndent); + } + + [Test] + public void TestNestedDo () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + do do +$"); + Assert.AreEqual("\t\t\t\t", indent.ThisLineIndent); + } + + [Test] + public void TestNestedDoContinuationSetBack () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + do do do +foo();$"); + Assert.AreEqual("\t\t\t\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t\t\t", indent.NewLineIndent); + } + + [Test] + public void TestWhile () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + while(true)$"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t\t", indent.NewLineIndent); + } + + [Ignore ("TODO")] + [Test] + public void TestParameters () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + Foo(true,$"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t ", indent.NewLineIndent); + } + + [Test] + public void TestParametersCase2 () + { + var indent = CreateEngine(@" +class Foo { + void Test () + { + Foo($"); + Assert.AreEqual("\t\t", indent.ThisLineIndent); + Assert.AreEqual("\t\t\t", indent.NewLineIndent); + } + } +} +