Repository: brendanzab/codespan (rust)

Issue:
Make single-line label rendering clearer
Instead of the following (see #203):
```
   ┌── one_line.rs:3:5 ───
   │
 3 │     v.push(v.pop().unwrap());
   │     - first borrow later used by call
   │       ---- first mutable borrow occurs here
   │            ^ second mutable borrow occurs here
   │
```
...we should [render something like rustc does](https://github.com/rust-lang/rust/blob/c20d7eecbc0928b57da8fe30b2ef8528e2bdd5be/src/test/ui/codemap_tests/one_line.stderr):
```
   ┌── one_line.rs:3:5 ───
   │
 3 │     v.push(v.pop().unwrap());
   │     - ---- ^ second mutable borrow occurs here
   │     │ │
   │     │ first mutable borrow occurs here
   │     first borrow later used by call
   │
```

I'm not sure how this would interact with multiline labels though…

Pull request:
Improve single line label rendering
This makes the rendering of carets smarter when there are multiple labels on the same line. Instead of this:

```
   ┌─ one_line.rs:3:5
   │
 3 │     v.push(v.pop().unwrap());
   │     - first borrow later used by call
   │       ---- first mutable borrow occurs here
   │            ^ second mutable borrow occurs here
   │
```
...we now render the following:
```
   ┌─ one_line.rs:3:5
   │
 3 │     v.push(v.pop().unwrap());
   │     - ---- ^ second mutable borrow occurs here
   │     │ │
   │     │ first mutable borrow occurs here
   │     first borrow later used by call
   │
```

Resolves #211.

Patch:
diff --git a/codespan-reporting/src/term/config.rs b/codespan-reporting/src/term/config.rs
index a3bd03a..8c68ac7 100644
--- a/codespan-reporting/src/term/config.rs
+++ b/codespan-reporting/src/term/config.rs
@@ -32,16 +32,14 @@ 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 {
+    /// Measure the unicode width of a character, taking into account the tab width.
+    pub fn width(&self, ch: char) -> usize {
         use unicode_width::UnicodeWidthChar;
 
-        s.chars()
-            .map(|ch| match ch {
-                '\t' => self.tab_width,
-                _ => ch.width().unwrap_or(0),
-            })
-            .sum()
+        match ch {
+            '\t' => self.tab_width,
+            _ => ch.width().unwrap_or(0),
+        }
     }
 
     /// Construct a source writer using the current config.
@@ -279,29 +277,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 +305,8 @@ impl Default for Chars {
             multi_bottom_left: '╰',
             multi_bottom: '─',
             multi_left: '│',
+
+            pointer_left: '│',
         }
     }
 }
diff --git a/codespan-reporting/src/term/renderer.rs b/codespan-reporting/src/term/renderer.rs
index 7ecd32b..fb4029b 100644
--- a/codespan-reporting/src/term/renderer.rs
+++ b/codespan-reporting/src/term/renderer.rs
@@ -84,8 +84,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
@@ -229,6 +231,10 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
         num_multi_labels: usize,
         multi_labels: &[(usize, 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
@@ -260,20 +266,223 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
             }
 
             // Write source
-            write!(self.config.source(self.writer), " {}", source.trim_end())?;
+            write!(self.config.source(self.writer), " {}", source)?;
             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;
+            for (byte_index, ch) in 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!
+                //   │             ^
+                // ```
+                //
+                // ' ' is used because its unicode width is equal to 1 according
+                // to `unicode_width::UnicodeWidthChar`.
+                .chain(std::iter::once((source.len(), ' ')))
+            {
+                // Find the current label style at this column
+                let column_range = byte_index..(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 byte_index < max_label_end => Some(' '),
+                    None => None,
+                };
+                if let Some(caret_ch) = caret_ch {
+                    // FIXME: improve rendering for carets that occurring within character boundaries
+                    for _ in 0..self.config.width(ch) {
+                        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
@@ -454,34 +663,39 @@ 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 (byte_index, ch) in char_indices {
+            let column_range = byte_index..(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 spaces = match label_style {
+                None => 0..self.config.width(ch),
+                Some(label_style) => {
+                    self.set_color(self.styles().label(severity, label_style))?;
+                    write!(self, "{}", self.chars().pointer_left)?;
+                    self.reset()?;
+                    1..self.config.width(ch)
+                }
+            };
+            // Only print padding if we are before the end of the last single line caret
+            if byte_index <= max_label_start {
+                for _ in spaces {
+                    write!(self, " ")?;
+                }
+            }
         }
-        self.reset()?;
-        write!(self, "\n")?;
+
         Ok(())
     }
 
@@ -511,36 +725,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,11 +772,22 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
         range: RangeTo<usize>,
     ) -> io::Result<()> {
         self.set_color(self.styles().label(severity, label_style))?;
-        let source = slice_at_char_boundaries(source, 0..range.end);
-        for _ in 0..(self.config.width(source) + 1) {
-            write!(self, "{}", self.chars().multi_top)?;
+
+        for (_, ch) in source
+            .char_indices()
+            .take_while(|(byte_index, _)| *byte_index < range.end + 1)
+        {
+            // FIXME: improve rendering for carets that occurring within character boundaries
+            for _ in 0..self.config.width(ch) {
+                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(())
@@ -612,11 +807,22 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
         message: &str,
     ) -> io::Result<()> {
         self.set_color(self.styles().label(severity, label_style))?;
-        let source = slice_at_char_boundaries(source, 0..range.end);
-        for _ in 0..self.config.width(source) {
-            write!(self, "{}", self.chars().multi_bottom)?;
+
+        for (_, ch) in source
+            .char_indices()
+            .take_while(|(byte_index, _)| *byte_index < range.end)
+        {
+            // FIXME: improve rendering for carets that occurring within character boundaries
+            for _ in 0..self.config.width(ch) {
+                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)?;
         }
@@ -633,8 +839,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(())
+            }
         }
     }
 
@@ -701,83 +915,31 @@ 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
+/// 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
 }
 
-/// 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;
-        }
+/// 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,
     }
-    0
 }
 
-/// 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]
-}
-
-#[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());
-        }
-    }
-
-    #[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/codespan-reporting/src/term/views.rs b/codespan-reporting/src/term/views.rs
index 695e9d3..8422ede 100644
--- a/codespan-reporting/src/term/views.rs
+++ b/codespan-reporting/src/term/views.rs
@@ -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
                 //
