plotnik_compiler/diagnostics/
printer.rs

1//! Builder-pattern printer for rendering diagnostics.
2
3use std::fmt::Write;
4
5use annotate_snippets::{AnnotationKind, Group, Level, Patch, Renderer, Snippet};
6use rowan::TextRange;
7
8use super::SourceMap;
9use super::message::{DiagnosticMessage, Severity};
10
11pub struct DiagnosticsPrinter<'q> {
12    diagnostics: Vec<DiagnosticMessage>,
13    sources: &'q SourceMap,
14    colored: bool,
15}
16
17impl<'q> DiagnosticsPrinter<'q> {
18    pub(crate) fn new(diagnostics: Vec<DiagnosticMessage>, sources: &'q SourceMap) -> Self {
19        Self {
20            diagnostics,
21            sources,
22            colored: false,
23        }
24    }
25
26    pub fn colored(mut self, value: bool) -> Self {
27        self.colored = value;
28        self
29    }
30
31    pub fn render(&self) -> String {
32        let mut out = String::new();
33        self.format(&mut out).expect("String write never fails");
34        out
35    }
36
37    pub fn format(&self, w: &mut impl Write) -> std::fmt::Result {
38        let renderer = if self.colored {
39            Renderer::styled()
40        } else {
41            Renderer::plain()
42        };
43
44        for (i, diag) in self.diagnostics.iter().enumerate() {
45            let primary_content = self.sources.content(diag.source);
46            let range = adjust_range(diag.range, primary_content.len());
47
48            let mut primary_snippet = Snippet::source(primary_content).line_start(1);
49            if let Some(name) = self.source_path(diag.source) {
50                primary_snippet = primary_snippet.path(name);
51            }
52            primary_snippet =
53                primary_snippet.annotation(AnnotationKind::Primary.span(range.clone()));
54
55            // Collect same-file and cross-file related info separately
56            let mut cross_file_snippets = Vec::new();
57
58            for related in &diag.related {
59                if related.span.source == diag.source {
60                    // Same file: add annotation to primary snippet
61                    primary_snippet = primary_snippet.annotation(
62                        AnnotationKind::Context
63                            .span(adjust_range(related.span.range, primary_content.len()))
64                            .label(&related.message),
65                    );
66                    continue;
67                }
68
69                // Different file: create separate snippet
70                let related_content = self.sources.content(related.span.source);
71                let mut snippet = Snippet::source(related_content).line_start(1);
72                if let Some(name) = self.source_path(related.span.source) {
73                    snippet = snippet.path(name);
74                }
75                snippet = snippet.annotation(
76                    AnnotationKind::Context
77                        .span(adjust_range(related.span.range, related_content.len()))
78                        .label(&related.message),
79                );
80                cross_file_snippets.push(snippet);
81            }
82
83            let level = severity_to_level(diag.severity());
84            let mut title_group = level.primary_title(&diag.message).element(primary_snippet);
85
86            for snippet in cross_file_snippets {
87                title_group = title_group.element(snippet);
88            }
89
90            let mut report: Vec<Group> = vec![title_group];
91
92            if let Some(fix) = &diag.fix {
93                report.push(
94                    Level::HELP.secondary_title(&fix.description).element(
95                        Snippet::source(primary_content)
96                            .line_start(1)
97                            .patch(Patch::new(range, &fix.replacement)),
98                    ),
99                );
100            }
101
102            for hint in &diag.hints {
103                report.push(Group::with_title(Level::HELP.secondary_title(hint)));
104            }
105
106            if i > 0 {
107                w.write_str("\n\n")?;
108            }
109            write!(w, "{}", renderer.render(&report))?;
110        }
111
112        Ok(())
113    }
114
115    fn source_path(&self, source: crate::query::SourceId) -> Option<&'q str> {
116        self.sources.path(source)
117    }
118}
119
120fn severity_to_level(severity: Severity) -> Level<'static> {
121    match severity {
122        Severity::Error => Level::ERROR,
123        Severity::Warning => Level::WARNING,
124    }
125}
126
127fn adjust_range(range: TextRange, limit: usize) -> std::ops::Range<usize> {
128    let start: usize = range.start().into();
129    let end: usize = range.end().into();
130
131    if start == end {
132        return start..(start + 1).min(limit);
133    }
134
135    start..end
136}