plotnik_compiler/diagnostics/
printer.rs1use 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 let mut cross_file_snippets = Vec::new();
57
58 for related in &diag.related {
59 if related.span.source == diag.source {
60 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 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}