about summary refs log tree commit diff
path: root/compiler/rustc_errors/src/emitter.rs
diff options
context:
space:
mode:
authorEsteban Küber <esteban@kuber.com.ar>2024-06-17 15:14:07 +0000
committerLeón Orell Valerian Liehr <me@fmease.dev>2024-11-10 23:57:18 +0100
commit1d78004575bea3b958b199d50a8491ae3fd65679 (patch)
tree61e6fcb1b058f58c6e8bb1c4b39661deb7d65150 /compiler/rustc_errors/src/emitter.rs
parentf61306d47bc98af8bb9d15f1adf6086785590a8c (diff)
downloadrust-1d78004575bea3b958b199d50a8491ae3fd65679.tar.gz
rust-1d78004575bea3b958b199d50a8491ae3fd65679.zip
Add Unicode block-drawing compiler output support
Add nightly-only theming support to rustc output using Unicode box
drawing characters instead of ASCII-art to draw the terminal UI:

After:

```
error: foo
  ╭▸ test.rs:3:3
  │
3 │       X0 Y0 Z0
  │   ┌───╿──│──┘
  │  ┌│───│──┘
  │ ┏││━━━┙
  │ ┃││
4 │ ┃││   X1 Y1 Z1
5 │ ┃││   X2 Y2 Z2
  │ ┃│└────╿──│──┘ `Z` label
  │ ┃└─────│──┤
  │ ┗━━━━━━┥  `Y` is a good letter too
  │        `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │ ┏   X1 Y1 Z1
5 │ ┃   X2 Y2 Z2
6 │ ┃   X3 Y3 Z3
  │ ┗━━━━━━━━━━┛
  ├ note: bar
  ╰ note: baz
note: qux
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━
```

Before:

```
error: foo
 --> test.rs:3:3
  |
3 |       X0 Y0 Z0
  |    ___^__-__-
  |   |___|__|
  |  ||___|
  | |||
4 | |||   X1 Y1 Z1
5 | |||   X2 Y2 Z2
  | |||____^__-__- `Z` label
  | ||_____|__|
  | |______|  `Y` is a good letter too
  |        `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 | /   X1 Y1 Z1
5 | |   X2 Y2 Z2
6 | |   X3 Y3 Z3
  | |__________^
  = note: bar
  = note: baz
note: qux
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
```
Diffstat (limited to 'compiler/rustc_errors/src/emitter.rs')
-rw-r--r--compiler/rustc_errors/src/emitter.rs703
1 files changed, 583 insertions, 120 deletions
diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs
index 0ccc71ae06c..120f5ba7d48 100644
--- a/compiler/rustc_errors/src/emitter.rs
+++ b/compiler/rustc_errors/src/emitter.rs
@@ -44,6 +44,7 @@ const DEFAULT_COLUMN_WIDTH: usize = 140;
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum HumanReadableErrorType {
     Default,
+    Unicode,
     AnnotateSnippet,
     Short,
 }
@@ -595,6 +596,12 @@ impl ColorConfig {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum OutputTheme {
+    Ascii,
+    Unicode,
+}
+
 /// Handles the writing of `HumanReadableErrorType::Default` and `HumanReadableErrorType::Short`
 #[derive(Setters)]
 pub struct HumanEmitter {
@@ -613,6 +620,7 @@ pub struct HumanEmitter {
     macro_backtrace: bool,
     track_diagnostics: bool,
     terminal_url: TerminalUrl,
+    theme: OutputTheme,
 }
 
 #[derive(Debug)]
@@ -637,6 +645,7 @@ impl HumanEmitter {
             macro_backtrace: false,
             track_diagnostics: false,
             terminal_url: TerminalUrl::No,
+            theme: OutputTheme::Ascii,
         }
     }
 
@@ -680,17 +689,19 @@ impl HumanEmitter {
             })
             .collect();
         buffer.puts(line_offset, code_offset, &code, Style::Quotation);
+        let placeholder = self.margin();
         if margin.was_cut_left() {
             // We have stripped some code/whitespace from the beginning, make it clear.
-            buffer.puts(line_offset, code_offset, "...", Style::LineNumber);
+            buffer.puts(line_offset, code_offset, placeholder, Style::LineNumber);
         }
         if margin.was_cut_right(line_len) {
+            let padding: usize = placeholder.chars().map(|ch| char_width(ch)).sum();
             // We have stripped some code after the rightmost span end, make it clear we did so.
-            buffer.puts(line_offset, code_offset + taken - 3, "...", Style::LineNumber);
+            buffer.puts(line_offset, code_offset + taken - padding, placeholder, Style::LineNumber);
         }
         buffer.puts(line_offset, 0, &self.maybe_anonymized(line_index), Style::LineNumber);
 
-        draw_col_separator_no_space(buffer, line_offset, width_offset - 2);
+        self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2);
     }
 
     #[instrument(level = "trace", skip(self), ret)]