@@ -231,8 +238,6 @@ where
             }
         }
 
-        // TODO: Insert `None` spaces in `labeled_files`
-
         // Header and message
         //
         // ```text


Test patch:
diff --git a/codespan-reporting/tests/snapshots/term__empty_ranges__rich_color.snap b/codespan-reporting/tests/snapshots/term__empty_ranges__rich_color.snap
index 4608113..b467854 100644
--- a/codespan-reporting/tests/snapshots/term__empty_ranges__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__empty_ranges__rich_color.snap
@@ -6,18 +6,24 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}┌─{/} hello:1:7
   {fg:Blue}│{/}
 {fg:Blue}1{/} {fg:Blue}│{/} Hello world!
-  {fg:Blue}│{/}       {fg:Green}^ middle{/}
+  {fg:Blue}│{/}       {fg:Green}^{/} {fg:Green}middle{/}
 
 {fg:Green bold bright}note{bold bright}: end of line{/}
   {fg:Blue}┌─{/} hello:1:13
   {fg:Blue}│{/}
 {fg:Blue}1{/} {fg:Blue}│{/} Hello world!
-  {fg:Blue}│{/}             {fg:Green}^ end of line{/}
+  {fg:Blue}│{/}             {fg:Green}^{/} {fg:Green}end of line{/}
 
-{fg:Green bold bright}note{bold bright}: end of file{/}
+{fg:Green bold bright}note{bold bright}: end of line{/}
   {fg:Blue}┌─{/} hello:2:11
   {fg:Blue}│{/}
 {fg:Blue}2{/} {fg:Blue}│{/} Bye world!
-  {fg:Blue}│{/}           {fg:Green}^ end of file{/}
+  {fg:Blue}│{/}           {fg:Green}^{/} {fg:Green}end of line{/}
+
+{fg:Green bold bright}note{bold bright}: end of file{/}
+  {fg:Blue}┌─{/} hello:3:4
+  {fg:Blue}│{/}
+{fg:Blue}3{/} {fg:Blue}│{/}    
+  {fg:Blue}│{/}    {fg:Green}^{/} {fg:Green}end of file{/}
 
 
diff --git a/codespan-reporting/tests/snapshots/term__empty_ranges__rich_no_color.snap b/codespan-reporting/tests/snapshots/term__empty_ranges__rich_no_color.snap
index 84c8059..862605c 100644
--- a/codespan-reporting/tests/snapshots/term__empty_ranges__rich_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__empty_ranges__rich_no_color.snap
@@ -14,10 +14,16 @@ note: end of line
 1 │ Hello world!
   │             ^ end of line
 
