summaryrefslogtreecommitdiffstats
path: root/third_party/rust/codespan-reporting/src/term/renderer.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/codespan-reporting/src/term/renderer.rs
parentInitial commit. (diff)
downloadfirefox-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.rs1020
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)
+}