| use std::ops::Range; |
| |
| use crate::diagnostic::{Diagnostic, LabelStyle}; |
| use crate::files::{Error, Files, Location}; |
| use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel}; |
| use crate::term::Config; |
| |
| /// Count the number of decimal digits in `n`. |
| fn count_digits(mut n: usize) -> usize { |
| let mut count = 0; |
| while n != 0 { |
| count += 1; |
| n /= 10; // remove last digit |
| } |
| count |
| } |
| |
| /// Output a richly formatted diagnostic, with source code previews. |
| pub struct RichDiagnostic<'diagnostic, 'config, FileId> { |
| diagnostic: &'diagnostic Diagnostic<FileId>, |
| config: &'config Config, |
| } |
| |
| impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId> |
| where |
| FileId: Copy + PartialEq, |
| { |
| pub fn new( |
| diagnostic: &'diagnostic Diagnostic<FileId>, |
| config: &'config Config, |
| ) -> RichDiagnostic<'diagnostic, 'config, FileId> { |
| RichDiagnostic { diagnostic, config } |
| } |
| |
| pub fn render<'files>( |
| &self, |
| files: &'files impl Files<'files, FileId = FileId>, |
| renderer: &mut Renderer<'_, '_>, |
| ) -> Result<(), Error> |
| where |
| FileId: 'files, |
| { |
| use std::collections::BTreeMap; |
| |
| struct LabeledFile<'diagnostic, FileId> { |
| file_id: FileId, |
| start: usize, |
| name: String, |
| location: Location, |
| num_multi_labels: usize, |
| lines: BTreeMap<usize, Line<'diagnostic>>, |
| max_label_style: LabelStyle, |
| } |
| |
| impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> { |
| fn get_or_insert_line( |
| &mut self, |
| line_index: usize, |
| line_range: Range<usize>, |
| line_number: usize, |
| ) -> &mut Line<'diagnostic> { |
| self.lines.entry(line_index).or_insert_with(|| Line { |
| range: line_range, |
| number: line_number, |
| single_labels: vec![], |
| multi_labels: vec![], |
| // This has to be false by default so we know if it must be rendered by another condition already. |
| must_render: false, |
| }) |
| } |
| } |
| |
| struct Line<'diagnostic> { |
| number: usize, |
| range: std::ops::Range<usize>, |
| // TODO: How do we reuse these allocations? |
| single_labels: Vec<SingleLabel<'diagnostic>>, |
| multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>, |
| must_render: bool, |
| } |
| |
| // TODO: Make this data structure external, to allow for allocation reuse |
| let mut labeled_files = Vec::<LabeledFile<'_, _>>::new(); |
| // Keep track of the outer padding to use when rendering the |
| // snippets of source code. |
| let mut outer_padding = 0; |
| |
| // Group labels by file |
| for label in &self.diagnostic.labels { |
| let start_line_index = files.line_index(label.file_id, label.range.start)?; |
| let start_line_number = files.line_number(label.file_id, start_line_index)?; |
| let start_line_range = files.line_range(label.file_id, start_line_index)?; |
| let end_line_index = files.line_index(label.file_id, label.range.end)?; |
| let end_line_number = files.line_number(label.file_id, end_line_index)?; |
| let end_line_range = files.line_range(label.file_id, end_line_index)?; |
| |
| outer_padding = std::cmp::max(outer_padding, count_digits(start_line_number)); |
| outer_padding = std::cmp::max(outer_padding, count_digits(end_line_number)); |
| |
| // NOTE: This could be made more efficient by using an associative |
| // data structure like a hashmap or B-tree, but we use a vector to |
| // preserve the order that unique files appear in the list of labels. |
| let labeled_file = match labeled_files |
| .iter_mut() |
| .find(|labeled_file| label.file_id == labeled_file.file_id) |
| { |
| Some(labeled_file) => { |
| // another diagnostic also referenced this file |
| if labeled_file.max_label_style > label.style |
| || (labeled_file.max_label_style == label.style |
| && labeled_file.start > label.range.start) |
| { |
| // this label has a higher style or has the same style but starts earlier |
| labeled_file.start = label.range.start; |
| labeled_file.location = files.location(label.file_id, label.range.start)?; |
| labeled_file.max_label_style = label.style; |
| } |
| labeled_file |
| } |
| None => { |
| // no other diagnostic referenced this file yet |
| labeled_files.push(LabeledFile { |
| file_id: label.file_id, |
| start: label.range.start, |
| name: files.name(label.file_id)?.to_string(), |
| location: files.location(label.file_id, label.range.start)?, |
| num_multi_labels: 0, |
| lines: BTreeMap::new(), |
| max_label_style: label.style, |
| }); |
| // this unwrap should never fail because we just pushed an element |
| labeled_files |
| .last_mut() |
| .expect("just pushed an element that disappeared") |
| } |
| }; |
| |
| if start_line_index == end_line_index { |
| // Single line |
| // |
| // ```text |
| // 2 │ (+ test "") |
| // │ ^^ expected `Int` but found `String` |
| // ``` |
| let label_start = label.range.start - start_line_range.start; |
| // Ensure that we print at least one caret, even when we |
| // have a zero-length source range. |
| let label_end = |
| usize::max(label.range.end - start_line_range.start, label_start + 1); |
| |
| let line = labeled_file.get_or_insert_line( |
| start_line_index, |
| start_line_range, |
| start_line_number, |
| ); |
| |
| // Ensure that the single line labels are lexicographically |
| // sorted by the range of source code that they cover. |
| let index = match line.single_labels.binary_search_by(|(_, range, _)| { |
| // `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)` |
| // to piggyback off its lexicographic comparison implementation. |
| (range.start, range.end).cmp(&(label_start, label_end)) |
| }) { |
| // If the ranges are the same, order the labels in reverse |
| // to how they were originally specified in the diagnostic. |
| // This helps with printing in the renderer. |
| Ok(index) | Err(index) => index, |
| }; |
| |
| line.single_labels |
| .insert(index, (label.style, label_start..label_end, &label.message)); |
| |
| // If this line is not rendered, the SingleLabel is not visible. |
| line.must_render = true; |
| } else { |
| // Multiple lines |
| // |
| // ```text |
| // 4 │ fizz₁ num = case (mod num 5) (mod num 3) of |
| // │ ╭─────────────^ |
| // 5 │ │ 0 0 => "FizzBuzz" |
| // 6 │ │ 0 _ => "Fizz" |
| // 7 │ │ _ 0 => "Buzz" |
| // 8 │ │ _ _ => num |
| // │ ╰──────────────^ `case` clauses have incompatible types |
| // ``` |
| |
| let label_index = labeled_file.num_multi_labels; |
| labeled_file.num_multi_labels += 1; |
| |
| // First labeled line |
| let label_start = label.range.start - start_line_range.start; |
| |
| let start_line = labeled_file.get_or_insert_line( |
| start_line_index, |
| start_line_range.clone(), |
| start_line_number, |
| ); |
| |
| start_line.multi_labels.push(( |
| label_index, |
| label.style, |
| MultiLabel::Top(label_start), |
| )); |
| |
| // The first line has to be rendered so the start of the label is visible. |
| start_line.must_render = true; |
| |
| // Marked lines |
| // |
| // ```text |
| // 5 │ │ 0 0 => "FizzBuzz" |
| // 6 │ │ 0 _ => "Fizz" |
| // 7 │ │ _ 0 => "Buzz" |
| // ``` |
| for line_index in (start_line_index + 1)..end_line_index { |
| let line_range = files.line_range(label.file_id, line_index)?; |
| let line_number = files.line_number(label.file_id, line_index)?; |
| |
| outer_padding = std::cmp::max(outer_padding, count_digits(line_number)); |
| |
| let line = labeled_file.get_or_insert_line(line_index, line_range, line_number); |
| |
| line.multi_labels |
| .push((label_index, label.style, MultiLabel::Left)); |
| |
| // The line should be rendered to match the configuration of how much context to show. |
| line.must_render |= |
| // Is this line part of the context after the start of the label? |
| line_index - start_line_index <= self.config.start_context_lines |
| || |
| // Is this line part of the context before the end of the label? |
| end_line_index - line_index <= self.config.end_context_lines; |
| } |
| |
| // Last labeled line |
| // |
| // ```text |
| // 8 │ │ _ _ => num |
| // │ ╰──────────────^ `case` clauses have incompatible types |
| // ``` |
| let label_end = label.range.end - end_line_range.start; |
| |
| let end_line = labeled_file.get_or_insert_line( |
| end_line_index, |
| end_line_range, |
| end_line_number, |
| ); |
| |
| end_line.multi_labels.push(( |
| label_index, |
| label.style, |
| MultiLabel::Bottom(label_end, &label.message), |
| )); |
| |
| // The last line has to be rendered so the end of the label is visible. |
| end_line.must_render = true; |
| } |
| } |
| |
| // Header and message |
| // |
| // ```text |
| // error[E0001]: unexpected type in `+` application |
| // ``` |
| renderer.render_header( |
| None, |
| self.diagnostic.severity, |
| self.diagnostic.code.as_deref(), |
| self.diagnostic.message.as_str(), |
| )?; |
| |
| // Source snippets |
| // |
| // ```text |
| // ┌─ test:2:9 |
| // │ |
| // 2 │ (+ test "") |
| // │ ^^ expected `Int` but found `String` |
| // │ |
| // ``` |
| let mut labeled_files = labeled_files.into_iter().peekable(); |
| while let Some(labeled_file) = labeled_files.next() { |
| let source = files.source(labeled_file.file_id)?; |
| let source = source.as_ref(); |
| |
| // Top left border and locus. |
| // |
| // ```text |
| // ┌─ test:2:9 |
| // ``` |
| if !labeled_file.lines.is_empty() { |
| renderer.render_snippet_start( |
| outer_padding, |
| &Locus { |
| name: labeled_file.name, |
| location: labeled_file.location, |
| }, |
| )?; |
| renderer.render_snippet_empty( |
| outer_padding, |
| self.diagnostic.severity, |
| labeled_file.num_multi_labels, |
| &[], |
| )?; |
| } |
| |
| let mut lines = labeled_file |
| .lines |
| .iter() |
| .filter(|(_, line)| line.must_render) |
| .peekable(); |
| |
| while let Some((line_index, line)) = lines.next() { |
| renderer.render_snippet_source( |
| outer_padding, |
| line.number, |
| &source[line.range.clone()], |
| self.diagnostic.severity, |
| &line.single_labels, |
| labeled_file.num_multi_labels, |
| &line.multi_labels, |
| )?; |
| |
| // Check to see if we need to render any intermediate stuff |
| // before rendering the next line. |
| if let Some((next_line_index, _)) = lines.peek() { |
| match next_line_index.checked_sub(*line_index) { |
| // Consecutive lines |
| Some(1) => {} |
| // One line between the current line and the next line |
| Some(2) => { |
| // Write a source line |
| let file_id = labeled_file.file_id; |
| |
| // This line was not intended to be rendered initially. |
| // To render the line right, we have to get back the original labels. |
| let labels = labeled_file |
| .lines |
| .get(&(line_index + 1)) |
| .map_or(&[][..], |line| &line.multi_labels[..]); |
| |
| renderer.render_snippet_source( |
| outer_padding, |
| files.line_number(file_id, line_index + 1)?, |
| &source[files.line_range(file_id, line_index + 1)?], |
| self.diagnostic.severity, |
| &[], |
| labeled_file.num_multi_labels, |
| labels, |
| )?; |
| } |
| // More than one line between the current line and the next line. |
| Some(_) | None => { |
| // Source break |
| // |
| // ```text |
| // · |
| // ``` |
| renderer.render_snippet_break( |
| outer_padding, |
| self.diagnostic.severity, |
| labeled_file.num_multi_labels, |
| &line.multi_labels, |
| )?; |
| } |
| } |
| } |
| } |
| |
| // Check to see if we should render a trailing border after the |
| // final line of the snippet. |
| if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() { |
| // We don't render a border if we are at the final newline |
| // without trailing notes, because it would end up looking too |
| // spaced-out in combination with the final new line. |
| } else { |
| // Render the trailing snippet border. |
| renderer.render_snippet_empty( |
| outer_padding, |
| self.diagnostic.severity, |
| labeled_file.num_multi_labels, |
| &[], |
| )?; |
| } |
| } |
| |
| // Additional notes |
| // |
| // ```text |
| // = expected type `Int` |
| // found type `String` |
| // ``` |
| for note in &self.diagnostic.notes { |
| renderer.render_snippet_note(outer_padding, note)?; |
| } |
| renderer.render_empty() |
| } |
| } |
| |
| /// Output a short diagnostic, with a line number, severity, and message. |
| pub struct ShortDiagnostic<'diagnostic, FileId> { |
| diagnostic: &'diagnostic Diagnostic<FileId>, |
| show_notes: bool, |
| } |
| |
| impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId> |
| where |
| FileId: Copy + PartialEq, |
| { |
| pub fn new( |
| diagnostic: &'diagnostic Diagnostic<FileId>, |
| show_notes: bool, |
| ) -> ShortDiagnostic<'diagnostic, FileId> { |
| ShortDiagnostic { |
| diagnostic, |
| show_notes, |
| } |
| } |
| |
| pub fn render<'files>( |
| &self, |
| files: &'files impl Files<'files, FileId = FileId>, |
| renderer: &mut Renderer<'_, '_>, |
| ) -> Result<(), Error> |
| where |
| FileId: 'files, |
| { |
| // Located headers |
| // |
| // ```text |
| // test:2:9: error[E0001]: unexpected type in `+` application |
| // ``` |
| let mut primary_labels_encountered = 0; |
| let labels = self.diagnostic.labels.iter(); |
| for label in labels.filter(|label| label.style == LabelStyle::Primary) { |
| primary_labels_encountered += 1; |
| |
| renderer.render_header( |
| Some(&Locus { |
| name: files.name(label.file_id)?.to_string(), |
| location: files.location(label.file_id, label.range.start)?, |
| }), |
| self.diagnostic.severity, |
| self.diagnostic.code.as_deref(), |
| self.diagnostic.message.as_str(), |
| )?; |
| } |
| |
| // Fallback to printing a non-located header if no primary labels were encountered |
| // |
| // ```text |
| // error[E0002]: Bad config found |
| // ``` |
| if primary_labels_encountered == 0 { |
| renderer.render_header( |
| None, |
| self.diagnostic.severity, |
| self.diagnostic.code.as_deref(), |
| self.diagnostic.message.as_str(), |
| )?; |
| } |
| |
| if self.show_notes { |
| // Additional notes |
| // |
| // ```text |
| // = expected type `Int` |
| // found type `String` |
| // ``` |
| for note in &self.diagnostic.notes { |
| renderer.render_snippet_note(0, note)?; |
| } |
| } |
| |
| Ok(()) |
| } |
| } |