summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2020-07-10 20:23:05 -0700
committerHaibo Huang <hhb@google.com>2020-07-10 20:23:05 -0700
commit688e0e8c1f55a91198d38c63a6f0ba1b55954fc1 (patch)
treec712b15986657e2a7071818de121771eca64276a /src
parentfac330005a43361980f1904fcda543da655b4883 (diff)
downloadcodespan-reporting-688e0e8c1f55a91198d38c63a6f0ba1b55954fc1.tar.gz
Upgrade rust/crates/codespan-reporting to 0.9.5
Change-Id: I2523d159d28d7a57c8dc6b5f4578905f56bf1088
Diffstat (limited to 'src')
-rw-r--r--src/diagnostic.rs2
-rw-r--r--src/files.rs1
-rw-r--r--src/term/config.rs80
-rw-r--r--src/term/renderer.rs569
-rw-r--r--src/term/views.rs55
5 files changed, 438 insertions, 269 deletions
diff --git a/src/diagnostic.rs b/src/diagnostic.rs
index 6655a91..559c899 100644
--- a/src/diagnostic.rs
+++ b/src/diagnostic.rs
@@ -67,7 +67,7 @@ pub struct Label<FileId> {
pub style: LabelStyle,
/// The file that we are labelling.
pub file_id: FileId,
- /// The range we are going to include in the final snippet.
+ /// The range in bytes we are going to include in the final snippet.
pub range: Range<usize>,
/// An optional message to provide some additional information for the
/// underlined code. These should not include line breaks.
diff --git a/src/files.rs b/src/files.rs
index a717555..41691b9 100644
--- a/src/files.rs
+++ b/src/files.rs
@@ -191,6 +191,7 @@ pub fn column_index(source: &str, line_range: Range<usize>, byte_index: usize) -
///
/// assert_eq!(line_index(&line_starts, 5), Some(1));
/// ```
+// NOTE: this is copied in `codespan::file::line_starts` and should be kept in sync.
pub fn line_starts<'source>(source: &'source str) -> impl 'source + Iterator<Item = usize> {
std::iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1))
}
diff --git a/src/term/config.rs b/src/term/config.rs
index a3bd03a..7b834d7 100644
--- a/src/term/config.rs
+++ b/src/term/config.rs
@@ -1,4 +1,3 @@
-use std::io;
use termcolor::{Color, ColorSpec};
use crate::diagnostic::{LabelStyle, Severity};
@@ -31,58 +30,6 @@ impl Default for Config {
}
}
-impl Config {
- /// Measure the width of a string, taking into account the tab width.
- pub fn width(&self, s: &str) -> usize {
- use unicode_width::UnicodeWidthChar;
-
- s.chars()
- .map(|ch| match ch {
- '\t' => self.tab_width,
- _ => ch.width().unwrap_or(0),
- })
- .sum()
- }
-
- /// Construct a source writer using the current config.
- pub fn source<'a, W: ?Sized>(&self, writer: &'a mut W) -> SourceWriter<&'a mut W> {
- SourceWriter {
- upstream: writer,
- tab_width: self.tab_width,
- }
- }
-}
-
-/// Writer that replaces tab characters with the configured number of spaces.
-pub struct SourceWriter<W> {
- upstream: W,
- tab_width: usize,
-}
-
-impl<W: io::Write> io::Write for SourceWriter<W> {
- fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
- let mut last_term = 0usize;
- for (i, ch) in buf.iter().enumerate() {
- if *ch == b'\t' {
- self.upstream.write_all(&buf[last_term..i])?;
- last_term = i + 1;
- write!(
- self.upstream,
- "{space: >width$}",
- space = "",
- width = self.tab_width,
- )?;
- }
- }
- self.upstream.write_all(&buf[last_term..])?;
- Ok(buf.len())
- }
-
- fn flush(&mut self) -> io::Result<()> {
- self.upstream.flush()
- }
-}
-
/// The display style to use when rendering diagnostics.
#[derive(Clone, Debug)]
pub enum DisplayStyle {
@@ -279,29 +226,10 @@ pub struct Chars {
/// The character to use for the left of a multi-line label.
/// Defaults to: `'│'`.
pub multi_left: char,
-}
-impl Chars {
- pub fn single_caret_char(&self, label_style: LabelStyle) -> char {
- match label_style {
- LabelStyle::Primary => self.single_primary_caret,
- LabelStyle::Secondary => self.single_secondary_caret,
- }
- }
-
- pub fn multi_caret_char_start(&self, label_style: LabelStyle) -> char {
- match label_style {
- LabelStyle::Primary => self.multi_primary_caret_start,
- LabelStyle::Secondary => self.multi_secondary_caret_start,
- }
- }
-
- pub fn multi_caret_char_end(&self, label_style: LabelStyle) -> char {
- match label_style {
- LabelStyle::Primary => self.multi_primary_caret_end,
- LabelStyle::Secondary => self.multi_secondary_caret_end,
- }
- }
+ /// The character to use for the left of a pointer underneath a caret.
+ /// Defaults to: `'│'`.
+ pub pointer_left: char,
}
impl Default for Chars {
@@ -326,6 +254,8 @@ impl Default for Chars {
multi_bottom_left: '╰',
multi_bottom: '─',
multi_left: '│',
+
+ pointer_left: '│',
}
}
}
diff --git a/src/term/renderer.rs b/src/term/renderer.rs
index a8b8601..ab5656e 100644
--- a/src/term/renderer.rs
+++ b/src/term/renderer.rs
@@ -30,25 +30,25 @@ pub enum MultiLabel<'diagnostic> {
/// ```text
/// ╭
/// ```
- TopLeft(LabelStyle),
+ TopLeft,
/// Multi-line label top.
///
/// ```text
/// ╭────────────^
/// ```
- Top(LabelStyle, RangeTo<usize>),
+ Top(RangeTo<usize>),
/// Left vertical labels for multi-line labels.
///
/// ```text
/// │
/// ```
- Left(LabelStyle),
+ Left,
/// Multi-line label bottom, with an optional message.
///
/// ```text
/// ╰────────────^ blah blah
/// ```
- Bottom(LabelStyle, RangeTo<usize>, &'diagnostic str),
+ Bottom(RangeTo<usize>, &'diagnostic str),
}
#[derive(Copy, Clone)]
@@ -71,7 +71,6 @@ type Underline = (LabelStyle, VerticalBound);
/// │ │ │ │ │
/// ┌────────────────────────────────────────────────────────────────────────────
/// header ── │ error[0001]: oh noes, a cupcake has occurred!
-/// empty ── │
/// snippet start ── │ ┌─ test:9:0
/// snippet empty ── │ │
/// snippet line ── │ 9 │ ╭ Cupcake ipsum dolor. Sit amet marshmallow topping cheesecake
@@ -84,8 +83,10 @@ type Underline = (LabelStyle, VerticalBound);
/// 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
+/// │ │ │ ^^^^^^^^^^^^^^^^^^^ -------------------- 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
@@ -227,8 +228,12 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
severity: Severity,
single_labels: &[SingleLabel<'_>],
num_multi_labels: usize,
- multi_labels: &[(usize, MultiLabel<'_>)],
+ multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
) -> io::Result<()> {
+ // 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
@@ -243,37 +248,272 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
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)) if *label_index == label_column => {
+ Some((label_index, label_style, label)) if *label_index == label_column => {
match label {
- MultiLabel::TopLeft(label_style) => {
+ MultiLabel::TopLeft => {
self.label_multi_top_left(severity, *label_style)?;
}
MultiLabel::Top(..) => self.inner_gutter_space()?,
- MultiLabel::Left(label_style) | MultiLabel::Bottom(label_style, ..) => {
+ MultiLabel::Left | MultiLabel::Bottom(..) => {
self.label_multi_left(severity, *label_style, None)?;
}
}
multi_labels_iter.next();
}
- Some((_, _)) | None => self.inner_gutter_space()?,
+ Some((_, _, _)) | None => self.inner_gutter_space()?,
}
}
- // Write source
- write!(self.config.source(self.writer), " {}", source.trim_end())?;
+ // 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(range) => column_range.start >= range.end,
+ MultiLabel::TopLeft | MultiLabel::Left => true,
+ MultiLabel::Bottom(range, _) => column_range.end <= range.end,
+ }
+ });
+
+ // 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()?;
+ }
write!(self, "\n")?;
}
// Write single labels underneath source
//
// ```text
- // │ │ │ ^^^^ oh noes
+ // │ - ---- ^^^ second mutable borrow occurs here
+ // │ │ │
+ // │ │ first mutable borrow occurs here
+ // │ first borrow later used by call
+ // │ help: some help here
// ```
- for (label_style, range, message) in single_labels.iter() {
+ 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)?;
- self.label_single(severity, *label_style, source, range.clone(), message)?;
+ 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()?;
+ }
+ write!(self, "\n")?;
+
+ // 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(),
+ )?;
+ write!(self, "\n")?;
+
+ // 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()?;
+ write!(self, "\n")?;
+ }
+ }
}
// Write top or bottom label carets underneath source
@@ -282,11 +522,11 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
// │ ╰───│──────────────────^ woops
// │ ╭─│─────────^
// ```
- for (multi_label_index, (_, label)) in multi_labels.iter().enumerate() {
+ for (multi_label_index, (_, label_style, label)) in multi_labels.iter().enumerate() {
let (label_style, range, bottom_message) = match label {
- MultiLabel::TopLeft(_) | MultiLabel::Left(_) => continue, // no label caret needed
- MultiLabel::Top(ls, range) => (*ls, range, None),
- MultiLabel::Bottom(ls, range, message) => (*ls, range, Some(message)),
+ MultiLabel::TopLeft | MultiLabel::Left => continue, // no label caret needed
+ MultiLabel::Top(range) => (*label_style, range, None),
+ MultiLabel::Bottom(range, message) => (*label_style, range, Some(message)),
};
self.outer_gutter(outer_padding)?;
@@ -301,22 +541,22 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
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, label))) if *label_index == label_column => {
+ Some((i, (label_index, ls, label))) if *label_index == label_column => {
match label {
- MultiLabel::TopLeft(ls) | MultiLabel::Left(ls) => {
+ MultiLabel::TopLeft | MultiLabel::Left => {
self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
}
- MultiLabel::Top(ls, ..) if multi_label_index > *i => {
+ MultiLabel::Top(..) if multi_label_index > *i => {
self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
}
- MultiLabel::Bottom(ls, ..) if multi_label_index < *i => {
+ MultiLabel::Bottom(..) if multi_label_index < *i => {
self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
}
- MultiLabel::Top(ls, ..) if multi_label_index == *i => {
+ MultiLabel::Top(..) if multi_label_index == *i => {
underline = Some((*ls, VerticalBound::Top));
self.label_multi_top_left(severity, label_style)?
}
- MultiLabel::Bottom(ls, ..) if multi_label_index == *i => {
+ MultiLabel::Bottom(..) if multi_label_index == *i => {
underline = Some((*ls, VerticalBound::Bottom));
self.label_multi_bottom_left(severity, label_style)?;
}
@@ -353,7 +593,7 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
outer_padding: usize,
severity: Severity,
num_multi_labels: usize,
- multi_labels: &[(usize, MultiLabel<'_>)],
+ multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
) -> io::Result<()> {
self.outer_gutter(outer_padding)?;
self.border_left()?;
@@ -372,7 +612,7 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
outer_padding: usize,
severity: Severity,
num_multi_labels: usize,
- multi_labels: &[(usize, MultiLabel<'_>)],
+ multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
) -> io::Result<()> {
self.outer_gutter(outer_padding)?;
self.border_left_break()?;
@@ -406,12 +646,39 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
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) -> io::Result<()> {
write!(
self,
- "{origin}:{line_number}:{column_number}",
- origin = locus.name,
+ "{name}:{line_number}:{column_number}",
+ name = locus.name,
line_number = locus.location.line_number,
column_number = locus.location.column_number,
)
@@ -419,7 +686,7 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
/// The outer gutter of a source line.
fn outer_gutter(&mut self, outer_padding: usize) -> io::Result<()> {
- write!(self, "{space: >width$}", space = "", width = outer_padding,)?;
+ write!(self, "{space: >width$}", space = "", width = outer_padding)?;
write!(self, " ")?;
Ok(())
}
@@ -454,34 +721,37 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
Ok(())
}
- /// Single-line label with a message.
- ///
- /// ```text
- /// ^^ expected `Int` but found `String`
- /// ```
- fn label_single(
+ /// Write vertical lines pointing to carets.
+ fn caret_pointers(
&mut self,
severity: Severity,
- label_style: LabelStyle,
- source: &str,
- range: Range<usize>,
- message: &str,
+ max_label_start: usize,
+ single_labels: &[SingleLabel<'_>],
+ trailing_label: Option<(usize, &SingleLabel<'_>)>,
+ char_indices: impl Iterator<Item = (usize, char)>,
) -> io::Result<()> {
- let space_source = slice_at_char_boundaries(source, 0..range.start);
- let space_len = self.config.width(space_source);
- write!(self, " {space: >width$}", space = "", width = space_len)?;
- self.set_color(self.styles().label(severity, label_style))?;
- let source = slice_at_char_boundaries(source, range);
- // We use `usize::max` here to ensure that we print at least one
- // label character - even when we have a zero-length span.
- for _ in 0..usize::max(self.config.width(source), 1) {
- write!(self, "{}", self.chars().single_caret_char(label_style))?;
- }
- if !message.is_empty() {
- write!(self, " {}", message)?;
+ 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, " "))?;
+ }
}
- self.reset()?;
- write!(self, "\n")?;
+
Ok(())
}
@@ -511,36 +781,6 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
Ok(())
}
- /// The top of a multi-line label.
- fn label_multi_top_line(
- &mut self,
- severity: Severity,
- label_style: LabelStyle,
- len: usize,
- ) -> io::Result<()> {
- self.set_color(self.styles().label(severity, label_style))?;
- for _ in 0..len {
- write!(self, "{}", self.config.chars.multi_top)?;
- }
- self.reset()?;
- Ok(())
- }
-
- /// The top of a multi-line label.
- fn label_multi_bottom_line(
- &mut self,
- severity: Severity,
- label_style: LabelStyle,
- len: usize,
- ) -> io::Result<()> {
- self.set_color(self.styles().label(severity, label_style))?;
- for _ in 0..len {
- write!(self, "{}", self.config.chars.multi_bottom)?;
- }
- self.reset()?;
- Ok(())
- }
-
/// The top-left of a multi-line label.
///
/// ```text
@@ -588,10 +828,21 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
range: RangeTo<usize>,
) -> io::Result<()> {
self.set_color(self.styles().label(severity, label_style))?;
- for _ in 0..(self.config.width(&source[range.clone()]) + 1) {
- write!(self, "{}", self.chars().multi_top)?;
+
+ for (metrics, _) in self
+ .char_metrics(source.char_indices())
+ .take_while(|(metrics, _)| metrics.byte_index < range.end + 1)
+ {
+ // FIXME: improve rendering of carets between character boundaries
+ (0..metrics.unicode_width)
+ .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?;
}
- write!(self, "{}", self.chars().multi_caret_char_start(label_style))?;
+
+ 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()?;
write!(self, "\n")?;
Ok(())
@@ -611,10 +862,21 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
message: &str,
) -> io::Result<()> {
self.set_color(self.styles().label(severity, label_style))?;
- for _ in 0..self.config.width(&source[range.clone()]) {
- write!(self, "{}", self.chars().multi_bottom)?;
+
+ for (metrics, _) in self
+ .char_metrics(source.char_indices())
+ .take_while(|(metrics, _)| metrics.byte_index < range.end)
+ {
+ // FIXME: improve rendering of carets between character boundaries
+ (0..metrics.unicode_width)
+ .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?;
}
- write!(self, "{}", self.chars().multi_caret_char_end(label_style))?;
+
+ 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)?;
}
@@ -631,8 +893,16 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
) -> io::Result<()> {
match underline {
None => self.inner_gutter_space(),
- Some((ls, VerticalBound::Top)) => self.label_multi_top_line(severity, ls, 2),
- Some((ls, VerticalBound::Bottom)) => self.label_multi_bottom_line(severity, ls, 2),
+ 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(())
+ }
}
}
@@ -646,16 +916,14 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
&mut self,
severity: Severity,
num_multi_labels: usize,
- multi_labels: &[(usize, MultiLabel<'_>)],
+ multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
) -> io::Result<()> {
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)) if *label_index == label_column => match label {
- MultiLabel::TopLeft(label_style)
- | MultiLabel::Left(label_style)
- | MultiLabel::Bottom(label_style, ..) => {
- self.label_multi_left(severity, *label_style, None)?;
+ Some((label_index, ls, label)) if *label_index == label_column => match label {
+ MultiLabel::TopLeft | MultiLabel::Left | MultiLabel::Bottom(..) => {
+ self.label_multi_left(severity, *ls, None)?;
multi_labels_iter.next();
}
MultiLabel::Top(..) => {
@@ -663,7 +931,7 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
multi_labels_iter.next();
}
},
- Some((_, _)) | None => self.inner_gutter_space()?,
+ Some((_, _, _)) | None => self.inner_gutter_space()?,
}
}
@@ -699,83 +967,36 @@ impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
}
}
-/// Searches for character boundary from byte_index towards the end of the string.
-fn closest_char_boundary(s: &str, byte_index: usize) -> usize {
- let length = s.len();
- for index in byte_index..=length {
- if s.is_char_boundary(index) {
- return index;
- }
- }
- length
-}
-
-/// Searches for character boundary from byte_index towards the start of the string.
-fn closest_char_boundary_rev(s: &str, byte_index: usize) -> usize {
- for index in (0..=byte_index).rev() {
- if s.is_char_boundary(index) {
- return index;
- }
- }
- 0
+struct Metrics {
+ byte_index: usize,
+ unicode_width: usize,
}
-/// Finds a valid unicode boundaries looking from `range.start` towards the beginning of the string.
-/// From `range.end` towards the end of the string. Returning a `&str` of all characters
-/// that overlapping the range.
-fn slice_at_char_boundaries<'a>(s: &'a str, range: Range<usize>) -> &'a str {
- let start = closest_char_boundary_rev(s, range.start);
- let end = closest_char_boundary(s, range.end);
- &s[start..end]
+/// 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
}
-#[cfg(test)]
-mod test {
- use super::*;
- use std::iter::repeat;
-
- #[test]
- fn test_boundary() {
- let s = "🌞";
- assert_eq!(closest_char_boundary(s, 0), 0);
- assert_eq!(closest_char_boundary_rev(s, 0), 0);
- for i in 1..s.len() {
- assert_eq!(closest_char_boundary_rev(s, i), 0);
- assert_eq!(closest_char_boundary(s, i), s.len());
- }
+/// 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,
}
+}
- #[test]
- fn test_boundaries() {
- let s = "🌑🌒🌓🌔";
- let individually = ["🌑", "🌒", "🌓", "🌔"];
-
- let mut expect = Vec::new();
- // [(0, 0, "", ""),
- // (0, 4, "🌑", "🌑"), repeated 4 times,
- // (4, 4, "", ""), once
- // (4, 8, "🌒", "🌒"), repeated 4 times, and so on (+4, +4)
- // ...
- // (16, 16, "", ""); 21]
- expect.push((0, 0, "", ""));
- for (idx, &char) in individually.iter().enumerate() {
- let n = char.len();
- assert_eq!(n, 4);
- let expected_start = (idx % n) * n;
- let expected_end = (idx % n) * n + n;
- expect.extend(repeat((expected_start, expected_end, char, char)).take(n - 1));
- expect.push((expected_end, expected_end, "", ""));
- }
-
- // drop mut.
- let expect = expect;
- let mut found = Vec::new();
- for i in 0..=s.len() {
- let sliced = slice_at_char_boundaries(s, i..i);
- let prev = closest_char_boundary_rev(s, i);
- let next = closest_char_boundary(s, i);
- found.push((prev, next, &s[prev..next], sliced));
- }
- assert_eq!(found, expect);
- }
+/// 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/src/term/views.rs b/src/term/views.rs
index e6f6cc1..aa79914 100644
--- a/src/term/views.rs
+++ b/src/term/views.rs
@@ -68,7 +68,7 @@ where
range: std::ops::Range<usize>,
// TODO: How do we reuse these allocations?
single_labels: Vec<SingleLabel<'diagnostic>>,
- multi_labels: Vec<(usize, MultiLabel<'diagnostic>)>,
+ multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>,
}
// TODO: Make this data structure external, to allow for allocation reuse
@@ -143,14 +143,21 @@ where
// 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 as they
- // originally were specified in the diagnostic.
- Ok(index) => index + 1,
- Err(index) => index,
+ // 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,
};
+ // Ensure that we print at least one caret, even when we
+ // have a zero-length source range.
+ let mut label_range = label_start..label_end;
+ if label_range.len() == 0 {
+ label_range.end = label_range.start + 1;
+ }
+
line.single_labels
- .insert(index, (label.style, label_start..label_end, &label.message));
+ .insert(index, (label.style, label_range, &label.message));
} else {
// Multiple lines
//
@@ -182,7 +189,7 @@ where
// ```text
// 4 │ ╭ case (mod num 5) (mod num 3) of
// ```
- "" => (label_index, MultiLabel::TopLeft(label.style)),
+ "" => (label_index, label.style, MultiLabel::TopLeft),
// There's source code in the prefix, so run a label
// underneath it to get to the start of the range.
//
@@ -190,7 +197,7 @@ where
// 4 │ fizz₁ num = case (mod num 5) (mod num 3) of
// │ ╭─────────────^
// ```
- _ => (label_index, MultiLabel::Top(label.style, ..label_start)),
+ _ => (label_index, label.style, MultiLabel::Top(..label_start)),
});
// Marked lines
@@ -210,7 +217,7 @@ where
labeled_file
.get_or_insert_line(line_index, line_range, line_number)
.multi_labels
- .push((label_index, MultiLabel::Left(label.style)));
+ .push((label_index, label.style, MultiLabel::Left));
}
// Last labeled line
@@ -226,13 +233,12 @@ where
.multi_labels
.push((
label_index,
- MultiLabel::Bottom(label.style, ..label_end, &label.message),
+ label.style,
+ MultiLabel::Bottom(..label_end, &label.message),
));
}
}
- // TODO: Insert `None` spaces in `labeled_files`
-
// Header and message
//
// ```text
@@ -254,7 +260,8 @@ where
// │ ^^ expected `Int` but found `String`
// │
// ```
- for labeled_file in labeled_files {
+ 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).unwrap();
let source = source.as_ref();
@@ -330,12 +337,22 @@ where
}
}
}
- renderer.render_snippet_empty(
- outer_padding,
- self.diagnostic.severity,
- labeled_file.num_multi_labels,
- &current_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,
+ &current_labels,
+ )?;
+ }
}
// Additional notes