using System.Text; using NubLang.Code; using NubLang.Parsing.Syntax; using NubLang.Tokenization; namespace NubLang.Diagnostics; public class Diagnostic { public class DiagnosticBuilder { private readonly DiagnosticSeverity _severity; private readonly string _message; private SourceFileSpan? _fileSpan; private string? _help; public DiagnosticBuilder(DiagnosticSeverity severity, string message) { _severity = severity; _message = message; } public DiagnosticBuilder At(SyntaxNode? node) { if (node != null) { var first = node.Tokens.FirstOrDefault(); if (first?.FileSpan != null) { var span = SourceSpan.Merge(node.Tokens.Select(x => x.FileSpan?.Span ?? SourceSpan.Zero)); At(new SourceFileSpan(first.FileSpan.SourceFile, span)); } } return this; } public DiagnosticBuilder At(Token? token) { if (token != null) { At(token.FileSpan); } return this; } public DiagnosticBuilder At(SourceFileSpan? fileSpan) { if (fileSpan != null) { _fileSpan = fileSpan; } return this; } public DiagnosticBuilder WithHelp(string help) { _help = help; return this; } public Diagnostic Build() => new(_severity, _message, _help, _fileSpan); } public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message); public static DiagnosticBuilder Warning(string message) => new(DiagnosticSeverity.Warning, message); public static DiagnosticBuilder Info(string message) => new(DiagnosticSeverity.Info, message); public DiagnosticSeverity Severity { get; } public string Message { get; } public string? Help { get; } public SourceFileSpan? FileSpan { get; } private Diagnostic(DiagnosticSeverity severity, string message, string? help, SourceFileSpan? fileSpan) { Severity = severity; Message = message; Help = help; FileSpan = fileSpan; } public string FormatANSI() { var sb = new StringBuilder(); sb.Append(Severity switch { DiagnosticSeverity.Error => ConsoleColors.Colorize("error", ConsoleColors.Bold + ConsoleColors.Red), DiagnosticSeverity.Warning => ConsoleColors.Colorize("warning", ConsoleColors.Bold + ConsoleColors.Yellow), DiagnosticSeverity.Info => ConsoleColors.Colorize("info", ConsoleColors.Bold + ConsoleColors.Blue), _ => ConsoleColors.Colorize("unknown", ConsoleColors.Bold + ConsoleColors.White) }); if (FileSpan != null) { sb.Append(ConsoleColors.Colorize($" at {FileSpan.SourceFile.Path}:{FileSpan.Span}", ConsoleColors.Faint)); } sb.Append(": "); sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite)); if (FileSpan != null) { sb.AppendLine(); var text = FileSpan.SourceFile.GetText(); var lines = text.Split('\n'); var startLine = FileSpan.Span.Start.Line; var endLine = FileSpan.Span.End.Line; const int CONTEXT_LINES = 3; var contextStartLine = Math.Max(1, startLine - CONTEXT_LINES); var contextEndLine = Math.Min(lines.Length, endLine + CONTEXT_LINES); var numberPadding = contextEndLine.ToString().Length; var codePadding = lines.Skip(contextStartLine - 1).Take(contextEndLine - contextStartLine).Max(x => x.Length); sb.Append('╭'); sb.Append(new string('─', numberPadding + 2)); sb.Append('┬'); sb.Append(new string('─', codePadding + 2)); sb.Append('╮'); sb.AppendLine(); var tokenizer = new Tokenizer(FileSpan.SourceFile); var tokens = tokenizer.Tokenize().ToList(); for (var i = contextStartLine; i <= contextEndLine; i++) { var line = lines[i - 1]; sb.Append("│ "); sb.Append(i.ToString().PadRight(numberPadding)); sb.Append(" │ "); sb.Append(ApplySyntaxHighlighting(line.PadRight(codePadding), i, tokens)); sb.Append(" │"); sb.AppendLine(); if (i >= startLine && i <= endLine) { var markerStartColumn = 1; var markerEndColumn = line.Length + 1; if (i == startLine) { markerStartColumn = FileSpan.Span.Start.Column; } if (i == endLine) { markerEndColumn = FileSpan.Span.End.Column; } var markerLength = markerEndColumn - markerStartColumn; var marker = new string('^', markerLength); var markerColor = Severity switch { DiagnosticSeverity.Info => ConsoleColors.Blue, DiagnosticSeverity.Warning => ConsoleColors.Yellow, DiagnosticSeverity.Error => ConsoleColors.Red, _ => ConsoleColors.White }; sb.Append("│ "); sb.Append(new string(' ', numberPadding)); sb.Append(" │ "); sb.Append(new string(' ', markerStartColumn - 1)); sb.Append(ConsoleColors.Colorize(marker, markerColor)); sb.Append(new string(' ', codePadding - markerEndColumn + 1)); sb.Append(" │"); sb.AppendLine(); } } sb.Append('╰'); sb.Append(new string('─', numberPadding + 2)); sb.Append('┴'); sb.Append(new string('─', codePadding + 2)); sb.Append('╯'); } if (Help != null) { sb.AppendLine(); sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan)); } return sb.ToString(); } private static string ApplySyntaxHighlighting(string line, int lineNumber, List tokens) { var sb = new StringBuilder(); var lineTokens = tokens .Where(t => t.FileSpan.Span.Start.Line == lineNumber) .OrderBy(t => t.FileSpan.Span.Start.Column) .ToList(); if (lineTokens.Count == 0) { return line; } var currentColumn = 1; foreach (var token in lineTokens) { var tokenStart = token.FileSpan.Span.Start.Column; var tokenEnd = token.FileSpan.Span.End.Column; if (tokenStart > currentColumn) { var beforeToken = line.Substring(currentColumn - 1, tokenStart - currentColumn); sb.Append(beforeToken); } var tokenLength = tokenEnd - tokenStart; if (tokenStart - 1 + tokenLength <= line.Length) { var tokenText = line.Substring(tokenStart - 1, tokenLength); var coloredToken = ColorizeToken(token, tokenText); sb.Append(coloredToken); } currentColumn = tokenEnd; } if (currentColumn <= line.Length) { var remaining = line[(currentColumn - 1)..]; sb.Append(remaining); } return sb.ToString(); } private static string ColorizeToken(Token token, string tokenText) { switch (token) { case IdentifierToken: { return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightWhite); } case LiteralToken literal: { if (literal.Kind == LiteralKind.String) { return ConsoleColors.Colorize(tokenText, ConsoleColors.Green); } return ConsoleColors.Colorize(tokenText, ConsoleColors.Magenta); } case SymbolToken symbolToken: { switch (symbolToken.Symbol) { case Symbol.Func: case Symbol.Return: case Symbol.If: case Symbol.Else: case Symbol.While: case Symbol.Break: case Symbol.Continue: case Symbol.Struct: case Symbol.Let: case Symbol.Calls: case Symbol.Interface: case Symbol.For: case Symbol.Extern: { return ConsoleColors.Colorize(tokenText, ConsoleColors.Bold + ConsoleColors.Blue); } case Symbol.Assign: case Symbol.Bang: case Symbol.Equal: case Symbol.NotEqual: case Symbol.LessThan: case Symbol.LessThanOrEqual: case Symbol.GreaterThan: case Symbol.GreaterThanOrEqual: case Symbol.Plus: case Symbol.Minus: case Symbol.Star: case Symbol.ForwardSlash: case Symbol.Caret: case Symbol.Ampersand: { return ConsoleColors.Colorize(tokenText, ConsoleColors.Yellow); } case Symbol.Colon: case Symbol.OpenParen: case Symbol.CloseParen: case Symbol.OpenBrace: case Symbol.CloseBrace: case Symbol.OpenBracket: case Symbol.CloseBracket: case Symbol.Comma: case Symbol.Period: case Symbol.Semi: { return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightBlack); } } break; } } return tokenText; } } public enum DiagnosticSeverity { Info, Warning, Error }