1use std::{fmt, str::from_utf8};
2
3use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5use crate::{
6 LabeledSpan, MietteSpanContents, ReportHandler, SourceCode, SourceSpan, SpanContents,
7 diagnostic_chain::DiagnosticChain,
8 protocol::{Diagnostic, Severity},
9};
10
11#[derive(Debug, Clone)]
17pub struct NarratableReportHandler {
18 context_lines: usize,
19 with_cause_chain: bool,
20 footer: Option<String>,
21}
22
23impl NarratableReportHandler {
24 #[must_use]
27 pub const fn new() -> Self {
28 Self { footer: None, context_lines: 1, with_cause_chain: true }
29 }
30
31 #[must_use]
34 pub const fn with_cause_chain(mut self) -> Self {
35 self.with_cause_chain = true;
36 self
37 }
38
39 #[must_use]
41 pub const fn without_cause_chain(mut self) -> Self {
42 self.with_cause_chain = false;
43 self
44 }
45
46 #[must_use]
48 pub fn with_footer(mut self, footer: String) -> Self {
49 self.footer = Some(footer);
50 self
51 }
52
53 #[must_use]
55 pub const fn with_context_lines(mut self, lines: usize) -> Self {
56 self.context_lines = lines;
57 self
58 }
59}
60
61impl Default for NarratableReportHandler {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl NarratableReportHandler {
68 pub fn render_report(
73 &self,
74 f: &mut impl fmt::Write,
75 diagnostic: &dyn Diagnostic,
76 ) -> fmt::Result {
77 self.render_header(f, diagnostic)?;
78 if self.with_cause_chain {
79 self.render_causes(f, diagnostic)?;
80 }
81 let src = diagnostic.source_code();
82 self.render_snippets(f, diagnostic, src)?;
83 self.render_footer(f, diagnostic)?;
84 self.render_related(f, diagnostic, src)?;
85 if let Some(footer) = &self.footer {
86 writeln!(f, "{footer}")?;
87 }
88 Ok(())
89 }
90
91 fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
92 writeln!(f, "{diagnostic}")?;
93 let severity = match diagnostic.severity() {
94 Some(Severity::Error) | None => "error",
95 Some(Severity::Warning) => "warning",
96 Some(Severity::Advice) => "advice",
97 };
98 writeln!(f, " Diagnostic severity: {severity}")?;
99 Ok(())
100 }
101
102 fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
103 if let Some(cause_iter) = diagnostic
104 .diagnostic_source()
105 .map(DiagnosticChain::from_diagnostic)
106 .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
107 {
108 for error in cause_iter {
109 writeln!(f, " Caused by: {error}")?;
110 }
111 }
112
113 Ok(())
114 }
115
116 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
117 if let Some(help) = diagnostic.help() {
118 writeln!(f, "diagnostic help: {help}")?;
119 }
120 if let Some(code) = diagnostic.code() {
121 writeln!(f, "diagnostic code: {code}")?;
122 }
123 if let Some(url) = diagnostic.url() {
124 writeln!(f, "For more details, see:\n{url}")?;
125 }
126 Ok(())
127 }
128
129 fn render_related(
130 &self,
131 f: &mut impl fmt::Write,
132 diagnostic: &dyn Diagnostic,
133 parent_src: Option<&dyn SourceCode>,
134 ) -> fmt::Result {
135 let related = diagnostic.related();
136 if !related.is_empty() {
137 writeln!(f)?;
138 for rel in related.iter().copied() {
139 match rel.severity() {
140 Some(Severity::Error) | None => write!(f, "Error: ")?,
141 Some(Severity::Warning) => write!(f, "Warning: ")?,
142 Some(Severity::Advice) => write!(f, "Advice: ")?,
143 };
144 self.render_header(f, rel)?;
145 writeln!(f)?;
146 self.render_causes(f, rel)?;
147 let src = rel.source_code().or(parent_src);
148 self.render_snippets(f, rel, src)?;
149 self.render_footer(f, rel)?;
150 self.render_related(f, rel, src)?;
151 }
152 }
153 Ok(())
154 }
155
156 fn render_snippets(
157 &self,
158 f: &mut impl fmt::Write,
159 diagnostic: &dyn Diagnostic,
160 source_code: Option<&dyn SourceCode>,
161 ) -> fmt::Result {
162 if let Some(source) = source_code {
163 {
164 let mut labels = diagnostic.labels();
165 labels.sort_unstable_by_key(|l| l.inner().offset());
166 if !labels.is_empty() {
167 let mut contexts: Vec<(LabeledSpan, MietteSpanContents<'_>)> =
168 Vec::with_capacity(labels.len());
169 for right in labels.iter() {
170 let right_conts = source
171 .read_span(right.inner(), self.context_lines, self.context_lines)
172 .map_err(|_| fmt::Error)?;
173
174 if contexts.is_empty() {
175 contexts.push((right.clone(), right_conts));
176 continue;
177 }
178
179 let (left, left_conts) = contexts.last().unwrap();
180 if left_conts.line() + left_conts.line_count() >= right_conts.line() {
181 let left_end = left.offset() + left.len();
183 let right_end = right.offset() + right.len();
184 let new_span = LabeledSpan::new(
185 left.label().map(String::from),
186 left.offset(),
187 if right_end >= left_end {
188 right_end - left.offset()
190 } else {
191 left.len()
193 },
194 );
195 if let Ok(new_conts) = source.read_span(
197 new_span.inner(),
198 self.context_lines,
199 self.context_lines,
200 ) {
201 contexts.pop();
202 contexts.push((new_span, new_conts));
203 continue;
204 }
205 }
206
207 contexts.push((right.clone(), right_conts));
208 }
209 for (_, conts) in contexts {
210 self.render_context(f, conts, &labels[..])?;
211 }
212 }
213 }
214 }
215 Ok(())
216 }
217
218 fn render_context(
219 &self,
220 f: &mut impl fmt::Write,
221 contents: MietteSpanContents<'_>,
222 labels: &[LabeledSpan],
223 ) -> fmt::Result {
224 let lines = self.get_lines(&contents);
225 write!(f, "Begin snippet")?;
226 if let Some(filename) = contents.name() {
227 write!(f, " for {filename}")?;
228 }
229 writeln!(f, " starting at line {}, column {}", contents.line() + 1, contents.column() + 1)?;
230 writeln!(f)?;
231 for line in &lines {
232 writeln!(f, "snippet line {}: {}", line.line_number, line.text)?;
233 let relevant =
234 labels.iter().filter_map(|l| line.span_attach(l.inner()).map(|a| (a, l)));
235 for (attach, label) in relevant {
236 match attach {
237 SpanAttach::Contained { col_start, col_end } if col_start == col_end => {
238 write!(f, " label at line {}, column {}", line.line_number, col_start,)?;
239 }
240 SpanAttach::Contained { col_start, col_end } => {
241 write!(
242 f,
243 " label at line {}, columns {} to {}",
244 line.line_number, col_start, col_end,
245 )?;
246 }
247 SpanAttach::Starts { col_start } => {
248 write!(
249 f,
250 " label starting at line {}, column {}",
251 line.line_number, col_start,
252 )?;
253 }
254 SpanAttach::Ends { col_end } => {
255 write!(
256 f,
257 " label ending at line {}, column {}",
258 line.line_number, col_end,
259 )?;
260 }
261 }
262 if let Some(label) = label.label() {
263 write!(f, ": {label}")?;
264 }
265 writeln!(f)?;
266 }
267 }
268 Ok(())
269 }
270
271 fn get_lines<'a>(&self, context_data: &MietteSpanContents<'a>) -> Vec<Line<'a>> {
276 let context = from_utf8(context_data.data()).expect("Bad utf8 detected");
277 let mut line = context_data.line();
278 let mut column = context_data.column();
279 let mut offset = context_data.span().offset() as usize;
280 let base = offset;
282 let mut line_offset = offset;
283 let mut iter = context.chars().peekable();
284 let mut line_len = 0usize;
286 let mut lines = Vec::new();
287 while let Some(char) = iter.next() {
288 offset += char.len_utf8();
289 let mut at_end_of_file = false;
290 match char {
291 '\r' => {
292 if iter.next_if_eq(&'\n').is_some() {
293 offset += 1;
294 line += 1;
295 column = 0;
296 } else {
297 line_len += char.len_utf8();
298 column += 1;
299 }
300 at_end_of_file = iter.peek().is_none();
301 }
302 '\n' => {
303 at_end_of_file = iter.peek().is_none();
304 line += 1;
305 column = 0;
306 }
307 _ => {
308 line_len += char.len_utf8();
309 column += 1;
310 }
311 }
312
313 if iter.peek().is_none() && !at_end_of_file {
314 line += 1;
315 }
316
317 if column == 0 || iter.peek().is_none() {
318 let text_start = line_offset - base;
319 lines.push(Line {
320 line_number: line,
321 offset: line_offset,
322 text: &context[text_start..text_start + line_len],
323 at_end_of_file,
324 });
325 line_len = 0;
326 line_offset = offset;
327 }
328 }
329 lines
330 }
331}
332
333impl ReportHandler for NarratableReportHandler {
334 fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 if f.alternate() {
336 return fmt::Debug::fmt(diagnostic, f);
337 }
338
339 self.render_report(f, diagnostic)
340 }
341}
342
343struct Line<'a> {
348 line_number: usize,
349 offset: usize,
350 text: &'a str,
351 at_end_of_file: bool,
352}
353
354enum SpanAttach {
355 Contained { col_start: usize, col_end: usize },
356 Starts { col_start: usize },
357 Ends { col_end: usize },
358}
359
360fn safe_get_column(text: &str, offset: usize, start: bool) -> usize {
363 let mut column = text.get(0..offset).map(UnicodeWidthStr::width).unwrap_or_else(|| {
364 let mut column = 0;
365 for (idx, c) in text.char_indices() {
366 if offset <= idx {
367 break;
368 }
369 column += c.width().unwrap_or(0);
370 }
371 column
372 });
373 if start {
374 column += 1;
376 } column
379}
380
381impl Line<'_> {
382 fn span_attach(&self, span: &SourceSpan) -> Option<SpanAttach> {
383 let span_offset = span.offset() as usize;
384 let span_end = span_offset + span.len() as usize;
385 let line_end = self.offset + self.text.len();
386
387 let start_after = span_offset >= self.offset;
388 let end_before = self.at_end_of_file || span_end <= line_end;
389
390 if start_after && end_before {
391 let col_start = safe_get_column(self.text, span_offset - self.offset, true);
392 let col_end = if span.is_empty() {
393 col_start
394 } else {
395 safe_get_column(self.text, span_end - self.offset, false)
398 };
399 return Some(SpanAttach::Contained { col_start, col_end });
400 }
401 if start_after && span_offset <= line_end {
402 let col_start = safe_get_column(self.text, span_offset - self.offset, true);
403 return Some(SpanAttach::Starts { col_start });
404 }
405 if end_before && span_end >= self.offset {
406 let col_end = safe_get_column(self.text, span_end - self.offset, false);
407 return Some(SpanAttach::Ends { col_end });
408 }
409 None
410 }
411}