@@ -702,6 +713,7 @@ impl HumanEmitter {
         width_offset: usize,
         code_offset: usize,
         margin: Margin,
+        close_window: bool,
     ) -> Vec<(usize, Style)> {
         // Draw:
         //
@@ -767,13 +779,10 @@ impl HumanEmitter {
         for ann in &line.annotations {
             if let AnnotationType::MultilineStart(depth) = ann.annotation_type {
                 if source_string.chars().take(ann.start_col.display).all(|c| c.is_whitespace()) {
-                    let style = if ann.is_primary {
-                        Style::UnderlinePrimary
-                    } else {
-                        Style::UnderlineSecondary
-                    };
-                    annotations.push((depth, style));
-                    buffer_ops.push((line_offset, width_offset + depth - 1, '/', style));
+                    let uline = self.underline(ann.is_primary);
+                    let chr = uline.multiline_whole_line;
+                    annotations.push((depth, uline.style));
+                    buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
                 } else {
                     short_start = false;
                     break;
@@ -970,7 +979,7 @@ impl HumanEmitter {
             // 3 │       X0 Y0 Z0
             //   │ ┏━━━━━┛  │  │     < We are writing these lines
             //   │ ┃┌───────┘  │     < by reverting the "depth" of
-            //   │ ┃│┌─────────┘     < their multilne spans.
+            //   │ ┃│┌─────────┘     < their multiline spans.
             // 4 │ ┃││   X1 Y1 Z1
             // 5 │ ┃││   X2 Y2 Z2
             //   │ ┃│└────╿──│──┘ `Z` label
@@ -997,7 +1006,10 @@ impl HumanEmitter {
         // 4 |   }
         //   |
         for pos in 0..=line_len {
-            draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2);
+            self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2);
+        }
+        if close_window {
+            self.draw_col_separator_end(buffer, line_offset + line_len + 1, width_offset - 2);
         }
 
         // Write the horizontal lines for multiline annotations
@@ -1013,21 +1025,17 @@ impl HumanEmitter {
         // 4 |   }
         //   |  _
         for &(pos, annotation) in &annotations_position {
-            let style = if annotation.is_primary {
-                Style::UnderlinePrimary
-            } else {
-                Style::UnderlineSecondary
-            };
+            let underline = self.underline(annotation.is_primary);
             let pos = pos + 1;
             match annotation.annotation_type {
                 AnnotationType::MultilineStart(depth) | AnnotationType::MultilineEnd(depth) => {
-                    draw_range(
+                    self.draw_range(
                         buffer,
-                        '_',
+                        underline.multiline_horizontal,
                         line_offset + pos,
                         width_offset + depth,
                         (code_offset + annotation.start_col.display).saturating_sub(left),
-                        style,
+                        underline.style,
                     );
                 }
                 _ if self.teach => {
@@ -1035,7 +1043,7 @@ impl HumanEmitter {
                         line_offset,
                         (code_offset + annotation.start_col.display).saturating_sub(left),
                         (code_offset + annotation.end_col.display).saturating_sub(left),
-                        style,
+                        underline.style,
                         annotation.is_primary,
                     );
                 }
@@ -1055,33 +1063,78 @@ impl HumanEmitter {
         // 4 | | }
         //   | |_
         for &(pos, annotation) in &annotations_position {
-            let style = if annotation.is_primary {
-                Style::UnderlinePrimary
-            } else {
-                Style::UnderlineSecondary
-            };
+            let underline = self.underline(annotation.is_primary);
             let pos = pos + 1;
 
             if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
                 for p in line_offset + 1..=line_offset + pos {
+                    if let AnnotationType::MultilineLine(_) = annotation.annotation_type {
+                        buffer.putc(
+                            p,
+                            (code_offset + annotation.start_col.display).saturating_sub(left),
+                            underline.multiline_vertical,
+                            underline.style,
+                        );
+                    } else {
+                        buffer.putc(
+                            p,
+                            (code_offset + annotation.start_col.display).saturating_sub(left),
+                            underline.vertical_text_line,
+                            underline.style,
+                        );
+                    }
+                }
+                if let AnnotationType::MultilineStart(_) = annotation.annotation_type {
                     buffer.putc(
-                        p,
+                        line_offset + pos,
                         (code_offset + annotation.start_col.display).saturating_sub(left),
-                        '|',
-                        style,
+                        underline.bottom_right,
+                        underline.style,
+                    );
+                }
+                if let AnnotationType::MultilineEnd(_) = annotation.annotation_type
+                    && annotation.has_label()
+                {
+                    buffer.putc(
+                        line_offset + pos,
+                        (code_offset + annotation.start_col.display).saturating_sub(left),
+                        underline.multiline_bottom_right_with_text,
+                        underline.style,
                     );
                 }
             }
             match annotation.annotation_type {
                 AnnotationType::MultilineStart(depth) => {
+                    buffer.putc(
+                        line_offset + pos,
+                        width_offset + depth - 1,
+                        underline.top_left,
+                        underline.style,
+                    );
                     for p in line_offset + pos + 1..line_offset + line_len + 2 {
-                        buffer.putc(p, width_offset + depth - 1, '|', style);
+                        buffer.putc(
+                            p,
+                            width_offset + depth - 1,
+                            underline.multiline_vertical,
+                            underline.style,
+                        );
                     }
                 }
                 AnnotationType::MultilineEnd(depth) => {
-                    for p in line_offset..=line_offset + pos {
-                        buffer.putc(p, width_offset + depth - 1, '|', style);
+                    for p in line_offset..line_offset + pos {
+                        buffer.putc(
+                            p,
+                            width_offset + depth - 1,
+                            underline.multiline_vertical,
+                            underline.style,
+                        );
                     }
+                    buffer.putc(
+                        line_offset + pos,
+                        width_offset + depth - 1,
+                        underline.bottom_left,
+                        underline.style,
+                    );
                 }
                 _ => (),
             }
@@ -1102,7 +1155,11 @@ impl HumanEmitter {
             let style =
                 if annotation.is_primary { Style::LabelPrimary } else { Style::LabelSecondary };
             let (pos, col) = if pos == 0 {
-                (pos + 1, (annotation.end_col.display + 1).saturating_sub(left))
+                if annotation.end_col.display == 0 {
+                    (pos + 1, (annotation.end_col.display + 2).saturating_sub(left))
+                } else {
+                    (pos + 1, (annotation.end_col.display + 1).saturating_sub(left))
+                }
             } else {
                 (pos + 2, annotation.start_col.display.saturating_sub(left))
             };
@@ -1135,18 +1192,60 @@ impl HumanEmitter {
         // 3 |
         // 4 |   }
         //   |  _^  test
-        for &(_, annotation) in &annotations_position {
-            let (underline, style) = if annotation.is_primary {
-                ('^', Style::UnderlinePrimary)
-            } else {
-                ('-', Style::UnderlineSecondary)
-            };
+        for &(pos, annotation) in &annotations_position {
+            let uline = self.underline(annotation.is_primary);
             for p in annotation.start_col.display..annotation.end_col.display {
+                // The default span label underline.
                 buffer.putc(
                     line_offset + 1,
                     (code_offset + p).saturating_sub(left),
-                    underline,
-                    style,
+                    uline.underline,
+                    uline.style,
+                );
+            }
+
+            if pos == 0
+                && matches!(
+                    annotation.annotation_type,
+                    AnnotationType::MultilineStart(_) | AnnotationType::MultilineEnd(_)
+                )
+            {
+                // The beginning of a multiline span with its leftward moving line on the same line.
+                buffer.putc(
+                    line_offset + 1,
+                    (code_offset + annotation.start_col.display).saturating_sub(left),
+                    match annotation.annotation_type {
+                        AnnotationType::MultilineStart(_) => uline.top_right_flat,
+                        AnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
+                        _ => panic!("unexpected annotation type: {annotation:?}"),
+                    },
+                    uline.style,
+                );
+            } else if pos != 0
+                && matches!(
+                    annotation.annotation_type,
+                    AnnotationType::MultilineStart(_) | AnnotationType::MultilineEnd(_)
+                )
+            {
+                // The beginning of a multiline span with its leftward moving line on another line,
+                // so we start going down first.
+                buffer.putc(
+                    line_offset + 1,
+                    (code_offset + annotation.start_col.display).saturating_sub(left),
+                    match annotation.annotation_type {
+                        AnnotationType::MultilineStart(_) => uline.multiline_start_down,
+                        AnnotationType::MultilineEnd(_) => uline.multiline_end_up,
+                        _ => panic!("unexpected annotation type: {annotation:?}"),
+                    },
+                    uline.style,
+                );
+            } else if pos != 0 && annotation.has_label() {
+                // The beginning of a span label with an actual label, we'll point down.
+                buffer.putc(
+                    line_offset + 1,
+                    (code_offset + annotation.start_col.display).saturating_sub(left),
+                    uline.label_start,
+                    uline.style,
                 );
             }
         }
@@ -1217,7 +1316,7 @@ impl HumanEmitter {
         padding: usize,
         label: &str,
         override_style: Option<Style>,
-    ) {
+    ) -> usize {
         // The extra 5 ` ` is padding that's always needed to align to the `note: `:
         //
         //   error: message
@@ -1281,6 +1380,7 @@ impl HumanEmitter {
                 buffer.append(line_number, text, style_or_override(*style, override_style));
             }
         }
+        line_number
     }
 
     #[instrument(level = "trace", skip(self, args), ret)]
@@ -1294,6 +1394,7 @@ impl HumanEmitter {
         max_line_num_len: usize,
         is_secondary: bool,
         emitted_at: Option<&DiagLocation>,
+        is_cont: bool,
     ) -> io::Result<()> {
         let mut buffer = StyledBuffer::new();
 
@@ -1303,12 +1404,29 @@ impl HumanEmitter {
             for _ in 0..max_line_num_len {
                 buffer.prepend(0, " ", Style::NoStyle);
             }
-            draw_note_separator(&mut buffer, 0, max_line_num_len + 1);
+            self.draw_note_separator(&mut buffer, 0, max_line_num_len + 1, is_cont);
             if *level != Level::FailureNote {
                 buffer.append(0, level.to_str(), Style::MainHeaderMsg);
                 buffer.append(0, ": ", Style::NoStyle);
             }
-            self.msgs_to_buffer(&mut buffer, msgs, args, max_line_num_len, "note", None);
+            let printed_lines =
+                self.msgs_to_buffer(&mut buffer, msgs, args, max_line_num_len, "note", None);
+            if is_cont && matches!(self.theme, OutputTheme::Unicode) {
+                // There's another note after this one, associated to the subwindow above.
+                // We write additional vertical lines to join them:
+                //   ╭▸ test.rs:3:3
+                //   │
+                // 3 │   code
+                //   │   ━━━━
+                //   │
+                //   ├ note: foo
+                //   │       bar
+                //   ╰ note: foo
+                //           bar
+                for i in 1..=printed_lines {
+                    self.draw_col_separator_no_space(&mut buffer, i, max_line_num_len + 1);
+                }
+            }
         } else {
             let mut label_width = 0;
             // The failure note level itself does not provide any useful diagnostic information
@@ -1439,9 +1557,13 @@ impl HumanEmitter {
                                 Style::LineAndColumn,
                             );
                             if annotation_id == 0 {
-                                buffer.prepend(line_idx, "--> ", Style::LineNumber);
+                                buffer.prepend(line_idx, self.file_start(), Style::LineNumber);
                             } else {
-                                buffer.prepend(line_idx, "::: ", Style::LineNumber);
+                                buffer.prepend(
+                                    line_idx,
+                                    self.secondary_file_start(),
+                                    Style::LineNumber,
+                                );
                             }
                             for _ in 0..max_line_num_len {
                                 buffer.prepend(line_idx, " ", Style::NoStyle);
@@ -1454,12 +1576,14 @@ impl HumanEmitter {
                             } else {
                                 Style::LabelSecondary
                             };
-                            buffer.prepend(line_idx, " |", Style::LineNumber);
+                            let pipe = self.col_separator();
+                            buffer.prepend(line_idx, &format!(" {pipe}"), Style::LineNumber);
                             for _ in 0..max_line_num_len {
                                 buffer.prepend(line_idx, " ", Style::NoStyle);
                             }
                             line_idx += 1;
-                            buffer.append(line_idx, " = note: ", style);
+                            let chr = self.note_separator();
+                            buffer.append(line_idx, &format!(" {chr} note: "), style);
                             for _ in 0..max_line_num_len {
                                 buffer.prepend(line_idx, " ", Style::NoStyle);
                             }
@@ -1480,7 +1604,7 @@ impl HumanEmitter {
                     // remember where we are in the output buffer for easy reference
                     let buffer_msg_line_offset = buffer.num_lines();
 
-                    buffer.prepend(buffer_msg_line_offset, "--> ", Style::LineNumber);
+                    buffer.prepend(buffer_msg_line_offset, self.file_start(), Style::LineNumber);
                     buffer.append(
                         buffer_msg_line_offset,
                         &format!(
@@ -1510,15 +1634,28 @@ impl HumanEmitter {
                 // remember where we are in the output buffer for easy reference
                 let buffer_msg_line_offset = buffer.num_lines();
 
-                // Add spacing line
-                draw_col_separator_no_space(
+                // Add spacing line, as shown:
+                //   --> $DIR/file:54:15
+                //    |
+                // LL |         code
+                //    |         ^^^^
+                //    | (<- It prints *this* line)
+                //   ::: $DIR/other_file.rs:15:5
+                //    |
+                // LL |     code
+                //    |     ----
+                self.draw_col_separator_no_space(
                     &mut buffer,
                     buffer_msg_line_offset,
                     max_line_num_len + 1,
                 );
 
                 // Then, the secondary file indicator
-                buffer.prepend(buffer_msg_line_offset + 1, "::: ", Style::LineNumber);
+                buffer.prepend(
+                    buffer_msg_line_offset + 1,
+                    self.secondary_file_start(),
+                    Style::LineNumber,
+                );
                 let loc = if let Some(first_line) = annotated_file.lines.first() {
                     let col = if let Some(first_annotation) = first_line.annotations.first() {
                         format!(":{}", first_annotation.start_col.file + 1)
@@ -1543,7 +1680,7 @@ impl HumanEmitter {
             if !self.short_message {
                 // Put in the spacer between the location and annotated source
                 let buffer_msg_line_offset = buffer.num_lines();
-                draw_col_separator_no_space(
+                self.draw_col_separator_no_space(
                     &mut buffer,
                     buffer_msg_line_offset,
                     max_line_num_len + 1,
@@ -1651,6 +1788,7 @@ impl HumanEmitter {
                         width_offset,
                         code_offset,
                         margin,
+                        !is_cont && line_idx + 1 == annotated_file.lines.len(),
                     );
 
                     let mut to_add = FxHashMap::default();
@@ -1666,7 +1804,13 @@ impl HumanEmitter {
                     // the code in this line.
                     for (depth, style) in &multilines {
                         for line in previous_buffer_line..buffer.num_lines() {
-                            draw_multiline_line(&mut buffer, line, width_offset, *depth, *style);
+                            self.draw_multiline_line(
+                                &mut buffer,
+                                line,
+                                width_offset,
+                                *depth,
+                                *style,
+                            );
                         }
                     }
                     // check to see if we need to print out or elide lines that come between
@@ -1676,11 +1820,15 @@ impl HumanEmitter {
                             - annotated_file.lines[line_idx].line_index;
                         if line_idx_delta > 2 {
                             let last_buffer_line_num = buffer.num_lines();
-                            buffer.puts(last_buffer_line_num, 0, "...", Style::LineNumber);
+                            self.draw_line_separator(
+                                &mut buffer,
+                                last_buffer_line_num,
+                                width_offset,
+                            );
 
                             // Set the multiline annotation vertical lines on `...` bridging line.
                             for (depth, style) in &multilines {
-                                draw_multiline_line(
+                                self.draw_multiline_line(
                                     &mut buffer,
                                     last_buffer_line_num,
                                     width_offset,
@@ -1695,7 +1843,7 @@ impl HumanEmitter {
                                         // In the case where we have elided the entire start of the
                                         // multispan because those lines were empty, we still need
                                         // to draw the `|`s across the `...`.
-                                        draw_multiline_line(
+                                        self.draw_multiline_line(
                                             &mut buffer,
                                             last_buffer_line_num,
                                             width_offset,
@@ -1728,7 +1876,7 @@ impl HumanEmitter {
                             );
 
                             for (depth, style) in &multilines {
-                                draw_multiline_line(
+                                self.draw_multiline_line(
                                     &mut buffer,
                                     last_buffer_line_num,
                                     width_offset,
@@ -1740,7 +1888,7 @@ impl HumanEmitter {
                                 for ann in &line.annotations {
                                     if let AnnotationType::MultilineStart(pos) = ann.annotation_type
                                     {
-                                        draw_multiline_line(
+                                        self.draw_multiline_line(
                                             &mut buffer,
                                             last_buffer_line_num,
                                             width_offset,
@@ -1824,13 +1972,24 @@ impl HumanEmitter {
         );
 
         let mut row_num = 2;
-        draw_col_separator_no_space(&mut buffer, 1, max_line_num_len + 1);
-        for (complete, parts, highlights, _) in suggestions.iter().take(MAX_SUGGESTIONS) {
+        for (i, (complete, parts, highlights, _)) in
+            suggestions.iter().enumerate().take(MAX_SUGGESTIONS)
+        {
             debug!(?complete, ?parts, ?highlights);
 
             let has_deletion = parts.iter().any(|p| p.is_deletion(sm));
             let is_multiline = complete.lines().count() > 1;
 
+            if i == 0 {
+                self.draw_col_separator_start(&mut buffer, row_num - 1, max_line_num_len + 1);
+            } else {
+                buffer.puts(
+                    row_num - 1,
+                    max_line_num_len + 1,
+                    self.multi_suggestion_separator(),
+                    Style::LineNumber,
+                );
+            }
             if let Some(span) = span.primary_span() {
                 // Compare the primary span of the diagnostic with the span of the suggestion
                 // being emitted. If they belong to the same file, we don't *need* to show the
@@ -1838,7 +1997,9 @@ impl HumanEmitter {
                 // telling users to make a change but not clarifying *where*.
                 let loc = sm.lookup_char_pos(parts[0].span.lo());
                 if loc.file.name != sm.span_to_filename(span) && loc.file.name.is_real() {
-                    let arrow = "--> ";
+                    // --> file.rs:line:col
+                    //  |
+                    let arrow = self.file_start();
                     buffer.puts(row_num - 1, 0, arrow, Style::LineNumber);
                     let filename = sm.filename_for_diagnostics(&loc.file.name);
                     let offset = sm.doctest_offset_line(&loc.file.name, loc.line);
@@ -1852,6 +2013,7 @@ impl HumanEmitter {
                     for _ in 0..max_line_num_len {
                         buffer.prepend(row_num - 1, " ", Style::NoStyle);
                     }
+                    self.draw_col_separator_no_space(&mut buffer, row_num, max_line_num_len + 1);
                     row_num += 1;
                 }
             }
@@ -1882,7 +2044,6 @@ impl HumanEmitter {
             assert!(!file_lines.lines.is_empty() || parts[0].span.is_dummy());
 
             let line_start = sm.lookup_char_pos(parts[0].span.lo()).line;
-            draw_col_separator_no_space(&mut buffer, row_num - 1, max_line_num_len + 1);
             let mut lines = complete.lines();
             if lines.clone().next().is_none() {
                 // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
@@ -1972,7 +2133,14 @@ impl HumanEmitter {
                             )
                         }
 
-                        buffer.puts(row_num, 0, "...", Style::LineNumber);
+                        let placeholder = self.margin();
+                        let padding: usize = placeholder.chars().map(|ch| char_width(ch)).sum();
+                        buffer.puts(
+                            row_num,
+                            max_line_num_len.saturating_sub(padding),
+                            placeholder,
+                            Style::LineNumber,
+                        );
                         row_num += 1;
 
                         if let Some((p, l)) = last_line {
@@ -2040,7 +2208,6 @@ impl HumanEmitter {
             if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
                 show_code_change
             {
-                draw_col_separator_no_space(&mut buffer, row_num, max_line_num_len + 1);
                 for part in parts {
                     let span_start_pos = sm.lookup_char_pos(part.span.lo()).col_display;
                     let span_end_pos = sm.lookup_char_pos(part.span.hi()).col_display;
@@ -2082,7 +2249,7 @@ impl HumanEmitter {
                             buffer.putc(
                                 row_num,
                                 (padding as isize + p) as usize,
-                                if part.is_addition(sm) { '+' } else { '~' },
+                                if part.is_addition(sm) { '+' } else { self.diff() },
                                 Style::Addition,
                             );
                         }
@@ -2114,10 +2281,23 @@ impl HumanEmitter {
 
             // if we elided some lines, add an ellipsis
             if lines.next().is_some() {
-                buffer.puts(row_num, max_line_num_len - 1, "...", Style::LineNumber);
-            } else if let DisplaySuggestion::None = show_code_change {
-                draw_col_separator_no_space(&mut buffer, row_num, max_line_num_len + 1);
-                row_num += 1;
+                let placeholder = self.margin();
+                let padding: usize = placeholder.chars().map(|ch| char_width(ch)).sum();
+                buffer.puts(
+                    row_num,
+                    max_line_num_len.saturating_sub(padding),
+                    placeholder,
+                    Style::LineNumber,
+                );
+            } else {
+                let row = match show_code_change {
+                    DisplaySuggestion::Diff
+                    | DisplaySuggestion::Add
+                    | DisplaySuggestion::Underline => row_num - 1,
+                    DisplaySuggestion::None => row_num,
+                };
+                self.draw_col_separator_end(&mut buffer, row, max_line_num_len + 1);
+                row_num = row + 1;
             }
         }
         if suggestions.len() > MAX_SUGGESTIONS {
@@ -2125,6 +2305,7 @@ impl HumanEmitter {
             let msg = format!("and {} other candidate{}", others, pluralize!(others));
             buffer.puts(row_num, max_line_num_len + 3, &msg, Style::NoStyle);
         }
+
         emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message)?;
         Ok(())
     }
@@ -2157,6 +2338,8 @@ impl HumanEmitter {
             max_line_num_len,
             false,
             emitted_at,
+            !children.is_empty()
+                || suggestions.iter().any(|s| s.style != SuggestionStyle::CompletelyHidden),
         ) {
             Ok(()) => {
                 if !children.is_empty()
@@ -2164,7 +2347,15 @@ impl HumanEmitter {
                 {
                     let mut buffer = StyledBuffer::new();
                     if !self.short_message {
-                        draw_col_separator_no_space(&mut buffer, 0, max_line_num_len + 1);
+                        if let Some(child) = children.iter().next()
+                            && child.span.primary_spans().is_empty()
+                        {
+                            // We'll continue the vertical bar to point into the next note.
+                            self.draw_col_separator_no_space(&mut buffer, 0, max_line_num_len + 1);
+                        } else {
+                            // We'll close the vertical bar to visually end the code window.
+                            self.draw_col_separator_end(&mut buffer, 0, max_line_num_len + 1);
+                        }
                     }
                     if let Err(e) = emit_to_destination(
                         &buffer.render(),
@@ -2176,9 +2367,14 @@ impl HumanEmitter {
                     }
                 }
                 if !self.short_message {
-                    for child in children {
+                    for (i, child) in children.iter().enumerate() {
                         assert!(child.level.can_be_subdiag());
                         let span = &child.span;
+                        // FIXME: audit that this behaves correctly with suggestions.
+                        let should_close = match children.get(i + 1) {
+                            Some(c) => !c.span.primary_spans().is_empty(),
+                            None => i + 1 == children.len(),
+                        };
                         if let Err(err) = self.emit_messages_default_inner(
                             span,
                             &child.messages,
@@ -2188,11 +2384,12 @@ impl HumanEmitter {
                             max_line_num_len,
                             true,
                             None,
+                            !should_close,
                         ) {
                             panic!("failed to emit error: {err}");
                         }
                     }
-                    for sugg in suggestions {
+                    for (i, sugg) in suggestions.iter().enumerate() {
                         match sugg.style {
                             SuggestionStyle::CompletelyHidden => {
                                 // do not display this suggestion, it is meant only for tools
@@ -2207,6 +2404,9 @@ impl HumanEmitter {
                                     max_line_num_len,
                                     true,
                                     None,
+                                    // FIXME: this needs to account for the suggestion type,
+                                    //        some don't take any space.
+                                    i + 1 != suggestions.len(),
                                 ) {
                                     panic!("failed to emit error: {e}");
                                 }
@@ -2323,10 +2523,17 @@ impl HumanEmitter {
                     buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition);
                 }
                 [] => {
-                    draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1);
+                    // FIXME: needed? Doesn't get excercised in any test.
+                    self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1);
                 }
                 _ => {
-                    buffer.puts(*row_num, max_line_num_len + 1, "~ ", Style::Addition);
+                    let diff = self.diff();
+                    buffer.puts(
+                        *row_num,
+                        max_line_num_len + 1,
+                        &format!("{diff} "),
+                        Style::Addition,
+                    );
                 }
             }
             //   LL | line_to_add
@@ -2346,7 +2553,7 @@ impl HumanEmitter {
             buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle);
         } else {
             buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber);
-            draw_col_separator(buffer, *row_num, max_line_num_len + 1);
+            self.draw_col_separator(buffer, *row_num, max_line_num_len + 1);
             buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle);
         }
 
@@ -2374,6 +2581,306 @@ impl HumanEmitter {
         }
         *row_num += 1;
     }
+
+    fn underline(&self, is_primary: bool) -> UnderlineParts {
+        //               X0 Y0
+        // label_start > ┯━━━━ < underline
+        //               │ < vertical_text_line
+        //               text
+
+        //    multiline_start_down ⤷ X0 Y0
+        //            top_left > ┌───╿──┘ < top_right_flat
+        //           top_left > ┏│━━━┙ < top_right
+        // multiline_vertical > ┃│
+        //                      ┃│   X1 Y1
+        //                      ┃│   X2 Y2
+        //                      ┃└────╿──┘ < multiline_end_same_line
+        //        bottom_left > ┗━━━━━┥ < bottom_right_with_text
+        //   multiline_horizontal ^   `X` is a good letter
+
+        // multiline_whole_line > ┏ X0 Y0
+        //                        ┃   X1 Y1
+        //                        ┗━━━━┛ < multiline_end_same_line
+
+        // multiline_whole_line > ┏ X0 Y0
+        //                        ┃ X1 Y1
+        //                        ┃  ╿ < multiline_end_up
+        //                        ┗━━┛ < bottom_right
+
+        match (self.theme, is_primary) {
+            (OutputTheme::Ascii, true) => UnderlineParts {
+                style: Style::UnderlinePrimary,
+                underline: '^',
+                label_start: '^',
+                vertical_text_line: '|',
+                multiline_vertical: '|',
+                multiline_horizontal: '_',
+                multiline_whole_line: '/',
+                multiline_start_down: '^',
+                bottom_right: '|',
+                top_left: ' ',
+                top_right_flat: '^',
+                bottom_left: '|',
+                multiline_end_up: '^',
+                multiline_end_same_line: '^',
+                multiline_bottom_right_with_text: '|',
+            },
+            (OutputTheme::Ascii, false) => UnderlineParts {
+                style: Style::UnderlineSecondary,
+                underline: '-',
+                label_start: '-',
+                vertical_text_line: '|',
+                multiline_vertical: '|',
+                multiline_horizontal: '_',
+                multiline_whole_line: '/',
+                multiline_start_down: '-',
+                bottom_right: '|',
+                top_left: ' ',
+                top_right_flat: '-',
+                bottom_left: '|',
+                multiline_end_up: '-',
+                multiline_end_same_line: '-',
+                multiline_bottom_right_with_text: '|',
+            },
+            (OutputTheme::Unicode, true) => UnderlineParts {
+                style: Style::UnderlinePrimary,
+                underline: '━',
+                label_start: '┯',
+                vertical_text_line: '│',
+                multiline_vertical: '┃',
+                multiline_horizontal: '━',
+                multiline_whole_line: '┏',
+                multiline_start_down: '╿',
+                bottom_right: '┙',
+                top_left: '┏',
+                top_right_flat: '┛',
+                bottom_left: '┗',
+                multiline_end_up: '╿',
+                multiline_end_same_line: '┛',
+                multiline_bottom_right_with_text: '┥',
+            },
+            (OutputTheme::Unicode, false) => UnderlineParts {
+                style: Style::UnderlineSecondary,
+                underline: '─',
+                label_start: '┬',
+                vertical_text_line: '│',
+                multiline_vertical: '│',
+                multiline_horizontal: '─',
+                multiline_whole_line: '┌',
+                multiline_start_down: '│',
+                bottom_right: '┘',
+                top_left: '┌',
+                top_right_flat: '┘',
+                bottom_left: '└',
+                multiline_end_up: '│',
+                multiline_end_same_line: '┘',
+                multiline_bottom_right_with_text: '┤',
+            },
+        }
+    }
+
+    fn col_separator(&self) -> char {
+        match self.theme {
+            OutputTheme::Ascii => '|',
+            OutputTheme::Unicode => '│',
+        }
+    }
+
+    fn note_separator(&self) -> char {
+        match self.theme {
+            OutputTheme::Ascii => '=',
+            OutputTheme::Unicode => '╰',
+        }
+    }
+
+    fn multi_suggestion_separator(&self) -> &'static str {
+        match self.theme {
+            OutputTheme::Ascii => "|",
+            OutputTheme::Unicode => "├╴",
+        }
+    }
+
+    fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
+        let chr = self.col_separator();
+        buffer.puts(line, col, &format!("{chr} "), Style::LineNumber);
+    }
+
+    fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
+        let chr = self.col_separator();
+        self.draw_col_separator_no_space_with_style(buffer, chr, line, col, Style::LineNumber);
+    }
+
+    fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
+        match self.theme {
+            OutputTheme::Ascii => {
+                self.draw_col_separator_no_space_with_style(
+                    buffer,
+                    '|',
+                    line,
+                    col,
+                    Style::LineNumber,
+                );
+            }
+            OutputTheme::Unicode => {
+                self.draw_col_separator_no_space_with_style(
+                    buffer,
+                    '╭',
+                    line,
+                    col,
+                    Style::LineNumber,
+                );
+                self.draw_col_separator_no_space_with_style(
+                    buffer,
+                    '╴',
+                    line,
+                    col + 1,
+                    Style::LineNumber,
+                );
+            }
+        }
+    }
+
+    fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
+        match self.theme {
+            OutputTheme::Ascii => {
+                self.draw_col_separator_no_space_with_style(
+                    buffer,
+                    '|',
+                    line,
+                    col,
+                    Style::LineNumber,
+                );
+            }
+            OutputTheme::Unicode => {
+                self.draw_col_separator_no_space_with_style(
+                    buffer,
+                    '╰',
+                    line,
+                    col,
+                    Style::LineNumber,
+                );
+                self.draw_col_separator_no_space_with_style(
+                    buffer,
+                    '╴',
+                    line,
+                    col + 1,
+                    Style::LineNumber,
+                );
+            }
+        }
+    }
+
+    fn draw_col_separator_no_space_with_style(
+        &self,
+        buffer: &mut StyledBuffer,
+        chr: char,
+        line: usize,
+        col: usize,
+        style: Style,
+    ) {
+        buffer.putc(line, col, chr, style);
+    }
+
+    fn draw_range(
+        &self,
+        buffer: &mut StyledBuffer,
+        symbol: char,
+        line: usize,
+        col_from: usize,
+        col_to: usize,
+        style: Style,
+    ) {
+        for col in col_from..col_to {
+            buffer.putc(line, col, symbol, style);
+        }
+    }
+
+    fn draw_note_separator(
+        &self,
+        buffer: &mut StyledBuffer,
+        line: usize,
+        col: usize,
+        is_cont: bool,
+    ) {
+        let chr = match self.theme {
+            OutputTheme::Ascii => "= ",
+            OutputTheme::Unicode if is_cont => "├ ",
+            OutputTheme::Unicode => "╰ ",
+        };
+        buffer.puts(line, col, chr, Style::LineNumber);
+    }
+
+    fn draw_multiline_line(
+        &self,
+        buffer: &mut StyledBuffer,
+        line: usize,
+        offset: usize,
+        depth: usize,
+        style: Style,
+    ) {
+        let chr = match (style, self.theme) {
+            (Style::UnderlinePrimary | Style::LabelPrimary, OutputTheme::Ascii) => '|',
+            (_, OutputTheme::Ascii) => '|',
+            (Style::UnderlinePrimary | Style::LabelPrimary, OutputTheme::Unicode) => '┃',
+            (_, OutputTheme::Unicode) => '│',
+        };
+        buffer.putc(line, offset + depth - 1, chr, style);
+    }
+
+    fn file_start(&self) -> &'static str {
+        match self.theme {
+            OutputTheme::Ascii => "--> ",
+            OutputTheme::Unicode => " ╭▸ ",
+        }
+    }
+
+    fn secondary_file_start(&self) -> &'static str {
+        match self.theme {
+            OutputTheme::Ascii => "::: ",
+            OutputTheme::Unicode => " ⸬ ",
+        }
+    }
+
+    fn diff(&self) -> char {
+        match self.theme {
+            OutputTheme::Ascii => '~',
+            OutputTheme::Unicode => '±',
+        }
+    }
+
+    fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
+        let (column, dots) = match self.theme {
+            OutputTheme::Ascii => (0, "..."),
+            OutputTheme::Unicode => (col - 2, "‡"),
+        };
+        buffer.puts(line, column, dots, Style::LineNumber);
+    }
+
+    fn margin(&self) -> &'static str {
+        match self.theme {
+            OutputTheme::Ascii => "...",
+            OutputTheme::Unicode => "…",
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct UnderlineParts {
+    style: Style,
+    underline: char,
+    label_start: char,
+    vertical_text_line: char,
+    multiline_vertical: char,
+    multiline_horizontal: char,
+    multiline_whole_line: char,
+    multiline_start_down: char,
+    bottom_right: char,
+    top_left: char,
+    top_right_flat: char,
+    bottom_left: char,
+    multiline_end_up: char,
+    multiline_end_same_line: char,
+    multiline_bottom_right_with_text: char,
 }
 
 #[derive(Clone, Copy, Debug)]
@@ -2661,50 +3168,6 @@ fn normalize_whitespace(s: &str) -> String {
     })
 }
 
-fn draw_col_separator(buffer: &mut StyledBuffer, line: usize, col: usize) {
-    buffer.puts(line, col, "| ", Style::LineNumber);
-}
-
-fn draw_col_separator_no_space(buffer: &mut StyledBuffer, line: usize, col: usize) {
-    draw_col_separator_no_space_with_style(buffer, line, col, Style::LineNumber);
-}
-
-fn draw_col_separator_no_space_with_style(
-    buffer: &mut StyledBuffer,
-    line: usize,
-    col: usize,
-    style: Style,
-) {
-    buffer.putc(line, col, '|', style);
-}
-
-fn draw_range(
-    buffer: &mut StyledBuffer,
-    symbol: char,
-    line: usize,
-    col_from: usize,
-    col_to: usize,
-    style: Style,
-) {
-    for col in col_from..col_to {
-        buffer.putc(line, col, symbol, style);
-    }
-}
-
-fn draw_note_separator(buffer: &mut StyledBuffer, line: usize, col: usize) {
-    buffer.puts(line, col, "= ", Style::LineNumber);
-}
-
-fn draw_multiline_line(
-    buffer: &mut StyledBuffer,
-    line: usize,
-    offset: usize,
-    depth: usize,
-    style: Style,
-) {
-    buffer.putc(line, offset + depth - 1, '|', style);
-}
-
 fn num_overlap(
     a_start: usize,
     a_end: usize,