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, config: &'config Config, } impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId> where FileId: Copy + PartialEq, { pub fn new( diagnostic: &'diagnostic Diagnostic, 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>, max_label_style: LabelStyle, } impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> { fn get_or_insert_line( &mut self, line_index: usize, line_range: Range, 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, // TODO: How do we reuse these allocations? single_labels: Vec>, 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::>::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` 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, show_notes: bool, } impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId> where FileId: Copy + PartialEq, { pub fn new( diagnostic: &'diagnostic Diagnostic, 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(()) } }