mirror of https://github.com/zkat/miette.git
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"
This commit is contained in:
parent
d0c114311d
commit
c7cbb07d84
|
|
@ -515,7 +515,19 @@ impl GraphicalReportHandler {
|
||||||
context: &LabeledSpan,
|
context: &LabeledSpan,
|
||||||
labels: &[LabeledSpan],
|
labels: &[LabeledSpan],
|
||||||
) -> fmt::Result {
|
) -> fmt::Result {
|
||||||
let (contents, lines) = self.get_lines(source, context.inner())?;
|
let (contents, mut lines) = self.get_lines(source, context.inner())?;
|
||||||
|
|
||||||
|
// If the number of lines doesn't match the content's line_count, it's
|
||||||
|
// because the content is either an empty source or because the last
|
||||||
|
// line finishes with a newline. In this case, add an empty line.
|
||||||
|
if lines.len() != contents.line_count() {
|
||||||
|
lines.push(Line {
|
||||||
|
line_number: contents.line() + contents.line_count(),
|
||||||
|
offset: contents.span().offset() + contents.span().len(),
|
||||||
|
length: 0,
|
||||||
|
text: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// only consider labels from the context as primary label
|
// only consider labels from the context as primary label
|
||||||
let ctx_labels = labels.iter().filter(|l| {
|
let ctx_labels = labels.iter().filter(|l| {
|
||||||
|
|
@ -570,6 +582,10 @@ impl GraphicalReportHandler {
|
||||||
self.theme.characters.hbar,
|
self.theme.characters.hbar,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Save that for later, since `content` might be moved before we need
|
||||||
|
// that info
|
||||||
|
let has_content = !contents.data().is_empty();
|
||||||
|
|
||||||
// If there is a primary label, then use its span
|
// If there is a primary label, then use its span
|
||||||
// as the reference point for line/column information.
|
// as the reference point for line/column information.
|
||||||
let primary_contents = match primary_label {
|
let primary_contents = match primary_label {
|
||||||
|
|
@ -600,48 +616,53 @@ impl GraphicalReportHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now it's time for the fun part--actually rendering everything!
|
// Now it's time for the fun part--actually rendering everything!
|
||||||
for line in &lines {
|
// (but only if we have content or if we wanted content, to avoid
|
||||||
// Line number, appropriately padded.
|
// pointless detailed rendering, e.g. arrows pointing to nothing in the
|
||||||
self.write_linum(f, linum_width, line.line_number)?;
|
// middle of nothing)
|
||||||
|
if has_content || self.context_lines.is_some() {
|
||||||
|
for (line_no, line) in lines.iter().enumerate() {
|
||||||
|
// Line number, appropriately padded.
|
||||||
|
self.write_linum(f, linum_width, line.line_number)?;
|
||||||
|
|
||||||
// Then, we need to print the gutter, along with any fly-bys We
|
// Then, we need to print the gutter, along with any fly-bys We
|
||||||
// have separate gutters depending on whether we're on the actual
|
// have separate gutters depending on whether we're on the actual
|
||||||
// line, or on one of the "highlight lines" below it.
|
// line, or on one of the "highlight lines" below it.
|
||||||
self.render_line_gutter(f, max_gutter, line, &labels)?;
|
self.render_line_gutter(f, max_gutter, line, &labels)?;
|
||||||
|
|
||||||
// And _now_ we can print out the line text itself!
|
// And _now_ we can print out the line text itself!
|
||||||
let styled_text =
|
let styled_text =
|
||||||
StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
|
StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
|
||||||
self.render_line_text(f, &styled_text)?;
|
self.render_line_text(f, &styled_text)?;
|
||||||
|
|
||||||
// Next, we write all the highlights that apply to this particular line.
|
// Next, we write all the highlights that apply to this particular line.
|
||||||
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
|
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|hl| line.span_applies(hl))
|
.filter(|hl| line.span_applies(hl, line_no == (lines.len() - 1)))
|
||||||
.partition(|hl| line.span_line_only(hl));
|
.partition(|hl| line.span_line_only(hl));
|
||||||
if !single_line.is_empty() {
|
if !single_line.is_empty() {
|
||||||
// no line number!
|
// no line number!
|
||||||
self.write_no_linum(f, linum_width)?;
|
self.write_no_linum(f, linum_width)?;
|
||||||
// gutter _again_
|
// gutter _again_
|
||||||
self.render_highlight_gutter(
|
self.render_highlight_gutter(
|
||||||
f,
|
f,
|
||||||
max_gutter,
|
max_gutter,
|
||||||
line,
|
line,
|
||||||
&labels,
|
&labels,
|
||||||
LabelRenderMode::SingleLine,
|
LabelRenderMode::SingleLine,
|
||||||
)?;
|
)?;
|
||||||
self.render_single_line_highlights(
|
self.render_single_line_highlights(
|
||||||
f,
|
f,
|
||||||
line,
|
line,
|
||||||
linum_width,
|
linum_width,
|
||||||
max_gutter,
|
max_gutter,
|
||||||
&single_line,
|
&single_line,
|
||||||
&labels,
|
&labels,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
for hl in multi_line {
|
for hl in multi_line {
|
||||||
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
|
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
|
||||||
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
|
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1271,18 +1292,30 @@ impl Line {
|
||||||
|
|
||||||
/// Returns whether `span` should be visible on this line, either in the gutter or under the
|
/// Returns whether `span` should be visible on this line, either in the gutter or under the
|
||||||
/// text on this line
|
/// text on this line
|
||||||
fn span_applies(&self, span: &FancySpan) -> bool {
|
///
|
||||||
|
/// An empty span at a line boundary will preferable apply to the start of
|
||||||
|
/// a line (i.e. the second/next line) rather than the end of one (i.e. the
|
||||||
|
/// first/previous line). However if there are no "second" line, the span
|
||||||
|
/// can only apply the "first". The `inclusive` parameter is there to
|
||||||
|
/// indicate that `self` is the last line, i.e. that there are no "second"
|
||||||
|
/// line.
|
||||||
|
fn span_applies(&self, span: &FancySpan, inclusive: bool) -> bool {
|
||||||
// A span applies if its start is strictly before the line's end,
|
// A span applies if its start is strictly before the line's end,
|
||||||
// i.e. the span is not after the line, and its end is strictly after
|
// i.e. the span is not after the line, and its end is strictly after
|
||||||
// the line's start, i.e. the span is not before the line.
|
// the line's start, i.e. the span is not before the line.
|
||||||
//
|
//
|
||||||
// One corner case: if the span length is 0, then the span also applies
|
// Two corner cases:
|
||||||
// if its end is *at* the line's start, not just strictly after.
|
// - if `inclusive` is true, then the span also applies if its start is
|
||||||
(span.offset() < self.offset + self.length)
|
// *at* the line's end, not just strictly before.
|
||||||
&& match span.len() == 0 {
|
// - if the span length is 0, then the span also applies if its end is
|
||||||
true => (span.offset() + span.len()) >= self.offset,
|
// *at* the line's start, not just strictly after.
|
||||||
false => (span.offset() + span.len()) > self.offset,
|
(match inclusive {
|
||||||
}
|
true => span.offset() <= self.offset + self.length,
|
||||||
|
false => span.offset() < self.offset + self.length,
|
||||||
|
}) && match span.len() == 0 {
|
||||||
|
true => (span.offset() + span.len()) >= self.offset,
|
||||||
|
false => (span.offset() + span.len()) > self.offset,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
|
/// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
|
||||||
|
|
@ -1290,7 +1323,7 @@ impl Line {
|
||||||
fn span_applies_gutter(&self, span: &FancySpan) -> bool {
|
fn span_applies_gutter(&self, span: &FancySpan) -> bool {
|
||||||
// The span must covers this line and at least one of its ends must be
|
// The span must covers this line and at least one of its ends must be
|
||||||
// on another line
|
// on another line
|
||||||
self.span_applies(span)
|
self.span_applies(span, false)
|
||||||
&& ((span.offset() < self.offset)
|
&& ((span.offset() < self.offset)
|
||||||
|| ((span.offset() + span.len()) >= (self.offset + self.length)))
|
|| ((span.offset() + span.len()) >= (self.offset + self.length)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,16 @@ fn context_info<'a>(
|
||||||
) -> Result<MietteSpanContents<'a>, MietteError> {
|
) -> Result<MietteSpanContents<'a>, MietteError> {
|
||||||
let mut iter = input
|
let mut iter = input
|
||||||
.split_inclusive(|b| *b == b'\n')
|
.split_inclusive(|b| *b == b'\n')
|
||||||
|
.chain(
|
||||||
|
// `split_inclusive()` does not generate a line if the input is
|
||||||
|
// empty or for the "last line" if it terminates with a new line.
|
||||||
|
// This `chain` fixes that.
|
||||||
|
match input.last() {
|
||||||
|
None => Some(&input[0..0]),
|
||||||
|
Some(b'\n') => Some(&input[input.len()..input.len()]),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(line_no, line)| {
|
.map(|(line_no, line)| {
|
||||||
// SAFETY:
|
// SAFETY:
|
||||||
|
|
@ -291,8 +301,8 @@ mod tests {
|
||||||
let contents = src.read_span(&(12, 0).into(), None, None)?;
|
let contents = src.read_span(&(12, 0).into(), None, None)?;
|
||||||
assert_eq!("", std::str::from_utf8(contents.data()).unwrap());
|
assert_eq!("", std::str::from_utf8(contents.data()).unwrap());
|
||||||
assert_eq!(SourceSpan::from((12, 0)), *contents.span());
|
assert_eq!(SourceSpan::from((12, 0)), *contents.span());
|
||||||
assert_eq!(2, contents.line());
|
assert_eq!(3, contents.line());
|
||||||
assert_eq!(4, contents.column());
|
assert_eq!(0, contents.column());
|
||||||
assert_eq!(1, contents.line_count());
|
assert_eq!(1, contents.line_count());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,9 @@ fn empty_source() -> Result<(), MietteError> {
|
||||||
|
|
||||||
× oops!
|
× oops!
|
||||||
╭─[bad_file.rs:1:1]
|
╭─[bad_file.rs:1:1]
|
||||||
|
1 │
|
||||||
|
· ▲
|
||||||
|
· ╰── this bit here
|
||||||
╰────
|
╰────
|
||||||
help: try doing it better next time?
|
help: try doing it better next time?
|
||||||
"#
|
"#
|
||||||
|
|
@ -633,6 +636,78 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_line_highlight_offset_end_of_file_no_newline() -> 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<String>,
|
||||||
|
#[label("this bit here")]
|
||||||
|
highlight: SourceSpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = "one\ntwo\nthree".to_string();
|
||||||
|
let err = MyBad {
|
||||||
|
src: NamedSource::new("bad_file.rs", src),
|
||||||
|
highlight: (13, 0).into(),
|
||||||
|
};
|
||||||
|
let out = fmt_report(err.into());
|
||||||
|
println!("Error: {}", out);
|
||||||
|
let expected = r#"oops::my::bad
|
||||||
|
|
||||||
|
× oops!
|
||||||
|
╭─[bad_file.rs:3:6]
|
||||||
|
2 │ two
|
||||||
|
3 │ three
|
||||||
|
· ▲
|
||||||
|
· ╰── this bit here
|
||||||
|
╰────
|
||||||
|
help: try doing it better next time?
|
||||||
|
"#
|
||||||
|
.trim_start()
|
||||||
|
.to_string();
|
||||||
|
assert_eq!(expected, out);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_line_highlight_offset_end_of_file_with_newline() -> 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<String>,
|
||||||
|
#[label("this bit here")]
|
||||||
|
highlight: SourceSpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = "one\ntwo\nthree\n".to_string();
|
||||||
|
let err = MyBad {
|
||||||
|
src: NamedSource::new("bad_file.rs", src),
|
||||||
|
highlight: (14, 0).into(),
|
||||||
|
};
|
||||||
|
let out = fmt_report(err.into());
|
||||||
|
println!("Error: {}", out);
|
||||||
|
let expected = r#"oops::my::bad
|
||||||
|
|
||||||
|
× oops!
|
||||||
|
╭─[bad_file.rs:4:1]
|
||||||
|
3 │ three
|
||||||
|
4 │
|
||||||
|
· ▲
|
||||||
|
· ╰── this bit here
|
||||||
|
╰────
|
||||||
|
help: try doing it better next time?
|
||||||
|
"#
|
||||||
|
.trim_start()
|
||||||
|
.to_string();
|
||||||
|
assert_eq!(expected, out);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
|
fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
|
||||||
#[derive(Debug, Diagnostic, Error)]
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
|
@ -1088,9 +1163,8 @@ fn multiline_highlight_flyby() -> Result<(), MietteError> {
|
||||||
line2
|
line2
|
||||||
line3
|
line3
|
||||||
line4
|
line4
|
||||||
line5
|
line5"#
|
||||||
"#
|
.to_string();
|
||||||
.to_string();
|
|
||||||
let len = src.len();
|
let len = src.len();
|
||||||
let err = MyBad {
|
let err = MyBad {
|
||||||
src: NamedSource::new("bad_file.rs", src),
|
src: NamedSource::new("bad_file.rs", src),
|
||||||
|
|
@ -1147,9 +1221,8 @@ fn multiline_highlight_no_label() -> Result<(), MietteError> {
|
||||||
line2
|
line2
|
||||||
line3
|
line3
|
||||||
line4
|
line4
|
||||||
line5
|
line5"#
|
||||||
"#
|
.to_string();
|
||||||
.to_string();
|
|
||||||
let len = src.len();
|
let len = src.len();
|
||||||
let err = MyBad {
|
let err = MyBad {
|
||||||
source: Inner(InnerInner),
|
source: Inner(InnerInner),
|
||||||
|
|
@ -2267,6 +2340,8 @@ fn multi_adjacent_zero_length_no_context() -> Result<(), MietteError> {
|
||||||
· ▲
|
· ▲
|
||||||
· ╰── this bit here
|
· ╰── this bit here
|
||||||
3 │ thr
|
3 │ thr
|
||||||
|
· ▲
|
||||||
|
· ╰── and here
|
||||||
╰────
|
╰────
|
||||||
help: try doing it better next time?
|
help: try doing it better next time?
|
||||||
"#
|
"#
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue