about summary refs log tree commit diff
path: root/compiler/rustc_parse/src/parser/tests.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_parse/src/parser/tests.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_parse/src/parser/tests.rs')
-rw-r--r--compiler/rustc_parse/src/parser/tests.rs1231
1 files changed, 1204 insertions, 27 deletions
diff --git a/compiler/rustc_parse/src/parser/tests.rs b/compiler/rustc_parse/src/parser/tests.rs
index 92684505ab0..decaecd2682 100644
--- a/compiler/rustc_parse/src/parser/tests.rs
+++ b/compiler/rustc_parse/src/parser/tests.rs
@@ -12,7 +12,7 @@ use rustc_ast::tokenstream::{DelimSpacing, DelimSpan, Spacing, TokenStream, Toke
 use rustc_ast::{self as ast, PatKind, visit};
 use rustc_ast_pretty::pprust::item_to_string;
 use rustc_data_structures::sync::Lrc;
-use rustc_errors::emitter::HumanEmitter;
+use rustc_errors::emitter::{HumanEmitter, OutputTheme};
 use rustc_errors::{DiagCtxt, MultiSpan, PResult};
 use rustc_session::parse::ParseSess;
 use rustc_span::source_map::{FilePathMapping, SourceMap};
@@ -36,16 +36,17 @@ fn string_to_parser(psess: &ParseSess, source_str: String) -> Parser<'_> {
     ))
 }
 
-fn create_test_handler() -> (DiagCtxt, Lrc<SourceMap>, Arc<Mutex<Vec<u8>>>) {
+fn create_test_handler(theme: OutputTheme) -> (DiagCtxt, Lrc<SourceMap>, Arc<Mutex<Vec<u8>>>) {
     let output = Arc::new(Mutex::new(Vec::new()));
     let source_map = Lrc::new(SourceMap::new(FilePathMapping::empty()));
     let fallback_bundle = rustc_errors::fallback_fluent_bundle(
         vec![crate::DEFAULT_LOCALE_RESOURCE, crate::DEFAULT_LOCALE_RESOURCE],
         false,
     );
-    let emitter = HumanEmitter::new(Box::new(Shared { data: output.clone() }), fallback_bundle)
+    let mut emitter = HumanEmitter::new(Box::new(Shared { data: output.clone() }), fallback_bundle)
         .sm(Some(source_map.clone()))
         .diagnostic_width(Some(140));
+    emitter = emitter.theme(theme);
     let dcx = DiagCtxt::new(Box::new(emitter));
     (dcx, source_map, output)
 }
@@ -69,7 +70,7 @@ fn with_expected_parse_error<T, F>(source_str: &str, expected_output: &str, f: F
 where
     F: for<'a> FnOnce(&mut Parser<'a>) -> PResult<'a, T>,
 {
-    let (handler, source_map, output) = create_test_handler();
+    let (handler, source_map, output) = create_test_handler(OutputTheme::Ascii);
     let psess = ParseSess::with_dcx(handler, source_map);
     let mut p = string_to_parser(&psess, source_str.to_string());
     let result = f(&mut p);
@@ -189,34 +190,55 @@ impl<T: Write> Write for Shared<T> {
 }
 
 #[allow(rustc::untranslatable_diagnostic)] // no translation needed for tests
-fn test_harness(file_text: &str, span_labels: Vec<SpanLabel>, expected_output: &str) {
+fn test_harness(
+    file_text: &str,
+    span_labels: Vec<SpanLabel>,
+    notes: Vec<(Option<(Position, Position)>, &'static str)>,
+    expected_output_ascii: &str,
+    expected_output_unicode: &str,
+) {
     create_default_session_globals_then(|| {
-        let (dcx, source_map, output) = create_test_handler();
-        source_map.new_source_file(Path::new("test.rs").to_owned().into(), file_text.to_owned());
-
-        let primary_span = make_span(&file_text, &span_labels[0].start, &span_labels[0].end);
-        let mut msp = MultiSpan::from_span(primary_span);
-        for span_label in span_labels {
-            let span = make_span(&file_text, &span_label.start, &span_label.end);
-            msp.push_span_label(span, span_label.label);
-            println!("span: {:?} label: {:?}", span, span_label.label);
-            println!("text: {:?}", source_map.span_to_snippet(span));
-        }
+        for (theme, expected_output) in [
+            (OutputTheme::Ascii, expected_output_ascii),
+            (OutputTheme::Unicode, expected_output_unicode),
+        ] {
+            let (dcx, source_map, output) = create_test_handler(theme);
+            source_map
+                .new_source_file(Path::new("test.rs").to_owned().into(), file_text.to_owned());
+
+            let primary_span = make_span(&file_text, &span_labels[0].start, &span_labels[0].end);
+            let mut msp = MultiSpan::from_span(primary_span);
+            for span_label in &span_labels {
+                let span = make_span(&file_text, &span_label.start, &span_label.end);
+                msp.push_span_label(span, span_label.label);
+                println!("span: {:?} label: {:?}", span, span_label.label);
+                println!("text: {:?}", source_map.span_to_snippet(span));
+            }
 
-        dcx.handle().span_err(msp, "foo");
+            let mut err = dcx.handle().struct_span_err(msp, "foo");
+            for (position, note) in &notes {
+                if let Some((start, end)) = position {
+                    let span = make_span(&file_text, &start, &end);
+                    err.span_note(span, *note);
+                } else {
+                    err.note(*note);
+                }
+            }
+            err.emit();
 
-        assert!(
-            expected_output.chars().next() == Some('\n'),
-            "expected output should begin with newline"
-        );
-        let expected_output = &expected_output[1..];
+            assert!(
+                expected_output.chars().next() == Some('\n'),
+                "expected output should begin with newline"
+            );
+            let expected_output = &expected_output[1..];
 
-        let bytes = output.lock().unwrap();
-        let actual_output = str::from_utf8(&bytes).unwrap();
-        println!("expected output:\n------\n{}------", expected_output);
-        println!("actual output:\n------\n{}------", actual_output);
+            let bytes = output.lock().unwrap();
+            let actual_output = str::from_utf8(&bytes).unwrap();
+            println!("expected output:\n------\n{}------", expected_output);
+            println!("actual output:\n------\n{}------", actual_output);
 
-        assert!(expected_output == actual_output)
+            assert!(expected_output == actual_output)
+        }
     })
 }
 
@@ -253,6 +275,7 @@ fn foo() {
             end: Position { string: "}", count: 1 },
             label: "test",
         }],
+        vec![],
         r#"
 error: foo
  --> test.rs:2:10
@@ -263,6 +286,16 @@ error: foo
   | |_^ test
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:2:10
+  │
+2 │   fn foo() {
+  │ ┏━━━━━━━━━━┛
+3 │ ┃ }
+  ╰╴┗━┛ test
+
+"#,
     );
 }
 
@@ -280,6 +313,7 @@ fn foo() {
             end: Position { string: "}", count: 1 },
             label: "test",
         }],
