diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/codespan-reporting/src/term | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/codespan-reporting/src/term')
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/config.rs | 321 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/renderer.rs | 1020 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/views.rs | 478 |
3 files changed, 1819 insertions, 0 deletions
diff --git a/third_party/rust/codespan-reporting/src/term/config.rs b/third_party/rust/codespan-reporting/src/term/config.rs new file mode 100644 index 0000000000..c92a6b6047 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term/config.rs @@ -0,0 +1,321 @@ +use termcolor::{Color, ColorSpec}; + +use crate::diagnostic::{LabelStyle, Severity}; + +/// Configures how a diagnostic is rendered. +#[derive(Clone, Debug)] +pub struct Config { + /// The display style to use when rendering diagnostics. + /// Defaults to: [`DisplayStyle::Rich`]. + /// + /// [`DisplayStyle::Rich`]: DisplayStyle::Rich + pub display_style: DisplayStyle, + /// Column width of tabs. + /// Defaults to: `4`. + pub tab_width: usize, + /// Styles to use when rendering the diagnostic. + pub styles: Styles, + /// Characters to use when rendering the diagnostic. + pub chars: Chars, + /// The minimum number of lines to be shown after the line on which a multiline [`Label`] begins. + /// + /// Defaults to: `3`. + /// + /// [`Label`]: crate::diagnostic::Label + pub start_context_lines: usize, + /// The minimum number of lines to be shown before the line on which a multiline [`Label`] ends. + /// + /// Defaults to: `1`. + /// + /// [`Label`]: crate::diagnostic::Label + pub end_context_lines: usize, +} + +impl Default for Config { + fn default() -> Config { + Config { + display_style: DisplayStyle::Rich, + tab_width: 4, + styles: Styles::default(), + chars: Chars::default(), + start_context_lines: 3, + end_context_lines: 1, + } + } +} + +/// The display style to use when rendering diagnostics. +#[derive(Clone, Debug)] +pub enum DisplayStyle { + /// Output a richly formatted diagnostic, with source code previews. + /// + /// ```text + /// error[E0001]: unexpected type in `+` application + /// ┌─ test:2:9 + /// │ + /// 2 │ (+ test "") + /// │ ^^ expected `Int` but found `String` + /// │ + /// = expected type `Int` + /// found type `String` + /// + /// error[E0002]: Bad config found + /// + /// ``` + Rich, + /// Output a condensed diagnostic, with a line number, severity, message and notes (if any). + /// + /// ```text + /// test:2:9: error[E0001]: unexpected type in `+` application + /// = expected type `Int` + /// found type `String` + /// + /// error[E0002]: Bad config found + /// ``` + Medium, + /// Output a short diagnostic, with a line number, severity, and message. + /// + /// ```text + /// test:2:9: error[E0001]: unexpected type in `+` application + /// error[E0002]: Bad config found + /// ``` + Short, +} + +/// Styles to use when rendering the diagnostic. +#[derive(Clone, Debug)] +pub struct Styles { + /// The style to use when rendering bug headers. + /// Defaults to `fg:red bold intense`. + pub header_bug: ColorSpec, + /// The style to use when rendering error headers. + /// Defaults to `fg:red bold intense`. + pub header_error: ColorSpec, + /// The style to use when rendering warning headers. + /// Defaults to `fg:yellow bold intense`. + pub header_warning: ColorSpec, + /// The style to use when rendering note headers. + /// Defaults to `fg:green bold intense`. + pub header_note: ColorSpec, + /// The style to use when rendering help headers. + /// Defaults to `fg:cyan bold intense`. + pub header_help: ColorSpec, + /// The style to use when the main diagnostic message. + /// Defaults to `bold intense`. + pub header_message: ColorSpec, + + /// The style to use when rendering bug labels. + /// Defaults to `fg:red`. + pub primary_label_bug: ColorSpec, + /// The style to use when rendering error labels. + /// Defaults to `fg:red`. + pub primary_label_error: ColorSpec, + /// The style to use when rendering warning labels. + /// Defaults to `fg:yellow`. + pub primary_label_warning: ColorSpec, + /// The style to use when rendering note labels. + /// Defaults to `fg:green`. + pub primary_label_note: ColorSpec, + /// The style to use when rendering help labels. + /// Defaults to `fg:cyan`. + pub primary_label_help: ColorSpec, + /// The style to use when rendering secondary labels. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub secondary_label: ColorSpec, + + /// The style to use when rendering the line numbers. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub line_number: ColorSpec, + /// The style to use when rendering the source code borders. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub source_border: ColorSpec, + /// The style to use when rendering the note bullets. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub note_bullet: ColorSpec, +} + +impl Styles { + /// The style used to mark a header at a given severity. + pub fn header(&self, severity: Severity) -> &ColorSpec { + match severity { + Severity::Bug => &self.header_bug, + Severity::Error => &self.header_error, + Severity::Warning => &self.header_warning, + Severity::Note => &self.header_note, + Severity::Help => &self.header_help, + } + } + + /// The style used to mark a primary or secondary label at a given severity. + pub fn label(&self, severity: Severity, label_style: LabelStyle) -> &ColorSpec { + match (label_style, severity) { + (LabelStyle::Primary, Severity::Bug) => &self.primary_label_bug, + (LabelStyle::Primary, Severity::Error) => &self.primary_label_error, + (LabelStyle::Primary, Severity::Warning) => &self.primary_label_warning, + (LabelStyle::Primary, Severity::Note) => &self.primary_label_note, + (LabelStyle::Primary, Severity::Help) => &self.primary_label_help, + (LabelStyle::Secondary, _) => &self.secondary_label, + } + } + + #[doc(hidden)] + pub fn with_blue(blue: Color) -> Styles { + let header = ColorSpec::new().set_bold(true).set_intense(true).clone(); + + Styles { + header_bug: header.clone().set_fg(Some(Color::Red)).clone(), + header_error: header.clone().set_fg(Some(Color::Red)).clone(), + header_warning: header.clone().set_fg(Some(Color::Yellow)).clone(), + header_note: header.clone().set_fg(Some(Color::Green)).clone(), + header_help: header.clone().set_fg(Some(Color::Cyan)).clone(), + header_message: header, + + primary_label_bug: ColorSpec::new().set_fg(Some(Color::Red)).clone(), + primary_label_error: ColorSpec::new().set_fg(Some(Color::Red)).clone(), + primary_label_warning: ColorSpec::new().set_fg(Some(Color::Yellow)).clone(), + primary_label_note: ColorSpec::new().set_fg(Some(Color::Green)).clone(), + primary_label_help: ColorSpec::new().set_fg(Some(Color::Cyan)).clone(), + secondary_label: ColorSpec::new().set_fg(Some(blue)).clone(), + + line_number: ColorSpec::new().set_fg(Some(blue)).clone(), + source_border: ColorSpec::new().set_fg(Some(blue)).clone(), + note_bullet: ColorSpec::new().set_fg(Some(blue)).clone(), + } + } +} + +impl Default for Styles { + fn default() -> Styles { + // Blue is really difficult to see on the standard windows command line + #[cfg(windows)] + const BLUE: Color = Color::Cyan; + #[cfg(not(windows))] + const BLUE: Color = Color::Blue; + + Self::with_blue(BLUE) + } +} + +/// Characters to use when rendering the diagnostic. +/// +/// By using [`Chars::ascii()`] you can switch to an ASCII-only format suitable +/// for rendering on terminals that do not support box drawing characters. +#[derive(Clone, Debug)] +pub struct Chars { + /// The characters to use for the top-left border of the snippet. + /// Defaults to: `"┌─"` or `"-->"` with [`Chars::ascii()`]. + pub snippet_start: String, + /// The character to use for the left border of the source. + /// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`]. + pub source_border_left: char, + /// The character to use for the left border break of the source. + /// Defaults to: `'·'` or `'.'` with [`Chars::ascii()`]. + pub source_border_left_break: char, + + /// The character to use for the note bullet. + /// Defaults to: `'='`. + pub note_bullet: char, + + /// The character to use for marking a single-line primary label. + /// Defaults to: `'^'`. + pub single_primary_caret: char, + /// The character to use for marking a single-line secondary label. + /// Defaults to: `'-'`. + pub single_secondary_caret: char, + + /// The character to use for marking the start of a multi-line primary label. + /// Defaults to: `'^'`. + pub multi_primary_caret_start: char, + /// The character to use for marking the end of a multi-line primary label. + /// Defaults to: `'^'`. + pub multi_primary_caret_end: char, + /// The character to use for marking the start of a multi-line secondary label. + /// Defaults to: `'\''`. + pub multi_secondary_caret_start: char, + /// The character to use for marking the end of a multi-line secondary label. + /// Defaults to: `'\''`. + pub multi_secondary_caret_end: char, + /// The character to use for the top-left corner of a multi-line label. + /// Defaults to: `'╭'` or `'/'` with [`Chars::ascii()`]. + pub multi_top_left: char, + /// The character to use for the top of a multi-line label. + /// Defaults to: `'─'` or `'-'` with [`Chars::ascii()`]. + pub multi_top: char, + /// The character to use for the bottom-left corner of a multi-line label. + /// Defaults to: `'╰'` or `'\'` with [`Chars::ascii()`]. + pub multi_bottom_left: char, + /// The character to use when marking the bottom of a multi-line label. + /// Defaults to: `'─'` or `'-'` with [`Chars::ascii()`]. + pub multi_bottom: char, + /// The character to use for the left of a multi-line label. + /// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`]. + pub multi_left: char, + + /// The character to use for the left of a pointer underneath a caret. + /// Defaults to: `'│'` or `'|'` with [`Chars::ascii()`]. + pub pointer_left: char, +} + +impl Default for Chars { + fn default() -> Chars { + Chars::box_drawing() + } +} + +impl Chars { + /// A character set that uses Unicode box drawing characters. + pub fn box_drawing() -> Chars { + Chars { + snippet_start: "┌─".into(), + source_border_left: '│', + source_border_left_break: '·', + + note_bullet: '=', + + single_primary_caret: '^', + single_secondary_caret: '-', + + multi_primary_caret_start: '^', + multi_primary_caret_end: '^', + multi_secondary_caret_start: '\'', + multi_secondary_caret_end: '\'', + multi_top_left: '╭', + multi_top: '─', + multi_bottom_left: '╰', + multi_bottom: '─', + multi_left: '│', + + pointer_left: '│', + } + } + + /// A character set that only uses ASCII characters. + /// + /// This is useful if your terminal's font does not support box drawing + /// characters well and results in output that looks similar to rustc's + /// diagnostic output. + pub fn ascii() -> Chars { + Chars { + snippet_start: "-->".into(), + source_border_left: '|', + source_border_left_break: '.', + + note_bullet: '=', + + single_primary_caret: '^', + single_secondary_caret: '-', + + multi_primary_caret_start: '^', + multi_primary_caret_end: '^', + multi_secondary_caret_start: '\'', + multi_secondary_caret_end: '\'', + multi_top_left: '/', + multi_top: '-', + multi_bottom_left: '\\', + multi_bottom: '-', + multi_left: '|', + + pointer_left: '|', + } + } +} diff --git a/third_party/rust/codespan-reporting/src/term/renderer.rs b/third_party/rust/codespan-reporting/src/term/renderer.rs new file mode 100644 index 0000000000..eeb8965d25 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term/renderer.rs @@ -0,0 +1,1020 @@ +use std::io::{self, Write}; +use std::ops::Range; +use termcolor::{ColorSpec, WriteColor}; + +use crate::diagnostic::{LabelStyle, Severity}; +use crate::files::{Error, Location}; +use crate::term::{Chars, Config, Styles}; + +/// The 'location focus' of a source code snippet. +pub struct Locus { + /// The user-facing name of the file. + pub name: String, + /// The location. + pub location: Location, +} + +/// Single-line label, with an optional message. +/// +/// ```text +/// ^^^^^^^^^ blah blah +/// ``` +pub type SingleLabel<'diagnostic> = (LabelStyle, Range<usize>, &'diagnostic str); + +/// A multi-line label to render. +/// +/// Locations are relative to the start of where the source code is rendered. +pub enum MultiLabel<'diagnostic> { + /// Multi-line label top. + /// The contained value indicates where the label starts. + /// + /// ```text + /// ╭────────────^ + /// ``` + /// + /// Can also be rendered at the beginning of the line + /// if there is only whitespace before the label starts. + /// + /// /// ```text + /// ╭ + /// ``` + Top(usize), + /// Left vertical labels for multi-line labels. + /// + /// ```text + /// │ + /// ``` + Left, + /// Multi-line label bottom, with an optional message. + /// The first value indicates where the label ends. + /// + /// ```text + /// ╰────────────^ blah blah + /// ``` + Bottom(usize, &'diagnostic str), +} + +#[derive(Copy, Clone)] +enum VerticalBound { + Top, + Bottom, +} + +type Underline = (LabelStyle, VerticalBound); + +/// A renderer of display list entries. +/// +/// The following diagram gives an overview of each of the parts of the renderer's output: +/// +/// ```text +/// ┌ outer gutter +/// │ ┌ left border +/// │ │ ┌ inner gutter +/// │ │ │ ┌─────────────────────────── source ─────────────────────────────┐ +/// │ │ │ │ │ +/// ┌──────────────────────────────────────────────────────────────────────────── +/// header ── │ error[0001]: oh noes, a cupcake has occurred! +/// snippet start ── │ ┌─ test:9:0 +/// snippet empty ── │ │ +/// snippet line ── │ 9 │ ╭ Cupcake ipsum dolor. Sit amet marshmallow topping cheesecake +/// snippet line ── │ 10 │ │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly +/// │ │ ╭─│─────────^ +/// snippet break ── │ · │ │ +/// snippet line ── │ 33 │ │ │ Muffin danish chocolate soufflé pastry icing bonbon oat cake. +/// snippet line ── │ 34 │ │ │ Powder cake jujubes oat cake. Lemon drops tootsie roll marshmallow +/// │ │ │ ╰─────────────────────────────^ blah blah +/// snippet break ── │ · │ +/// snippet line ── │ 38 │ │ Brownie lemon drops chocolate jelly-o candy canes. Danish marzipan +/// snippet line ── │ 39 │ │ jujubes soufflé carrot cake marshmallow tiramisu caramels candy canes. +/// │ │ │ ^^^^^^^^^^^^^^^^^^^ -------------------- blah blah +/// │ │ │ │ +/// │ │ │ blah blah +/// │ │ │ note: this is a note +/// snippet line ── │ 40 │ │ Fruitcake jelly-o danish toffee. Tootsie roll pastry cheesecake +/// snippet line ── │ 41 │ │ soufflé marzipan. Chocolate bar oat cake jujubes lollipop pastry +/// snippet line ── │ 42 │ │ cupcake. Candy canes cupcake toffee gingerbread candy canes muffin +/// │ │ │ ^^^^^^^^^^^^^^^^^^ blah blah +/// │ │ ╰──────────^ blah blah +/// snippet break ── │ · +/// snippet line ── │ 82 │ gingerbread toffee chupa chups chupa chups jelly-o cotton candy. +/// │ │ ^^^^^^ ------- blah blah +/// snippet empty ── │ │ +/// snippet note ── │ = blah blah +/// snippet note ── │ = blah blah blah +/// │ blah blah +/// snippet note ── │ = blah blah blah +/// │ blah blah +/// empty ── │ +/// ``` +/// +/// Filler text from http://www.cupcakeipsum.com +pub struct Renderer<'writer, 'config> { + writer: &'writer mut dyn WriteColor, + config: &'config Config, +} + +impl<'writer, 'config> Renderer<'writer, 'config> { + /// Construct a renderer from the given writer and config. + pub fn new( + writer: &'writer mut dyn WriteColor, + config: &'config Config, + ) -> Renderer<'writer, 'config> { + Renderer { writer, config } + } + + fn chars(&self) -> &'config Chars { + &self.config.chars + } + + fn styles(&self) -> &'config Styles { + &self.config.styles + } + + /// Diagnostic header, with severity, code, and message. + /// + /// ```text + /// error[E0001]: unexpected type in `+` application + /// ``` + pub fn render_header( + &mut self, + locus: Option<&Locus>, + severity: Severity, + code: Option<&str>, + message: &str, + ) -> Result<(), Error> { + // Write locus + // + // ```text + // test:2:9: + // ``` + if let Some(locus) = locus { + self.snippet_locus(locus)?; + write!(self, ": ")?; + } + + // Write severity name + // + // ```text + // error + // ``` + self.set_color(self.styles().header(severity))?; + match severity { + Severity::Bug => write!(self, "bug")?, + Severity::Error => write!(self, "error")?, + Severity::Warning => write!(self, "warning")?, + Severity::Help => write!(self, "help")?, + Severity::Note => write!(self, "note")?, + } + + // Write error code + // + // ```text + // [E0001] + // ``` + if let Some(code) = &code.filter(|code| !code.is_empty()) { + write!(self, "[{}]", code)?; + } + + // Write diagnostic message + // + // ```text + // : unexpected type in `+` application + // ``` + self.set_color(&self.styles().header_message)?; + write!(self, ": {}", message)?; + self.reset()?; + + writeln!(self)?; + + Ok(()) + } + + /// Empty line. + pub fn render_empty(&mut self) -> Result<(), Error> { + writeln!(self)?; + Ok(()) + } + + /// Top left border and locus. + /// + /// ```text + /// ┌─ test:2:9 + /// ``` + pub fn render_snippet_start( + &mut self, + outer_padding: usize, + locus: &Locus, + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + + self.set_color(&self.styles().source_border)?; + write!(self, "{}", self.chars().snippet_start)?; + self.reset()?; + + write!(self, " ")?; + self.snippet_locus(&locus)?; + + writeln!(self)?; + + Ok(()) + } + + /// A line of source code. + /// + /// ```text + /// 10 │ │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly + /// │ ╭─│─────────^ + /// ``` + pub fn render_snippet_source( + &mut self, + outer_padding: usize, + line_number: usize, + source: &str, + severity: Severity, + single_labels: &[SingleLabel<'_>], + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + // Trim trailing newlines, linefeeds, and null chars from source, if they exist. + // FIXME: Use the number of trimmed placeholders when rendering single line carets + let source = source.trim_end_matches(['\n', '\r', '\0'].as_ref()); + + // Write source line + // + // ```text + // 10 │ │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly + // ``` + { + // Write outer gutter (with line number) and border + self.outer_gutter_number(line_number, outer_padding)?; + self.border_left()?; + + // Write inner gutter (with multi-line continuations on the left if necessary) + let mut multi_labels_iter = multi_labels.iter().peekable(); + for label_column in 0..num_multi_labels { + match multi_labels_iter.peek() { + Some((label_index, label_style, label)) if *label_index == label_column => { + match label { + MultiLabel::Top(start) + if *start <= source.len() - source.trim_start().len() => + { + self.label_multi_top_left(severity, *label_style)?; + } + MultiLabel::Top(..) => self.inner_gutter_space()?, + MultiLabel::Left | MultiLabel::Bottom(..) => { + self.label_multi_left(severity, *label_style, None)?; + } + } + multi_labels_iter.next(); + } + Some((_, _, _)) | None => self.inner_gutter_space()?, + } + } + + // Write source text + write!(self, " ")?; + let mut in_primary = false; + for (metrics, ch) in self.char_metrics(source.char_indices()) { + let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); + + // Check if we are overlapping a primary label + let is_primary = single_labels.iter().any(|(ls, range, _)| { + *ls == LabelStyle::Primary && is_overlapping(range, &column_range) + }) || multi_labels.iter().any(|(_, ls, label)| { + *ls == LabelStyle::Primary + && match label { + MultiLabel::Top(start) => column_range.start >= *start, + MultiLabel::Left => true, + MultiLabel::Bottom(start, _) => column_range.end <= *start, + } + }); + + // Set the source color if we are in a primary label + if is_primary && !in_primary { + self.set_color(self.styles().label(severity, LabelStyle::Primary))?; + in_primary = true; + } else if !is_primary && in_primary { + self.reset()?; + in_primary = false; + } + + match ch { + '\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?, + _ => write!(self, "{}", ch)?, + } + } + if in_primary { + self.reset()?; + } + writeln!(self)?; + } + + // Write single labels underneath source + // + // ```text + // │ - ---- ^^^ second mutable borrow occurs here + // │ │ │ + // │ │ first mutable borrow occurs here + // │ first borrow later used by call + // │ help: some help here + // ``` + if !single_labels.is_empty() { + // Our plan is as follows: + // + // 1. Do an initial scan to find: + // - The number of non-empty messages. + // - The right-most start and end positions of labels. + // - A candidate for a trailing label (where the label's message + // is printed to the left of the caret). + // 2. Check if the trailing label candidate overlaps another label - + // if so we print it underneath the carets with the other labels. + // 3. Print a line of carets, and (possibly) the trailing message + // to the left. + // 4. Print vertical lines pointing to the carets, and the messages + // for those carets. + // + // We try our best avoid introducing new dynamic allocations, + // instead preferring to iterate over the labels multiple times. It + // is unclear what the performance tradeoffs are however, so further + // investigation may be required. + + // The number of non-empty messages to print. + let mut num_messages = 0; + // The right-most start position, eg: + // + // ```text + // -^^^^---- ^^^^^^^ + // │ + // right-most start position + // ``` + let mut max_label_start = 0; + // The right-most end position, eg: + // + // ```text + // -^^^^---- ^^^^^^^ + // │ + // right-most end position + // ``` + let mut max_label_end = 0; + // A trailing message, eg: + // + // ```text + // ^^^ second mutable borrow occurs here + // ``` + let mut trailing_label = None; + + for (label_index, label) in single_labels.iter().enumerate() { + let (_, range, message) = label; + if !message.is_empty() { + num_messages += 1; + } + max_label_start = std::cmp::max(max_label_start, range.start); + max_label_end = std::cmp::max(max_label_end, range.end); + // This is a candidate for the trailing label, so let's record it. + if range.end == max_label_end { + if message.is_empty() { + trailing_label = None; + } else { + trailing_label = Some((label_index, label)); + } + } + } + if let Some((trailing_label_index, (_, trailing_range, _))) = trailing_label { + // Check to see if the trailing label candidate overlaps any of + // the other labels on the current line. + if single_labels + .iter() + .enumerate() + .filter(|(label_index, _)| *label_index != trailing_label_index) + .any(|(_, (_, range, _))| is_overlapping(trailing_range, range)) + { + // If it does, we'll instead want to render it below the + // carets along with the other hanging labels. + trailing_label = None; + } + } + + // Write a line of carets + // + // ```text + // │ ^^^^^^ -------^^^^^^^^^-------^^^^^----- ^^^^ trailing label message + // ``` + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + write!(self, " ")?; + + let mut previous_label_style = None; + let placeholder_metrics = Metrics { + byte_index: source.len(), + unicode_width: 1, + }; + for (metrics, ch) in self + .char_metrics(source.char_indices()) + // Add a placeholder source column at the end to allow for + // printing carets at the end of lines, eg: + // + // ```text + // 1 │ Hello world! + // │ ^ + // ``` + .chain(std::iter::once((placeholder_metrics, '\0'))) + { + // Find the current label style at this column + let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); + let current_label_style = single_labels + .iter() + .filter(|(_, range, _)| is_overlapping(range, &column_range)) + .map(|(label_style, _, _)| *label_style) + .max_by_key(label_priority_key); + + // Update writer style if necessary + if previous_label_style != current_label_style { + match current_label_style { + None => self.reset()?, + Some(label_style) => { + self.set_color(self.styles().label(severity, label_style))?; + } + } + } + + let caret_ch = match current_label_style { + Some(LabelStyle::Primary) => Some(self.chars().single_primary_caret), + Some(LabelStyle::Secondary) => Some(self.chars().single_secondary_caret), + // Only print padding if we are before the end of the last single line caret + None if metrics.byte_index < max_label_end => Some(' '), + None => None, + }; + if let Some(caret_ch) = caret_ch { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width).try_for_each(|_| write!(self, "{}", caret_ch))?; + } + + previous_label_style = current_label_style; + } + // Reset style if it was previously set + if previous_label_style.is_some() { + self.reset()?; + } + // Write first trailing label message + if let Some((_, (label_style, _, message))) = trailing_label { + write!(self, " ")?; + self.set_color(self.styles().label(severity, *label_style))?; + write!(self, "{}", message)?; + self.reset()?; + } + writeln!(self)?; + + // Write hanging labels pointing to carets + // + // ```text + // │ │ │ + // │ │ first mutable borrow occurs here + // │ first borrow later used by call + // │ help: some help here + // ``` + if num_messages > trailing_label.iter().count() { + // Write first set of vertical lines before hanging labels + // + // ```text + // │ │ │ + // ``` + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + write!(self, " ")?; + self.caret_pointers( + severity, + max_label_start, + single_labels, + trailing_label, + source.char_indices(), + )?; + writeln!(self)?; + + // Write hanging labels pointing to carets + // + // ```text + // │ │ first mutable borrow occurs here + // │ first borrow later used by call + // │ help: some help here + // ``` + for (label_style, range, message) in + hanging_labels(single_labels, trailing_label).rev() + { + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + write!(self, " ")?; + self.caret_pointers( + severity, + max_label_start, + single_labels, + trailing_label, + source + .char_indices() + .take_while(|(byte_index, _)| *byte_index < range.start), + )?; + self.set_color(self.styles().label(severity, *label_style))?; + write!(self, "{}", message)?; + self.reset()?; + writeln!(self)?; + } + } + } + + // Write top or bottom label carets underneath source + // + // ```text + // │ ╰───│──────────────────^ woops + // │ ╭─│─────────^ + // ``` + for (multi_label_index, (_, label_style, label)) in multi_labels.iter().enumerate() { + let (label_style, range, bottom_message) = match label { + MultiLabel::Left => continue, // no label caret needed + // no label caret needed if this can be started in front of the line + MultiLabel::Top(start) if *start <= source.len() - source.trim_start().len() => { + continue + } + MultiLabel::Top(range) => (*label_style, range, None), + MultiLabel::Bottom(range, message) => (*label_style, range, Some(message)), + }; + + self.outer_gutter(outer_padding)?; + self.border_left()?; + + // Write inner gutter. + // + // ```text + // │ ╭─│───│ + // ``` + let mut underline = None; + let mut multi_labels_iter = multi_labels.iter().enumerate().peekable(); + for label_column in 0..num_multi_labels { + match multi_labels_iter.peek() { + Some((i, (label_index, ls, label))) if *label_index == label_column => { + match label { + MultiLabel::Left => { + self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?; + } + MultiLabel::Top(..) if multi_label_index > *i => { + self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?; + } + MultiLabel::Bottom(..) if multi_label_index < *i => { + self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?; + } + MultiLabel::Top(..) if multi_label_index == *i => { + underline = Some((*ls, VerticalBound::Top)); + self.label_multi_top_left(severity, label_style)? + } + MultiLabel::Bottom(..) if multi_label_index == *i => { + underline = Some((*ls, VerticalBound::Bottom)); + self.label_multi_bottom_left(severity, label_style)?; + } + MultiLabel::Top(..) | MultiLabel::Bottom(..) => { + self.inner_gutter_column(severity, underline)?; + } + } + multi_labels_iter.next(); + } + Some((_, _)) | None => self.inner_gutter_column(severity, underline)?, + } + } + + // Finish the top or bottom caret + match bottom_message { + None => self.label_multi_top_caret(severity, label_style, source, *range)?, + Some(message) => { + self.label_multi_bottom_caret(severity, label_style, source, *range, message)? + } + } + } + + Ok(()) + } + + /// An empty source line, for providing additional whitespace to source snippets. + /// + /// ```text + /// │ │ │ + /// ``` + pub fn render_snippet_empty( + &mut self, + outer_padding: usize, + severity: Severity, + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + writeln!(self)?; + Ok(()) + } + + /// A broken source line, for labeling skipped sections of source. + /// + /// ```text + /// · │ │ + /// ``` + pub fn render_snippet_break( + &mut self, + outer_padding: usize, + severity: Severity, + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + self.border_left_break()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + writeln!(self)?; + Ok(()) + } + + /// Additional notes. + /// + /// ```text + /// = expected type `Int` + /// found type `String` + /// ``` + pub fn render_snippet_note( + &mut self, + outer_padding: usize, + message: &str, + ) -> Result<(), Error> { + for (note_line_index, line) in message.lines().enumerate() { + self.outer_gutter(outer_padding)?; + match note_line_index { + 0 => { + self.set_color(&self.styles().note_bullet)?; + write!(self, "{}", self.chars().note_bullet)?; + self.reset()?; + } + _ => write!(self, " ")?, + } + // Write line of message + writeln!(self, " {}", line)?; + } + + Ok(()) + } + + /// Adds tab-stop aware unicode-width computations to an iterator over + /// character indices. Assumes that the character indices begin at the start + /// of the line. + fn char_metrics( + &self, + char_indices: impl Iterator<Item = (usize, char)>, + ) -> impl Iterator<Item = (Metrics, char)> { + use unicode_width::UnicodeWidthChar; + + let tab_width = self.config.tab_width; + let mut unicode_column = 0; + + char_indices.map(move |(byte_index, ch)| { + let metrics = Metrics { + byte_index, + unicode_width: match (ch, tab_width) { + ('\t', 0) => 0, // Guard divide-by-zero + ('\t', _) => tab_width - (unicode_column % tab_width), + (ch, _) => ch.width().unwrap_or(0), + }, + }; + unicode_column += metrics.unicode_width; + + (metrics, ch) + }) + } + + /// Location focus. + fn snippet_locus(&mut self, locus: &Locus) -> Result<(), Error> { + write!( + self, + "{name}:{line_number}:{column_number}", + name = locus.name, + line_number = locus.location.line_number, + column_number = locus.location.column_number, + )?; + Ok(()) + } + + /// The outer gutter of a source line. + fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> { + write!(self, "{space: >width$} ", space = "", width = outer_padding)?; + Ok(()) + } + + /// The outer gutter of a source line, with line number. + fn outer_gutter_number( + &mut self, + line_number: usize, + outer_padding: usize, + ) -> Result<(), Error> { + self.set_color(&self.styles().line_number)?; + write!( + self, + "{line_number: >width$}", + line_number = line_number, + width = outer_padding, + )?; + self.reset()?; + write!(self, " ")?; + Ok(()) + } + + /// The left-hand border of a source line. + fn border_left(&mut self) -> Result<(), Error> { + self.set_color(&self.styles().source_border)?; + write!(self, "{}", self.chars().source_border_left)?; + self.reset()?; + Ok(()) + } + + /// The broken left-hand border of a source line. + fn border_left_break(&mut self) -> Result<(), Error> { + self.set_color(&self.styles().source_border)?; + write!(self, "{}", self.chars().source_border_left_break)?; + self.reset()?; + Ok(()) + } + + /// Write vertical lines pointing to carets. + fn caret_pointers( + &mut self, + severity: Severity, + max_label_start: usize, + single_labels: &[SingleLabel<'_>], + trailing_label: Option<(usize, &SingleLabel<'_>)>, + char_indices: impl Iterator<Item = (usize, char)>, + ) -> Result<(), Error> { + for (metrics, ch) in self.char_metrics(char_indices) { + let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); + let label_style = hanging_labels(single_labels, trailing_label) + .filter(|(_, range, _)| column_range.contains(&range.start)) + .map(|(label_style, _, _)| *label_style) + .max_by_key(label_priority_key); + + let mut spaces = match label_style { + None => 0..metrics.unicode_width, + Some(label_style) => { + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().pointer_left)?; + self.reset()?; + 1..metrics.unicode_width + } + }; + // Only print padding if we are before the end of the last single line caret + if metrics.byte_index <= max_label_start { + spaces.try_for_each(|_| write!(self, " "))?; + } + } + + Ok(()) + } + + /// The left of a multi-line label. + /// + /// ```text + /// │ + /// ``` + fn label_multi_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + underline: Option<LabelStyle>, + ) -> Result<(), Error> { + match underline { + None => write!(self, " ")?, + // Continue an underline horizontally + Some(label_style) => { + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_top)?; + self.reset()?; + } + } + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_left)?; + self.reset()?; + Ok(()) + } + + /// The top-left of a multi-line label. + /// + /// ```text + /// ╭ + /// ``` + fn label_multi_top_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + ) -> Result<(), Error> { + write!(self, " ")?; + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_top_left)?; + self.reset()?; + Ok(()) + } + + /// The bottom left of a multi-line label. + /// + /// ```text + /// ╰ + /// ``` + fn label_multi_bottom_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + ) -> Result<(), Error> { + write!(self, " ")?; + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_bottom_left)?; + self.reset()?; + Ok(()) + } + + /// Multi-line label top. + /// + /// ```text + /// ─────────────^ + /// ``` + fn label_multi_top_caret( + &mut self, + severity: Severity, + label_style: LabelStyle, + source: &str, + start: usize, + ) -> Result<(), Error> { + self.set_color(self.styles().label(severity, label_style))?; + + for (metrics, _) in self + .char_metrics(source.char_indices()) + .take_while(|(metrics, _)| metrics.byte_index < start + 1) + { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width) + .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?; + } + + let caret_start = match label_style { + LabelStyle::Primary => self.config.chars.multi_primary_caret_start, + LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start, + }; + write!(self, "{}", caret_start)?; + self.reset()?; + writeln!(self)?; + Ok(()) + } + + /// Multi-line label bottom, with a message. + /// + /// ```text + /// ─────────────^ expected `Int` but found `String` + /// ``` + fn label_multi_bottom_caret( + &mut self, + severity: Severity, + label_style: LabelStyle, + source: &str, + start: usize, + message: &str, + ) -> Result<(), Error> { + self.set_color(self.styles().label(severity, label_style))?; + + for (metrics, _) in self + .char_metrics(source.char_indices()) + .take_while(|(metrics, _)| metrics.byte_index < start) + { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width) + .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?; + } + + let caret_end = match label_style { + LabelStyle::Primary => self.config.chars.multi_primary_caret_start, + LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start, + }; + write!(self, "{}", caret_end)?; + if !message.is_empty() { + write!(self, " {}", message)?; + } + self.reset()?; + writeln!(self)?; + Ok(()) + } + + /// Writes an empty gutter space, or continues an underline horizontally. + fn inner_gutter_column( + &mut self, + severity: Severity, + underline: Option<Underline>, + ) -> Result<(), Error> { + match underline { + None => self.inner_gutter_space(), + Some((label_style, vertical_bound)) => { + self.set_color(self.styles().label(severity, label_style))?; + let ch = match vertical_bound { + VerticalBound::Top => self.config.chars.multi_top, + VerticalBound::Bottom => self.config.chars.multi_bottom, + }; + write!(self, "{0}{0}", ch)?; + self.reset()?; + Ok(()) + } + } + } + + /// Writes an empty gutter space. + fn inner_gutter_space(&mut self) -> Result<(), Error> { + write!(self, " ")?; + Ok(()) + } + + /// Writes an inner gutter, with the left lines if necessary. + fn inner_gutter( + &mut self, + severity: Severity, + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + let mut multi_labels_iter = multi_labels.iter().peekable(); + for label_column in 0..num_multi_labels { + match multi_labels_iter.peek() { + Some((label_index, ls, label)) if *label_index == label_column => match label { + MultiLabel::Left | MultiLabel::Bottom(..) => { + self.label_multi_left(severity, *ls, None)?; + multi_labels_iter.next(); + } + MultiLabel::Top(..) => { + self.inner_gutter_space()?; + multi_labels_iter.next(); + } + }, + Some((_, _, _)) | None => self.inner_gutter_space()?, + } + } + + Ok(()) + } +} + +impl<'writer, 'config> Write for Renderer<'writer, 'config> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.writer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> { + fn supports_color(&self) -> bool { + self.writer.supports_color() + } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + self.writer.set_color(spec) + } + + fn reset(&mut self) -> io::Result<()> { + self.writer.reset() + } + + fn is_synchronous(&self) -> bool { + self.writer.is_synchronous() + } +} + +struct Metrics { + byte_index: usize, + unicode_width: usize, +} + +/// Check if two ranges overlap +fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool { + let start = std::cmp::max(range0.start, range1.start); + let end = std::cmp::min(range0.end, range1.end); + start < end +} + +/// For prioritizing primary labels over secondary labels when rendering carets. +fn label_priority_key(label_style: &LabelStyle) -> u8 { + match label_style { + LabelStyle::Secondary => 0, + LabelStyle::Primary => 1, + } +} + +/// Return an iterator that yields the labels that require hanging messages +/// rendered underneath them. +fn hanging_labels<'labels, 'diagnostic>( + single_labels: &'labels [SingleLabel<'diagnostic>], + trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>, +) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>> { + single_labels + .iter() + .enumerate() + .filter(|(_, (_, _, message))| !message.is_empty()) + .filter(move |(i, _)| trailing_label.map_or(true, |(j, _)| *i != j)) + .map(|(_, label)| label) +} diff --git a/third_party/rust/codespan-reporting/src/term/views.rs b/third_party/rust/codespan-reporting/src/term/views.rs new file mode 100644 index 0000000000..f09d9582a6 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term/views.rs @@ -0,0 +1,478 @@ +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(()) + } +} |