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.
This commit is contained in:
Matt 2023-11-02 12:33:55 -04:00 committed by GitHub
parent d37ada876a
commit a8b4ae012a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 89 additions and 4 deletions

View File

@ -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;

View File

@ -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(())
}