Skip to content

Commit

Permalink
Really crude formatter works
Browse files Browse the repository at this point in the history
  • Loading branch information
LPeter1997 committed Oct 23, 2023
1 parent 02ac36c commit add3a88
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 9 deletions.
13 changes: 13 additions & 0 deletions src/Draco.Compiler/Api/Syntax/SyntaxTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -151,6 +152,18 @@ public ImmutableArray<TextEdit> SyntaxTreeDiff(SyntaxTree other) =>
// TODO: We can use a better diff algo
ImmutableArray.Create(new TextEdit(this.Root.Range, other.ToString()));

/// <summary>
/// Syntactically formats this <see cref="SyntaxTree"/>.
/// </summary>
/// <returns>The formatted tree.</returns>
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());

/// <summary>
/// The internal root of the tree.
/// </summary>
Expand Down
193 changes: 184 additions & 9 deletions src/Draco.Compiler/Internal/Syntax/Formatting/Formatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,112 @@ 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();

/// <summary>
/// The settings of the formatter.
/// </summary>
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<SyntaxTrivia>.Builder currentTrivia = new();

public Formatter(FormatterSettings settings)
{
this.Settings = settings;
}

private IEnumerable<SyntaxNode?> 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<SyntaxNode?> AppendSequence(params object?[] elements) =>
this.AppendSequence(elements.AsEnumerable());

private IEnumerable<SyntaxNode?> AppendSequence(IEnumerable<object?> elements)
{
foreach (var element in elements)
{
if (element is null)
{
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)
{
Expand All @@ -65,4 +134,110 @@ public Formatter(FormatterSettings settings)
}
}
}

private void AddNormalizedLeadingTrivia(SyntaxList<SyntaxTrivia> 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<SyntaxTrivia> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ internal sealed class FormatterSettings
/// The indentation sequence.
/// </summary>
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());
}
}

0 comments on commit add3a88

Please sign in to comment.