-note: end of file
+note: end of line
   ┌─ hello:2:11
   │
 2 │ Bye world!
-  │           ^ end of file
+  │           ^ end of line
+
+note: end of file
+  ┌─ hello:3:4
+  │
+3 │    
+  │    ^ end of file
 
 
diff --git a/codespan-reporting/tests/snapshots/term__empty_ranges__short_color.snap b/codespan-reporting/tests/snapshots/term__empty_ranges__short_color.snap
index b3930e0..3713ee4 100644
--- a/codespan-reporting/tests/snapshots/term__empty_ranges__short_color.snap
+++ b/codespan-reporting/tests/snapshots/term__empty_ranges__short_color.snap
@@ -4,5 +4,6 @@ expression: TEST_DATA.emit_color(&config)
 ---
 hello:1:7: {fg:Green bold bright}note{bold bright}: middle{/}
 hello:1:13: {fg:Green bold bright}note{bold bright}: end of line{/}
-hello:2:11: {fg:Green bold bright}note{bold bright}: end of file{/}
+hello:2:11: {fg:Green bold bright}note{bold bright}: end of line{/}
+hello:3:4: {fg:Green bold bright}note{bold bright}: end of file{/}
 
diff --git a/codespan-reporting/tests/snapshots/term__empty_ranges__short_no_color.snap b/codespan-reporting/tests/snapshots/term__empty_ranges__short_no_color.snap
index 5e2edfc..635a830 100644
--- a/codespan-reporting/tests/snapshots/term__empty_ranges__short_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__empty_ranges__short_no_color.snap
@@ -4,5 +4,6 @@ expression: TEST_DATA.emit_no_color(&config)
 ---
 hello:1:7: note: middle
 hello:1:13: note: end of line
-hello:2:11: note: end of file
+hello:2:11: note: end of line
+hello:3:4: note: end of file
 
diff --git a/codespan-reporting/tests/snapshots/term__fizz_buzz__rich_color.snap b/codespan-reporting/tests/snapshots/term__fizz_buzz__rich_color.snap
index 63bfdb9..11f9f68 100644
--- a/codespan-reporting/tests/snapshots/term__fizz_buzz__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__fizz_buzz__rich_color.snap
@@ -6,14 +6,14 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}┌─{/} FizzBuzz.fun:3:15
   {fg:Blue}│{/}  
 {fg:Blue}3{/} {fg:Blue}│{/}   fizz₁ : Nat → String
-  {fg:Blue}│{/}                 {fg:Blue}------ expected type `String` found here{/}
+  {fg:Blue}│{/}                 {fg:Blue}------{/} {fg:Blue}expected type `String` found here{/}
 {fg:Blue}4{/} {fg:Blue}│{/}   fizz₁ num = case (mod num 5) (mod num 3) of
   {fg:Blue}│{/} {fg:Blue}╭{/}{fg:Blue}─────────────'{/}
 {fg:Blue}5{/} {fg:Blue}│{/} {fg:Blue}│{/}     0 0 => "FizzBuzz"
 {fg:Blue}6{/} {fg:Blue}│{/} {fg:Blue}│{/}     0 _ => "Fizz"
 {fg:Blue}7{/} {fg:Blue}│{/} {fg:Blue}│{/}     _ 0 => "Buzz"
 {fg:Blue}8{/} {fg:Blue}│{/} {fg:Blue}│{/}     _ _ => num
-  {fg:Blue}│{/} {fg:Blue}│{/}            {fg:Red}^^^ expected `String`, found `Nat`{/}
+  {fg:Blue}│{/} {fg:Blue}│{/}            {fg:Red}^^^{/} {fg:Red}expected `String`, found `Nat`{/}
   {fg:Blue}│{/} {fg:Blue}╰{/}{fg:Blue}──────────────' `case` clauses have incompatible types{/}
   {fg:Blue}│{/}  
   {fg:Blue}={/} expected type `String`
@@ -23,17 +23,17 @@ expression: TEST_DATA.emit_color(&config)
    {fg:Blue}┌─{/} FizzBuzz.fun:10:15
    {fg:Blue}│{/}  
 {fg:Blue}10{/} {fg:Blue}│{/}   fizz₂ : Nat → String
