📄 cli/src/Colorizer.cs
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Cfa.Cli;

class Colorizer() : CSharpSyntaxWalker(SyntaxWalkerDepth.Token)
{
    public static string ColorMember(ISymbol member)
    {
        if (member.ToString() is not string text)
        {
            return "";
        }
        var tree = CSharpSyntaxTree.ParseText(text);
        var root = tree.GetCompilationUnitRoot();
        var colorVisitor = new Colorizer();
        root.Accept(colorVisitor);
        return colorVisitor.ToString();
    }

    private StringBuilder sb = new();

    private int accesses = 0;
    private Stack<int> generics = [];
    private IdentifierKind identifierKind = IdentifierKind.Top;

    public override void VisitTypeArgumentList(TypeArgumentListSyntax node)
    {
        generics.Push(accesses);
        accesses = 0;
        var previousIdentifierKind = identifierKind;
        identifierKind = IdentifierKind.TypeParameter;
        base.VisitTypeArgumentList(node);
        identifierKind = previousIdentifierKind;
        accesses = generics.Pop();
    }

    public override void VisitArgumentList(ArgumentListSyntax node)
    {
        var previousIdentifierKind = identifierKind;
        identifierKind = IdentifierKind.Parameter;
        base.VisitArgumentList(node);
        identifierKind = previousIdentifierKind;
    }

    public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
    {
        accesses++;
        base.VisitMemberAccessExpression(node);
        accesses--;
    }

    public override void VisitQualifiedName(QualifiedNameSyntax node)
    {
        accesses++;
        base.VisitQualifiedName(node);
        accesses--;
    }

    public override void VisitToken(SyntaxToken token)
    {
        switch (token.Kind())
        {
            case (SyntaxKind.DotToken) when identifierKind is not IdentifierKind.Top:
                break;
            case (SyntaxKind.LessThanToken or SyntaxKind.GreaterThanToken):
                sb.Append($"\e[38;2;218;112;214m{token}\e[0m");
                break;
            case (SyntaxKind.OpenParenToken or SyntaxKind.CloseParenToken):
                sb.Append($"\e[38;2;255;215;0m{token}\e[0m");
                break;
            case (SyntaxKind.IdentifierToken):
                switch (identifierKind)
                {
                    case (IdentifierKind.Top) when accesses <= 1:
                        sb.Append($"\e[38;2;210;168;255m{token}\e[0m");
                        break;
                    case (IdentifierKind.Top):
                        sb.Append($"\e[38;2;78;201;176m{token}\e[0m");
                        break;
                    case (IdentifierKind.Parameter) when accesses <= 1:
                        sb.Append($"\e[38;2;156;220;254m{token}\e[0m");
                        break;
                    case (IdentifierKind.TypeParameter) when accesses <= 1:
                        sb.Append($"\e[38;2;86;156;214m{token}\e[0m");
                        break;
                }
                break;
            case SyntaxKind.CommaToken:
                sb.Append(", ");
                break;
            case var k when (int)k is >= 8304 and <= 8319:
                sb.Append($"\e[38;2;255;123;114m{token}\e[0m");
                break;
            default:
                sb.Append(token);
                break;
        }
    }

    public override string ToString() => sb.ToString();

    enum IdentifierKind
    {
        Top,
        Parameter,
        TypeParameter,
    }
}