+        vec![],
         r#"
 error: foo
  --> test.rs:2:10
@@ -291,6 +325,17 @@ error: foo
   | |___^ test
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:2:10
+  │
+2 │   fn foo() {
+  │ ┏━━━━━━━━━━┛
+  ‡ ┃
+5 │ ┃   }
+  ╰╴┗━━━┛ test
+
+"#,
     );
 }
 #[test]
@@ -315,6 +360,7 @@ fn foo() {
                 label: "`Y` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -329,6 +375,20 @@ error: foo
   |       `X` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │      X0 Y0
+  │ ┏━━━━┛  │
+  │ ┃┌──────┘
+4 │ ┃│   X1 Y1
+5 │ ┃│   X2 Y2
+  │ ┃└────╿──┘ `Y` is a good letter too
+  │ ┗━━━━━┥
+  ╰╴      `X` is a good letter
+
+"#,
     );
 }
 
@@ -353,6 +413,7 @@ fn foo() {
                 label: "`Y` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -366,6 +427,72 @@ error: foo
   |       `Y` is a good letter too
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │      X0 Y0
+  │ ┏━━━━┛  │
+  │ ┃┌──────┘
+4 │ ┃│   Y1 X1
+  │ ┗│━━━━│━━┛ `X` is a good letter
+  │  └────┤
+  ╰╴      `Y` is a good letter too
+
+"#,
+    );
+}
+
+#[test]
+fn multiline_and_normal_overlap() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![
+            SpanLabel {
+                start: Position { string: "Y0", count: 1 },
+                end: Position { string: "X2", count: 1 },
+                label: "`X` is a good letter",
+            },
+            SpanLabel {
+                start: Position { string: "X0", count: 1 },
+                end: Position { string: "Y0", count: 1 },
+                label: "`Y` is a good letter too",
+            },
+        ],
+        vec![],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |     X0 Y0 Z0
+  |  ___---^-
+  | |   |
+  | |   `Y` is a good letter too
+4 | |   X1 Y1 Z1
+5 | |   X2 Y2 Z2
+  | |____^ `X` is a good letter
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │     X0 Y0 Z0
+  │ ┏━━━┬──┛─
+  │ ┃   │
+  │ ┃   `Y` is a good letter too
+4 │ ┃   X1 Y1 Z1
+5 │ ┃   X2 Y2 Z2
+  ╰╴┗━━━━┛ `X` is a good letter
+
+"#,
     );
 }
 
@@ -392,6 +519,7 @@ fn foo() {
                 label: "`Y` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:6
@@ -406,6 +534,789 @@ error: foo
   |  |____- `Y` is a good letter too
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │      X0 Y0 Z0
+  │ ┏━━━━━━━┛
+4 │ ┃    X1 Y1 Z1
+  │ ┃┌─────────┘
+5 │ ┃│   X2 Y2 Z2
+  │ ┗│━━━━┛ `X` is a good letter
+6 │  │   X3 Y3 Z3
+  ╰╴ └────┘ `Y` is a good letter too
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_1() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![(None, "bar")],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+  = note: bar
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  │
+  ╰ note: bar
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_2() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![(None, "bar"), (None, "qux")],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+  = note: bar
+  = note: qux
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  │
+  ├ note: bar
+  ╰ note: qux
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_3() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![(None, "bar"), (None, "baz"), (None, "qux")],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+  = note: bar
+  = note: baz
+  = note: qux
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  │
+  ├ note: bar
+  ├ note: baz
+  ╰ note: qux
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_1() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![(
+            Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+            "bar",
+        )],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_2() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "bar",
+            ),
+            (
+                Some((Position { string: "X2", count: 1 }, Position { string: "Y2", count: 1 })),
+                "qux",
+            ),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+note: qux
+ --> test.rs:5:3
+  |
+5 |   X2 Y2 Z2
+  |   ^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+note: qux
+  ╭▸ test.rs:5:3
+  │
+5 │   X2 Y2 Z2
+  ╰╴  ━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_3() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "bar",
+            ),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "baz",
+            ),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "qux",
+            ),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+note: baz
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+note: qux
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+note: baz
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+note: qux
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_4() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "bar",
+            ),
+            (None, "qux"),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+  = note: qux
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  │   ━━━━━━━━
+  ╰ note: qux
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_5() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (None, "bar"),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "qux",
+            ),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+  = note: bar
+note: qux
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  │
+  ╰ note: bar
+note: qux
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_6() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (None, "bar"),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "baz",
+            ),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "qux",
+            ),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+  = note: bar
+note: baz
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+note: qux
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  │
+  ╰ note: bar
+note: baz
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+note: qux
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_7() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z3", count: 1 })),
+                "bar",
+            ),
+            (None, "baz"),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "qux",
+            ),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 | /   X1 Y1 Z1
+5 | |   X2 Y2 Z2
+6 | |   X3 Y3 Z3
+  | |__________^
+  = note: baz
+note: qux
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │ ┏   X1 Y1 Z1
+5 │ ┃   X2 Y2 Z2
+6 │ ┃   X3 Y3 Z3
+  │ ┗━━━━━━━━━━┛
+  ╰ note: baz
+note: qux
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_8() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "bar",
+            ),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "baz",
+            ),
+            (None, "qux"),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+note: baz
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+  = note: qux
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+note: baz
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  │   ━━━━━━━━
+  ╰ note: qux
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_9() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (None, "bar"),
+            (None, "baz"),
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "qux",
+            ),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+  = note: bar
+  = note: baz
+note: qux
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  │
+  ├ note: bar
+  ╰ note: baz
+note: qux
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  ╰╴  ━━━━━━━━
+
+"#,
+    );
+}
+
+#[test]
+fn different_note_spanned_10() {
+    test_harness(
+        r#"
+fn foo() {
+  X0 Y0 Z0
+  X1 Y1 Z1
+  X2 Y2 Z2
+  X3 Y3 Z3
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "Y0", count: 1 },
+            end: Position { string: "Z0", count: 1 },
+            label: "`X` is a good letter",
+        }],
+        vec![
+            (
+                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
+                "bar",
+            ),
+            (None, "baz"),
+            (None, "qux"),
+        ],
+        r#"
+error: foo
+ --> test.rs:3:6
+  |
+3 |   X0 Y0 Z0
+  |      ^^^^^ `X` is a good letter
+  |
+note: bar
+ --> test.rs:4:3
+  |
+4 |   X1 Y1 Z1
+  |   ^^^^^^^^
+  = note: baz
+  = note: qux
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │   X0 Y0 Z0
+  │      ━━━━━ `X` is a good letter
+  ╰╴
+note: bar
+  ╭▸ test.rs:4:3
+  │
+4 │   X1 Y1 Z1
+  │   ━━━━━━━━
+  ├ note: baz
+  ╰ note: qux
+
+"#,
     );
 }
 
