diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 44d92b77..144eb0da 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -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, @@ -487,7 +487,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, @@ -499,11 +505,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)?; } } } @@ -517,6 +519,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, @@ -585,6 +672,7 @@ impl GraphicalReportHandler { max_gutter: usize, line: &Line, highlights: &[FancySpan], + render_mode: LabelRenderMode, ) -> fmt::Result { if max_gutter == 0 { return Ok(()); @@ -594,15 +682,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()); @@ -751,27 +857,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; } } } @@ -779,13 +898,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(()) } @@ -864,6 +1050,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, @@ -881,10 +1077,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 @@ -914,7 +1110,10 @@ impl Line { #[derive(Debug, Clone)] struct FancySpan { - label: Option, + /// 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>, span: SourceSpan, style: Style, } @@ -925,9 +1124,17 @@ impl PartialEq for FancySpan { } } +fn split_label(v: String) -> Vec { + v.split('\n').map(|i| i.to_string()).collect() +} + impl FancySpan { fn new(label: Option, span: SourceSpan, style: Style) -> Self { - FancySpan { label, span, style } + FancySpan { + label: label.map(split_label), + span, + style, + } } fn style(&self) -> Style { @@ -937,7 +1144,15 @@ impl FancySpan { fn label(&self) -> Option { 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> { + self.label.as_ref().map(|l| { + l.iter() + .map(|i| i.style(self.style()).to_string()) + .collect() + }) } fn offset(&self) -> usize { diff --git a/tests/graphical.rs b/tests/graphical.rs index 31db4dc1..12195010 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -67,6 +67,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)] @@ -541,6 +587,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)] @@ -669,6 +803,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)]