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