Skip to content

Commit c7cbb07

Browse files
committed
fix(zero length): Improve rendering for zero-length error spans
A zero-length span at a line boundary can be associated either with the previous or the next line. Current code prefer to use the next line, however there isn't always a "next line" (end of source, or "no context" configuration). There is also the extra-special case of an empty document which has no "next line" but also no "previous line". This commit adds an empty newline at the end of a document if appropriate so that there is a "next line", or use the "previous line"
1 parent d0c1143 commit c7cbb07

File tree

3 files changed

+178
-60
lines changed

3 files changed

+178
-60
lines changed

src/handlers/graphical.rs

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,19 @@ impl GraphicalReportHandler {
515515
context: &LabeledSpan,
516516
labels: &[LabeledSpan],
517517
) -> fmt::Result {
518-
let (contents, lines) = self.get_lines(source, context.inner())?;
518+
let (contents, mut lines) = self.get_lines(source, context.inner())?;
519+
520+
// If the number of lines doesn't match the content's line_count, it's
521+
// because the content is either an empty source or because the last
522+
// line finishes with a newline. In this case, add an empty line.
523+
if lines.len() != contents.line_count() {
524+
lines.push(Line {
525+
line_number: contents.line() + contents.line_count(),
526+
offset: contents.span().offset() + contents.span().len(),
527+
length: 0,
528+
text: String::new(),
529+
});
530+
}
519531

520532
// only consider labels from the context as primary label
521533
let ctx_labels = labels.iter().filter(|l| {
@@ -570,6 +582,10 @@ impl GraphicalReportHandler {
570582
self.theme.characters.hbar,
571583
)?;
572584

585+
// Save that for later, since `content` might be moved before we need
586+
// that info
587+
let has_content = !contents.data().is_empty();
588+
573589
// If there is a primary label, then use its span
574590
// as the reference point for line/column information.
575591
let primary_contents = match primary_label {
@@ -600,48 +616,53 @@ impl GraphicalReportHandler {
600616
}
601617

602618
// Now it's time for the fun part--actually rendering everything!
603-
for line in &lines {
604-
// Line number, appropriately padded.
605-
self.write_linum(f, linum_width, line.line_number)?;
606-
607-
// Then, we need to print the gutter, along with any fly-bys We
608-
// have separate gutters depending on whether we're on the actual
609-
// line, or on one of the "highlight lines" below it.
610-
self.render_line_gutter(f, max_gutter, line, &labels)?;
611-
612-
// And _now_ we can print out the line text itself!
613-
let styled_text =
614-
StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
615-
self.render_line_text(f, &styled_text)?;
616-
617-
// Next, we write all the highlights that apply to this particular line.
618-
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
619-
.iter()
620-
.filter(|hl| line.span_applies(hl))
621-
.partition(|hl| line.span_line_only(hl));
622-
if !single_line.is_empty() {
623-
// no line number!
624-
self.write_no_linum(f, linum_width)?;
625-
// gutter _again_
626-
self.render_highlight_gutter(
627-
f,
628-
max_gutter,
629-
line,
630-
&labels,
631-
LabelRenderMode::SingleLine,
632-
)?;
633-
self.render_single_line_highlights(
634-
f,
635-
line,
636-
linum_width,
637-
max_gutter,
638-
&single_line,
639-
&labels,
640-
)?;
641-
}
642-
for hl in multi_line {
643-
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
644-
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
619+
// (but only if we have content or if we wanted content, to avoid
620+
// pointless detailed rendering, e.g. arrows pointing to nothing in the
621+
// middle of nothing)
622+
if has_content || self.context_lines.is_some() {
623+
for (line_no, line) in lines.iter().enumerate() {
624+
// Line number, appropriately padded.
625+
self.write_linum(f, linum_width, line.line_number)?;
626+
627+
// Then, we need to print the gutter, along with any fly-bys We
628+
// have separate gutters depending on whether we're on the actual
629+
// line, or on one of the "highlight lines" below it.
630+
self.render_line_gutter(f, max_gutter, line, &labels)?;
631+
632+
// And _now_ we can print out the line text itself!
633+
let styled_text =
634+
StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
635+
self.render_line_text(f, &styled_text)?;
636+
637+
// Next, we write all the highlights that apply to this particular line.
638+
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
639+
.iter()
640+
.filter(|hl| line.span_applies(hl, line_no == (lines.len() - 1)))
641+
.partition(|hl| line.span_line_only(hl));
642+
if !single_line.is_empty() {
643+
// no line number!
644+
self.write_no_linum(f, linum_width)?;
645+
// gutter _again_
646+
self.render_highlight_gutter(
647+
f,
648+
max_gutter,
649+
line,
650+
&labels,
651+
LabelRenderMode::SingleLine,
652+
)?;
653+
self.render_single_line_highlights(
654+
f,
655+
line,
656+
linum_width,
657+
max_gutter,
658+
&single_line,
659+
&labels,
660+
)?;
661+
}
662+
for hl in multi_line {
663+
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
664+
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
665+
}
645666
}
646667
}
647668
}
@@ -1271,26 +1292,38 @@ impl Line {
12711292

12721293
/// Returns whether `span` should be visible on this line, either in the gutter or under the
12731294
/// text on this line
1274-
fn span_applies(&self, span: &FancySpan) -> bool {
1295+
///
1296+
/// An empty span at a line boundary will preferable apply to the start of
1297+
/// a line (i.e. the second/next line) rather than the end of one (i.e. the
1298+
/// first/previous line). However if there are no "second" line, the span
1299+
/// can only apply the "first". The `inclusive` parameter is there to
1300+
/// indicate that `self` is the last line, i.e. that there are no "second"
1301+
/// line.
1302+
fn span_applies(&self, span: &FancySpan, inclusive: bool) -> bool {
12751303
// A span applies if its start is strictly before the line's end,
12761304
// i.e. the span is not after the line, and its end is strictly after
12771305
// the line's start, i.e. the span is not before the line.
12781306
//
1279-
// One corner case: if the span length is 0, then the span also applies
1280-
// if its end is *at* the line's start, not just strictly after.
1281-
(span.offset() < self.offset + self.length)
1282-
&& match span.len() == 0 {
1283-
true => (span.offset() + span.len()) >= self.offset,
1284-
false => (span.offset() + span.len()) > self.offset,
1285-
}
1307+
// Two corner cases:
1308+
// - if `inclusive` is true, then the span also applies if its start is
1309+
// *at* the line's end, not just strictly before.
1310+
// - if the span length is 0, then the span also applies if its end is
1311+
// *at* the line's start, not just strictly after.
1312+
(match inclusive {
1313+
true => span.offset() <= self.offset + self.length,
1314+
false => span.offset() < self.offset + self.length,
1315+
}) && match span.len() == 0 {
1316+
true => (span.offset() + span.len()) >= self.offset,
1317+
false => (span.offset() + span.len()) > self.offset,
1318+
}
12861319
}
12871320

12881321
/// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
12891322
/// that are only visible on this line and do not span multiple lines)
12901323
fn span_applies_gutter(&self, span: &FancySpan) -> bool {
12911324
// The span must covers this line and at least one of its ends must be
12921325
// on another line
1293-
self.span_applies(span)
1326+
self.span_applies(span, false)
12941327
&& ((span.offset() < self.offset)
12951328
|| ((span.offset() + span.len()) >= (self.offset + self.length)))
12961329
}

src/source_impls.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ fn context_info<'a>(
2020
) -> Result<MietteSpanContents<'a>, MietteError> {
2121
let mut iter = input
2222
.split_inclusive(|b| *b == b'\n')
23+
.chain(
24+
// `split_inclusive()` does not generate a line if the input is
25+
// empty or for the "last line" if it terminates with a new line.
26+
// This `chain` fixes that.
27+
match input.last() {
28+
None => Some(&input[0..0]),
29+
Some(b'\n') => Some(&input[input.len()..input.len()]),
30+
_ => None,
31+
},
32+
)
2333
.enumerate()
2434
.map(|(line_no, line)| {
2535
// SAFETY:
@@ -291,8 +301,8 @@ mod tests {
291301
let contents = src.read_span(&(12, 0).into(), None, None)?;
292302
assert_eq!("", std::str::from_utf8(contents.data()).unwrap());
293303
assert_eq!(SourceSpan::from((12, 0)), *contents.span());
294-
assert_eq!(2, contents.line());
295-
assert_eq!(4, contents.column());
304+
assert_eq!(3, contents.line());
305+
assert_eq!(0, contents.column());
296306
assert_eq!(1, contents.line_count());
297307
Ok(())
298308
}

tests/graphical.rs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ fn empty_source() -> Result<(), MietteError> {
288288
289289
× oops!
290290
╭─[bad_file.rs:1:1]
291+
1 │
292+
· ▲
293+
· ╰── this bit here
291294
╰────
292295
help: try doing it better next time?
293296
"#
@@ -633,6 +636,78 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
633636
Ok(())
634637
}
635638

639+
#[test]
640+
fn single_line_highlight_offset_end_of_file_no_newline() -> Result<(), MietteError> {
641+
#[derive(Debug, Diagnostic, Error)]
642+
#[error("oops!")]
643+
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
644+
struct MyBad {
645+
#[source_code]
646+
src: NamedSource<String>,
647+
#[label("this bit here")]
648+
highlight: SourceSpan,
649+
}
650+
651+
let src = "one\ntwo\nthree".to_string();
652+
let err = MyBad {
653+
src: NamedSource::new("bad_file.rs", src),
654+
highlight: (13, 0).into(),
655+
};
656+
let out = fmt_report(err.into());
657+
println!("Error: {}", out);
658+
let expected = r#"oops::my::bad
659+
660+
× oops!
661+
╭─[bad_file.rs:3:6]
662+
2 │ two
663+
3 │ three
664+
· ▲
665+
· ╰── this bit here
666+
╰────
667+
help: try doing it better next time?
668+
"#
669+
.trim_start()
670+
.to_string();
671+
assert_eq!(expected, out);
672+
Ok(())
673+
}
674+
675+
#[test]
676+
fn single_line_highlight_offset_end_of_file_with_newline() -> Result<(), MietteError> {
677+
#[derive(Debug, Diagnostic, Error)]
678+
#[error("oops!")]
679+
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
680+
struct MyBad {
681+
#[source_code]
682+
src: NamedSource<String>,
683+
#[label("this bit here")]
684+
highlight: SourceSpan,
685+
}
686+
687+
let src = "one\ntwo\nthree\n".to_string();
688+
let err = MyBad {
689+
src: NamedSource::new("bad_file.rs", src),
690+
highlight: (14, 0).into(),
691+
};
692+
let out = fmt_report(err.into());
693+
println!("Error: {}", out);
694+
let expected = r#"oops::my::bad
695+
696+
× oops!
697+
╭─[bad_file.rs:4:1]
698+
3 │ three
699+
4 │
700+
· ▲
701+
· ╰── this bit here
702+
╰────
703+
help: try doing it better next time?
704+
"#
705+
.trim_start()
706+
.to_string();
707+
assert_eq!(expected, out);
708+
Ok(())
709+
}
710+
636711
#[test]
637712
fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
638713
#[derive(Debug, Diagnostic, Error)]
@@ -1088,9 +1163,8 @@ fn multiline_highlight_flyby() -> Result<(), MietteError> {
10881163
line2
10891164
line3
10901165
line4
1091-
line5
1092-
"#
1093-
.to_string();
1166+
line5"#
1167+
.to_string();
10941168
let len = src.len();
10951169
let err = MyBad {
10961170
src: NamedSource::new("bad_file.rs", src),
@@ -1147,9 +1221,8 @@ fn multiline_highlight_no_label() -> Result<(), MietteError> {
11471221
line2
11481222
line3
11491223
line4
1150-
line5
1151-
"#
1152-
.to_string();
1224+
line5"#
1225+
.to_string();
11531226
let len = src.len();
11541227
let err = MyBad {
11551228
source: Inner(InnerInner),
@@ -2267,6 +2340,8 @@ fn multi_adjacent_zero_length_no_context() -> Result<(), MietteError> {
22672340
· ▲
22682341
· ╰── this bit here
22692342
3 │ thr
2343+
· ▲
2344+
· ╰── and here
22702345
╰────
22712346
help: try doing it better next time?
22722347
"#

0 commit comments

Comments
 (0)