From add3a880854a62d90a8028ee4f4b7e7ed4822fe4 Mon Sep 17 00:00:00 2001 From: LPeter1997 Date: Mon, 23 Oct 2023 18:29:15 +0200 Subject: [PATCH] Really crude formatter works --- src/Draco.Compiler/Api/Syntax/SyntaxTree.cs | 13 ++ .../Internal/Syntax/Formatting/Formatter.cs | 193 +++++++++++++++++- .../Syntax/Formatting/FormatterSettings.cs | 9 + 3 files changed, 206 insertions(+), 9 deletions(-) diff --git a/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs b/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs index e560a518d..4bf17c864 100644 --- a/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs +++ b/src/Draco.Compiler/Api/Syntax/SyntaxTree.cs @@ -5,6 +5,7 @@ using Draco.Compiler.Api.Diagnostics; using Draco.Compiler.Internal; using Draco.Compiler.Internal.Syntax; +using Draco.Compiler.Internal.Syntax.Formatting; using Draco.Compiler.Internal.Syntax.Rewriting; namespace Draco.Compiler.Api.Syntax; @@ -151,6 +152,18 @@ public ImmutableArray SyntaxTreeDiff(SyntaxTree other) => // TODO: We can use a better diff algo ImmutableArray.Create(new TextEdit(this.Root.Range, other.ToString())); + /// + /// Syntactically formats this . + /// + /// The formatted tree. + public SyntaxTree Format() => new( + // TODO: Correct to inherit source text? + sourceText: this.SourceText, + // TODO: Better API? + greenRoot: this.GreenRoot.Accept(new Formatter(FormatterSettings.Default)), + // TODO: Anything smarter to pass here? + syntaxDiagnostics: new()); + /// /// The internal root of the tree. /// diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs index b3e12d801..b3217424c 100644 --- a/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs +++ b/src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs @@ -17,21 +17,86 @@ internal sealed class Formatter : SyntaxRewriter private static readonly object Space = new(); private static readonly object Newline = new(); private static readonly object Newline2 = new(); + private static readonly object Indent = new(); + private static readonly object Unindent = new(); /// /// The settings of the formatter. /// public FormatterSettings Settings { get; } + private SyntaxTrivia? LastTrivia + { + get + { + if (this.currentTrivia.Count > 0) return this.currentTrivia[^1]; + if (this.lastToken is null) return null; + if (this.lastToken.TrailingTrivia.Count == 0) return null; + return this.lastToken.TrailingTrivia[^1]; + } + } + private int indentation; private SyntaxToken? lastToken; + private SyntaxList.Builder currentTrivia = new(); public Formatter(FormatterSettings settings) { this.Settings = settings; } - private IEnumerable AppendSequence(params object[] elements) + public override SyntaxNode VisitFunctionDeclaration(FunctionDeclarationSyntax node) => node.Update(this.AppendSequence( + node.VisibilityModifier, + Space, + node.FunctionKeyword, + Space, + node.Name, + node.Generics, + node.OpenParen, + node.ParameterList, + node.CloseParen, + node.ReturnType, + Space, + node.Body)); + + public override SyntaxNode VisitBlockFunctionBody(BlockFunctionBodySyntax node) => node.Update(this.AppendSequence( + node.OpenBrace, + Newline, + Indent, + node.Statements, + Unindent, + node.CloseBrace)); + + public override SyntaxNode VisitGroupingExpression(GroupingExpressionSyntax node) => + node.Update(this.AppendSequence(node.OpenParen, node.Expression, node.CloseParen)); + + public override SyntaxNode VisitSyntaxToken(SyntaxToken node) + { + this.AddNormalizedLeadingTrivia(node.LeadingTrivia); + this.EnsureIndentation(this.indentation); + + var leadingTrivia = this.currentTrivia.ToSyntaxList(); + this.currentTrivia.Clear(); + + this.AddNormalizedTrailingTrivia(node.TrailingTrivia); + var trailingTrivia = this.currentTrivia.ToSyntaxList(); + this.currentTrivia.Clear(); + + // TODO: Not too efficient, we copy twice... + var newTokenBuilder = SyntaxToken.Builder.From(node); + newTokenBuilder.LeadingTrivia = leadingTrivia.ToBuilder(); + newTokenBuilder.TrailingTrivia = trailingTrivia.ToBuilder(); + var newToken = newTokenBuilder.Build(); + + this.lastToken = newToken; + + return newToken; + } + + private IEnumerable AppendSequence(params object?[] elements) => + this.AppendSequence(elements.AsEnumerable()); + + private IEnumerable AppendSequence(IEnumerable elements) { foreach (var element in elements) { @@ -39,21 +104,25 @@ public Formatter(FormatterSettings settings) { yield return null; } - if (ReferenceEquals(element, Space)) + else if (ReferenceEquals(element, Indent)) { - // TODO: Ensure space between now and lastToken + ++this.indentation; } - else if (ReferenceEquals(element, Newline)) + else if (ReferenceEquals(element, Unindent)) { - // TODO: Ensure newline between now and lastToken + --this.indentation; } - else if (ReferenceEquals(element, Newline2)) + else if (ReferenceEquals(element, Space)) + { + this.EnsureSpace(); + } + else if (ReferenceEquals(element, Newline)) { - // TODO: Ensure 2 newlines between now and lastToken + this.EnsureNewlines(1); } - else if (element is SyntaxToken token) + else if (ReferenceEquals(element, Newline2)) { - // TODO: Construct token with accumulated leading trivia + this.EnsureNewlines(2); } else if (element is SyntaxNode node) { @@ -65,4 +134,110 @@ public Formatter(FormatterSettings settings) } } } + + private void AddNormalizedLeadingTrivia(SyntaxList trivia) + { + // We only add comments and newlines after that + foreach (var t in trivia) + { + if (t.Kind is not TriviaKind.LineComment or TriviaKind.DocumentationComment) continue; + + // Indent the trivia + this.EnsureIndentation(this.indentation); + // Add comment + this.currentTrivia.Add(t); + // Add a newline after + this.currentTrivia.Add(this.Settings.NewlineTrivia); + } + } + + private void AddNormalizedTrailingTrivia(SyntaxList trivia) + { + // We only add comments and newlines after that + var first = true; + foreach (var t in trivia) + { + if (t.Kind is not TriviaKind.LineComment or TriviaKind.DocumentationComment) continue; + + // Indent the trivia + if (!first) this.EnsureIndentation(this.indentation); + // Add comment + this.currentTrivia.Add(t); + // Add a newline after + this.currentTrivia.Add(this.Settings.NewlineTrivia); + first = false; + } + } + + private void EnsureNewlines(int amount) + { + var existingNewlines = 0; + + // Count how many newlines in current trivia + var allNewline = true; + for (var i = this.currentTrivia.Count - 1; i >= 0; --i) + { + var trivia = this.currentTrivia[i]; + if (trivia.Kind == TriviaKind.Newline) + { + ++existingNewlines; + } + else + { + allNewline = false; + break; + } + } + + // If it was all newlines, add the last tokens trailing trivia newlines + if (allNewline && this.lastToken is not null) + { + var trailingTrivia = this.lastToken.TrailingTrivia; + for (var i = trailingTrivia.Count - 1; i >= 0; --i) + { + var trivia = trailingTrivia[i]; + if (trivia.Kind == TriviaKind.Newline) + { + ++existingNewlines; + } + else + { + break; + } + } + } + + // Add newlines if needed + for (var i = existingNewlines; i < amount; ++i) + { + this.currentTrivia.Add(this.Settings.NewlineTrivia); + } + } + + private void EnsureSpace() + { + // Assume no need to indent first token + if (this.lastToken is null) return; + + var lastTrivia = this.LastTrivia; + + // Done, equivalent + if (lastTrivia is not null && lastTrivia.Kind is TriviaKind.Whitespace or TriviaKind.Newline) return; + + // Need a space + this.currentTrivia.Add(this.Settings.SpaceTrivia); + } + + private void EnsureIndentation(int indentation) + { + // Assume no need to indent first token + if (this.lastToken is null) return; + + // Check prev trivia + var lastTrivia = this.LastTrivia; + if (lastTrivia is null || lastTrivia.Kind != TriviaKind.Newline) return; + + // Not indented, last trivia was a newline + this.currentTrivia.Add(this.Settings.IndentationTrivia(indentation)); + } } diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs index 53e0d32b0..9d0f43b09 100644 --- a/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs +++ b/src/Draco.Compiler/Internal/Syntax/Formatting/FormatterSettings.cs @@ -25,4 +25,13 @@ internal sealed class FormatterSettings /// The indentation sequence. /// public string Indentation { get; init; } = " "; + + public SyntaxTrivia NewlineTrivia => new(Api.Syntax.TriviaKind.Newline, this.Newline); + public SyntaxTrivia SpaceTrivia => new(Api.Syntax.TriviaKind.Whitespace, " "); + public SyntaxTrivia IndentationTrivia(int amount = 1) + { + var sb = new StringBuilder(); + for (var i = 0; i < amount; ++i) sb.Append(this.Indentation); + return new(Api.Syntax.TriviaKind.Whitespace, sb.ToString()); + } }