tamer: diagnose::report: Render span marks under lines
This has the effect of highlighting the columns of the source lines using '^' as an underline. The next step will be to have the underline character depend on the `Level`. If this commit message doesn't sound all that exciting, given what it finally achieved after all this time, it's because I'm exhausted, and my prototype has already taken my excitement. But this is significant, given all the work leading up to it. There is some code cleanup needed and some unit tests that ought to be written rather than relying on integration, but considering how much this is being refactored, I don't want to add to that refactoring cost just yet before gutters are introduced and I know things are settled for now. DEV-12151main
parent
5db026ed76
commit
8119d1ca0d
|
@ -261,6 +261,7 @@ impl<'s, 'd> Section<'d> {
|
|||
extend: Option<&mut Section<'d>>,
|
||||
) -> Option<Self> {
|
||||
match extend {
|
||||
// TODO: Take highest level.
|
||||
Some(extend_sec) if self.span == extend_sec.span => {
|
||||
// TODO: At the time of writing this will cause duplication of
|
||||
// system labels,
|
||||
|
@ -540,7 +541,6 @@ impl<'d> Display for SectionSourceLine<'d> {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, " |\n")?;
|
||||
write!(f, " | {src}\n", src = self.src)?;
|
||||
write!(f, " |\n")?;
|
||||
write!(f, "{}", self.mark)
|
||||
}
|
||||
}
|
||||
|
@ -556,6 +556,19 @@ struct LineMark<'d> {
|
|||
|
||||
impl<'d> Display for LineMark<'d> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self.col {
|
||||
Some(col) => {
|
||||
let underline = "^"
|
||||
.repeat((col.end().get() - col.start().get()) as usize + 1);
|
||||
let lpad = col.start().get() as usize - 1;
|
||||
|
||||
write!(f, " | {:lpad$}{underline}\n", "")?;
|
||||
}
|
||||
_ => {
|
||||
write!(f, " |\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(label) = self.label.as_ref() {
|
||||
write!(f, " = {level}: {label}\n", level = self.level)?;
|
||||
}
|
||||
|
@ -897,4 +910,6 @@ mod test {
|
|||
|
||||
// TODO: Section squashing is currently only covered by integration
|
||||
// tests!
|
||||
|
||||
// TODO: Most `Display::fmt` only covered by integration tests!
|
||||
}
|
||||
|
|
|
@ -86,8 +86,9 @@ impl Diagnostic for StubError {
|
|||
|
||||
const FILE_FOO_BAR: &[u8] =
|
||||
b"foo/bar line 1\nfoo/bar line 2\nfoo/bar line 3\nfoo/bar line 4";
|
||||
// |------------| |------------| |------------| |------------|
|
||||
// 0 13 15 28 30 43 45 58
|
||||
// |-------+--+-| |-------+--+-| |-------+--+-| |-------+--+-|
|
||||
// 0 | |13 15 | |28 30 | |43 45 | |58
|
||||
// 8 11 23 26 38 41 53 56
|
||||
// len: 14
|
||||
|
||||
const FILE_BAR_BAZ: &[u8] =
|
||||
|
@ -140,7 +141,7 @@ fn no_spans() {
|
|||
|
||||
#[test]
|
||||
fn span_error_no_label() {
|
||||
let span = Context::from("foo/bar").span(50, 5);
|
||||
let span = Context::from("foo/bar").span(53, 4);
|
||||
|
||||
assert_report!(
|
||||
"single span no label",
|
||||
|
@ -148,17 +149,17 @@ fn span_error_no_label() {
|
|||
// Context and span are rendered without a label.
|
||||
"\
|
||||
error: single span no label
|
||||
--> foo/bar:4:6
|
||||
--> foo/bar:4:9
|
||||
|
|
||||
| foo/bar line 4
|
||||
|
|
||||
| ^^^^
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_error_with_label() {
|
||||
let span = Context::from("bar/baz").span(30, 2);
|
||||
let span = Context::from("bar/baz").span(30, 3);
|
||||
|
||||
assert_report!(
|
||||
"single span with label",
|
||||
|
@ -169,7 +170,7 @@ error: single span with label
|
|||
--> bar/baz:3:1
|
||||
|
|
||||
| bar/baz line 3
|
||||
|
|
||||
| ^^^
|
||||
= error: span label here
|
||||
"
|
||||
);
|
||||
|
@ -178,7 +179,7 @@ error: single span with label
|
|||
#[test]
|
||||
fn adjacent_eq_span_no_labels_collapsed() {
|
||||
let ctx = Context::from("foo/bar");
|
||||
let span = ctx.span(50, 1);
|
||||
let span = ctx.span(53, 1);
|
||||
|
||||
assert_report!(
|
||||
"multiple adjacent same span no label",
|
||||
|
@ -189,10 +190,10 @@ fn adjacent_eq_span_no_labels_collapsed() {
|
|||
// duplicate spans without some additional context.
|
||||
"\
|
||||
error: multiple adjacent same span no label
|
||||
--> foo/bar:4:6
|
||||
--> foo/bar:4:9
|
||||
|
|
||||
| foo/bar line 4
|
||||
|
|
||||
| ^
|
||||
"
|
||||
);
|
||||
}
|
||||
|
@ -200,7 +201,7 @@ error: multiple adjacent same span no label
|
|||
#[test]
|
||||
fn adjacent_eq_span_labels_collapsed() {
|
||||
let ctx = Context::from("bar/baz");
|
||||
let span = ctx.span(10, 5);
|
||||
let span = ctx.span(8, 6);
|
||||
|
||||
assert_report!(
|
||||
"multiple adjacent same span with labels",
|
||||
|
@ -213,10 +214,10 @@ fn adjacent_eq_span_labels_collapsed() {
|
|||
// spans are the same.
|
||||
"\
|
||||
error: multiple adjacent same span with labels
|
||||
--> bar/baz:1:11
|
||||
--> bar/baz:1:9
|
||||
|
|
||||
| bar/baz line 1
|
||||
|
|
||||
| ^^^^^^
|
||||
= error: A label
|
||||
= error: C label
|
||||
"
|
||||
|
@ -231,43 +232,43 @@ fn adjacent_eq_context_neq_offset_len_spans_not_collapsed() {
|
|||
"eq context neq offset/len",
|
||||
vec![
|
||||
// -->
|
||||
ctx.span(0, 5).mark_error(),
|
||||
ctx.span(0, 5).error("A, first label"), // collapse
|
||||
ctx.span(0, 3).mark_error(),
|
||||
ctx.span(0, 3).error("A, first label"), // collapse
|
||||
// -->
|
||||
ctx.span(0, 6).error("B, different length"),
|
||||
ctx.span(0, 6).mark_error(), // collapse
|
||||
ctx.span(0, 6).error("B, collapse"),
|
||||
ctx.span(0, 7).error("B, different length"),
|
||||
ctx.span(0, 7).mark_error(), // collapse
|
||||
ctx.span(0, 7).error("B, collapse"),
|
||||
// -->
|
||||
ctx.span(15, 6).error("C, different offset"),
|
||||
ctx.span(15, 4).error("C, different offset"),
|
||||
// -->
|
||||
// Back to (10, 6), but not adjacent to previous
|
||||
ctx.span(0, 6).error("B', not adjacent"),
|
||||
// Back to (0, 7), but not adjacent to previous
|
||||
ctx.span(0, 7).error("B', not adjacent"),
|
||||
],
|
||||
"\
|
||||
error: eq context neq offset/len
|
||||
--> bar/baz:1:1
|
||||
|
|
||||
| bar/baz line 1
|
||||
|
|
||||
| ^^^
|
||||
= error: A, first label
|
||||
|
||||
--> bar/baz:1:1
|
||||
|
|
||||
| bar/baz line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
= error: B, different length
|
||||
= error: B, collapse
|
||||
|
||||
--> bar/baz:2:1
|
||||
|
|
||||
| bar/baz line 2
|
||||
|
|
||||
| ^^^^
|
||||
= error: C, different offset
|
||||
|
||||
--> bar/baz:1:1
|
||||
|
|
||||
| bar/baz line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
= error: B', not adjacent
|
||||
"
|
||||
);
|
||||
|
@ -277,8 +278,8 @@ error: eq context neq offset/len
|
|||
fn adjacent_neq_context_spans_not_collapsed() {
|
||||
// Note that the offsets and lengths are purposefully the same to
|
||||
// ensure that the differentiator is exclusively the context.
|
||||
let span_a = Context::from("foo/bar").span(10, 3);
|
||||
let span_b = Context::from("bar/baz").span(10, 3);
|
||||
let span_a = Context::from("foo/bar").span(0, 7);
|
||||
let span_b = Context::from("bar/baz").span(0, 7);
|
||||
|
||||
assert_report!(
|
||||
"multiple adjacent different context",
|
||||
|
@ -305,35 +306,35 @@ fn adjacent_neq_context_spans_not_collapsed() {
|
|||
],
|
||||
"\
|
||||
error: multiple adjacent different context
|
||||
--> foo/bar:1:11
|
||||
--> foo/bar:1:1
|
||||
|
|
||||
| foo/bar line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
= error: A, first
|
||||
= error: A, collapsed
|
||||
|
||||
--> bar/baz:1:11
|
||||
--> bar/baz:1:1
|
||||
|
|
||||
| bar/baz line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
= error: B, first
|
||||
= error: B, collapsed
|
||||
|
||||
--> foo/bar:1:11
|
||||
--> foo/bar:1:1
|
||||
|
|
||||
| foo/bar line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
= error: A, not collapsed
|
||||
|
||||
--> bar/baz:1:11
|
||||
--> bar/baz:1:1
|
||||
|
|
||||
| bar/baz line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
|
||||
--> foo/bar:1:11
|
||||
--> foo/bar:1:1
|
||||
|
|
||||
| foo/bar line 1
|
||||
|
|
||||
| ^^^^^^^
|
||||
"
|
||||
);
|
||||
}
|
||||
|
@ -341,7 +342,7 @@ error: multiple adjacent different context
|
|||
#[test]
|
||||
fn severity_levels_reflected() {
|
||||
let ctx = Context::from("foo/bar");
|
||||
let span = ctx.span(50, 5);
|
||||
let span = ctx.span(53, 6);
|
||||
|
||||
assert_report!(
|
||||
"multiple spans with labels of different severity level",
|
||||
|
@ -353,10 +354,10 @@ fn severity_levels_reflected() {
|
|||
],
|
||||
"\
|
||||
internal error: multiple spans with labels of different severity level
|
||||
--> foo/bar:4:6
|
||||
--> foo/bar:4:9
|
||||
|
|
||||
| foo/bar line 4
|
||||
|
|
||||
| ^^^^^^
|
||||
= internal error: an internal error
|
||||
= error: an error
|
||||
= note: a note
|
||||
|
@ -370,7 +371,7 @@ fn multi_line_span() {
|
|||
let ctx = Context::from("foo/bar");
|
||||
|
||||
// First two lines.
|
||||
let span = ctx.span(0, 29);
|
||||
let span = ctx.span(8, 19);
|
||||
|
||||
// This is obviously terrible-looking;
|
||||
// it'll be condensed as this evolves further.
|
||||
|
@ -379,13 +380,13 @@ fn multi_line_span() {
|
|||
vec![span.error("label to be on last line")],
|
||||
"\
|
||||
error: multi-line span
|
||||
--> foo/bar:1:1
|
||||
--> foo/bar:1:9
|
||||
|
|
||||
| foo/bar line 1
|
||||
|
|
||||
| ^^^^^^
|
||||
|
|
||||
| foo/bar line 2
|
||||
|
|
||||
| ^^^^^^^^^^^^
|
||||
= error: label to be on last line
|
||||
"
|
||||
);
|
||||
|
@ -461,3 +462,47 @@ error: column resolution failure
|
|||
")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_at_newline_marked() {
|
||||
let ctx = Context::from("foo/bar");
|
||||
|
||||
// This represents the newline of line 1.
|
||||
let span = ctx.span(14, 1);
|
||||
|
||||
assert_report!(
|
||||
"offset at newline",
|
||||
vec![span.mark_error()],
|
||||
// Note that the trailing newline _should not be rendered_,
|
||||
// which would otherwise result in an extra (un-guttered) line
|
||||
// between the source line and the mark line.
|
||||
"\
|
||||
error: offset at newline
|
||||
--> foo/bar:1:15
|
||||
|
|
||||
| foo/bar line 1
|
||||
| ^
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_length_span_column() {
|
||||
let ctx = Context::from("foo/bar");
|
||||
|
||||
// Before the space, after "bar".
|
||||
let span = ctx.span(7, 0);
|
||||
|
||||
assert_report!(
|
||||
"offset at newline",
|
||||
vec![span.mark_error()],
|
||||
// Renders _at_ the character it comes before.
|
||||
"\
|
||||
error: offset at newline
|
||||
--> foo/bar:1:8
|
||||
|
|
||||
| foo/bar line 1
|
||||
| ^
|
||||
"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -218,6 +218,22 @@ pub enum Column {
|
|||
Before(NonZeroU32),
|
||||
}
|
||||
|
||||
impl Column {
|
||||
pub fn start(&self) -> NonZeroU32 {
|
||||
match self {
|
||||
Self::Endpoints(start, _) => *start,
|
||||
Self::Before(start) => *start,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> NonZeroU32 {
|
||||
match self {
|
||||
Self::Endpoints(_, end) => *end,
|
||||
Self::Before(end) => *end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Column {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
@ -270,7 +286,15 @@ impl Display for SourceLine {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// TODO: Just store String instead of a byte vector so we're not
|
||||
// validating UTF-8 twice.
|
||||
write!(f, "{}", String::from_utf8_lossy(&self.text))
|
||||
let s = String::from_utf8_lossy(&self.text);
|
||||
|
||||
let disp = if self.text.last() == Some(&b'\n') {
|
||||
&s[0..s.len() - 1]
|
||||
} else {
|
||||
&s[..]
|
||||
};
|
||||
|
||||
f.write_str(disp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue