Repository: zkat/miette
Pull request #314 (2023-11-04 06:30:14)

Fix formatting bug when an empty span is not aligned to a char boundary
This bug was originally identified in https://github.com/zkat/miette/pull/312#issuecomment-1791075099.

The bad output looked like this:

```
   ╭─[bad_file.rs:2:4]
 1 │ source
 2 │   👼🏼text
   ·   ─▲
   ·    ╰── this bit here
 3 │     here
   ╰────
```

Another possible option for the output (other than the one I implemented) would be

```
   ╭─[bad_file.rs:2:4]
 1 │ source
 2 │   👼🏼text
   ·    ▲
   ·    ╰── this bit here
 3 │     here
   ╰────
```

I prefer pointing the `▲` at the first column of the wide char rather than the second, but can see an argument that pointing at the second column is visually communicates that the span is unaligned. I think that's pretty niche, and would be surprised if any users are generating unaligned spans on purpose.

---

I also included a minor refactor in the PR, removing an unnecessary branch. The branch was always taken both before and after the bugfix. I can remove that bit if you want.


Patch:
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 44d92b7..8cec88b 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -718,31 +718,33 @@ impl GraphicalReportHandler {
                 let byte_start = hl.offset();
                 let byte_end = hl.offset() + hl.len();
                 let start = self.visual_offset(line, byte_start, true).max(highest);
-                let end = self.visual_offset(line, byte_end, false).max(start + 1);
+                let end = if hl.len() == 0 {
+                    start + 1
+                } else {
+                    self.visual_offset(line, byte_end, false).max(start + 1)
+                };
 
                 let vbar_offset = (start + end) / 2;
                 let num_left = vbar_offset - start;
                 let num_right = end - vbar_offset - 1;
-                if start < end {
-                    underlines.push_str(
-                        &format!(
-                            "{:width$}{}{}{}",
-                            "",
-                            chars.underline.to_string().repeat(num_left),
-                            if hl.len() == 0 {
-                                chars.uarrow
-                            } else if hl.label().is_some() {
-                                chars.underbar
-                            } else {
-                                chars.underline
-                            },
-                            chars.underline.to_string().repeat(num_right),
-                            width = start.saturating_sub(highest),
-                        )
-                        .style(hl.style)
-                        .to_string(),
-                    );
-                }
+                underlines.push_str(
+                    &format!(
+                        "{:width$}{}{}{}",
+                        "",
+                        chars.underline.to_string().repeat(num_left),
+                        if hl.len() == 0 {
+                            chars.uarrow
+                        } else if hl.label().is_some() {
+                            chars.underbar
+                        } else {
+                            chars.underline
+                        },
+                        chars.underline.to_string().repeat(num_right),
+                        width = start.saturating_sub(highest),
+                    )
+                    .style(hl.style)
+                    .to_string(),
+                );
                 highest = std::cmp::max(highest, end);
 
                 (hl, vbar_offset)


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index 31db4dc..d24879a 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -1321,3 +1321,40 @@ fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> {
     assert_eq!(expected, out);
     Ok(())
 }
+
+#[test]
+fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label("this bit here")]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  👼🏼text\n    here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight: (10, 0).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:2:4]
+ 1 │ source
+ 2 │   👼🏼text
+   ·   ▲
+   ·   ╰── this bit here
+ 3 │     here
+   ╰────
+  help: try doing it better next time?
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}