plotnik_lib/diagnostics/
printer.rs1use 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}