-   {fg:Blue}│{/}                 {fg:Blue}------ expected type `String` found here{/}
+   {fg:Blue}│{/}                 {fg:Blue}------{/} {fg:Blue}expected type `String` found here{/}
 {fg:Blue}11{/} {fg:Blue}│{/}   fizz₂ num =
 {fg:Blue}12{/} {fg:Blue}│{/} {fg:Blue}╭{/}     case (mod num 5) (mod num 3) of
 {fg:Blue}13{/} {fg:Blue}│{/} {fg:Blue}│{/}         0 0 => "FizzBuzz"
-   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}---------- this is found to be of type `String`{/}
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}----------{/} {fg:Blue}this is found to be of type `String`{/}
 {fg:Blue}14{/} {fg:Blue}│{/} {fg:Blue}│{/}         0 _ => "Fizz"
-   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}------ this is found to be of type `String`{/}
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}------{/} {fg:Blue}this is found to be of type `String`{/}
 {fg:Blue}15{/} {fg:Blue}│{/} {fg:Blue}│{/}         _ 0 => "Buzz"
-   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}------ this is found to be of type `String`{/}
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Blue}------{/} {fg:Blue}this is found to be of type `String`{/}
 {fg:Blue}16{/} {fg:Blue}│{/} {fg:Blue}│{/}         _ _ => num
-   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Red}^^^ expected `String`, found `Nat`{/}
+   {fg:Blue}│{/} {fg:Blue}│{/}                {fg:Red}^^^{/} {fg:Red}expected `String`, found `Nat`{/}
    {fg:Blue}│{/} {fg:Blue}╰{/}{fg:Blue}──────────────────' `case` clauses have incompatible types{/}
    {fg:Blue}│{/}  
    {fg:Blue}={/} expected type `String`
