diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatAtom.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatAtom.cs new file mode 100644 index 000000000..e1a0e75bf --- /dev/null +++ b/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatAtom.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Draco.Compiler.Api.Syntax; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// A significant atomic piece of syntax in terms of formatting. +/// +/// This means general token and comments from trivia. +/// Newlines and whitespace are not considered atoms. +/// +/// The token kind, in case it's a token. +/// The trivia kind, in case it's trivia. +/// The text the atom represents. +internal readonly record struct SyntaxFormatAtom( + TokenKind? TokenKind, + TriviaKind? TriviaKind, + string? Text); diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatter.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatter.cs new file mode 100644 index 000000000..9b770f97c --- /dev/null +++ b/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// A formatter for the syntax tree. +/// +internal sealed class SyntaxFormatter +{ + /// + /// The settings of the formatter. + /// + public SyntaxFormatterSettings Settings { get; } + + public SyntaxFormatter(SyntaxFormatterSettings settings) + { + this.Settings = settings; + } +} diff --git a/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatterSettings.cs b/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatterSettings.cs new file mode 100644 index 000000000..6f2c66e02 --- /dev/null +++ b/src/Draco.Compiler/Internal/Syntax/Formatting/SyntaxFormatterSettings.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Draco.Compiler.Internal.Syntax.Formatting; + +/// +/// The settings of the formatter. +/// +internal sealed class SyntaxFormatterSettings +{ +} diff --git a/src/Draco.Compiler/Internal/Syntax/SyntaxTreeFormatter.cs b/src/Draco.Compiler/Internal/Syntax/SyntaxTreeFormatter.cs deleted file mode 100644 index 85d4926d3..000000000 --- a/src/Draco.Compiler/Internal/Syntax/SyntaxTreeFormatter.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Draco.Compiler.Api.Syntax; - -namespace Draco.Compiler.Internal.Syntax; - -/// -/// Settings that the formatter will use. -/// -internal sealed record class SyntaxTreeFormatterSettings(string Indentation) -{ - internal static readonly SyntaxTreeFormatterSettings Default = new(" "); -} - -/// -/// The formatter for . -/// -internal sealed class SyntaxTreeFormatter : SyntaxRewriter -{ - private static readonly SyntaxList oneSpaceTrivia = CreateTrivia(TriviaKind.Whitespace, " "); - private static readonly SyntaxList noSpaceTrivia = CreateTrivia(TriviaKind.Whitespace, ""); - private static readonly SyntaxList newlineTrivia = CreateTrivia(TriviaKind.Newline, Environment.NewLine); - - private TokenKind? lastToken; - private TokenKind? nextToken; - private IEnumerator? tokens; - private readonly SyntaxTreeFormatterSettings settings; - private int indentCount = 0; - private string Indentation - { - get - { - var result = new StringBuilder(); - for (var i = 0; i < this.indentCount; ++i) result.Append(this.settings.Indentation); - return result.ToString(); - } - } - - internal SyntaxTreeFormatter(SyntaxTreeFormatterSettings settings) - { - this.settings = settings; - } - - private SyntaxToken.Builder AddIndentation(SyntaxToken.Builder newToken) - { - this.AddIndentation(); - return newToken; - } - - private SyntaxToken.Builder RemoveIndentation(SyntaxToken.Builder newToken) - { - this.RemoveIndentation(); - return newToken; - } - - private void AddIndentation() => this.indentCount++; - - private void RemoveIndentation() => this.indentCount--; - - /// - /// Formats the given . - /// - /// The to be formatted. - /// The formatted . - public SyntaxTree Format(SyntaxTree tree) => new( - sourceText: tree.SourceText, - greenRoot: this.Format(tree.GreenRoot), - // TODO: Anything smarter to pass here? - syntaxDiagnostics: new()); - - /// - /// Formats the given . - /// - /// The to be formatted. - /// The formatted . - public SyntaxNode Format(SyntaxNode tree) - { - this.tokens = tree.Tokens.GetEnumerator(); - this.lastToken = TokenKind.EndOfInput; - // We need to be one token ahead, because the next token affects the current one, so we must advance twice here - if (!(this.tokens.MoveNext() && this.tokens.MoveNext())) return tree; - this.nextToken = this.tokens.Current.Kind; - return tree.Accept(this); - } - - public override SyntaxNode VisitLabelDeclaration(LabelDeclarationSyntax node) - { - // Labels are indented one less than te rest of the code - this.RemoveIndentation(); - var trIdentifier = this.VisitSyntaxToken(node.Name); - var trColonToken = SetTrivia(SyntaxToken.Builder.From(node.Colon), noSpaceTrivia, newlineTrivia).Build(); - this.AddIndentation(); - var colonTokenChanged = CheckTriviaEqual(trColonToken, node.Colon); - // We need to advance to the next token by hand, because we don't call TransformToken - this.lastToken = TokenKind.Colon; - if (this.tokens!.MoveNext()) this.nextToken = this.tokens!.Current.Kind; - else this.nextToken = TokenKind.EndOfInput; - var identifierChanged = !ReferenceEquals(node.Name, trIdentifier); - var changed = identifierChanged || colonTokenChanged; - if (!changed) return node; - return new LabelDeclarationSyntax(trIdentifier, trColonToken); - } - - public override SyntaxToken VisitSyntaxToken(SyntaxToken node) - { - if (node.Kind == TokenKind.EndOfInput) return node; - var resultToken = this.FormatToken(node); - this.lastToken = resultToken.Kind; - this.tokens!.MoveNext(); - if (this.tokens.Current is null) this.nextToken = TokenKind.EndOfInput; - else this.nextToken = this.tokens.Current.Kind; - if (resultToken is not null) - { - // If the original token andthe new one have the same trivia, we set changed to false, so the entire subtree is not recalculated - if (!CheckTriviaEqual(resultToken, node)) return resultToken; - } - // No change - return node; - } - - private SyntaxToken FormatToken(SyntaxToken token) - { - var newToken = token.ToBuilder(); - - newToken = newToken.Kind switch - { - // TODO: Maybe use dictionary in future to allow user to alter "stickiness" of some tokens - // Tokens that always have one space after - // Note: TokenKind.Comma is exception, in case it is in label declaration, it is handeled outside of this function - TokenKind.Assign or TokenKind.Colon or TokenKind.Comma or TokenKind.Equal or - TokenKind.GreaterEqual or TokenKind.GreaterThan or TokenKind.InterpolationStart or - TokenKind.KeywordAnd or TokenKind.KeywordImport or - TokenKind.KeywordMod or TokenKind.KeywordNot or TokenKind.KeywordOr or - TokenKind.KeywordRem or TokenKind.LessEqual or TokenKind.LessThan or - TokenKind.Minus or TokenKind.MinusAssign or TokenKind.NotEqual or - TokenKind.Plus or TokenKind.PlusAssign or TokenKind.Slash or - TokenKind.SlashAssign or TokenKind.Star or TokenKind.StarAssign => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - - // Tokens that statement can start with, which means they need to have indentation before them - // Note: TokenKind.Identifier is handeled separately, base on tokens that are before or after the Identifier - TokenKind.KeywordVal or TokenKind.KeywordVar or TokenKind.KeywordFunc or - TokenKind.KeywordReturn or TokenKind.KeywordGoto or TokenKind.KeywordWhile => - SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), oneSpaceTrivia), - - TokenKind.ParenOpen => SetTrivia(newToken, noSpaceTrivia, noSpaceTrivia), - - TokenKind.ParenClose => this.nextToken switch - { - TokenKind.ParenClose or TokenKind.Semicolon => SetTrivia(newToken, noSpaceTrivia, noSpaceTrivia), - _ => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - }, - - TokenKind.Semicolon => this.nextToken switch - { - TokenKind.KeywordElse => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - _ => SetTrivia(newToken, noSpaceTrivia, newlineTrivia), - }, - - TokenKind.CurlyOpen => this.lastToken switch - { - TokenKind.Semicolon or TokenKind.CurlyOpen or TokenKind.CurlyClose or - TokenKind.EndOfInput or TokenKind.Colon => - this.AddIndentation(SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), newlineTrivia)), - _ => SetTrailingTrivia(this.AddIndentation(newToken), newlineTrivia), - }, - - TokenKind.CurlyClose => this.nextToken switch - { - TokenKind.Semicolon => - SetTrivia(this.RemoveIndentation(newToken), CreateTrivia(TriviaKind.Whitespace, this.Indentation), noSpaceTrivia), - _ => SetTrivia(this.RemoveIndentation(newToken), CreateTrivia(TriviaKind.Whitespace, this.Indentation), newlineTrivia) - }, - - // If and else keywords are formatted diferently when they are used as expressions and when they are used as statements - TokenKind.KeywordIf => this.lastToken switch - { - TokenKind.Semicolon or TokenKind.CurlyClose or TokenKind.CurlyOpen or TokenKind.Colon => - SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), oneSpaceTrivia), - _ => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - }, - - TokenKind.KeywordElse => this.lastToken switch - { - TokenKind.CurlyClose or TokenKind.Colon => - SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), oneSpaceTrivia), - _ => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - }, - - // Identifier is handeled based on the context in which it is used - TokenKind.Identifier => (this.lastToken, this.nextToken) switch - { - { lastToken: TokenKind.KeywordVal or TokenKind.KeywordVar, nextToken: TokenKind.Colon } => - SetTrivia(newToken, noSpaceTrivia, noSpaceTrivia), - { - lastToken: TokenKind.KeywordVal or TokenKind.KeywordVar or TokenKind.Colon, - nextToken: TokenKind.Semicolon or TokenKind.Assign or TokenKind.PlusAssign or - TokenKind.MinusAssign or TokenKind.Slash or TokenKind.StarAssign - } => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - - { - lastToken: TokenKind.Semicolon or TokenKind.CurlyOpen or - TokenKind.CurlyClose or TokenKind.EndOfInput or TokenKind.Colon, - nextToken: TokenKind.Assign or TokenKind.PlusAssign or - TokenKind.MinusAssign or TokenKind.Slash or TokenKind.StarAssign - } => SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), oneSpaceTrivia), - - { lastToken: TokenKind.Semicolon or TokenKind.CurlyOpen or TokenKind.CurlyClose or TokenKind.EndOfInput or TokenKind.Colon } => - SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), noSpaceTrivia), - - { nextToken: TokenKind.Semicolon or TokenKind.ParenOpen or TokenKind.ParenClose } => - SetTrivia(newToken, noSpaceTrivia, noSpaceTrivia), - - _ => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - }, - - // Literals are handeled based on the context in which they are used - // Note: TokenKind.MultiLineStringEnd is handeled separately, beacause we can't alter its leading trivia - TokenKind.LiteralInteger or TokenKind.LiteralFloat or TokenKind.KeywordFalse or TokenKind.KeywordTrue or TokenKind.LiteralCharacter or TokenKind.LineStringEnd => (this.lastToken, this.nextToken) switch - { - { nextToken: TokenKind.Semicolon or TokenKind.ParenClose } => SetTrivia(newToken, noSpaceTrivia, noSpaceTrivia), - - { lastToken: TokenKind.Semicolon or TokenKind.CurlyOpen, nextToken: TokenKind.CurlyClose } => - SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), newlineTrivia), - - { lastToken: TokenKind.Semicolon or TokenKind.CurlyOpen } => - SetTrivia(newToken, CreateTrivia(TriviaKind.Whitespace, this.Indentation), oneSpaceTrivia), - - _ => SetTrivia(newToken, noSpaceTrivia, oneSpaceTrivia), - }, - - TokenKind.MultiLineStringEnd => (this.lastToken, this.nextToken) switch - { - { nextToken: TokenKind.Semicolon or TokenKind.ParenClose } => SetTrailingTrivia(newToken, noSpaceTrivia), - _ => SetTrailingTrivia(newToken, oneSpaceTrivia), - }, - - _ => newToken, - }; - - return newToken.Build(); - } - - private static SyntaxToken.Builder SetTrivia( - SyntaxToken.Builder builder, - SyntaxList leading, - SyntaxList trailing) => SetTrailingTrivia(SetLeadingTrivia(builder, leading), trailing); - - private static SyntaxToken.Builder SetLeadingTrivia(SyntaxToken.Builder builder, SyntaxList trivia) - { - builder.LeadingTrivia.Clear(); - builder.LeadingTrivia.AddRange(trivia); - return builder; - } - - private static SyntaxToken.Builder SetTrailingTrivia(SyntaxToken.Builder builder, SyntaxList trivia) - { - builder.TrailingTrivia.Clear(); - builder.TrailingTrivia.AddRange(trivia); - return builder; - } - - private static bool CheckTriviaEqual(SyntaxToken tok1, SyntaxToken tok2) - { - if (tok1.TrailingTrivia.Count != tok2.TrailingTrivia.Count) return false; - if (tok1.LeadingTrivia.Count != tok2.LeadingTrivia.Count) return false; - - for (var i = 0; i < tok1.TrailingTrivia.Count; i++) - { - if (tok1.TrailingTrivia[i].Text != tok2.TrailingTrivia[i].Text) return false; - } - for (var i = 0; i < tok1.LeadingTrivia.Count; i++) - { - if (tok1.LeadingTrivia[i].Text != tok2.LeadingTrivia[i].Text) return false; - } - - return true; - } - - private static SyntaxList CreateTrivia(TriviaKind kind, string text) => - SyntaxList.Create(SyntaxTrivia.From(kind, text)); -}