From d696bf33163c13b998fa8361c46fa391cf8ae462 Mon Sep 17 00:00:00 2001 From: beeb <703631+beeb@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:52:24 +0100 Subject: [PATCH 1/2] fix: continuation line on multiline labels --- src/handlers/graphical.rs | 85 ++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 37b6bf8..c6a1ade 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -740,7 +740,7 @@ impl GraphicalReportHandler { max_gutter, line, &labels, - LabelRenderMode::SingleLine, + LabelRenderMode::MultiLineEndPending, )?; self.render_single_line_highlights( f, @@ -930,32 +930,41 @@ impl GraphicalReportHandler { let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); for (i, hl) in applicable.enumerate() { if !line.span_line_only(hl) && line.span_ends(hl) { - 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 - let horizontal_space = max_gutter.saturating_sub(i) + 2; - for _ in 0..horizontal_space { - gutter.push(' '); + match 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 + let horizontal_space = max_gutter.saturating_sub(i) + 2; + for _ in 0..horizontal_space { + gutter.push(' '); + } + // account for one more horizontal space, since in multiline mode + // we also add in the vertical line before the label like this: + // 2 │ ╭─▶ text + // 3 │ ├─▶ here + // · ╰──┤ these two lines + // · │ are the problem + // ^this + gutter_cols += horizontal_space + 1; } - // account for one more horizontal space, since in multiline mode - // we also add in the vertical line before the label like this: - // 2 │ ╭─▶ text - // 3 │ ├─▶ here - // · ╰──┤ these two lines - // · │ are the problem - // ^this - gutter_cols += horizontal_space + 1; - } else { - let num_repeat = max_gutter.saturating_sub(i) + 2; + LabelRenderMode::MultiLineEndPending => { + // we're rendering continuation lines for single-line highlights on a line + // where a multiline span ends + // render │ to show the span is still visually "open" until we render its own label with ╰── + gutter.push_str(&chars.vbar.style(hl.style).to_string()); + gutter_cols += 1; + } + LabelRenderMode::SingleLine | LabelRenderMode::MultiLineFirst => { + let num_repeat = max_gutter.saturating_sub(i) + 2; - gutter.push_str(&chars.lbot.style(hl.style).to_string()); + gutter.push_str(&chars.lbot.style(hl.style).to_string()); - gutter.push_str( - &chars - .hbar - .to_string() - .repeat( - num_repeat + gutter.push_str( + &chars + .hbar + .to_string() + .repeat( + num_repeat // if we are rendering a multiline label, then leave a bit of space for the // rcross character - if render_mode == LabelRenderMode::MultiLineFirst { @@ -963,16 +972,17 @@ impl GraphicalReportHandler { } else { 0 }, - ) - .style(hl.style) - .to_string(), - ); + ) + .style(hl.style) + .to_string(), + ); - // we count 1 for the lbot char, and then a few more, the same number - // as we just repeated for. For each repeat we only add 1, even though - // due to ansi escape codes the number of bytes in the string could grow - // a lot each time. - gutter_cols += num_repeat + 1; + // we count 1 for the lbot char, and then a few more, the same number + // as we just repeated for. For each repeat we only add 1, even though + // due to ansi escape codes the number of bytes in the string could grow + // a lot each time. + gutter_cols += num_repeat + 1; + } } break; } else { @@ -1244,7 +1254,7 @@ impl GraphicalReportHandler { max_gutter, line, all_highlights, - LabelRenderMode::SingleLine, + LabelRenderMode::MultiLineEndPending, )?; let mut curr_offset = 1usize; for (offset_hl, vbar_offset) in vbar_offsets { @@ -1257,7 +1267,7 @@ impl GraphicalReportHandler { curr_offset += 1; } else { let lines = match render_mode { - LabelRenderMode::SingleLine => format!( + LabelRenderMode::SingleLine | LabelRenderMode::MultiLineEndPending => format!( "{}{} {}", chars.lbot, chars.hbar.to_string().repeat(2), @@ -1285,7 +1295,7 @@ impl GraphicalReportHandler { render_mode: LabelRenderMode, ) -> fmt::Result { match render_mode { - LabelRenderMode::SingleLine => { + LabelRenderMode::SingleLine | LabelRenderMode::MultiLineEndPending => { writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?; } LabelRenderMode::MultiLineFirst => { @@ -1382,6 +1392,9 @@ enum LabelRenderMode { MultiLineFirst, /// we're rendering the rest of a multiline label MultiLineRest, + /// we're rendering continuation lines for single-line highlights on a line where + /// a multi-line span ends - render │ instead of ╰── for ending spans + MultiLineEndPending, } #[derive(Debug)] From 638d0bde10b0c2e75bd445c2ed6d32c598408d19 Mon Sep 17 00:00:00 2001 From: beeb <703631+beeb@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:59:09 +0100 Subject: [PATCH 2/2] test: add regression test for multiline labels --- tests/graphical.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/graphical.rs b/tests/graphical.rs index 93c9bce..8a05ac3 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -489,6 +489,54 @@ if true { assert_eq!(expected, out); } +#[test] +fn multiline_span_end_same_line_as_singleline() { + #[derive(Error, Debug, Diagnostic)] + #[error("oops!")] + #[diagnostic(severity(Error))] + struct MyBad { + #[source_code] + src: NamedSource<&'static str>, + #[label("multiline label")] + multiline: SourceSpan, + #[label("singleline label")] + singleline: SourceSpan, + } + let src = "\ + function test( + bool a, + ) internal returns (uint256 helloWorld, Test someTest) { + // oh hai + } +"; + let err = MyBad { + src: NamedSource::new("issue", src), + // multiline span from "function" to end of ")" + multiline: (0, 38).into(), + // singleline span on "uint256 helloWorld" + singleline: (55, 18).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + + // The │ should continue on the underline and label lines until the multiline span's + // own label is rendered with ╰──── + let expected = r#" + × oops! + ╭─[issue:1:1] + 1 │ ╭─▶ function test( + 2 │ │ bool a, + 3 │ ├─▶ ) internal returns (uint256 helloWorld, Test someTest) { + · │ ─────────┬──────── + · │ ╰── singleline label + · ╰──── multiline label + 4 │ // oh hai + ╰──── +"# + .trim_start_matches('\n'); + assert_eq!(expected, out); +} + #[test] fn single_line_highlight_span_full_line() { #[derive(Error, Debug, Diagnostic)]