Editor/Projection/CSharp/CSharpApplier.cs

using System.Collections.Generic;
using System.Text;

namespace Grains.RazorDesigner.Projection.CSharp;

public static class CSharpApplier
{
    private const string IndentUnit = "    "; // 4 spaces

    // Apply the full op list to a fresh StringBuilder and return the file text.
    public static string Apply( IReadOnlyList<CSharpOp> ops )
    {
        var sb     = new StringBuilder();
        int indent = 0;
        foreach ( var op in ops )
            ApplyOp( sb, ref indent, op );
        return sb.ToString();
    }

    public static void ApplyOp( StringBuilder sb, ref int indent, CSharpOp op )
    {
        switch ( op )
        {
            case HeaderBanner h:
                sb.AppendLine( "// <auto-generated by Grains.RazorDesigner — DO NOT EDIT —" );
                sb.AppendLine( $"//  source-of-truth: {h.ClassName}.razor.json" );
                sb.AppendLine( $"//  hand-author additions in {h.ClassName}.User.cs (partial sibling)." );
                sb.AppendLine( "// />" );
                sb.AppendLine( "#nullable disable" );
                break;

            case UsingDirective u:
                sb.AppendLine( $"using {u.Namespace};" );
                break;

            case NamespaceOpen n:
                sb.AppendLine();
                sb.AppendLine( $"namespace {n.Namespace};" );
                sb.AppendLine();
                break;

            case ClassOpen c:
                sb.AppendLine( Indent( indent ) + $"public partial class {c.ClassName} : {c.BaseClass}" );
                sb.AppendLine( Indent( indent ) + "{" );
                indent++;
                break;

            case ClassClose:
                indent--;
                sb.AppendLine( Indent( indent ) + "}" );
                break;

            case FieldDecl f when f.IsParameter:
                sb.AppendLine( Indent( indent ) +
                    $"[Parameter] public {f.Type} {f.Name} {{ get; set; }} = {f.InitialExpr};" );
                break;

            case FieldDecl f when f.IsProperty:
                sb.AppendLine( Indent( indent ) +
                    $"{f.Visibility} {f.Type} {f.Name} {{ get; set; }} = {f.InitialExpr};" );
                break;

            case FieldDecl f:
                sb.AppendLine( Indent( indent ) +
                    $"{f.Visibility} {f.Type} {f.Name} = {f.InitialExpr};" );
                break;

            case MethodOpen m:
            {
                var overrideKw = m.IsOverride ? "override " : "";
                var asyncKw    = m.IsAsync    ? "async "    : "";
                sb.AppendLine();
                sb.AppendLine( Indent( indent ) +
                    $"{m.Visibility} {overrideKw}{asyncKw}{m.ReturnType} {m.Name}( {m.ParameterList} )" );
                sb.AppendLine( Indent( indent ) + "{" );
                indent++;
                break;
            }

            case MethodClose:
                indent--;
                sb.AppendLine( Indent( indent ) + "}" );
                break;

            case Statement s:
                sb.AppendLine( Indent( indent ) + s.Code + ";" );
                break;

            case BlockOpen b:
                sb.AppendLine( Indent( indent ) + b.Header );
                sb.AppendLine( Indent( indent ) + "{" );
                indent++;
                break;

            case BlockClose:
                indent--;
                sb.AppendLine( Indent( indent ) + "}" );
                break;

            case BlankLine:
                sb.AppendLine();
                break;

            case Comment cm:
                sb.AppendLine( Indent( indent ) + "// " + cm.Text );
                break;

            default:
                throw new System.Diagnostics.UnreachableException(
                    $"CSharpApplier: unhandled CSharpOp variant '{op?.GetType().Name}'. " +
                    "Add the arm here AND add the variant to CSharpOpExhaustivenessTest's inline array in the same commit." );
        }
    }

    private static string Indent( int level ) =>
        level <= 0 ? "" : new string( ' ', level * 4 );

    // Used by CSharpOpExhaustivenessTest.
    public static void ApplyOpToScratch( CSharpOp op )
    {
        var sb     = new StringBuilder();
        int indent = 0;
        ApplyOp( sb, ref indent, op );
    }
}