diff --git a/codespan-reporting/tests/snapshots/term__multifile__rich_color.snap b/codespan-reporting/tests/snapshots/term__multifile__rich_color.snap
index 5c3be31..53ede26 100644
--- a/codespan-reporting/tests/snapshots/term__multifile__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__multifile__rich_color.snap
@@ -6,7 +6,7 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}┌─{/} Data/Nat.fun:7:13
   {fg:Blue}│{/}
 {fg:Blue}7{/} {fg:Blue}│{/} {-# BUILTIN NATRAL Nat #-}
-  {fg:Blue}│{/}             {fg:Red}^^^^^^ unknown builtin{/}
+  {fg:Blue}│{/}             {fg:Red}^^^^^^{/} {fg:Red}unknown builtin{/}
   {fg:Blue}│{/}
   {fg:Blue}={/} there is a builtin with a similar name: `NATURAL`
 
@@ -14,7 +14,7 @@ expression: TEST_DATA.emit_color(&config)
    {fg:Blue}┌─{/} Data/Nat.fun:17:16
    {fg:Blue}│{/}
 {fg:Blue}17{/} {fg:Blue}│{/} zero    - succ n₂ = zero
-   {fg:Blue}│{/}                {fg:Yellow}^^ unused parameter{/}
+   {fg:Blue}│{/}                {fg:Yellow}^^{/} {fg:Yellow}unused parameter{/}
    {fg:Blue}│{/}
    {fg:Blue}={/} consider using a wildcard pattern: `_`
 
@@ -22,12 +22,12 @@ expression: TEST_DATA.emit_color(&config)
    {fg:Blue}┌─{/} Test.fun:4:11
    {fg:Blue}│{/}
 {fg:Blue} 4{/} {fg:Blue}│{/} _ = 123 + "hello"
-   {fg:Blue}│{/}           {fg:Red}^^^^^^^ expected `Nat`, found `String`{/}
+   {fg:Blue}│{/}           {fg:Red}^^^^^^^{/} {fg:Red}expected `Nat`, found `String`{/}
    {fg:Blue}│{/}
    {fg:Blue}┌─{/} Data/Nat.fun:11:1
    {fg:Blue}│{/}
 {fg:Blue}11{/} {fg:Blue}│{/} _+_ : Nat → Nat → Nat
-   {fg:Blue}│{/} {fg:Blue}--------------------- based on the definition of `_+_`{/}
+   {fg:Blue}│{/} {fg:Blue}---------------------{/} {fg:Blue}based on the definition of `_+_`{/}
    {fg:Blue}│{/}
    {fg:Blue}={/} expected type `Nat`
         found type `String`
diff --git a/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap b/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap
index 87f8bed..c927246 100644
--- a/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__multiline_overlapping__rich_color.snap
@@ -7,9 +7,9 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}│{/}    
 {fg:Blue}1{/} {fg:Blue}│{/}   {fg:Blue}╭{/}         match line_index.compare(self.last_line_index()) {
 {fg:Blue}2{/} {fg:Blue}│{/}   {fg:Blue}│{/}             Ordering::Less => Ok(self.line_starts()[line_index.to_usize()]),
-  {fg:Blue}│{/}   {fg:Blue}│{/}                               {fg:Blue}--------------------------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`{/}
+  {fg:Blue}│{/}   {fg:Blue}│{/}                               {fg:Blue}---------------------------------------------{/} {fg:Blue}this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`{/}
 {fg:Blue}3{/} {fg:Blue}│{/}   {fg:Blue}│{/}             Ordering::Equal => Ok(self.source_span().end()),
-  {fg:Blue}│{/}   {fg:Blue}│{/}                                {fg:Blue}---------------------------- this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`{/}
+  {fg:Blue}│{/}   {fg:Blue}│{/}                                {fg:Blue}----------------------------{/} {fg:Blue}this is found to be of type `Result<ByteIndex, LineIndexOutOfBoundsError>`{/}
 {fg:Blue}4{/} {fg:Blue}│{/}   {fg:Blue}│{/}             Ordering::Greater => LineIndexOutOfBoundsError {
   {fg:Blue}│{/} {fg:Red}╭{/}{fg:Red}─{/}{fg:Blue}│{/}{fg:Red}──────────────────────────────────^{/}
 {fg:Blue}5{/} {fg:Blue}│{/} {fg:Red}│{/} {fg:Blue}│{/}                 given: line_index,
diff --git a/codespan-reporting/tests/snapshots/term__overlapping__rich_color.snap b/codespan-reporting/tests/snapshots/term__overlapping__rich_color.snap
index b37e9bd..029a674 100644
--- a/codespan-reporting/tests/snapshots/term__overlapping__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__overlapping__rich_color.snap
@@ -6,29 +6,35 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}┌─{/} nested_impl_trait.rs:5:46
   {fg:Blue}│{/}
 {fg:Blue}5{/} {fg:Blue}│{/} fn bad_in_ret_position(x: impl Into<u32>) -> impl Into<impl Debug> { x }
-  {fg:Blue}│{/}                                              {fg:Blue}--------------------- outer `impl Trait`{/}
-  {fg:Blue}│{/}                                                        {fg:Red}^^^^^^^^^^ nested `impl Trait` here{/}
+  {fg:Blue}│{/}                                              {fg:Blue}----------{fg:Red}^^^^^^^^^^{fg:Blue}-{/}
+  {fg:Blue}│{/}                                              {fg:Blue}│{/}         {fg:Red}│{/}
+  {fg:Blue}│{/}                                              {fg:Blue}│{/}         {fg:Red}nested `impl Trait` here{/}
+  {fg:Blue}│{/}                                              {fg:Blue}outer `impl Trait`{/}
 
 {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
   {fg:Blue}┌─{/} typeck_type_placeholder_item.rs:1:18
   {fg:Blue}│{/}
 {fg:Blue}1{/} {fg:Blue}│{/} fn fn_test1() -> _ { 5 }
-  {fg:Blue}│{/}                  {fg:Red}^ not allowed in type signatures{/}
-  {fg:Blue}│{/}                  {fg:Blue}- help: replace with the correct return type: `i32`{/}
+  {fg:Blue}│{/}                  {fg:Red}^{/}
+  {fg:Blue}│{/}                  {fg:Red}│{/}
+  {fg:Blue}│{/}                  {fg:Red}not allowed in type signatures{/}
+  {fg:Blue}│{/}                  {fg:Blue}help: replace with the correct return type: `i32`{/}
 
 {fg:Red bold bright}error[E0121]{bold bright}: the type placeholder `_` is not allowed within types on item signatures{/}
   {fg:Blue}┌─{/} typeck_type_placeholder_item.rs:2:24
   {fg:Blue}│{/}
 {fg:Blue}2{/} {fg:Blue}│{/} fn fn_test2(x: i32) -> (_, _) { (x, x) }
-  {fg:Blue}│{/}                        {fg:Blue}------ help: replace with the correct return type: `(i32, i32)`{/}
-  {fg:Blue}│{/}                         {fg:Red}^ not allowed in type signatures{/}
-  {fg:Blue}│{/}                            {fg:Red}^ not allowed in type signatures{/}
+  {fg:Blue}│{/}                        {fg:Blue}-{fg:Red}^{fg:Blue}--{fg:Red}^{fg:Blue}-{/}
+  {fg:Blue}│{/}                        {fg:Blue}│{/}{fg:Red}│{/}  {fg:Red}│{/}
+  {fg:Blue}│{/}                        {fg:Blue}│{/}{fg:Red}│{/}  {fg:Red}not allowed in type signatures{/}
+  {fg:Blue}│{/}                        {fg:Blue}│{/}{fg:Red}not allowed in type signatures{/}
+  {fg:Blue}│{/}                        {fg:Blue}help: replace with the correct return type: `(i32, i32)`{/}
 
 {fg:Red bold bright}error[E0277]{bold bright}: `std::rc::Rc<()>` cannot be sent between threads safely{/}
    {fg:Blue}┌─{/} no_send_res_ports.rs:25:5
    {fg:Blue}│{/}  
 {fg:Blue}25{/} {fg:Blue}│{/}       thread::spawn(move|| {
-   {fg:Blue}│{/}       {fg:Red}^^^^^^^^^^^^^ `std::rc::Rc<()>` cannot be sent between threads safely{/}
+   {fg:Blue}│{/}       {fg:Red}^^^^^^^^^^^^^{/} {fg:Red}`std::rc::Rc<()>` cannot be sent between threads safely{/}
    {fg:Blue}│{/} {fg:Blue}╭{/}{fg:Blue}───────────────────'{/}
 {fg:Blue}26{/} {fg:Blue}│{/} {fg:Blue}│{/}         let y = x;
 {fg:Blue}27{/} {fg:Blue}│{/} {fg:Blue}│{/}         println!("{:?}", y);
@@ -38,7 +44,7 @@ expression: TEST_DATA.emit_color(&config)
    {fg:Blue}┌─{/} libstd/thread/mod.rs:5:8
    {fg:Blue}│{/}
 {fg:Blue} 5{/} {fg:Blue}│{/}     F: Send + 'static,
-   {fg:Blue}│{/}        {fg:Blue}---- required by this bound in `std::thread::spawn`{/}
+   {fg:Blue}│{/}        {fg:Blue}----{/} {fg:Blue}required by this bound in `std::thread::spawn`{/}
    {fg:Blue}│{/}
    {fg:Blue}={/} help: within `[closure@no_send_res_ports.rs:29:19: 33:6 x:main::Foo]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
    {fg:Blue}={/} note: required because it appears within the type `Port<()>`
diff --git a/codespan-reporting/tests/snapshots/term__overlapping__rich_no_color.snap b/codespan-reporting/tests/snapshots/term__overlapping__rich_no_color.snap
index 04b1788..e4a944d 100644
--- a/codespan-reporting/tests/snapshots/term__overlapping__rich_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__overlapping__rich_no_color.snap
@@ -6,23 +6,29 @@ error[E0666]: nested `impl Trait` is not allowed
   ┌─ nested_impl_trait.rs:5:46
   │
 5 │ fn bad_in_ret_position(x: impl Into<u32>) -> impl Into<impl Debug> { x }
-  │                                              --------------------- outer `impl Trait`
-  │                                                        ^^^^^^^^^^ nested `impl Trait` here
+  │                                              ----------^^^^^^^^^^-
+  │                                              │         │
+  │                                              │         nested `impl Trait` here
+  │                                              outer `impl Trait`
 
 error[E0121]: the type placeholder `_` is not allowed within types on item signatures
   ┌─ typeck_type_placeholder_item.rs:1:18
   │
 1 │ fn fn_test1() -> _ { 5 }
-  │                  ^ not allowed in type signatures
-  │                  - help: replace with the correct return type: `i32`
+  │                  ^
+  │                  │
+  │                  not allowed in type signatures
+  │                  help: replace with the correct return type: `i32`
 
 error[E0121]: the type placeholder `_` is not allowed within types on item signatures
   ┌─ typeck_type_placeholder_item.rs:2:24
   │
 2 │ fn fn_test2(x: i32) -> (_, _) { (x, x) }
-  │                        ------ help: replace with the correct return type: `(i32, i32)`
-  │                         ^ not allowed in type signatures
-  │                            ^ not allowed in type signatures
+  │                        -^--^-
+  │                        ││  │
+  │                        ││  not allowed in type signatures
+  │                        │not allowed in type signatures
+  │                        help: replace with the correct return type: `(i32, i32)`
 
 error[E0277]: `std::rc::Rc<()>` cannot be sent between threads safely
    ┌─ no_send_res_ports.rs:25:5
diff --git a/codespan-reporting/tests/snapshots/term__same_line__rich_color.snap b/codespan-reporting/tests/snapshots/term__same_line__rich_color.snap
index 82d2eb4..ad82cdf 100644
--- a/codespan-reporting/tests/snapshots/term__same_line__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__same_line__rich_color.snap
@@ -6,9 +6,10 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}┌─{/} one_line.rs:3:5
   {fg:Blue}│{/}
 {fg:Blue}3{/} {fg:Blue}│{/}     v.push(v.pop().unwrap());