@@ -436,6 +1347,7 @@ fn foo() {
                 label: "`Z` label",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -452,6 +1364,22 @@ error: foo
   |        `X` is a good letter
 
 "#,
+        r#"
+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
+
+"#,
     );
 }
 
@@ -482,6 +1410,7 @@ fn foo() {
                 label: "`Z` label",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -496,6 +1425,20 @@ error: foo
   |      `Z` label
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │ ┏   X0 Y0 Z0
+4 │ ┃   X1 Y1 Z1
+5 │ ┃   X2 Y2 Z2
+  │ ┃    ╿
+  │ ┃    │
+  │ ┃    `X` is a good letter
+  │ ┗━━━━`Y` is a good letter too
+  ╰╴     `Z` label
+
+"#,
     );
 }
 
@@ -527,6 +1470,7 @@ fn foo() {
                 label: "`Z`",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:6
@@ -545,6 +1489,24 @@ error: foo
   |  |_______- `Z`
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │      X0 Y0 Z0
+  │ ┏━━━━━━━┛
+4 │ ┃    X1 Y1 Z1
+  │ ┃┌────╿─┘
+  │ ┗│━━━━┥
+  │  │    `X` is a good letter
+5 │  │   X2 Y2 Z2
+  │  └───│──────┘ `Y` is a good letter too
+  │  ┌───┘
+  │  │
+6 │  │   X3 Y3 Z3
+  ╰╴ └───────┘ `Z`
+
+"#,
     );
 }
 
@@ -571,6 +1533,7 @@ fn foo() {
                 label: "`Y` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -584,6 +1547,19 @@ error: foo
   | |__________- `Y` is a good letter too
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │ ┏   X0 Y0 Z0
+4 │ ┃   X1 Y1 Z1
+  │ ┗━━━━┛ `X` is a good letter
+5 │     X2 Y2 Z2
+  │ ┌──────┘
+6 │ │   X3 Y3 Z3
+  ╰╴└──────────┘ `Y` is a good letter too
+
+"#,
     );
 }
 
@@ -610,6 +1586,7 @@ fn foo() {
                 label: "`Y` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:6
@@ -625,6 +1602,21 @@ error: foo
   |  |__________- `Y` is a good letter too
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:6
+  │
+3 │      X0 Y0 Z0
+  │ ┏━━━━━━━┛
+4 │ ┃    X1 Y1 Z1
+  │ ┃┌────╿────┘
+  │ ┗│━━━━┥
+  │  │    `X` is a good letter
+5 │  │   X2 Y2 Z2
+6 │  │   X3 Y3 Z3
+  ╰╴ └──────────┘ `Y` is a good letter too
+
+"#,
     );
 }
 
@@ -653,6 +1645,7 @@ fn foo() {
                 label: "",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:7
@@ -661,6 +1654,57 @@ error: foo
   |   ----^^^^-^^-- `a` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:7
+  │
+3 │   a { b { c } d }
+  ╰╴  ────━━━━─━━── `a` is a good letter
+
+"#,
+    );
+}
+
+#[test]
+fn multiline_notes() {
+    test_harness(
+        r#"
+fn foo() {
+  a { b { c } d }
+}
+"#,
+        vec![SpanLabel {
+            start: Position { string: "a", count: 1 },
+            end: Position { string: "d", count: 1 },
+            label: "`a` is a good letter",
+        }],
+        vec![(None, "foo\nbar"), (None, "foo\nbar")],
+        r#"
+error: foo
+ --> test.rs:3:3
+  |
+3 |   a { b { c } d }
+  |   ^^^^^^^^^^^^^ `a` is a good letter
+  |
+  = note: foo
+          bar
+  = note: foo
+          bar
+
+"#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  │   ━━━━━━━━━━━━━ `a` is a good letter
+  │
+  ├ note: foo
+  │       bar
+  ╰ note: foo
+          bar
+
+"#,
     );
 }
 
@@ -684,6 +1728,7 @@ fn foo() {
                 label: "",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -692,6 +1737,14 @@ error: foo
   |   ^^^^-------^^ `a` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  ╰╴  ━━━━───────━━ `a` is a good letter
+
+"#,
     );
 }
 
