diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/codespan-reporting/src/term/renderer.rs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/codespan-reporting/src/term/renderer.rs')
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/renderer.rs | 1020 |
1 files changed, 1020 insertions, 0 deletions
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) +} |