-  {fg:Blue}│{/}     {fg:Blue}- first borrow later used by call{/}
-  {fg:Blue}│{/}       {fg:Blue}---- first mutable borrow occurs here{/}
-  {fg:Blue}│{/}            {fg:Red}^ second mutable borrow occurs here{/}
+  {fg:Blue}│{/}     {fg:Blue}-{/} {fg:Blue}----{/} {fg:Red}^{/} {fg:Red}second mutable borrow occurs here{/}
+  {fg:Blue}│{/}     {fg:Blue}│{/} {fg:Blue}│{/}     
+  {fg:Blue}│{/}     {fg:Blue}│{/} {fg:Blue}first mutable borrow occurs here{/}
+  {fg:Blue}│{/}     {fg:Blue}first borrow later used by call{/}
 
 {fg:Red bold bright}error{bold bright}: aborting due to previous error{/}
  {fg:Blue}={/} For more information about this error, try `rustc --explain E0499`.
diff --git a/codespan-reporting/tests/snapshots/term__same_line__rich_no_color.snap b/codespan-reporting/tests/snapshots/term__same_line__rich_no_color.snap
index c2d1c34..7342f3f 100644
--- a/codespan-reporting/tests/snapshots/term__same_line__rich_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__same_line__rich_no_color.snap
@@ -6,9 +6,10 @@ error[E0499]: cannot borrow `v` as mutable more than once at a time
   ┌─ one_line.rs:3:5
   │
 3 │     v.push(v.pop().unwrap());
