feat(graphical): support rendering labels that contain newlines (#318)

Fixes: https://github.com/zkat/miette/issues/85
This commit is contained in:
Jonathan Dönszelmann 2023-11-15 19:39:29 +01:00 committed by GitHub
parent 251d6d5929
commit 865d67c8dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 435 additions and 49 deletions

View File

@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler:
See [`set_hook()`](crate::set_hook) for more details on customizing your global
printer.
*/
*/
#[derive(Debug, Clone)]
pub struct GraphicalReportHandler {
pub(crate) links: LinkStyle,
@ -545,7 +545,13 @@ impl GraphicalReportHandler {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::SingleLine,
)?;
self.render_single_line_highlights(
f,
line,
@ -557,11 +563,7 @@ impl GraphicalReportHandler {
}
for hl in multi_line {
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
self.render_multi_line_end(f, hl)?;
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
}
}
}
@ -575,6 +577,91 @@ impl GraphicalReportHandler {
Ok(())
}
fn render_multi_line_end(
&self,
f: &mut impl fmt::Write,
labels: &[FancySpan],
max_gutter: usize,
linum_width: usize,
line: &Line,
label: &FancySpan,
) -> fmt::Result {
// no line number!
self.write_no_linum(f, linum_width)?;
if let Some(label_parts) = label.label_parts() {
// if it has a label, how long is it?
let (first, rest) = label_parts
.split_first()
.expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
if rest.is_empty() {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::SingleLine,
)?;
self.render_multi_line_end_single(
f,
first,
label.style,
LabelRenderMode::SingleLine,
)?;
} else {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::MultiLineFirst,
)?;
self.render_multi_line_end_single(
f,
first,
label.style,
LabelRenderMode::MultiLineFirst,
)?;
for label_line in rest {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::MultiLineRest,
)?;
self.render_multi_line_end_single(
f,
label_line,
label.style,
LabelRenderMode::MultiLineRest,
)?;
}
}
} else {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
&labels,
LabelRenderMode::SingleLine,
)?;
// has no label
writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
}
Ok(())
}
fn render_line_gutter(
&self,
f: &mut impl fmt::Write,
@ -643,6 +730,7 @@ impl GraphicalReportHandler {
max_gutter: usize,
line: &Line,
highlights: &[FancySpan],
render_mode: LabelRenderMode,
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
@ -652,15 +740,33 @@ impl GraphicalReportHandler {
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) {
gutter.push_str(&chars.lbot.style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(max_gutter.saturating_sub(i) + 2)
.style(hl.style)
.to_string(),
);
if render_mode == LabelRenderMode::MultiLineRest {
// this is to make multiline labels work. We want to make the right amount
// of horizontal space for them, but not actually draw the lines
for _ in 0..max_gutter.saturating_sub(i) + 2 {
gutter.push(' ');
}
} else {
gutter.push_str(&chars.lbot.style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(
max_gutter.saturating_sub(i)
// if we are rendering a multiline label, then leave a bit of space for the
// rcross character
+ if render_mode == LabelRenderMode::MultiLineFirst {
1
} else {
2
},
)
.style(hl.style)
.to_string(),
);
}
break;
} else {
gutter.push_str(&chars.vbar.style(hl.style).to_string());
@ -811,27 +917,40 @@ impl GraphicalReportHandler {
writeln!(f, "{}", underlines)?;
for hl in single_liners.iter().rev() {
if let Some(label) = hl.label() {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
let mut curr_offset = 1usize;
for (offset_hl, vbar_offset) in &vbar_offsets {
while curr_offset < *vbar_offset + 1 {
write!(f, " ")?;
curr_offset += 1;
}
if *offset_hl != hl {
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
curr_offset += 1;
} else {
let lines = format!(
"{}{} {}",
chars.lbot,
chars.hbar.to_string().repeat(2),
label,
);
writeln!(f, "{}", lines.style(hl.style))?;
break;
if let Some(label) = hl.label_parts() {
if label.len() == 1 {
self.write_label_text(
f,
line,
linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
&label[0],
LabelRenderMode::SingleLine,
)?;
} else {
let mut first = true;
for label_line in &label {
self.write_label_text(
f,
line,
linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
label_line,
if first {
LabelRenderMode::MultiLineFirst
} else {
LabelRenderMode::MultiLineRest
},
)?;
first = false;
}
}
}
@ -839,13 +958,80 @@ impl GraphicalReportHandler {
Ok(())
}
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
writeln!(
// I know it's not good practice, but making this a function makes a lot of sense
// and making a struct for this does not...
#[allow(clippy::too_many_arguments)]
fn write_label_text(
&self,
f: &mut impl fmt::Write,
line: &Line,
linum_width: usize,
max_gutter: usize,
all_highlights: &[FancySpan],
chars: &ThemeCharacters,
vbar_offsets: &[(&&FancySpan, usize)],
hl: &&FancySpan,
label: &str,
render_mode: LabelRenderMode,
) -> fmt::Result {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(
f,
"{} {}",
self.theme.characters.hbar.style(hl.style),
hl.label().unwrap_or_else(|| "".into()),
max_gutter,
line,
all_highlights,
LabelRenderMode::SingleLine,
)?;
let mut curr_offset = 1usize;
for (offset_hl, vbar_offset) in vbar_offsets {
while curr_offset < *vbar_offset + 1 {
write!(f, " ")?;
curr_offset += 1;
}
if *offset_hl != hl {
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
curr_offset += 1;
} else {
let lines = match render_mode {
LabelRenderMode::SingleLine => format!(
"{}{} {}",
chars.lbot,
chars.hbar.to_string().repeat(2),
label,
),
LabelRenderMode::MultiLineFirst => {
format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
}
LabelRenderMode::MultiLineRest => {
format!(" {} {}", chars.vbar, label,)
}
};
writeln!(f, "{}", lines.style(hl.style))?;
break;
}
}
Ok(())
}
fn render_multi_line_end_single(
&self,
f: &mut impl fmt::Write,
label: &str,
style: Style,
render_mode: LabelRenderMode,
) -> fmt::Result {
match render_mode {
LabelRenderMode::SingleLine => {
writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
}
LabelRenderMode::MultiLineFirst => {
writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
}
LabelRenderMode::MultiLineRest => {
writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
}
}
Ok(())
}
@ -924,6 +1110,16 @@ impl ReportHandler for GraphicalReportHandler {
Support types
*/
#[derive(PartialEq, Debug)]
enum LabelRenderMode {
/// we're rendering a single line label (or not rendering in any special way)
SingleLine,
/// we're rendering a multiline label
MultiLineFirst,
/// we're rendering the rest of a multiline label
MultiLineRest,
}
#[derive(Debug)]
struct Line {
line_number: usize,
@ -941,10 +1137,10 @@ impl Line {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
// Span starts in this line
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
// Span passes through this line
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
// Span passes through this line
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
}
// A 'flyby' is a multi-line span that technically covers this line, but
@ -974,7 +1170,10 @@ impl Line {
#[derive(Debug, Clone)]
struct FancySpan {
label: Option<String>,
/// this is deliberately an option of a vec because I wanted to be very explicit
/// that there can also be *no* label. If there is a label, it can have multiple
/// lines which is what the vec is for.
label: Option<Vec<String>>,
span: SourceSpan,
style: Style,
}
@ -985,9 +1184,17 @@ impl PartialEq for FancySpan {
}
}
fn split_label(v: String) -> Vec<String> {
v.split('\n').map(|i| i.to_string()).collect()
}
impl FancySpan {
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
FancySpan { label, span, style }
FancySpan {
label: label.map(split_label),
span,
style,
}
}
fn style(&self) -> Style {
@ -997,7 +1204,15 @@ impl FancySpan {
fn label(&self) -> Option<String> {
self.label
.as_ref()
.map(|l| l.style(self.style()).to_string())
.map(|l| l.join("\n").style(self.style()).to_string())
}
fn label_parts(&self) -> Option<Vec<String>> {
self.label.as_ref().map(|l| {
l.iter()
.map(|i| i.style(self.style()).to_string())
.collect()
})
}
fn offset(&self) -> usize {

View File

@ -251,6 +251,52 @@ fn empty_source() -> Result<(), MietteError> {
Ok(())
}
#[test]
fn multiple_spans_multiline() {
#[derive(Error, Debug, Diagnostic)]
#[error("oops!")]
#[diagnostic(severity(Error))]
struct MyBad {
#[source_code]
src: NamedSource,
#[label("big")]
big: SourceSpan,
#[label("small")]
small: SourceSpan,
}
let err = MyBad {
src: NamedSource::new(
"issue",
"\
if true {
a
} else {
b
}",
),
big: (0, 32).into(),
small: (14, 1).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#" × oops!
[issue:1:1]
1 if true {
2 a
·
· small
3 } else {
4 b
5 }
· big
"#
.to_string();
assert_eq!(expected, out);
}
#[test]
fn single_line_highlight_span_full_line() {
#[derive(Error, Debug, Diagnostic)]
@ -725,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
Ok(())
}
#[test]
fn multiline_label() -> 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\nand\nthis\ntoo")]
highlight: SourceSpan,
}
let src = "source\ntext\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (7, 4).into(),
};
let out = fmt_report(err.into());
println!("Error: {}", out);
let expected = r#"oops::my::bad
× oops!
[bad_file.rs:2:1]
1 source
2 text
·
· this bit here
· and
· this
· too
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_multi_line_labels() -> 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 = "x\ny"]
highlight1: SourceSpan,
#[label = "z\nw"]
highlight2: SourceSpan,
#[label = "a\nb"]
highlight3: SourceSpan,
}
let src = "source\n text text text text text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight1: (9, 4).into(),
highlight2: (14, 4).into(),
highlight3: (24, 4).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 text text text text
·
· a
· b
· z
· w
· x
· y
3 here
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@ -853,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
Ok(())
}
#[test]
fn multiline_highlight_multiline_label() -> 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 = "these two lines\nare the problem"]
highlight: SourceSpan,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 11).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
3 here
· these two lines
· are the problem
help: try doing it better next time?
"#
.trim_start()
.to_string();
assert_eq!(expected, out);
Ok(())
}
#[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]