plotnik_lib/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::message::{DiagnosticMessage, Severity};
9
10pub struct DiagnosticsPrinter<'a> {
11    diagnostics: Vec<DiagnosticMessage>,
12    source: &'a str,
13    path: Option<&'a str>,
14    colored: bool,
15}
16
17impl<'a> DiagnosticsPrinter<'a> {
18    pub(crate) fn new(diagnostics: Vec<DiagnosticMessage>, source: &'a str) -> Self {
19        Self {
20            diagnostics,
21            source,
22            path: None,
23            colored: false,
24        }
25    }
26
27    pub fn path(mut self, path: &'a str) -> Self {
28        self.path = Some(path);
29        self
30    }
31
32    pub fn colored(mut self, value: bool) -> Self {
33        self.colored = value;
34        self
35    }
36
37    pub fn render(&self) -> String {
38        let mut out = String::new();
39        self.format(&mut out).expect("String write never fails");
40        out
41    }
42
43    pub fn format(&self, w: &mut impl Write) -> std::fmt::Result {
44        let renderer = if self.colored {
45            Renderer::styled()
46        } else {
47            Renderer::plain()
48        };
49
50        for (i, diag) in self.diagnostics.iter().enumerate() {
51            let range = adjust_range(diag.range, self.source.len());
52
53            let mut snippet = Snippet::source(self.source)
54                .line_start(1)
55                .annotation(AnnotationKind::Primary.span(range.clone()));
56
57            if let Some(p) = self.path {
58                snippet = snippet.path(p);
59            }
60
61            for related in &diag.related {
62                snippet = snippet.annotation(
63                    AnnotationKind::Context
64                        .span(adjust_range(related.range, self.source.len()))
65                        .label(&related.message),
66                );
67            }
68
69            let level = severity_to_level(diag.severity());
70            let title_group = level.primary_title(&diag.message).element(snippet);
71
72            let mut report: Vec<Group> = vec![title_group];
73
74            if let Some(fix) = &diag.fix {
75                report.push(
76                    Level::HELP.secondary_title(&fix.description).element(
77                        Snippet::source(self.source)
78                            .line_start(1)
79                            .patch(Patch::new(range, &fix.replacement)),
80                    ),
81                );
82            }
83
84            for hint in &diag.hints {
85                report.push(Group::with_title(Level::HELP.secondary_title(hint)));
86            }
87
88            if i > 0 {
89                w.write_str("\n\n")?;
90            }
91            write!(w, "{}", renderer.render(&report))?;
92        }
93
94        Ok(())
95    }
96}
97
98fn severity_to_level(severity: Severity) -> Level<'static> {
99    match severity {
100        Severity::Error => Level::ERROR,
101        Severity::Warning => Level::WARNING,
102    }
103}
104
105fn adjust_range(range: TextRange, limit: usize) -> std::ops::Range<usize> {
106    let start: usize = range.start().into();
107    let end: usize = range.end().into();
108
109    if start == end {
110        return start..(start + 1).min(limit);
111    }
112
113    start..end
114}