-  │     - first borrow later used by call
-  │       ---- first mutable borrow occurs here
-  │            ^ second mutable borrow occurs here
+  │     - ---- ^ second mutable borrow occurs here
+  │     │ │     
+  │     │ first mutable borrow occurs here
+  │     first borrow later used by call
 
 error: aborting due to previous error
  = For more information about this error, try `rustc --explain E0499`.
diff --git a/codespan-reporting/tests/snapshots/term__same_ranges__rich_color.snap b/codespan-reporting/tests/snapshots/term__same_ranges__rich_color.snap
index cdc9ea8..49d3c86 100644
--- a/codespan-reporting/tests/snapshots/term__same_ranges__rich_color.snap
+++ b/codespan-reporting/tests/snapshots/term__same_ranges__rich_color.snap
@@ -6,7 +6,9 @@ expression: TEST_DATA.emit_color(&config)
   {fg:Blue}┌─{/} same_range:1:5
   {fg:Blue}│{/}
 {fg:Blue}1{/} {fg:Blue}│{/} ::S { }
-  {fg:Blue}│{/}     {fg:Red}^ Unexpected '{'{/}
-  {fg:Blue}│{/}     {fg:Blue}- Expected '('{/}
+  {fg:Blue}│{/}     {fg:Red}^{/}
+  {fg:Blue}│{/}     {fg:Red}│{/}
+  {fg:Blue}│{/}     {fg:Red}Unexpected '{'{/}
+  {fg:Blue}│{/}     {fg:Blue}Expected '('{/}
 
 
diff --git a/codespan-reporting/tests/snapshots/term__same_ranges__rich_no_color.snap b/codespan-reporting/tests/snapshots/term__same_ranges__rich_no_color.snap
index 4dd6071..09510e6 100644
--- a/codespan-reporting/tests/snapshots/term__same_ranges__rich_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__same_ranges__rich_no_color.snap
@@ -6,7 +6,9 @@ error: Unexpected token
   ┌─ same_range:1:5
   │
 1 │ ::S { }
