1use std::fmt;
2
3use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5use crate::{
6 LabeledSpan, MietteError, 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 if let Some(related) = diagnostic.related() {
136 writeln!(f)?;
137 for rel in related {
138 match rel.severity() {
139 Some(Severity::Error) | None => write!(f, "Error: ")?,
140 Some(Severity::Warning) => write!(f, "Warning: ")?,
141 Some(Severity::Advice) => write!(f, "Advice: ")?,
142 };
143 self.render_header(f, rel)?;
144 writeln!(f)?;
145 self.render_causes(f, rel)?;
146 let src = rel.source_code().or(parent_src);
147 self.render_snippets(f, rel, src)?;
148 self.render_footer(f, rel)?;
149 self.render_related(f, rel, src)?;
150 }
151 }
152 Ok(())
153 }
154
155 fn render_snippets(
156 &self,
157 f: &mut impl fmt::Write,
158 diagnostic: &dyn Diagnostic,
159 source_code: Option<&dyn SourceCode>,
160 ) -> fmt::Result {
161 if let Some(source) = source_code {
162 if let Some(labels) = diagnostic.labels() {
163 let mut labels = labels.collect::<Vec<_>>();
164 labels.sort_unstable_by_key(|l| l.inner().offset());
165 if !labels.is_empty() {
166 let contents = labels
167 .iter()
168 .map(|label| {
169 source.read_span(label.inner(), self.context_lines, self.context_lines)
170 })
171 .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
172 .map_err(|_| fmt::Error)?;
173 let mut contexts = Vec::new();
174 for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
175 if contexts.is_empty() {
176 contexts.push((right, right_conts));
177 } else {
178 let (left, left_conts) = contexts.last().unwrap().clone();
179 let left_end = left.offset() + left.len();
180 let right_end = right.offset() + right.len();
181 if left_conts.line() + left_conts.line_count() >= right_conts.line() {
182 let new_span = LabeledSpan::new(
184 left.label().map(String::from),
185 left.offset(),
186 if right_end >= left_end {
187 right_end - left.offset()
189 } else {
190 left.len()
192 },
193 );
194 if source
195 .read_span(
196 new_span.inner(),
197 self.context_lines,
198 self.context_lines,
199 )
200 .is_ok()
201 {
202 contexts.pop();
203 contexts.push((
204 new_span, left_conts,
206 ));
207 } else {
208 contexts.push((right, right_conts));
209 }
210 } else {
211 contexts.push((right, right_conts));
212 }
213 }
214 }
215 for (ctx, _) in contexts {
216 self.render_context(f, source, &ctx, &labels[..])?;
217 }
218 }
219 }
220 }
221 Ok(())
222 }
223
224 fn render_context(
225 &self,
226 f: &mut impl fmt::Write,
227 source: &dyn SourceCode,
228 context: &LabeledSpan,
229 labels: &[LabeledSpan],
230 ) -> fmt::Result {
231 let (contents, lines) = self.get_lines(source, context.inner())?;
232 write!(f, "Begin snippet")?;
233 if let Some(filename) = contents.name() {
234 write!(f, " for {filename}")?;
235 }
236 writeln!(f, " starting at line {}, column {}", contents.line() + 1, contents.column() + 1)?;
237 writeln!(f)?;
238 for line in &lines {
239 writeln!(f, "snippet line {}: {}", line.line_number, line.text)?;
240 let relevant =
241 labels.iter().filter_map(|l| line.span_attach(l.inner()).map(|a| (a, l)));
242 for (attach, label) in relevant {
243 match attach {
244 SpanAttach::Contained { col_start, col_end } if col_start == col_end => {
245 write!(f, " label at line {}, column {}", line.line_number, col_start,)?;
246 }
247 SpanAttach::Contained { col_start, col_end } => {
248 write!(
249 f,
250 " label at line {}, columns {} to {}",
251 line.line_number, col_start, col_end,
252 )?;
253 }
254 SpanAttach::Starts { col_start } => {
255 write!(
256 f,
257 " label starting at line {}, column {}",
258 line.line_number, col_start,
259 )?;
260 }
261 SpanAttach::Ends { col_end } => {
262 write!(
263 f,
264 " label ending at line {}, column {}",
265 line.line_number, col_end,
266 )?;
267 }
268 }
269 if let Some(label) = label.label() {
270 write!(f, ": {label}")?;
271 }
272 writeln!(f)?;
273 }
274 }
275 Ok(())
276 }
277
278 fn get_lines<'a>(
279 &'a self,
280 source: &'a dyn SourceCode,
281 context_span: &'a SourceSpan,
282 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
283 let context_data = source
284 .read_span(context_span, self.context_lines, self.context_lines)
285 .map_err(|_| fmt::Error)?;
286 let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
287 let mut line = context_data.line();
288 let mut column = context_data.column();
289 let mut offset = context_data.span().offset();
290 let mut line_offset = offset;
291 let mut iter = context.chars().peekable();
292 let mut line_str = String::new();
293 let mut lines = Vec::new();
294 while let Some(char) = iter.next() {
295 offset += char.len_utf8();
296 let mut at_end_of_file = false;
297 match char {
298 '\r' => {
299 if iter.next_if_eq(&'\n').is_some() {
300 offset += 1;
301 line += 1;
302 column = 0;
303 } else {
304 line_str.push(char);
305 column += 1;
306 }
307 at_end_of_file = iter.peek().is_none();
308 }
309 '\n' => {
310 at_end_of_file = iter.peek().is_none();
311 line += 1;
312 column = 0;
313 }
314 _ => {
315 line_str.push(char);
316 column += 1;
317 }
318 }
319
320 if iter.peek().is_none() && !at_end_of_file {
321 line += 1;
322 }
323
324 if column == 0 || iter.peek().is_none() {
325 lines.push(Line {
326 line_number: line,
327 offset: line_offset,
328 text: line_str.clone(),
329 at_end_of_file,
330 });
331 line_str.clear();
332 line_offset = offset;
333 }
334 }
335 Ok((context_data, lines))
336 }
337}
338
339impl ReportHandler for NarratableReportHandler {
340 fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 if f.alternate() {
342 return fmt::Debug::fmt(diagnostic, f);
343 }
344
345 self.render_report(f, diagnostic)
346 }
347}
348
349struct Line {
354 line_number: usize,
355 offset: usize,
356 text: String,
357 at_end_of_file: bool,
358}
359
360enum SpanAttach {
361 Contained { col_start: usize, col_end: usize },
362 Starts { col_start: usize },
363 Ends { col_end: usize },
364}
365
366fn safe_get_column(text: &str, offset: usize, start: bool) -> usize {
369 let mut column = text.get(0..offset).map(UnicodeWidthStr::width).unwrap_or_else(|| {
370 let mut column = 0;
371 for (idx, c) in text.char_indices() {
372 if offset <= idx {
373 break;
374 }
375 column += c.width().unwrap_or(0);
376 }
377 column
378 });
379 if start {
380 column += 1;
382 } column
385}
386
387impl Line {
388 fn span_attach(&self, span: &SourceSpan) -> Option<SpanAttach> {
389 let span_end = span.offset() + span.len();
390 let line_end = self.offset + self.text.len();
391
392 let start_after = span.offset() >= self.offset;
393 let end_before = self.at_end_of_file || span_end <= line_end;
394
395 if start_after && end_before {
396 let col_start = safe_get_column(&self.text, span.offset() - self.offset, true);
397 let col_end = if span.is_empty() {
398 col_start
399 } else {
400 safe_get_column(&self.text, span_end - self.offset, false)
403 };
404 return Some(SpanAttach::Contained { col_start, col_end });
405 }
406 if start_after && span.offset() <= line_end {
407 let col_start = safe_get_column(&self.text, span.offset() - self.offset, true);
408 return Some(SpanAttach::Starts { col_start });
409 }
410 if end_before && span_end >= self.offset {
411 let col_end = safe_get_column(&self.text, span_end - self.offset, false);
412 return Some(SpanAttach::Ends { col_end });
413 }
414 None
415 }
416}