diff --git a/src/Draco.Compiler.Cli/Program.cs b/src/Draco.Compiler.Cli/Program.cs index 1002d8917..5daabf3ae 100644 --- a/src/Draco.Compiler.Cli/Program.cs +++ b/src/Draco.Compiler.Cli/Program.cs @@ -179,7 +179,7 @@ private static void FormatCommand(FileInfo input, FileInfo? output) { var syntaxTree = GetSyntaxTrees(input).First(); using var outputStream = OpenOutputOrStdout(output); - new StreamWriter(outputStream).Write(syntaxTree.Format().ToString()); + new StreamWriter(outputStream).Write(syntaxTree.Format()); } private static ImmutableArray GetSyntaxTrees(params FileInfo[] input) diff --git a/src/Draco.Compiler.Tests/Syntax/ParseTreeFormatterTests.cs b/src/Draco.Compiler.Tests/Syntax/ParseTreeFormatterTests.cs index 10bc8b841..b19d9c275 100644 --- a/src/Draco.Compiler.Tests/Syntax/ParseTreeFormatterTests.cs +++ b/src/Draco.Compiler.Tests/Syntax/ParseTreeFormatterTests.cs @@ -1,11 +1,13 @@ using Draco.Compiler.Api.Syntax; +using Draco.Compiler.Internal.Syntax.Formatting; +using Xunit.Abstractions; namespace Draco.Compiler.Tests.Syntax; -public sealed class SyntaxTreeFormatterTests +public sealed class SyntaxTreeFormatterTests(ITestOutputHelper logger) { [Fact] - public void TestFormatting() + public void SomeCodeSampleShouldBeFormattedCorrectly() { var input = """" func main ( ) { @@ -17,6 +19,7 @@ func main ( ) { val singleLineString = "" ; var multilineString = #""" something + test """# ; val y = 4-2 @@ -58,6 +61,7 @@ func main() { val singleLineString = ""; var multilineString = #""" something + test """#; val y = 4 - 2 mod 4 + 3; while (true) { @@ -89,12 +93,12 @@ func main() { """"; - var actual = SyntaxTree.Parse(input).Format().ToString(); + var actual = SyntaxTree.Parse(input).Format(); Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); } [Fact] - public void TestFormattingInlineMethod() + public void InlineMethodShouldBeFormattedCorrectly() { var input = """ import System.Console; @@ -109,14 +113,263 @@ func main() { var expected = """ import System.Console; - func max(a:int32, b:int32): int32 = if (a > b) a else b; + func max(a: int32, b: int32): int32 = if (a > b) a else b; func main() { WriteLine(max(12, 34)); } """; - var actual = SyntaxTree.Parse(input).Format().ToString(); + var actual = SyntaxTree.Parse(input).Format(); + Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void SimpleExpressionShouldBeFormattedCorrectly() + { + var input = """ + func aLongMethodName() = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10; + """; + var expected = """ + func aLongMethodName() = 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 10; + + """; + var actual = SyntaxTree.Parse(input).Format(new Internal.Syntax.Formatting.FormatterSettings() + { + LineWidth = 60 + }); + Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void ExpressionInMultiLineStringDoesNotChange() + { + var input = """" + func main() { + val someMultiLineString = """ + the result:\{1 + 2 + 3 + 4 + 5 + + 6 + 7 + 8 + 9 + 10} + """; + } + + """"; + var actual = SyntaxTree.Parse(input).Format(new Internal.Syntax.Formatting.FormatterSettings() + { + LineWidth = 50 + }); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void MultiReturnInMultiLineStringArePreserved() + { + var input = """" + func main() { + val someMultiLineString = """ + bla bla + + + bla bla + + """; + } + + """"; + var actual = SyntaxTree.Parse(input).Format(); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void MultiReturnInMultiLineStringArePreserved2() + { + var input = """" + func main() { + val someMultiLineString = """ + bla bla + + + bla bla + + + """; + } + + """"; + var actual = SyntaxTree.Parse(input).Format(); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void IfElseChainFormatsCorrectly() + { + var input = """" + func main() { + if (false) + expr1 + else if (false) + expr2 + else if (false) expr3 + else expr4 + } + """"; + var expected = """" + func main() { + if (false) expr1 + else if (false) expr2 + else if (false) expr3 + else expr4 + } + + """"; + var actual = SyntaxTree.Parse(input).Format(); Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); } + + [Fact] + public void TooLongArgsFoldsInsteadOfExpr() + { + var input = """ + func main(lots: Of, arguments: That, will: Be, fold: But) = nnot + this; + """; + var expected = """ + func main( + lots: Of, + arguments: That, + will: Be, + fold: But) = nnot + this; + + """; + var actual = SyntaxTree.Parse(input).Format(new Internal.Syntax.Formatting.FormatterSettings() + { + LineWidth = 60 + }); + Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void NoLineReturnInSingleLineString() + { + var input = """" + func main() { + val value = "Value: \{if (input < value) "low" else "high"}"; + } + + """"; + var actual = SyntaxTree.Parse(input).Format(new Internal.Syntax.Formatting.FormatterSettings() + { + LineWidth = 10 + }); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void Sample1() + { + var input = """" + import System; + import System.Console; + + func main() { + val value = Random.Shared.Next(1, 101); + while (true) { + Write("Guess a number (1-100): "); + val input = Convert.ToInt32(ReadLine()); + if (input == value) goto break; + WriteLine("Incorrect. Too \{if (input < value) "low" else "high"}"); + } + WriteLine("You guessed it!"); + } + + """"; + var actual = SyntaxTree.Parse(input).Format(new Internal.Syntax.Formatting.FormatterSettings() + { + LineWidth = 50 + }); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void CursedSample() + { + var input = """" + //test + func //test + main + // another + // foobar + () { + var + // test + opponent = "R"; + var me = "P"; + if // heh + (me == opponent) return println("draw"); + if ( + // heh + me == "R") { + println(if (opponent == "P") "lose" else "win"); + } + else if ( // heh + me == "P") { + println(if (opponent == "R") "win" else "lose"); + } + else if (me == "S") { + println(if (opponent == "P") "win" else "lose"); + } + } // oh hello + // oops. + + """"; + var actual = SyntaxTree.Parse(input).Format(); + logger.WriteLine(actual); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void CursedSample2() + { + var input = """" + func foo(a: int32) { + if ({ + var x = a * 2; + x > 50 + }) { + WriteLine("ohno"); + } + } + + """"; + var actual = SyntaxTree.Parse(input).Format(); + logger.WriteLine(actual); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } + + [Fact] + public void DontMergeTokens() + { + var input = """" + func foo() { + var x + + = + 1; + } + + """"; + + var actual = SyntaxTree.Parse(input).Format(new FormatterSettings() + { + SpaceAroundBinaryOperators = false + }); + logger.WriteLine(actual); + Assert.Equal(input, actual, ignoreLineEndingDifferences: true); + } } diff --git a/src/Draco.Compiler.Tests/Syntax/SyntaxHelpersTests.cs b/src/Draco.Compiler.Tests/Syntax/SyntaxHelpersTests.cs new file mode 100644 index 000000000..79e96805b --- /dev/null +++ b/src/Draco.Compiler.Tests/Syntax/SyntaxHelpersTests.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Draco.Compiler.Api.Syntax; + +namespace Draco.Compiler.Tests.Syntax; +public class SyntaxHelpersTests +{ + + [Fact] + public void TestIfElseIfExpression() + { + var res = SyntaxTree.Parse("func main() { var a = if (true) 1 else if (false) 2 else 3 }"); + var statement = (((res.Root as CompilationUnitSyntax)!.Declarations.Single() as FunctionDeclarationSyntax)!.Body as BlockFunctionBodySyntax)!.Statements.Single() as DeclarationStatementSyntax; + var variableDeclaration = statement!.Declaration as VariableDeclarationSyntax; + var ifExpression = variableDeclaration!.Value!.Value as IfExpressionSyntax; + Assert.True(ifExpression!.Else!.IsElseIf); + } +} diff --git a/src/Draco.Compiler/Api/Syntax/ElseClauseSyntax.cs b/src/Draco.Compiler/Api/Syntax/ElseClauseSyntax.cs new file mode 100644 index 000000000..673eed85e --- /dev/null +++ b/src/Draco.Compiler/Api/Syntax/ElseClauseSyntax.cs @@ -0,0 +1,13 @@ +namespace Draco.Compiler.Api.Syntax; + +public partial class ElseClauseSyntax +{ + /// + /// Returns when the else clause is followed by an if expression. + /// + public bool IsElseIf => + this.Expression is IfExpressionSyntax + || this.Expression is StatementExpressionSyntax statementExpression + && statementExpression.Statement is ExpressionStatementSyntax expressionStatement + && expressionStatement.Expression is IfExpressionSyntax; +} diff --git a/src/Draco.Compiler/Api/Syntax/StringPartSyntax.cs b/src/Draco.Compiler/Api/Syntax/StringPartSyntax.cs new file mode 100644 index 000000000..bd739afa8 --- /dev/null +++ b/src/Draco.Compiler/Api/Syntax/StringPartSyntax.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq; + +namespace Draco.Compiler.Api.Syntax; +public partial class StringPartSyntax +{ + /// + /// when this is a with . + /// + public bool IsNewLine => this.Children.Count() == 1 && this.Children.SingleOrDefault() is SyntaxToken and { Kind: TokenKind.StringNewline }; +} diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs b/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs index d51a7bde5..087128aec 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxFacts.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using Draco.Compiler.Internal.Syntax.Formatting; +using Draco.Compiler.Internal.Syntax; namespace Draco.Compiler.Api.Syntax; @@ -161,4 +163,12 @@ internal static string ComputeCutoff(Internal.Syntax.StringExpressionSyntax str) Debug.Assert(str.CloseQuotes.LeadingTrivia[1].Kind == TriviaKind.Whitespace); return str.CloseQuotes.LeadingTrivia[1].Text; } + + public static bool WillTokenMerges(string leftToken, string rightToken) + { + var lexer = new Lexer(SourceReader.From(leftToken + rightToken), default); + lexer.Lex(); + var secondToken = lexer.Lex(); + return secondToken.Kind == TokenKind.EndOfInput; + } } diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs b/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs index 808531f78..18b93be48 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs @@ -156,7 +156,7 @@ public ImmutableArray SyntaxTreeDiff(SyntaxTree other) => /// Syntactically formats this . /// /// The formatted tree. - public SyntaxTree Format() => Formatter.Format(this); + public string Format(FormatterSettings? settings = null) => DracoFormatter.Format(this, settings); /// /// The internal root of the tree. diff --git a/src/Draco.Compiler/Draco.Compiler.csproj b/src/Draco.Compiler/Draco.Compiler.csproj index 18c36e7b5..00b74f414 100644 --- a/src/Draco.Compiler/Draco.Compiler.csproj +++ b/src/Draco.Compiler/Draco.Compiler.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/DracoFormatter.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/DracoFormatter.cs new file mode 100644 index 000000000..5532a1abc --- /dev/null +++ b/src/Draco.Compiler/Internal/Syntax/Formatting/DracoFormatter.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Draco.Compiler.Api.Syntax; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +internal sealed class DracoFormatter : Api.Syntax.SyntaxVisitor +{ + private readonly FormatterSettings settings; + private FormatterEngine formatter = null!; + private DracoFormatter(FormatterSettings settings) + { + this.settings = settings; + } + + /// + /// Formats the given syntax tree. + /// + /// The syntax tree to format. + /// The formatter settings to use. + /// The formatted tree. + public static string Format(SyntaxTree tree, FormatterSettings? settings = null) + { + settings ??= FormatterSettings.Default; + + var formatter = new DracoFormatter(settings); + tree.Root.Accept(formatter); + return formatter.formatter.Format(); + } + + public override void VisitCompilationUnit(Api.Syntax.CompilationUnitSyntax node) + { + this.formatter = new FormatterEngine(node.Tokens.Count(), this.settings); + base.VisitCompilationUnit(node); + } + + private static WhitespaceBehavior GetFormattingTokenKind(Api.Syntax.SyntaxToken token) => token.Kind switch + { + TokenKind.KeywordAnd => WhitespaceBehavior.PadAround, + TokenKind.KeywordElse => WhitespaceBehavior.PadAround, + TokenKind.KeywordFor => WhitespaceBehavior.PadAround, + TokenKind.KeywordGoto => WhitespaceBehavior.PadAround, + TokenKind.KeywordImport => WhitespaceBehavior.PadAround, + TokenKind.KeywordIn => WhitespaceBehavior.PadAround, + TokenKind.KeywordInternal => WhitespaceBehavior.PadAround, + TokenKind.KeywordModule => WhitespaceBehavior.PadAround, + TokenKind.KeywordOr => WhitespaceBehavior.PadAround, + TokenKind.KeywordReturn => WhitespaceBehavior.PadAround, + TokenKind.KeywordPublic => WhitespaceBehavior.PadAround, + TokenKind.KeywordVar => WhitespaceBehavior.PadAround, + TokenKind.KeywordVal => WhitespaceBehavior.PadAround, + TokenKind.KeywordIf => WhitespaceBehavior.PadAround, + TokenKind.KeywordWhile => WhitespaceBehavior.PadAround, + + TokenKind.KeywordTrue => WhitespaceBehavior.PadAround, + TokenKind.KeywordFalse => WhitespaceBehavior.PadAround, + TokenKind.KeywordMod => WhitespaceBehavior.PadAround, + TokenKind.KeywordRem => WhitespaceBehavior.PadAround, + + TokenKind.KeywordFunc => WhitespaceBehavior.PadAround, + + TokenKind.Semicolon => WhitespaceBehavior.BehaveAsWhiteSpaceForPreviousToken, + TokenKind.CurlyOpen => WhitespaceBehavior.SpaceBefore | WhitespaceBehavior.BehaveAsWhiteSpaceForNextToken, + TokenKind.ParenOpen => WhitespaceBehavior.BehaveAsWhiteSpaceForNextToken, + TokenKind.ParenClose => WhitespaceBehavior.BehaveAsWhiteSpaceForPreviousToken, + TokenKind.InterpolationStart => WhitespaceBehavior.Whitespace, + TokenKind.Dot => WhitespaceBehavior.Whitespace, + TokenKind.Colon => WhitespaceBehavior.BehaveAsWhiteSpaceForPreviousToken, + + TokenKind.Assign => WhitespaceBehavior.PadAround, + TokenKind.LineStringStart => WhitespaceBehavior.SpaceBefore, + TokenKind.MultiLineStringStart => WhitespaceBehavior.SpaceBefore, + TokenKind.Plus => WhitespaceBehavior.SpaceBefore, + TokenKind.Minus => WhitespaceBehavior.SpaceBefore, + TokenKind.Star => WhitespaceBehavior.SpaceBefore, + TokenKind.Slash => WhitespaceBehavior.SpaceBefore, + TokenKind.PlusAssign => WhitespaceBehavior.SpaceBefore, + TokenKind.MinusAssign => WhitespaceBehavior.SpaceBefore, + TokenKind.StarAssign => WhitespaceBehavior.SpaceBefore, + TokenKind.SlashAssign => WhitespaceBehavior.SpaceBefore, + TokenKind.GreaterEqual => WhitespaceBehavior.SpaceBefore, + TokenKind.GreaterThan => WhitespaceBehavior.SpaceBefore, + TokenKind.LessEqual => WhitespaceBehavior.SpaceBefore, + TokenKind.LessThan => WhitespaceBehavior.SpaceBefore, + TokenKind.Equal => WhitespaceBehavior.SpaceBefore, + TokenKind.LiteralFloat => WhitespaceBehavior.SpaceBefore, + TokenKind.LiteralInteger => WhitespaceBehavior.SpaceBefore, + + TokenKind.Identifier => WhitespaceBehavior.SpaceBefore, + + _ => WhitespaceBehavior.NoFormatting + }; + + public override void VisitSyntaxToken(Api.Syntax.SyntaxToken node) + { + this.HandleTokenComments(node); + var formattingKind = GetFormattingTokenKind(node); + + var firstToken = this.formatter.CurrentIdx == 0; + var insertSpace = formattingKind.HasFlag(WhitespaceBehavior.SpaceBefore); + var doesReturnLine = this.formatter.CurrentToken.DoesReturnLine; + var insertNewline = doesReturnLine is not null && doesReturnLine.IsCompleted && doesReturnLine.Value; + var whitespaceNode = node.Kind == TokenKind.StringNewline || node.Kind == TokenKind.EndOfInput; + if (!insertSpace + && !firstToken + && !insertNewline + && !whitespaceNode) + { + if (SyntaxFacts.WillTokenMerges(this.formatter.PreviousToken.Text, node.Text)) + { + this.formatter.CurrentToken.Kind = WhitespaceBehavior.SpaceBefore; + } + } + + this.formatter.SetCurrentTokenInfo(formattingKind, node.Text); + + base.VisitSyntaxToken(node); + } + + private void HandleTokenComments(Api.Syntax.SyntaxToken node) + { + var trivia = node.TrailingTrivia; + if (trivia.Count > 0) + { + var comment = trivia + .Where(x => x.Kind == TriviaKind.LineComment || x.Kind == TriviaKind.DocumentationComment) + .Select(x => x.Text) + .SingleOrDefault(); + if (comment != null) + { + this.formatter.CurrentToken.Text = node.Text + " " + comment; + this.formatter.NextToken.DoesReturnLine = true; + } + } + var leadingComments = node.LeadingTrivia + .Where(x => x.Kind == TriviaKind.LineComment || x.Kind == TriviaKind.DocumentationComment) + .Select(x => x.Text) + .ToArray(); + this.formatter.CurrentToken.LeadingTrivia ??= []; + this.formatter.CurrentToken.LeadingTrivia.AddRange(leadingComments); + if (this.formatter.CurrentToken.LeadingTrivia.Count > 0) + { + this.formatter.CurrentToken.DoesReturnLine = true; + } + } + + public override void VisitSeparatedSyntaxList(Api.Syntax.SeparatedSyntaxList node) + { + if (node is Api.Syntax.SeparatedSyntaxList + or Api.Syntax.SeparatedSyntaxList) + { + using var _ = this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsSoonAsPossible); + base.VisitSeparatedSyntaxList(node); + } + else + { + base.VisitSeparatedSyntaxList(node); + } + } + + public override void VisitParameter(Api.Syntax.ParameterSyntax node) + { + this.formatter.CurrentToken.DoesReturnLine = this.formatter.Scope.Folded; + base.VisitParameter(node); + } + + public override void VisitDeclaration(Api.Syntax.DeclarationSyntax node) + { + this.formatter.CurrentToken.DoesReturnLine = true; + base.VisitDeclaration(node); + } + + public override void VisitStringExpression(Api.Syntax.StringExpressionSyntax node) + { + if (node.OpenQuotes.Kind != TokenKind.MultiLineStringStart) + { + this.HandleSingleLineString(node); + return; + } + + this.HandleMultiLineString(node); + } + + private void HandleMultiLineString(Api.Syntax.StringExpressionSyntax node) + { + node.OpenQuotes.Accept(this); + using var _ = this.formatter.CreateScope(this.settings.Indentation); + var blockCurrentIndentCount = SyntaxFacts.ComputeCutoff(node).Length; + + for (var i = 0; i < node.Parts.Count; i++) + { + var curr = node.Parts[i]; + + if (curr.IsNewLine || i == 0) + { + curr.Accept(this); + if (i == node.Parts.Count - 1) break; + HandleNewLine(node, blockCurrentIndentCount, i, curr); + continue; + } + + var startIdx = this.formatter.CurrentIdx; // capture position before visiting the tokens of this parts (this will move forward the position) + + // for parts that contains expressions and have return lines. + foreach (var token in curr.Tokens) + { + var newLineCount = token.TrailingTrivia.Where(t => t.Kind == TriviaKind.Newline).Count(); + if (newLineCount > 0) + { + this.formatter.NextToken.DoesReturnLine = true; + this.formatter.CurrentToken.Text = token.Text + string.Concat(Enumerable.Repeat(this.settings.Newline, newLineCount - 1)); + } + token.Accept(this); + } + + // sets all unset tokens to not return a line. + var j = 0; + foreach (var token in curr.Tokens) + { + this.formatter.TokensMetadata[startIdx + j].DoesReturnLine ??= false; + j++; + } + } + this.formatter.CurrentToken.DoesReturnLine = true; + node.CloseQuotes.Accept(this); + + void HandleNewLine(Api.Syntax.StringExpressionSyntax node, int blockCurrentIndentCount, int i, Api.Syntax.StringPartSyntax curr) + { + var next = node.Parts[i + 1]; + var tokenText = next.Tokens.First().ValueText!; + this.formatter.CurrentToken.Text = tokenText[blockCurrentIndentCount..]; + this.formatter.CurrentToken.DoesReturnLine = true; + + if (i > 0 && node.Parts[i - 1].IsNewLine) + { + this.formatter.PreviousToken.Text = ""; // PreviousToken is a newline, CurrentToken.DoesReturnLine will produce the newline. + } + } + } + + private void HandleSingleLineString(Api.Syntax.StringExpressionSyntax node) + { + // this is a single line string + node.OpenQuotes.Accept(this); + // we just sets all tokens in this string to not return a line. + foreach (var item in node.Parts.Tokens) + { + this.formatter.CurrentToken.DoesReturnLine = false; + item.Accept(this); + } + // including the close quote. + this.formatter.CurrentToken.DoesReturnLine = false; + node.CloseQuotes.Accept(this); + } + + public override void VisitBinaryExpression(Api.Syntax.BinaryExpressionSyntax node) + { + var closeScope = null as IDisposable; + var kind = node.Operator.Kind; + if (node.Parent is not Api.Syntax.BinaryExpressionSyntax { Operator.Kind: var previousKind } || previousKind != kind) + { + closeScope = this.formatter.CreateFoldableScope("", FoldPriority.AsLateAsPossible); + } + + node.Left.Accept(this); + + this.formatter.CurrentToken.DoesReturnLine ??= this.formatter.Scope.Folded; + node.Operator.Accept(this); + node.Right.Accept(this); + closeScope?.Dispose(); + } + + public override void VisitBlockFunctionBody(Api.Syntax.BlockFunctionBodySyntax node) + { + node.OpenBrace.Accept(this); + this.formatter.CreateScope(this.settings.Indentation, () => node.Statements.Accept(this)); + this.formatter.CurrentToken.DoesReturnLine = true; + node.CloseBrace.Accept(this); + } + + public override void VisitInlineFunctionBody(Api.Syntax.InlineFunctionBodySyntax node) + { + var curr = this.formatter.CurrentIdx; + node.Assign.Accept(this); + + using var _ = this.formatter.CreateFoldableScope(curr, FoldPriority.AsSoonAsPossible); + node.Value.Accept(this); + node.Semicolon.Accept(this); + } + + public override void VisitFunctionDeclaration(Api.Syntax.FunctionDeclarationSyntax node) + { + this.VisitDeclaration(node); + if (GetPrevious(node) != null) + { + this.formatter.CurrentToken.LeadingTrivia = [""]; + } + + var disposable = this.formatter.CreateScopeAfterNextToken(this.settings.Indentation); + node.VisibilityModifier?.Accept(this); + node.FunctionKeyword.Accept(this); + node.Name.Accept(this); + if (node.Generics is not null) + { + using var _ = this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsLateAsPossible); + node.Generics?.Accept(this); + } + node.OpenParen.Accept(this); + disposable.Dispose(); + node.ParameterList.Accept(this); + node.CloseParen.Accept(this); + node.ReturnType?.Accept(this); + node.Body.Accept(this); + } + + public override void VisitStatement(Api.Syntax.StatementSyntax node) + { + if (node is Api.Syntax.DeclarationStatementSyntax { Declaration: Api.Syntax.LabelDeclarationSyntax }) + { + this.formatter.CurrentToken.DoesReturnLine = true; + this.formatter.CurrentToken.Kind = WhitespaceBehavior.RemoveOneIndentation; + } + else + { + var shouldBeMultiLine = node.Parent is Api.Syntax.BlockExpressionSyntax || node.Parent is Api.Syntax.BlockFunctionBodySyntax; + this.formatter.CurrentToken.DoesReturnLine = new Future(shouldBeMultiLine); + } + base.VisitStatement(node); + } + + public override void VisitWhileExpression(Api.Syntax.WhileExpressionSyntax node) + { + node.WhileKeyword.Accept(this); + node.OpenParen.Accept(this); + this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsSoonAsPossible, () => node.Condition.Accept(this)); + node.CloseParen.Accept(this); + this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsSoonAsPossible, () => node.Then.Accept(this)); + } + + public override void VisitIfExpression(Api.Syntax.IfExpressionSyntax node) + { + using var _ = this.formatter.CreateFoldableScope("", FoldPriority.AsSoonAsPossible); + + node.IfKeyword.Accept(this); + IDisposable? disposable = null; + node.OpenParen.Accept(this); + if (this.formatter.PreviousToken.DoesReturnLine?.Value ?? false) + { + // there is no reason for an OpenParen to return line except if there is a comment. + disposable = this.formatter.CreateScope(this.settings.Indentation); + this.formatter.PreviousToken.ScopeInfo = this.formatter.Scope; // it's easier to change our mind that compute ahead of time. + } + this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsSoonAsPossible, () => + { + var firstTokenIdx = this.formatter.CurrentIdx; + node.Condition.Accept(this); + var firstToken = this.formatter.TokensMetadata[firstTokenIdx]; + if (firstToken.DoesReturnLine?.Value ?? false) + { + firstToken.ScopeInfo.Folded.SetValue(true); + } + }); + node.CloseParen.Accept(this); + disposable?.Dispose(); + + this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsSoonAsPossible, () => node.Then.Accept(this)); + + node.Else?.Accept(this); + } + public override void VisitElseClause(Api.Syntax.ElseClauseSyntax node) + { + if (node.IsElseIf || node.Parent!.Parent is Api.Syntax.ExpressionStatementSyntax) + { + this.formatter.CurrentToken.DoesReturnLine = true; + } + else + { + this.formatter.CurrentToken.DoesReturnLine = this.formatter.Scope.Folded; + } + node.ElseKeyword.Accept(this); + this.formatter.CreateFoldableScope(this.settings.Indentation, FoldPriority.AsSoonAsPossible, () => node.Expression.Accept(this)); + } + + public override void VisitBlockExpression(Api.Syntax.BlockExpressionSyntax node) + { + // this means we are in a if/while/else, and *can* create an indentation with a regular expression folding: + // if (blabla) an expression; + // it can fold: + // if (blabla) + // an expression; + // but since we are in a block we create our own scope and the if/while/else will never create it's own scope. + var folded = this.formatter.Scope.Folded; + if (!folded.IsCompleted) folded.SetValue(false); + + node.OpenBrace.Accept(this); + + this.formatter.CreateScope(this.settings.Indentation, () => + { + node.Statements.Accept(this); + if (node.Value != null) + { + this.formatter.CurrentToken.DoesReturnLine = true; + node.Value.Accept(this); + } + }); + node.CloseBrace.Accept(this); + this.formatter.PreviousToken.DoesReturnLine = true; + } + + public override void VisitVariableDeclaration(Api.Syntax.VariableDeclarationSyntax node) + { + var disposable = this.formatter.CreateScopeAfterNextToken(this.settings.Indentation); + node.VisibilityModifier?.Accept(this); + node.Keyword.Accept(this); + node.Name.Accept(this); + disposable.Dispose(); + node.Type?.Accept(this); + node.Value?.Accept(this); + node.Semicolon.Accept(this); + } + + private static Api.Syntax.SyntaxNode? GetPrevious(Api.Syntax.SyntaxNode node) + { + var previous = null as Api.Syntax.SyntaxNode; + foreach (var child in node.Parent!.Children) + { + if (child == node) return previous; + + // TODO: temp fix for AST problem. + if (child is IReadOnlyList list) + { + var previous2 = null as Api.Syntax.SyntaxNode; + foreach (var item in list) + { + if (item == node) return previous2; + previous2 = item; + } + } + previous = child; + } + + return null; + } +} diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs deleted file mode 100644 index 615d83f4a..000000000 --- a/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs +++ /dev/null @@ -1,645 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Draco.Compiler.Api.Syntax; - -namespace Draco.Compiler.Internal.Syntax.Formatting; - -/// -/// A formatter for the syntax tree. -/// -internal sealed class Formatter : SyntaxVisitor -{ - /// - /// Formats the given syntax tree. - /// - /// The syntax tree to format. - /// The formatter settings to use. - /// The formatted tree. - public static SyntaxTree Format(SyntaxTree tree, FormatterSettings? settings = null) - { - settings ??= FormatterSettings.Default; - var formatter = new Formatter(settings); - - // Construct token sequence - tree.GreenRoot.Accept(formatter); - - // Apply constraints - formatter.ApplyConstraints(); - - // Re-parse into tree - var tokens = formatter.tokens - .Select(t => t.Build()) - .ToArray(); - var tokenSource = TokenSource.From(tokens.AsMemory()); - // TODO: Pass in anything for diagnostics? - var parser = new Parser(tokenSource, diagnostics: new()); - // TODO: Is it correct to assume compilation unit? - var formattedRoot = parser.ParseCompilationUnit(); - - return new SyntaxTree( - // TODO: Is this correct to pass it in? - sourceText: tree.SourceText, - greenRoot: formattedRoot, - // TODO: Anything smarter to pass in? - syntaxDiagnostics: new()); - } - - /// - /// The settings of the formatter. - /// - public FormatterSettings Settings { get; } - - private readonly List tokens = []; - private readonly SyntaxList.Builder currentTrivia = []; - // A list of groups of indices of tokens that should be aligned together - private readonly List> alignmentGroupIndices = []; - private int indentation; - - private Formatter(FormatterSettings settings) - { - this.Settings = settings; - } - - // Constraints ///////////////////////////////////////////////////////////// - - private void ApplyConstraints() - { - foreach (var group in this.alignmentGroupIndices) this.ApplyAlignmentConstraint(group); - } - - private void ApplyAlignmentConstraint(ImmutableArray indices) - { - // Compute the offset of each token - var tokensWithOffsets = indices - .Select(i => (Index: i, Offset: this.GetColumnOfToken(i))) - .ToList(); - - // Find the largest offset among the tokens - var maxColumn = tokensWithOffsets.Max(p => p.Offset); - - // Add padding to all tokens - foreach (var (index, offset) in tokensWithOffsets) - { - var token = this.tokens[index]; - var padding = maxColumn - offset; - if (padding > 0) token.LeadingTrivia.Add(this.Settings.PaddingTrivia(padding)); - } - } - - // Formatters ////////////////////////////////////////////////////////////// - - public override void VisitCompilationUnit(CompilationUnitSyntax node) - { - this.FormatWithImports(node.Declarations); - // NOTE: Is is safe to clear this? - this.currentTrivia.Clear(); - this.Newline(); - this.Place(node.End); - } - - public override void VisitDeclarationStatement(DeclarationStatementSyntax node) - { - this.Place(node.Declaration); - this.Newline(); - } - - public override void VisitImportDeclaration(ImportDeclarationSyntax node) - { - this.Place(node.ImportKeyword); - this.Space(); - this.Place(node.Path); - this.Place(node.Semicolon); - this.Newline(); - } - - public override void VisitLabelDeclaration(LabelDeclarationSyntax node) - { - this.Unindent(); - this.Place(node.Name); - this.Place(node.Colon); - this.Indent(); - } - - public override void VisitVariableDeclaration(VariableDeclarationSyntax node) - { - this.Place(node.Keyword); - this.Space(); - this.Place(node.Name); - this.Place(node.Type); - this.Place(node.Value); - this.Place(node.Semicolon); - } - - public override void VisitFunctionDeclaration(FunctionDeclarationSyntax node) - { - this.Place(node.FunctionKeyword); - this.Space(); - this.Place(node.Name); - this.Place(node.Generics); - this.Place(node.OpenParen); - this.AfterSeparator(node.ParameterList, this.Space); - this.Place(node.CloseParen); - this.Place(node.ReturnType); - this.Space(); - this.Place(node.Body); - } - - public override void VisitGenericParameterList(GenericParameterListSyntax node) - { - this.Place(node.OpenBracket); - this.AfterSeparator(node.Parameters, this.Space); - this.Place(node.CloseBracket); - } - - public override void VisitBlockFunctionBody(BlockFunctionBodySyntax node) - { - this.Place(node.OpenBrace); - if (node.Statements.Count > 0) this.Newline(); - this.Indent(); - this.FormatWithImports(node.Statements); - this.Unindent(); - this.Place(node.CloseBrace); - this.Newline(2); - } - - public override void VisitInlineFunctionBody(InlineFunctionBodySyntax node) - { - this.Place(node.Assign); - this.Space(); - this.Indent(); - this.Place(node.Value); - this.Place(node.Semicolon); - this.Unindent(); - this.Newline(2); - } - - public override void VisitGenericType(GenericTypeSyntax node) - { - this.Place(node.Instantiated); - this.Place(node.OpenBracket); - this.AfterSeparator(node.Arguments, this.Space); - this.Place(node.CloseBracket); - } - - public override void VisitExpressionStatement(ExpressionStatementSyntax node) - { - this.Place(node.Expression); - this.Place(node.Semicolon); - this.Newline(); - } - - public override void VisitReturnExpression(ReturnExpressionSyntax node) - { - this.Place(node.ReturnKeyword); - this.SpaceBeforeNotNull(node.Value); - } - - public override void VisitGotoExpression(GotoExpressionSyntax node) - { - this.Place(node.GotoKeyword); - this.Space(); - this.Place(node.Target); - } - - public override void VisitIfExpression(IfExpressionSyntax node) - { - this.Place(node.IfKeyword); - this.Space(); - this.Place(node.OpenParen); - this.Place(node.Condition); - this.Place(node.CloseParen); - this.Space(); - this.Place(node.Then); - this.SpaceBeforeNotNull(node.Else); - } - - public override void VisitElseClause(ElseClauseSyntax node) - { - this.Place(node.ElseKeyword); - this.Space(); - this.Place(node.Expression); - } - - public override void VisitWhileExpression(WhileExpressionSyntax node) - { - this.Place(node.WhileKeyword); - this.Space(); - this.Place(node.OpenParen); - this.Place(node.Condition); - this.Place(node.CloseParen); - this.Space(); - this.Place(node.Then); - } - - public override void VisitForExpression(ForExpressionSyntax node) - { - this.Place(node.ForKeyword); - this.Space(); - this.Place(node.OpenParen); - this.Place(node.Iterator); - this.Place(node.ElementType); - this.Space(); - this.Place(node.InKeyword); - this.Space(); - this.Place(node.Sequence); - this.Place(node.CloseParen); - this.Space(); - this.Place(node.Then); - } - - public override void VisitBlockExpression(BlockExpressionSyntax node) - { - this.Place(node.OpenBrace); - if (node.Statements.Count > 0 || node.Value is not null) this.Newline(); - this.Indent(); - this.FormatWithImports(node.Statements); - if (node.Value is not null) - { - this.Place(node.Value); - this.Newline(); - } - this.Unindent(); - this.Place(node.CloseBrace); - } - - public override void VisitCallExpression(CallExpressionSyntax node) - { - this.Place(node.Function); - this.Place(node.OpenParen); - this.AfterSeparator(node.ArgumentList, this.Space); - this.Place(node.CloseParen); - } - - public override void VisitGenericExpression(GenericExpressionSyntax node) - { - this.Place(node.Instantiated); - this.Place(node.OpenBracket); - this.AfterSeparator(node.Arguments, this.Space); - this.Place(node.CloseBracket); - } - - public override void VisitUnaryExpression(UnaryExpressionSyntax node) - { - this.Place(node.Operator); - if (SyntaxFacts.IsKeyword(node.Operator.Kind)) this.Space(); - this.Place(node.Operand); - } - - public override void VisitBinaryExpression(BinaryExpressionSyntax node) - { - this.Place(node.Left); - this.Space(); - this.Place(node.Operator); - this.Space(); - this.Place(node.Right); - } - - public override void VisitComparisonElement(ComparisonElementSyntax node) - { - this.Space(); - this.Place(node.Operator); - this.Space(); - this.Place(node.Right); - } - - public override void VisitTypeSpecifier(TypeSpecifierSyntax node) - { - this.Place(node.Colon); - this.Space(); - this.Place(node.Type); - } - - public override void VisitValueSpecifier(ValueSpecifierSyntax node) - { - this.Space(); - this.Place(node.Assign); - this.Space(); - this.Place(node.Value); - } - - // Formatting a list with potential import declarations within - private void FormatWithImports(SyntaxList list) - where T : SyntaxNode - { - var lastWasImport = false; - foreach (var item in list) - { - if (item is ImportDeclarationSyntax) - { - if (!lastWasImport) this.Newline(2); - lastWasImport = true; - } - else - { - if (lastWasImport) this.Newline(2); - lastWasImport = false; - } - this.Place(item); - } - } - - public override void VisitStringExpression(StringExpressionSyntax node) - { - var isMultiline = node.OpenQuotes.Kind == TokenKind.MultiLineStringStart; - var cutoff = SyntaxFacts.ComputeCutoff(node); - - this.Place(node.OpenQuotes); - if (isMultiline) - { - this.Newline(); - this.Indent(); - } - - var cutoffString = this.Settings.IndentationString(this.indentation); - - var isNewLine = true; - foreach (var part in node.Parts) - { - var toInsert = part; - if (isMultiline && isNewLine) - { - if (part is TextStringPartSyntax { Content.Kind: TokenKind.StringContent } textPart) - { - var content = textPart.Content.ToBuilder(); - if (content.Text is not null && content.Text.StartsWith(cutoff)) - { - content.Text = content.Text[cutoff.Length..]; - content.Text = string.Concat(cutoffString, content.Text); - content.Value = content.Text; - } - toInsert = new TextStringPartSyntax(content.Build()); - } - } - this.Place(toInsert); - isNewLine = part is TextStringPartSyntax { Content.Kind: TokenKind.StringNewline }; - } - - if (isMultiline) this.Newline(); - this.Place(node.CloseQuotes); - if (isMultiline) this.Unindent(); - } - - // ELemental token formatting - public override void VisitSyntaxToken(SyntaxToken node) - { - var builder = node.ToBuilder(); - - if (this.Settings.NormalizeStringNewline && builder.Kind == TokenKind.StringNewline) - { - builder.Text = this.Settings.Newline; - } - - if (!IsStringContent(node.Kind)) - { - // Normalize trivia - this.NormalizeLeadingTrivia(builder.LeadingTrivia, this.indentation); - this.NormalizeTrailingTrivia(builder.TrailingTrivia, this.indentation); - } - - // Add what is accumulated - builder.LeadingTrivia.InsertRange(0, this.currentTrivia); - - // Indent - if (this.tokens.Count > 0 && !IsStringContent(node.Kind)) - { - this.EnsureIndentation(this.tokens[^1].TrailingTrivia, builder.LeadingTrivia, this.indentation); - } - - // Clear state - this.currentTrivia.Clear(); - - // Append - this.tokens.Add(builder); - } - - // Format actions ////////////////////////////////////////////////////////// - - private int Place(SyntaxNode? node) - { - var index = this.tokens.Count; - node?.Accept(this); - return index; - } - private void Indent() => ++this.indentation; - private void Unindent() => --this.indentation; - private void Space() - { - if (this.tokens.Count == 0) return; - this.EnsureSpace(this.tokens[^1].TrailingTrivia, this.currentTrivia); - } - private void Newline(int amount = 1) - { - if (this.tokens.Count == 0) return; - this.EnsureNewline(this.tokens[^1].TrailingTrivia, this.currentTrivia, amount); - } - private void SpaceBeforeNotNull(SyntaxNode? node) - { - if (node is null) return; - this.Space(); - this.Place(node); - } - private void AfterSeparator(SeparatedSyntaxList list, Action afterSep) - where T : SyntaxNode - { - var isSeparator = false; - foreach (var item in list) - { - this.Place(item); - if (isSeparator) afterSep(); - isSeparator = !isSeparator; - } - } - - // Low level utilities ///////////////////////////////////////////////////// - - private void NormalizeLeadingTrivia( - SyntaxList.Builder trivia, - int indentation) - { - static bool IsSpace(SyntaxTrivia trivia) => - trivia.Kind is TriviaKind.Newline or TriviaKind.Whitespace; - - static bool IsComment(SyntaxTrivia trivia) => - trivia.Kind is TriviaKind.LineComment or TriviaKind.DocumentationComment; - - // Remove all space - for (var i = 0; i < trivia.Count;) - { - if (IsSpace(trivia[i])) trivia.RemoveAt(i); - else ++i; - } - - // Indent the trivia if needed - if (this.tokens.Count > 0) - { - this.EnsureIndentation(this.tokens[^1].TrailingTrivia, trivia, indentation); - } - - // Before each comment or doc comment, we add a newline, then indentation - // Except the first one, which just got indented - var isFirst = true; - for (var i = 0; i < trivia.Count; ++i) - { - if (!IsComment(trivia[i])) continue; - if (isFirst) - { - isFirst = false; - continue; - } - // A comment comes next, add newline then indentation - trivia.Insert(i, this.Settings.NewlineTrivia); - if (indentation > 0) trivia.Insert(i + 1, this.Settings.IndentationTrivia(indentation)); - } - } - - private void NormalizeTrailingTrivia( - SyntaxList.Builder trivia, - int indentation) - { - static bool IsSpace(SyntaxTrivia trivia) => - trivia.Kind is TriviaKind.Newline or TriviaKind.Whitespace; - - // Remove all space - for (var i = 0; i < trivia.Count;) - { - if (IsSpace(trivia[i])) trivia.RemoveAt(i); - else ++i; - } - - // If nonempty, add a space and a newline at the end - if (trivia.Count > 0) - { - trivia.Insert(0, this.Settings.SpaceTrivia); - trivia.Add(this.Settings.NewlineTrivia); - } - } - - private void EnsureIndentation( - SyntaxList.Builder first, - SyntaxList.Builder second, - int indentation) - { - // The first didn't end in a newline, no need to indent - if (first.Count == 0) return; - if (first[^1].Kind != TriviaKind.Newline) return; - - // Trim the second one - TrimLeft(second, TriviaKind.Whitespace); - - // Add the indentation, if it's > 0 - if (indentation > 0) second.Insert(0, this.Settings.IndentationTrivia(indentation)); - } - - private void EnsureSpace( - SyntaxList.Builder first, - SyntaxList.Builder second) - { - static bool IsSpace(SyntaxTrivia trivia) => - trivia.Kind is TriviaKind.Newline or TriviaKind.Whitespace; - - if (first.Count > 0 && IsSpace(first[^1])) return; - if (second.Count > 0 && IsSpace(second[0])) return; - - // We can just append at the end of the first - first.Add(this.Settings.SpaceTrivia); - } - - private void EnsureNewline( - SyntaxList.Builder first, - SyntaxList.Builder second, - int amount) - { - // Count existing - var firstNewlines = 0; - for (var i = first.Count - 1; i >= 0; --i) - { - if (first[i].Kind != TriviaKind.Newline) break; - ++firstNewlines; - } - var secondNewlines = 0; - for (var i = 0; i < second.Count; ++i) - { - if (second[i].Kind != TriviaKind.Newline) break; - ++secondNewlines; - } - - // Append any that's needed - var missing = amount - (firstNewlines + secondNewlines); - for (var i = 0; i < missing; ++i) - { - if (i == 0 && firstNewlines == 0) - { - // The first didn't end in a newline, its trailing trivia can end in a newline - // Add the first one there - first.Add(this.Settings.NewlineTrivia); - } - else - { - // Add to second - second.Insert(0, this.Settings.NewlineTrivia); - } - } - } - - /// - /// Computes the starting column of a token. - /// - /// The index of the token in question. - /// The offset of the token from the start of its line. - private int GetColumnOfToken(int tokenIndex) - { - var token = this.tokens[tokenIndex]; - var offset = 0; - // First consider leading trivia - for (var i = token.LeadingTrivia.Count - 1; i >= 0; --i) - { - var trivia = token.LeadingTrivia[i]; - if (trivia.Kind == TriviaKind.Newline) return offset; - offset += trivia.FullWidth; - } - // Then all other tokens previously - for (var i = tokenIndex - 1; i >= 0; --i) - { - var prevToken = this.tokens[i]; - // Consider its trailing trivia - for (var j = prevToken.TrailingTrivia.Count - 1; j >= 0; --j) - { - var trivia = prevToken.TrailingTrivia[j]; - if (trivia.Kind == TriviaKind.Newline) return offset; - offset += trivia.FullWidth; - } - // Then the token itself - offset += prevToken.Text?.Length ?? 0; - // Then its leading trivia - for (var j = prevToken.LeadingTrivia.Count - 1; j >= 0; --j) - { - var trivia = prevToken.LeadingTrivia[j]; - if (trivia.Kind == TriviaKind.Newline) return offset; - offset += trivia.FullWidth; - } - } - // We're at the start of the token sequence, we were in the first line - return offset; - } - - private static void TrimLeft(SyntaxList.Builder builder, params TriviaKind[] toTrim) - { - var n = 0; - while (builder.Count > n && toTrim.Contains(builder[n].Kind)) ++n; - builder.RemoveRange(0, n); - } - - private static void TrimRight(SyntaxList.Builder builder, params TriviaKind[] toTrim) - { - var n = 0; - while (builder.Count > n && toTrim.Contains(builder[builder.Count - n - 1].Kind)) ++n; - builder.RemoveRange(builder.Count - n - 1, n); - } - - // Token facts ///////////////////////////////////////////////////////////// - - private static bool IsStringContent(TokenKind kind) => - kind is TokenKind.StringContent or TokenKind.StringNewline; -} diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs deleted file mode 100644 index fa3100be0..000000000 --- a/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Text; - -namespace Draco.Compiler.Internal.Syntax.Formatting; - -/// -/// The settings of the formatter. -/// -internal sealed class FormatterSettings -{ - /// - /// The default formatting settings. - /// - public static FormatterSettings Default { get; } = new(); - - /// - /// The newline sequence. - /// - public string Newline { get; init; } = "\n"; - - /// - /// The indentation sequence. - /// - public string Indentation { get; init; } = " "; - - /// - /// True, if newlines in strings should be normalized to the sequence. - /// - public bool NormalizeStringNewline { get; init; } = true; - - public string IndentationString(int amount = 1) - { - var sb = new StringBuilder(); - for (var i = 0; i < amount; ++i) sb.Append(this.Indentation); - return sb.ToString(); - } - public string PaddingString(int width = 1) - { - var sb = new StringBuilder(); - for (var i = 0; i < width; ++i) sb.Append(' '); - return sb.ToString(); - } - - public SyntaxTrivia NewlineTrivia => new(Api.Syntax.TriviaKind.Newline, this.Newline); - public SyntaxTrivia SpaceTrivia => new(Api.Syntax.TriviaKind.Whitespace, " "); - public SyntaxTrivia IndentationTrivia(int amount = 1) => - new(Api.Syntax.TriviaKind.Whitespace, this.IndentationString(amount)); - public SyntaxTrivia PaddingTrivia(int width = 1) => - new(Api.Syntax.TriviaKind.Whitespace, this.PaddingString(width)); -} diff --git a/src/Draco.FormatterEngine/Draco.FormatterEngine.csproj b/src/Draco.FormatterEngine/Draco.FormatterEngine.csproj new file mode 100644 index 000000000..8d2b2325b --- /dev/null +++ b/src/Draco.FormatterEngine/Draco.FormatterEngine.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + enable + + + diff --git a/src/Draco.FormatterEngine/FoldPriority.cs b/src/Draco.FormatterEngine/FoldPriority.cs new file mode 100644 index 000000000..642883c47 --- /dev/null +++ b/src/Draco.FormatterEngine/FoldPriority.cs @@ -0,0 +1,20 @@ +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// The priority when folding a scope. +/// +public enum FoldPriority +{ + /// + /// The scope will never be folded. Mostly used for scopes that are already folded. + /// + Never, + /// + /// This scope will be folded as late as possible. + /// + AsLateAsPossible, + /// + /// This scope will be folded as soon as possible. + /// + AsSoonAsPossible +} diff --git a/src/Draco.FormatterEngine/FormatterEngine.cs b/src/Draco.FormatterEngine/FormatterEngine.cs new file mode 100644 index 000000000..9a65bfc4d --- /dev/null +++ b/src/Draco.FormatterEngine/FormatterEngine.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +public sealed class FormatterEngine +{ + private readonly ScopeGuard scopePopper; + private readonly TokenMetadata[] tokensMetadata; + private readonly FormatterSettings settings; + + public FormatterEngine(int tokenCount, FormatterSettings settings) + { + this.scopePopper = new ScopeGuard(this); + this.tokensMetadata = new TokenMetadata[tokenCount]; + this.Scope = new(null, settings, FoldPriority.Never, ""); + this.Scope.Folded.SetValue(true); + this.settings = settings; + } + + private class ScopeGuard(FormatterEngine formatter) : IDisposable + { + public void Dispose() => formatter.PopScope(); + } + + public int CurrentIdx { get; private set; } + public Scope Scope { get; private set; } + + private Scope? scopeForNextToken; + + public TokenMetadata[] TokensMetadata => this.tokensMetadata; + + public ref TokenMetadata PreviousToken => ref this.tokensMetadata[this.CurrentIdx - 1]; + public ref TokenMetadata CurrentToken => ref this.tokensMetadata[this.CurrentIdx]; + public ref TokenMetadata NextToken => ref this.tokensMetadata[this.CurrentIdx + 1]; + + public void SetCurrentTokenInfo(WhitespaceBehavior kind, string text) + { + this.CurrentToken.ScopeInfo = this.Scope; + this.CurrentToken.Kind |= kind; // may have been set before visiting for convenience. + this.CurrentToken.Text ??= text; // same + this.CurrentIdx++; + if (this.scopeForNextToken != null) + { + this.Scope = this.scopeForNextToken; + this.scopeForNextToken = null; + } + } + + private void PopScope() => this.Scope = this.Scope.Parent!; + + public IDisposable CreateScope(string indentation) + { + this.Scope = new Scope(this.Scope, this.settings, FoldPriority.Never, indentation); + this.Scope.Folded.SetValue(true); + return this.scopePopper; + } + + public void CreateScope(string indentation, Action action) + { + using (this.CreateScope(indentation)) action(); + } + + public IDisposable CreateScopeAfterNextToken(string indentation) + { + this.scopeForNextToken = new Scope(this.Scope, this.settings, FoldPriority.Never, indentation); + this.scopeForNextToken.Folded.SetValue(true); + return this.scopePopper; + } + + + public IDisposable CreateFoldableScope(string indentation, FoldPriority foldBehavior) + { + this.Scope = new Scope(this.Scope, this.settings, foldBehavior, indentation); + return this.scopePopper; + } + + public IDisposable CreateFoldableScope(int indexOfLevelingToken, FoldPriority foldBehavior) + { + this.Scope = new Scope(this.Scope, this.settings, foldBehavior, (this.tokensMetadata, indexOfLevelingToken)); + return this.scopePopper; + } + + public void CreateFoldableScope(string indentation, FoldPriority foldBehavior, Action action) + { + using (this.CreateFoldableScope(indentation, foldBehavior)) action(); + } + + public string Format() + { + FoldTooLongLine(this.tokensMetadata, this.settings); + var builder = new StringBuilder(); + var stateMachine = new LineStateMachine(string.Concat(this.tokensMetadata[0].ScopeInfo.CurrentTotalIndent)); + + stateMachine.AddToken(this.tokensMetadata[0], this.settings, false); + + for (var x = 1; x < this.tokensMetadata.Length; x++) + { + var metadata = this.tokensMetadata[x]; + // we ignore multiline string newline tokens because we handle them in the string expression visitor. + + if (metadata.DoesReturnLine?.IsCompleted == true && metadata.DoesReturnLine.Value) + { + builder.Append(stateMachine); + builder.Append(this.settings.Newline); + stateMachine = new LineStateMachine(string.Concat(metadata.ScopeInfo.CurrentTotalIndent)); + } + + stateMachine.AddToken(metadata, this.settings, x == this.tokensMetadata.Length - 1); + } + builder.Append(stateMachine); + builder.Append(this.settings.Newline); + return builder.ToString(); + } + + private static void FoldTooLongLine(IReadOnlyList metadatas, FormatterSettings settings) + { + var stateMachine = new LineStateMachine(string.Concat(metadatas[0].ScopeInfo.CurrentTotalIndent)); + var currentLineStart = 0; + List foldedScopes = []; + + for (var x = 0; x < metadatas.Count; x++) + { + var curr = metadatas[x]; + if (curr.DoesReturnLine?.IsCompleted == true && curr.DoesReturnLine.Value) // if it's a new line + { + // we recreate a state machine for the new line. + stateMachine = new LineStateMachine(string.Concat(curr.ScopeInfo.CurrentTotalIndent)); + currentLineStart = x; + // we do not clear this, because we can be in the middle of trying to make the line fit in the width. + } + + stateMachine.AddToken(curr, settings, false); + + if (stateMachine.LineWidth <= settings.LineWidth) + { + // we clear the folded scope, because the line is complete and we won't need it anymore. + if (x != metadatas.Count - 1) + { + var doesReturnLine = metadatas[x + 1].DoesReturnLine; + if (doesReturnLine?.IsCompleted == true && doesReturnLine.Value) + { + foldedScopes.Clear(); + } + } + continue; + } + + // the line is too long... + + var folded = curr.ScopeInfo.Fold(); // folding can fail if there is nothing else to fold. + if (folded != null) + { + x = currentLineStart - 1; + foldedScopes.Add(folded); + stateMachine.Reset(); + continue; + } + + // we can't fold the current scope anymore, so we revert our folding, and we fold the previous scopes on the line. + // there can be other strategy taken in the future, parametrable through settings. + + // first rewind and fold any "as soon as possible" scopes. + for (var i = x - 1; i >= currentLineStart; i--) + { + var scope = metadatas[i].ScopeInfo; + if (scope.Folded?.IsCompleted == true && scope.Folded.Value) continue; + if (scope.FoldPriority != FoldPriority.AsSoonAsPossible) continue; + var prevFolded = scope.Fold(); + if (prevFolded != null) + { + Backtrack(); + goto continue2; + } + } + // there was no high priority scope to fold, we try to get the low priority then. + for (var i = x - 1; i >= currentLineStart; i--) + { + var scope = metadatas[i].ScopeInfo; + if (scope.Folded?.Value ?? false) continue; + var prevFolded = scope.Fold(); + if (prevFolded != null) + { + Backtrack(); + goto continue2; + } + } + + continue2: + + // we couldn't fold any scope, we just give up. + + void Backtrack() + { + foreach (var scope in foldedScopes) + { + scope.Folded.Reset(); + } + foldedScopes.Clear(); + x = currentLineStart - 1; + } + } + } +} diff --git a/src/Draco.FormatterEngine/FormatterSettings.cs b/src/Draco.FormatterEngine/FormatterSettings.cs new file mode 100644 index 000000000..3d2766e6b --- /dev/null +++ b/src/Draco.FormatterEngine/FormatterSettings.cs @@ -0,0 +1,34 @@ +using System.Text; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// The settings of the formatter. +/// +public class FormatterSettings +{ + /// + /// The default formatting settings. + /// + public static FormatterSettings Default { get; } = new(); + + /// + /// The newline sequence. + /// + public string Newline { get; init; } = "\n"; + + /// + /// The indentation sequence. + /// + public string Indentation { get; init; } = " "; + + /// + /// The max line width the formatter will try to respect. + /// + public int LineWidth { get; init; } = 160; + + /// + /// Insert a whitespace around binary operators. + /// + public bool SpaceAroundBinaryOperators { get; init; } = true; +} diff --git a/src/Draco.FormatterEngine/Future.cs b/src/Draco.FormatterEngine/Future.cs new file mode 100644 index 000000000..71a074319 --- /dev/null +++ b/src/Draco.FormatterEngine/Future.cs @@ -0,0 +1,71 @@ +using System; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// Represent a value that may be set later. +/// +/// The type of the value +public class Future +{ + /// + /// A future that may be set later. + /// + public Future() + { + } + + /// + /// An already completed future. + /// + /// The value of the future. + public Future(T value) + { + this.IsCompleted = true; + this.value = value; + } + + private T? value; + + /// + /// Whether the future is completed or not. + /// + public bool IsCompleted { get; private set; } + + /// + /// The value of the future. + /// Will throw when accessed if the future is not completed. + /// + public T Value + { + get + { + if (!this.IsCompleted) + { + throw new InvalidOperationException("Cannot access value when not completed."); + } + return this.value!; + } + } + + /// + /// Sets the value of the future. + /// + /// + public void SetValue(T value) + { + this.IsCompleted = true; + this.value = value; + } + + /// + /// Reset the future to be not completed. + /// + public void Reset() + { + this.IsCompleted = false; + this.value = default; + } + + public static implicit operator Future(T value) => new(value); +} diff --git a/src/Draco.FormatterEngine/LineStateMachine.cs b/src/Draco.FormatterEngine/LineStateMachine.cs new file mode 100644 index 000000000..f5e6df2a7 --- /dev/null +++ b/src/Draco.FormatterEngine/LineStateMachine.cs @@ -0,0 +1,74 @@ +using System.Text; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +internal sealed class LineStateMachine(string indentation) +{ + private readonly StringBuilder sb = new(); + private bool previousIsWhitespace = true; + private bool prevTokenNeedRightPad = false; + private bool shouldIndent = true; + + public int LineWidth { get; set; } = indentation.Length; + + public void AddToken(TokenMetadata metadata, FormatterSettings settings, bool endOfInput) + { + this.HandleLeadingComments(metadata, settings, endOfInput); + if (this.shouldIndent) + { + this.shouldIndent = false; + if (metadata.Kind.HasFlag(WhitespaceBehavior.RemoveOneIndentation)) + { + this.Append(indentation.Remove(settings.Indentation.Length)); + } + else + { + this.Append(indentation); + } + } + + var requestedLeftPad = this.prevTokenNeedRightPad || metadata.Kind.HasFlag(WhitespaceBehavior.SpaceBefore); + var haveWhitespace = (metadata.Kind.HasFlag(WhitespaceBehavior.BehaveAsWhiteSpaceForPreviousToken) || this.previousIsWhitespace); + var shouldLeftPad = (requestedLeftPad && !haveWhitespace); + + if (shouldLeftPad) + { + this.Append(" "); + } + this.Append(metadata.Text); + + this.prevTokenNeedRightPad = metadata.Kind.HasFlag(WhitespaceBehavior.SpaceAfter); + this.previousIsWhitespace = metadata.Kind.HasFlag(WhitespaceBehavior.BehaveAsWhiteSpaceForNextToken); + } + + private void HandleLeadingComments(TokenMetadata metadata, FormatterSettings settings, bool endOfInput) + { + if (metadata.LeadingTrivia == null) return; + + foreach (var trivia in metadata.LeadingTrivia) + { + if (!string.IsNullOrWhiteSpace(trivia)) this.sb.Append(indentation); + this.sb.Append(trivia); + if (!endOfInput) + { + this.sb.Append(settings.Newline); + } + } + } + + private void Append(string text) + { + this.sb.Append(text); + this.LineWidth += text.Length; + } + + public void Reset() + { + this.sb.Clear(); + this.sb.Append(indentation); + this.LineWidth = indentation.Length; + } + + + public override string ToString() => this.sb.ToString(); +} diff --git a/src/Draco.FormatterEngine/Scope.cs b/src/Draco.FormatterEngine/Scope.cs new file mode 100644 index 000000000..e45b28cf3 --- /dev/null +++ b/src/Draco.FormatterEngine/Scope.cs @@ -0,0 +1,184 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// Represent an indentation level in the code. +/// +public sealed class Scope +{ + private readonly string? indentation; + private readonly (IReadOnlyList tokens, int indexOfLevelingToken)? levelingToken; + private readonly FormatterSettings settings; + + [MemberNotNullWhen(true, nameof(levelingToken))] + [MemberNotNullWhen(false, nameof(indentation))] + private bool DrivenByLevelingToken => this.levelingToken.HasValue; + + private Scope(Scope? parent, FormatterSettings settings, FoldPriority foldPriority) + { + this.Parent = parent; + this.settings = settings; + this.FoldPriority = foldPriority; + } + + /// + /// Create a scope that add a new indentation level. + /// + /// The parent scope. + /// The settings of the formatter. + /// The fold priority of the scope. + /// The indentation this scope will add. + public Scope(Scope? parent, FormatterSettings settings, FoldPriority foldPriority, string indentation) + : this(parent, settings, foldPriority) + { + this.indentation = indentation; + } + + /// + /// Create a scope that override the indentation level and follow the position of a token instead. + /// + /// The parent scope. + /// The settings of the formatter. + /// The fold priority of the scope. + /// The list of the tokens of the formatter and the index of the token to follow the position. + public Scope(Scope? parent, FormatterSettings settings, FoldPriority foldPriority, (IReadOnlyList tokens, int indexOfLevelingToken) levelingToken) + : this(parent, settings, foldPriority) + { + this.levelingToken = levelingToken; + } + + /// + /// The parent scope of this scope. + /// + public Scope? Parent { get; } + + /// + /// Represent if the scope is folded or not. + /// An unfolded scope is a potential scope, which is not folded yet. + /// items.Select(x => x).ToList() have an unfolded scope. + /// It can be folded like: + /// + /// items + /// .Select(x => x) + /// .ToList() + /// + /// + public Future Folded { get; } = new Future(); + + /// + /// All the indentation parts of the current scope and it's parents. + /// + public IEnumerable CurrentTotalIndent + { + get + { + if (!this.Folded.IsCompleted || !this.Folded.Value) + { + if (this.Parent is null) return []; + return this.Parent.CurrentTotalIndent; + } + + if (!this.DrivenByLevelingToken) + { + if (this.Parent is null) return [this.indentation]; + return this.Parent.CurrentTotalIndent.Append(this.indentation); + } + + var (tokens, indexOfLevelingToken) = this.levelingToken.Value; + + int GetStartLineTokenIndex() + { + for (var i = indexOfLevelingToken; i >= 0; i--) + { + if (tokens[i].DoesReturnLine?.Value ?? false) + { + return i; + } + } + return 0; + } + + var startLine = GetStartLineTokenIndex(); + var startToken = this.levelingToken.Value.tokens[startLine]; + var stateMachine = new LineStateMachine(string.Concat(startToken.ScopeInfo.CurrentTotalIndent)); + for (var i = startLine; i <= indexOfLevelingToken; i++) + { + var curr = this.levelingToken.Value.tokens[i]; + stateMachine.AddToken(curr, this.settings, false); + } + var levelingToken = this.levelingToken.Value.tokens[indexOfLevelingToken]; + return [new string(' ', stateMachine.LineWidth - levelingToken.Text.Length)]; + + } + } + + /// + /// The fold priority of the scope. It's used to determine which scope should be folded first when folding. + /// + public FoldPriority FoldPriority { get; } + + /// + /// All the parents of this scope, plus this scope. + /// + public IEnumerable ThisAndParents => this.Parents.Prepend(this); + + /// + /// All the parents of this scope. + /// + public IEnumerable Parents + { + get + { + if (this.Parent == null) yield break; + yield return this.Parent; + foreach (var item in this.Parent.Parents) + { + yield return item; + } + } + } + + /// + /// Try to fold a scope by materializing a scope. + /// + /// The scope that have been fold, else if no scope can be fold. + public Scope? Fold() + { + var asSoonAsPossible = this.ThisAndParents + .Reverse() + .Where(item => !item.Folded.IsCompleted) + .Where(item => item.FoldPriority == FoldPriority.AsSoonAsPossible) + .FirstOrDefault(); + + if (asSoonAsPossible != null) + { + asSoonAsPossible.Folded.SetValue(true); + return asSoonAsPossible; + } + + var asLateAsPossible = this.ThisAndParents + .Where(item => !item.Folded.IsCompleted) + .Where(item => item.FoldPriority == FoldPriority.AsLateAsPossible) + .FirstOrDefault(); + + if (asLateAsPossible != null) + { + asLateAsPossible.Folded.SetValue(true); + return asLateAsPossible; + } + + return null; + } + + /// + /// A debug string. + /// + public override string ToString() + { + var folded = (this.Folded.IsCompleted ? this.Folded.Value ? "M" : "U" : "?"); + return $"{folded}{this.FoldPriority}{this.indentation?.Length.ToString() ?? "L"}"; + } +} diff --git a/src/Draco.FormatterEngine/TokenMetadata.cs b/src/Draco.FormatterEngine/TokenMetadata.cs new file mode 100644 index 000000000..02e3ecbe1 --- /dev/null +++ b/src/Draco.FormatterEngine/TokenMetadata.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// Represent the formatting metadata of a token. +/// +/// The whitespace behavior of this token. +/// The text of the token. +/// +/// +/// An optional reference to a nullable boolean, indicate whether the line returns or not. +/// +/// +/// If the field is null, it means there was no decision was made on this token, we default to not returning a line. +/// +/// +/// If the field is set to a reference, but the value of the Box null, it's because this is a pending +/// +/// +/// The deepest scope this token belong to. +/// Lines of the leading trivia. Each line have an newline apppended to it when outputing the text. +public record struct TokenMetadata( + WhitespaceBehavior Kind, + string Text, + [DisallowNull] Future? DoesReturnLine, + Scope ScopeInfo, + List LeadingTrivia) +{ + public readonly override string ToString() + { + var merged = string.Join(',', this.ScopeInfo.ThisAndParents); + var returnLine = this.DoesReturnLine?.IsCompleted == true ? this.DoesReturnLine.Value ? "Y" : "N" : "?"; + return $"{merged} {returnLine}"; + } +} diff --git a/src/Draco.FormatterEngine/WhitespaceBehavior.cs b/src/Draco.FormatterEngine/WhitespaceBehavior.cs new file mode 100644 index 000000000..dbee2d938 --- /dev/null +++ b/src/Draco.FormatterEngine/WhitespaceBehavior.cs @@ -0,0 +1,43 @@ +using System; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// Whitespace behavior that will be respected when formatting the file. +/// +[Flags] +public enum WhitespaceBehavior +{ + /// + /// No formatting behavior is set. Unspecified behavior. + /// + NoFormatting = 0, + /// + /// Add a left whitespace if necessary. + /// + SpaceBefore = 1 << 0, + /// + /// Add a right whitespace if necessary + /// + SpaceAfter = 1 << 1, + /// + /// The next token will think of this token as a whitespace. + /// + BehaveAsWhiteSpaceForNextToken = 1 << 3, + /// + /// The previous token will think of this as a whitespace. + /// + BehaveAsWhiteSpaceForPreviousToken = 1 << 4, + /// + /// Remove one indentation level. + /// + RemoveOneIndentation = 1 << 6, + /// + /// Add a whitespace on the left and right, if necessary. + /// + PadAround = SpaceBefore | SpaceAfter, + /// + /// This token behave as a whitespace. + /// + Whitespace = BehaveAsWhiteSpaceForNextToken | BehaveAsWhiteSpaceForPreviousToken, +} diff --git a/src/Draco.LanguageServer/Capabilities/TextDocumentFormatting.cs b/src/Draco.LanguageServer/Capabilities/TextDocumentFormatting.cs index 3222d4ec4..444d9e4c1 100644 --- a/src/Draco.LanguageServer/Capabilities/TextDocumentFormatting.cs +++ b/src/Draco.LanguageServer/Capabilities/TextDocumentFormatting.cs @@ -21,10 +21,9 @@ internal sealed partial class DracoLanguageServer : ITextDocumentFormatting if (syntaxTree is null) return Task.FromResult(null as IList); var originalRange = syntaxTree.Root.Range; - syntaxTree = syntaxTree.Format(); var edit = new TextEdit() { - NewText = syntaxTree.ToString(), + NewText = syntaxTree.Format(), Range = Translator.ToLsp(originalRange), }; return Task.FromResult?>([edit]); diff --git a/src/Draco.SourceGeneration/Templates/SyntaxTree.sbncs b/src/Draco.SourceGeneration/Templates/SyntaxTree.sbncs index 71cda902f..5e9efe445 100644 --- a/src/Draco.SourceGeneration/Templates/SyntaxTree.sbncs +++ b/src/Draco.SourceGeneration/Templates/SyntaxTree.sbncs @@ -20,12 +20,15 @@ {{if return_value}} return node.Accept(this); {{else}} - node.Accept(this); + //node.Accept(this); {{end}} } {{else}} public virtual {{return_type}} Visit{{remove_suffix($node.Name, 'Syntax')}}({{$node.Name}} node) { + {{if $node.Base?.Base != null && !return_value}} + Visit{{remove_suffix($node.Base.Name, 'Syntax')}}(node); + {{end}} {{for $field in $node.Fields}} node.{{$field.Name}}{{nullable($field)}}.Accept(this); {{end}} diff --git a/src/Draco.sln b/src/Draco.sln index e6c20e5e6..4ad4c75c7 100644 --- a/src/Draco.sln +++ b/src/Draco.sln @@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Draco.JsonRpc", "Draco.Json EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Draco.Debugger.Tests", "Draco.Debugger.Tests\Draco.Debugger.Tests.csproj", "{9E5A4FEB-109B-4BA7-AEDE-0802CF3E765E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Draco.FormatterEngine", "Draco.FormatterEngine\Draco.FormatterEngine.csproj", "{D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Draco.Chr", "Draco.Chr\Draco.Chr.csproj", "{E8DF7730-54AE-45F1-9D36-809B444A5F7A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Draco.Chr.Tests", "Draco.Chr.Tests\Draco.Chr.Tests.csproj", "{88839EDB-4B29-4A96-A7C7-D9904EA9C988}" @@ -168,6 +170,12 @@ Global {9E5A4FEB-109B-4BA7-AEDE-0802CF3E765E}.Nuget|Any CPU.Build.0 = Debug|Any CPU {9E5A4FEB-109B-4BA7-AEDE-0802CF3E765E}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E5A4FEB-109B-4BA7-AEDE-0802CF3E765E}.Release|Any CPU.Build.0 = Release|Any CPU + {D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}.Nuget|Any CPU.ActiveCfg = Debug|Any CPU + {D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}.Nuget|Any CPU.Build.0 = Debug|Any CPU + {D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5DC7780-B933-400C-9AA8-4AFF1FF45DF7}.Release|Any CPU.Build.0 = Release|Any CPU {E8DF7730-54AE-45F1-9D36-809B444A5F7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E8DF7730-54AE-45F1-9D36-809B444A5F7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8DF7730-54AE-45F1-9D36-809B444A5F7A}.Nuget|Any CPU.ActiveCfg = Debug|Any CPU