-  │     ^ Unexpected '{'
-  │     - Expected '('
+  │     ^
+  │     │
+  │     Unexpected '{'
+  │     Expected '('
 
 
diff --git a/codespan-reporting/tests/snapshots/term__unicode_spans__rich_no_color.snap b/codespan-reporting/tests/snapshots/term__unicode_spans__rich_no_color.snap
index 371f1e0..653a60e 100644
--- a/codespan-reporting/tests/snapshots/term__unicode_spans__rich_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__unicode_spans__rich_no_color.snap
@@ -6,9 +6,24 @@ error[E01]: cow may not jump during new moon.
   ┌─ moon_jump.rs:1:1
   │
 1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
-  │   ^^ Invalid jump
-  │   -- Cow range does not start at boundary.
-  │   ------ Cow does not start or end at boundary.
+  │ ^^ Invalid jump
+
+note: invalid unicode range
+  ┌─ moon_jump.rs:1:1
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
+  │ -- Cow range does not start at boundary.
+
+note: invalid unicode range
+  ┌─ moon_jump.rs:1:3
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
   │     -- Cow range does not end at boundary.
 
+note: invalid unicode range
+  ┌─ moon_jump.rs:1:1
+  │
+1 │ 🐄🌑🐄🌒🐄🌓🐄🌔🐄🌕🐄🌖🐄🌗🐄🌘🐄
+  │ ------ Cow does not start or end at boundary.
+
 
diff --git a/codespan-reporting/tests/snapshots/term__unicode_spans__short_no_color.snap b/codespan-reporting/tests/snapshots/term__unicode_spans__short_no_color.snap
index 9085082..1b19451 100644
--- a/codespan-reporting/tests/snapshots/term__unicode_spans__short_no_color.snap
+++ b/codespan-reporting/tests/snapshots/term__unicode_spans__short_no_color.snap
@@ -3,4 +3,7 @@ source: codespan-reporting/tests/term.rs
 expression: TEST_DATA.emit_no_color(&config)
 ---
 moon_jump.rs:1:1: error[E01]: cow may not jump during new moon.
+note: invalid unicode range
+note: invalid unicode range
+note: invalid unicode range
 
diff --git a/codespan-reporting/tests/term.rs b/codespan-reporting/tests/term.rs
index 0714c56..600132a 100644
--- a/codespan-reporting/tests/term.rs
+++ b/codespan-reporting/tests/term.rs
@@ -330,7 +330,7 @@ mod empty_ranges {
 
     lazy_static::lazy_static! {
         static ref TEST_DATA: TestData<'static, SimpleFile<&'static str, &'static str>> = {
-            let file = SimpleFile::new("hello", "Hello world!\nBye world!");
+            let file = SimpleFile::new("hello", "Hello world!\nBye world!\n   ");
             let eof = file.source().len();
 
             let diagnostics = vec![
@@ -340,6 +340,9 @@ mod empty_ranges {
                 Diagnostic::note()
                     .with_message("end of line")
                     .with_labels(vec![Label::primary((), 12..12).with_message("end of line")]),
+                Diagnostic::note()
+                    .with_message("end of line")
+                    .with_labels(vec![Label::primary((), 23..23).with_message("end of line")]),
                 Diagnostic::note()
                     .with_message("end of file")
                     .with_labels(vec![Label::primary((), eof..eof).with_message("end of file")]),
@@ -741,14 +744,26 @@ mod unicode_spans {
                     .with_labels(vec![
                         Label::primary((), invalid_start..invalid_end)
                             .with_message("Invalid jump"),
+                    ]),
+                Diagnostic::note()
+                    .with_message("invalid unicode range")
+                    .with_labels(vec![
                         Label::secondary((), invalid_start.."🐄".len())
                             .with_message("Cow range does not start at boundary."),
+                    ]),
+                Diagnostic::note()
+                    .with_message("invalid unicode range")
+                    .with_labels(vec![
                         Label::secondary((), "🐄🌑".len().."🐄🌑🐄".len() - 1)
                             .with_message("Cow range does not end at boundary."),
+                    ]),
+                Diagnostic::note()
+                    .with_message("invalid unicode range")
+                    .with_labels(vec![
                         Label::secondary((), invalid_start.."🐄🌑🐄".len() - 1)
                             .with_message("Cow does not start or end at boundary."),
-
-                    ])];
+                    ]),
+            ];
             TestData{files: file, diagnostics }
         };
     }