@@ -720,6 +1773,7 @@ fn foo() {
                 label: "",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:7
@@ -730,6 +1784,16 @@ error: foo
   |       `b` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:7
+  │
+3 │   a { b { c } d }
+  │   ────┯━━━─━━──
+  │       │
+  ╰╴      `b` is a good letter
+
+"#,
     );
 }
 
@@ -753,6 +1817,7 @@ fn foo() {
                 label: "`b` is a good letter",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -763,6 +1828,16 @@ error: foo
   |       `b` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  │   ━━━━┬──────━━
+  │       │
+  ╰╴      `b` is a good letter
+
+"#,
     );
 }
 
@@ -786,6 +1861,7 @@ fn foo() {
                 label: "",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -796,6 +1872,16 @@ error: foo
   |   `a` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a  bc  d
+  │   ┯━━━────
+  │   │
+  ╰╴  `a` is a good letter
+
+"#,
     );
 }
 
@@ -819,6 +1905,7 @@ fn foo() {
                 label: "",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -827,6 +1914,14 @@ error: foo
   |   ^^^^-------^^
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  ╰╴  ━━━━───────━━
+
+"#,
     );
 }
 
@@ -855,6 +1950,7 @@ fn foo() {
                 label: "",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:7
@@ -863,6 +1959,14 @@ error: foo
   |   ----^^^^-^^--
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:7
+  │
+3 │   a { b { c } d }
+  ╰╴  ────━━━━─━━──
+
+"#,
     );
 }
 
@@ -886,6 +1990,7 @@ fn foo() {
                 label: "`b` is a good letter",
             },
         ],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -897,6 +2002,17 @@ error: foo
   |   `a` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  │   ┯━━━┬──────━━
+  │   │   │
+  │   │   `b` is a good letter
+  ╰╴  `a` is a good letter
+
+"#,
     );
 }
 
@@ -913,6 +2029,7 @@ fn foo() {
             end: Position { string: "d", count: 1 },
             label: "`a` is a good letter",
         }],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -921,6 +2038,14 @@ error: foo
   |   ^^^^^^^^^^^^^ `a` is a good letter
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  ╰╴  ━━━━━━━━━━━━━ `a` is a good letter
+
+"#,
     );
 }
 
@@ -937,6 +2062,7 @@ fn foo() {
             end: Position { string: "d", count: 1 },
             label: "",
         }],
+        vec![],
         r#"
 error: foo
  --> test.rs:3:3
@@ -945,6 +2071,14 @@ error: foo
   |   ^^^^^^^^^^^^^
 
 "#,
+        r#"
+error: foo
+  ╭▸ test.rs:3:3
+  │
+3 │   a { b { c } d }
+  ╰╴  ━━━━━━━━━━━━━
+
+"#,
     );
 }
 
@@ -981,6 +2115,7 @@ fn foo() {
                 label: "`Y` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
   --> test.rs:3:6
@@ -1000,6 +2135,25 @@ error: foo
    |  |__________- `Y` is a good letter too
 
 "#,
+        r#"
+error: foo
+   ╭▸ test.rs:3:6
+   │
+3  │      X0 Y0 Z0
+   │ ┏━━━━━━━┛
+4  │ ┃    X1 Y1 Z1
+   │ ┃┌────╿────┘
+   │ ┗│━━━━┥
+   │  │    `X` is a good letter
+5  │  │ 1
+6  │  │ 2
+7  │  │ 3
+   ‡  │
+15 │  │   X2 Y2 Z2
+16 │  │   X3 Y3 Z3
+   ╰╴ └──────────┘ `Y` is a good letter too
+
+"#,
     );
 }
 
@@ -1036,6 +2190,7 @@ fn foo() {
                 label: "`Z` is a good letter too",
             },
         ],
+        vec![],
         r#"
 error: foo
   --> test.rs:3:6
@@ -1058,6 +2213,28 @@ error: foo
    | |________^ `Y` is a good letter
 
 "#,
+        r#"
+error: foo
+   ╭▸ test.rs:3:6
+   │
+3  │      X0 Y0 Z0
+   │ ┏━━━━━━━┛
+4  │ ┃  1
+5  │ ┃  2
+6  │ ┃  3
+7  │ ┃    X1 Y1 Z1
+   │ ┃┌─────────┘
+8  │ ┃│ 4
+9  │ ┃│ 5
+10 │ ┃│ 6
+11 │ ┃│   X2 Y2 Z2
+   │ ┃└──────────┘ `Z` is a good letter too
+   ‡ ┃
+15 │ ┃  10
+16 │ ┃    X3 Y3 Z3
+   ╰╴┗━━━━━━━━┛ `Y` is a good letter
+
+"#,
     );
 }