Skip to main content

aver/
tty_render.rs

1//! Terminal adapter for canonical `Diagnostic`.
2//!
3//! All color-aware (`colored`) rendering lives here — the canonical model in
4//! `aver::diagnostics` stays runtime-neutral so the playground / LSP can reuse
5//! it without pulling tty dependencies.
6//!
7//! Single entry point: [`render_tty`] — adaptive color rendering for terminals.
8//! JSON emission goes through canonical serde
9//! ([`aver::diagnostics::AnalysisReport::to_json`] for bundles,
10//! `serde_json::to_string(&diagnostic)` for individual records).
11
12use aver::diagnostics::{Diagnostic, Severity};
13use colored::Colorize;
14use std::fmt::Write;
15
16/// Adaptive terminal renderer. Errors get full treatment (fields + source);
17/// warnings stay compact unless `verbose` is set.
18pub fn render_tty(d: &Diagnostic, verbose: bool) -> String {
19    let mut out = String::new();
20
21    // --- header ---
22    let tag = match d.severity {
23        Severity::Error => "error",
24        Severity::Warning => "warning",
25        Severity::Fail => "fail",
26        Severity::Hint => "hint",
27    };
28    let header_text = format!("{}[{}]: {}", tag, d.slug, d.summary);
29    let header = match d.severity {
30        Severity::Error | Severity::Fail => header_text.red().bold().to_string(),
31        Severity::Warning => header_text.yellow().bold().to_string(),
32        Severity::Hint => header_text.cyan().bold().to_string(),
33    };
34    let _ = writeln!(out, "{}", header);
35
36    // --- at ---
37    let at_label = "at:".blue().to_string();
38    let _ = writeln!(
39        out,
40        "  {} {}:{}:{}",
41        at_label, d.span.file, d.span.line, d.span.col
42    );
43
44    // --- in-fn ---
45    if let Some(ref fn_name) = d.fn_name {
46        let key = "in-fn:".blue().to_string();
47        let _ = writeln!(out, "  {} {}", key, fn_name);
48    }
49
50    // --- intent (verbose only) ---
51    if verbose && let Some(ref intent) = d.intent {
52        let key = "intent:".blue().to_string();
53        let _ = writeln!(out, "  {} {}", key, intent.dimmed());
54    }
55
56    let is_error = d.is_error();
57
58    // --- conflict (errors) ---
59    if is_error && let Some(ref conflict) = d.conflict {
60        let key = "conflict:".blue().to_string();
61        let _ = writeln!(out, "  {} {}", key, conflict);
62    }
63
64    // --- fields ---
65    let field_limit = if verbose {
66        d.fields.len()
67    } else if is_error {
68        4
69    } else {
70        2
71    };
72    for (key, value) in d.fields.iter().take(field_limit) {
73        let colored_key = format!("{}:", key).blue().to_string();
74        let _ = writeln!(out, "  {} {}", colored_key, value);
75    }
76
77    // --- repair.primary ---
78    if let Some(ref repair) = d.repair.primary {
79        let key = "repair:".blue().to_string();
80        let _ = writeln!(out, "  {} {}", key, repair.cyan());
81    }
82
83    // --- repair.alternatives (verbose only) ---
84    if verbose {
85        for alt in &d.repair.alternatives {
86            let key = "repair.alt:".blue().to_string();
87            let _ = writeln!(out, "  {} {}", key, alt.cyan());
88        }
89    }
90
91    // --- repair.example (verbose only) ---
92    if verbose && let Some(ref example) = d.repair.example {
93        let key = "repair.example:".blue().to_string();
94        let _ = writeln!(out, "  {} {}", key, example.cyan());
95    }
96
97    // --- source snippet (multi-region) ---
98    let skip_snippet = matches!(
99        d.slug,
100        "missing-verify" | "verify-effectful" | "missing-description"
101    );
102    let show_source = (is_error || verbose) && !skip_snippet;
103    let has_source = d.regions.iter().any(|r| !r.source_lines.is_empty());
104    if show_source && has_source {
105        let max_num = d
106            .regions
107            .iter()
108            .flat_map(|r| r.source_lines.iter().map(|sl| sl.line_num))
109            .max()
110            .unwrap_or(0);
111        let gutter_width = format!("{}", max_num).len();
112        let gutter_pad: String = " ".repeat(gutter_width);
113
114        let _ = writeln!(out, "  {} {}", gutter_pad, "|".blue());
115
116        let mut last_emitted: Option<usize> = None;
117
118        for region in &d.regions {
119            if let Some(first_sl) = region.source_lines.first()
120                && let Some(last) = last_emitted
121                && first_sl.line_num > last + 1
122            {
123                let _ = writeln!(out, "  {}", "...".blue());
124            }
125
126            for sl in &region.source_lines {
127                if let Some(last) = last_emitted
128                    && sl.line_num <= last
129                {
130                    continue;
131                }
132                let num_str = format!("{:>width$}", sl.line_num, width = gutter_width);
133                let _ = writeln!(out, "  {} {} {}", num_str.dimmed(), "|".blue(), sl.text);
134                last_emitted = Some(sl.line_num);
135            }
136
137            if let Some(ref ul) = region.underline {
138                let pad: String = " ".repeat(ul.col.saturating_sub(1));
139                let carets: String = "^".repeat(ul.len.max(1));
140                let colored_carets = match d.severity {
141                    Severity::Error | Severity::Fail => carets.red().to_string(),
142                    Severity::Warning => carets.yellow().to_string(),
143                    Severity::Hint => carets.cyan().to_string(),
144                };
145                let _ = writeln!(
146                    out,
147                    "  {} {} {}{}  {}",
148                    gutter_pad,
149                    "|".blue(),
150                    pad,
151                    colored_carets,
152                    ul.label.dimmed()
153                );
154            }
155        }
156    }
157
158    out
159}