From a8b4ae012aa0cf03b53a18f013c2b3f76c5040e7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Nov 2023 12:33:55 -0400 Subject: [PATCH] fix(graphical): Extend error text span to whole code points (#312) Fixes: https://github.com/zkat/miette/issues/223 This fixes a panic when an error starts inside a Unicode code point. The range is extended to start (or end) at the beginning (or end) of the character inside which the byte offset is located. --- src/handlers/graphical.rs | 19 +++++++--- tests/graphical.rs | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 770944d8..44d92b77 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -651,11 +651,22 @@ impl GraphicalReportHandler { } /// Returns the visual column position of a byte offset on a specific line. - fn visual_offset(&self, line: &Line, offset: usize) -> usize { + /// + /// If the offset occurs in the middle of a character, the returned column + /// corresponds to that character's first column in `start` is true, or its + /// last column if `start` is false. + fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize { let line_range = line.offset..=(line.offset + line.length); assert!(line_range.contains(&offset)); - let text_index = offset - line.offset; + let mut text_index = offset - line.offset; + while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) { + if start { + text_index -= 1; + } else { + text_index += 1; + } + } let text = &line.text[..text_index.min(line.text.len())]; let text_width = self.line_visual_char_width(text).sum(); if text_index > line.text.len() { @@ -706,8 +717,8 @@ impl GraphicalReportHandler { .map(|hl| { let byte_start = hl.offset(); let byte_end = hl.offset() + hl.len(); - let start = self.visual_offset(line, byte_start).max(highest); - let end = self.visual_offset(line, byte_end).max(start + 1); + let start = self.visual_offset(line, byte_start, true).max(highest); + let end = self.visual_offset(line, byte_end, false).max(start + 1); let vbar_offset = (start + end) / 2; let num_left = vbar_offset - start; diff --git a/tests/graphical.rs b/tests/graphical.rs index 887e4548..31db4dc1 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1247,3 +1247,77 @@ fn primary_label() { assert_eq!(expected, out); } + +#[test] +fn single_line_with_wide_char_unaligned_span_start() -> 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, 5).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(()) +} + +#[test] +fn single_line_with_wide_char_unaligned_span_end() -> 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: (9, 6).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + let expected = r#"oops::my::bad + + Γ— oops! + ╭─[bad_file.rs:2:3] + 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(()) +}