Skip to content

Commit 865d67c

Browse files
authoredNov 15, 2023
feat(graphical): support rendering labels that contain newlines (#318)
Fixes: #85
1 parent 251d6d5 commit 865d67c

File tree

2 files changed

+435
-49
lines changed

2 files changed

+435
-49
lines changed
 

‎src/handlers/graphical.rs

+264-49
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler:
2020
2121
See [`set_hook()`](crate::set_hook) for more details on customizing your global
2222
printer.
23-
*/
23+
*/
2424
#[derive(Debug, Clone)]
2525
pub struct GraphicalReportHandler {
2626
pub(crate) links: LinkStyle,
@@ -545,7 +545,13 @@ impl GraphicalReportHandler {
545545
// no line number!
546546
self.write_no_linum(f, linum_width)?;
547547
// gutter _again_
548-
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
548+
self.render_highlight_gutter(
549+
f,
550+
max_gutter,
551+
line,
552+
&labels,
553+
LabelRenderMode::SingleLine,
554+
)?;
549555
self.render_single_line_highlights(
550556
f,
551557
line,
@@ -557,11 +563,7 @@ impl GraphicalReportHandler {
557563
}
558564
for hl in multi_line {
559565
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
560-
// no line number!
561-
self.write_no_linum(f, linum_width)?;
562-
// gutter _again_
563-
self.render_highlight_gutter(f, max_gutter, line, &labels)?;
564-
self.render_multi_line_end(f, hl)?;
566+
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
565567
}
566568
}
567569
}
@@ -575,6 +577,91 @@ impl GraphicalReportHandler {
575577
Ok(())
576578
}
577579

580+
fn render_multi_line_end(
581+
&self,
582+
f: &mut impl fmt::Write,
583+
labels: &[FancySpan],
584+
max_gutter: usize,
585+
linum_width: usize,
586+
line: &Line,
587+
label: &FancySpan,
588+
) -> fmt::Result {
589+
// no line number!
590+
self.write_no_linum(f, linum_width)?;
591+
592+
if let Some(label_parts) = label.label_parts() {
593+
// if it has a label, how long is it?
594+
let (first, rest) = label_parts
595+
.split_first()
596+
.expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
597+
598+
if rest.is_empty() {
599+
// gutter _again_
600+
self.render_highlight_gutter(
601+
f,
602+
max_gutter,
603+
line,
604+
&labels,
605+
LabelRenderMode::SingleLine,
606+
)?;
607+
608+
self.render_multi_line_end_single(
609+
f,
610+
first,
611+
label.style,
612+
LabelRenderMode::SingleLine,
613+
)?;
614+
} else {
615+
// gutter _again_
616+
self.render_highlight_gutter(
617+
f,
618+
max_gutter,
619+
line,
620+
&labels,
621+
LabelRenderMode::MultiLineFirst,
622+
)?;
623+
624+
self.render_multi_line_end_single(
625+
f,
626+
first,
627+
label.style,
628+
LabelRenderMode::MultiLineFirst,
629+
)?;
630+
for label_line in rest {
631+
// no line number!
632+
self.write_no_linum(f, linum_width)?;
633+
// gutter _again_
634+
self.render_highlight_gutter(
635+
f,
636+
max_gutter,
637+
line,
638+
&labels,
639+
LabelRenderMode::MultiLineRest,
640+
)?;
641+
self.render_multi_line_end_single(
642+
f,
643+
label_line,
644+
label.style,
645+
LabelRenderMode::MultiLineRest,
646+
)?;
647+
}
648+
}
649+
} else {
650+
// gutter _again_
651+
self.render_highlight_gutter(
652+
f,
653+
max_gutter,
654+
line,
655+
&labels,
656+
LabelRenderMode::SingleLine,
657+
)?;
658+
// has no label
659+
writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
660+
}
661+
662+
Ok(())
663+
}
664+
578665
fn render_line_gutter(
579666
&self,
580667
f: &mut impl fmt::Write,
@@ -643,6 +730,7 @@ impl GraphicalReportHandler {
643730
max_gutter: usize,
644731
line: &Line,
645732
highlights: &[FancySpan],
733+
render_mode: LabelRenderMode,
646734
) -> fmt::Result {
647735
if max_gutter == 0 {
648736
return Ok(());
@@ -652,15 +740,33 @@ impl GraphicalReportHandler {
652740
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
653741
for (i, hl) in applicable.enumerate() {
654742
if !line.span_line_only(hl) && line.span_ends(hl) {
655-
gutter.push_str(&chars.lbot.style(hl.style).to_string());
656-
gutter.push_str(
657-
&chars
658-
.hbar
659-
.to_string()
660-
.repeat(max_gutter.saturating_sub(i) + 2)
661-
.style(hl.style)
662-
.to_string(),
663-
);
743+
if render_mode == LabelRenderMode::MultiLineRest {
744+
// this is to make multiline labels work. We want to make the right amount
745+
// of horizontal space for them, but not actually draw the lines
746+
for _ in 0..max_gutter.saturating_sub(i) + 2 {
747+
gutter.push(' ');
748+
}
749+
} else {
750+
gutter.push_str(&chars.lbot.style(hl.style).to_string());
751+
752+
gutter.push_str(
753+
&chars
754+
.hbar
755+
.to_string()
756+
.repeat(
757+
max_gutter.saturating_sub(i)
758+
// if we are rendering a multiline label, then leave a bit of space for the
759+
// rcross character
760+
+ if render_mode == LabelRenderMode::MultiLineFirst {
761+
1
762+
} else {
763+
2
764+
},
765+
)
766+
.style(hl.style)
767+
.to_string(),
768+
);
769+
}
664770
break;
665771
} else {
666772
gutter.push_str(&chars.vbar.style(hl.style).to_string());
@@ -811,41 +917,121 @@ impl GraphicalReportHandler {
811917
writeln!(f, "{}", underlines)?;
812918

813919
for hl in single_liners.iter().rev() {
814-
if let Some(label) = hl.label() {
815-
self.write_no_linum(f, linum_width)?;
816-
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
817-
let mut curr_offset = 1usize;
818-
for (offset_hl, vbar_offset) in &vbar_offsets {
819-
while curr_offset < *vbar_offset + 1 {
820-
write!(f, " ")?;
821-
curr_offset += 1;
822-
}
823-
if *offset_hl != hl {
824-
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
825-
curr_offset += 1;
826-
} else {
827-
let lines = format!(
828-
"{}{} {}",
829-
chars.lbot,
830-
chars.hbar.to_string().repeat(2),
831-
label,
832-
);
833-
writeln!(f, "{}", lines.style(hl.style))?;
834-
break;
920+
if let Some(label) = hl.label_parts() {
921+
if label.len() == 1 {
922+
self.write_label_text(
923+
f,
924+
line,
925+
linum_width,
926+
max_gutter,
927+
all_highlights,
928+
chars,
929+
&vbar_offsets,
930+
hl,
931+
&label[0],
932+
LabelRenderMode::SingleLine,
933+
)?;
934+
} else {
935+
let mut first = true;
936+
for label_line in &label {
937+
self.write_label_text(
938+
f,
939+
line,
940+
linum_width,
941+
max_gutter,
942+
all_highlights,
943+
chars,
944+
&vbar_offsets,
945+
hl,
946+
label_line,
947+
if first {
948+
LabelRenderMode::MultiLineFirst
949+
} else {
950+
LabelRenderMode::MultiLineRest
951+
},
952+
)?;
953+
first = false;
835954
}
836955
}
837956
}
838957
}
839958
Ok(())
840959
}
841960

842-
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
843-
writeln!(
961+
// I know it's not good practice, but making this a function makes a lot of sense
962+
// and making a struct for this does not...
963+
#[allow(clippy::too_many_arguments)]
964+
fn write_label_text(
965+
&self,
966+
f: &mut impl fmt::Write,
967+
line: &Line,
968+
linum_width: usize,
969+
max_gutter: usize,
970+
all_highlights: &[FancySpan],
971+
chars: &ThemeCharacters,
972+
vbar_offsets: &[(&&FancySpan, usize)],
973+
hl: &&FancySpan,
974+
label: &str,
975+
render_mode: LabelRenderMode,
976+
) -> fmt::Result {
977+
self.write_no_linum(f, linum_width)?;
978+
self.render_highlight_gutter(
844979
f,
845-
"{} {}",
846-
self.theme.characters.hbar.style(hl.style),
847-
hl.label().unwrap_or_else(|| "".into()),
980+
max_gutter,
981+
line,
982+
all_highlights,
983+
LabelRenderMode::SingleLine,
848984
)?;
985+
let mut curr_offset = 1usize;
986+
for (offset_hl, vbar_offset) in vbar_offsets {
987+
while curr_offset < *vbar_offset + 1 {
988+
write!(f, " ")?;
989+
curr_offset += 1;
990+
}
991+
if *offset_hl != hl {
992+
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
993+
curr_offset += 1;
994+
} else {
995+
let lines = match render_mode {
996+
LabelRenderMode::SingleLine => format!(
997+
"{}{} {}",
998+
chars.lbot,
999+
chars.hbar.to_string().repeat(2),
1000+
label,
1001+
),
1002+
LabelRenderMode::MultiLineFirst => {
1003+
format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
1004+
}
1005+
LabelRenderMode::MultiLineRest => {
1006+
format!(" {} {}", chars.vbar, label,)
1007+
}
1008+
};
1009+
writeln!(f, "{}", lines.style(hl.style))?;
1010+
break;
1011+
}
1012+
}
1013+
Ok(())
1014+
}
1015+
1016+
fn render_multi_line_end_single(
1017+
&self,
1018+
f: &mut impl fmt::Write,
1019+
label: &str,
1020+
style: Style,
1021+
render_mode: LabelRenderMode,
1022+
) -> fmt::Result {
1023+
match render_mode {
1024+
LabelRenderMode::SingleLine => {
1025+
writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
1026+
}
1027+
LabelRenderMode::MultiLineFirst => {
1028+
writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
1029+
}
1030+
LabelRenderMode::MultiLineRest => {
1031+
writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
1032+
}
1033+
}
1034+
8491035
Ok(())
8501036
}
8511037

@@ -924,6 +1110,16 @@ impl ReportHandler for GraphicalReportHandler {
9241110
Support types
9251111
*/
9261112

1113+
#[derive(PartialEq, Debug)]
1114+
enum LabelRenderMode {
1115+
/// we're rendering a single line label (or not rendering in any special way)
1116+
SingleLine,
1117+
/// we're rendering a multiline label
1118+
MultiLineFirst,
1119+
/// we're rendering the rest of a multiline label
1120+
MultiLineRest,
1121+
}
1122+
9271123
#[derive(Debug)]
9281124
struct Line {
9291125
line_number: usize,
@@ -941,10 +1137,10 @@ impl Line {
9411137
let spanlen = if span.len() == 0 { 1 } else { span.len() };
9421138
// Span starts in this line
9431139
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
944-
// Span passes through this line
945-
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
946-
// Span ends on this line
947-
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
1140+
// Span passes through this line
1141+
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
1142+
// Span ends on this line
1143+
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
9481144
}
9491145

9501146
// A 'flyby' is a multi-line span that technically covers this line, but
@@ -974,7 +1170,10 @@ impl Line {
9741170

9751171
#[derive(Debug, Clone)]
9761172
struct FancySpan {
977-
label: Option<String>,
1173+
/// this is deliberately an option of a vec because I wanted to be very explicit
1174+
/// that there can also be *no* label. If there is a label, it can have multiple
1175+
/// lines which is what the vec is for.
1176+
label: Option<Vec<String>>,
9781177
span: SourceSpan,
9791178
style: Style,
9801179
}
@@ -985,9 +1184,17 @@ impl PartialEq for FancySpan {
9851184
}
9861185
}
9871186

1187+
fn split_label(v: String) -> Vec<String> {
1188+
v.split('\n').map(|i| i.to_string()).collect()
1189+
}
1190+
9881191
impl FancySpan {
9891192
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
990-
FancySpan { label, span, style }
1193+
FancySpan {
1194+
label: label.map(split_label),
1195+
span,
1196+
style,
1197+
}
9911198
}
9921199

9931200
fn style(&self) -> Style {
@@ -997,7 +1204,15 @@ impl FancySpan {
9971204
fn label(&self) -> Option<String> {
9981205
self.label
9991206
.as_ref()
1000-
.map(|l| l.style(self.style()).to_string())
1207+
.map(|l| l.join("\n").style(self.style()).to_string())
1208+
}
1209+
1210+
fn label_parts(&self) -> Option<Vec<String>> {
1211+
self.label.as_ref().map(|l| {
1212+
l.iter()
1213+
.map(|i| i.style(self.style()).to_string())
1214+
.collect()
1215+
})
10011216
}
10021217

10031218
fn offset(&self) -> usize {

‎tests/graphical.rs

+171
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,52 @@ fn empty_source() -> Result<(), MietteError> {
251251
Ok(())
252252
}
253253

254+
#[test]
255+
fn multiple_spans_multiline() {
256+
#[derive(Error, Debug, Diagnostic)]
257+
#[error("oops!")]
258+
#[diagnostic(severity(Error))]
259+
struct MyBad {
260+
#[source_code]
261+
src: NamedSource,
262+
#[label("big")]
263+
big: SourceSpan,
264+
#[label("small")]
265+
small: SourceSpan,
266+
}
267+
let err = MyBad {
268+
src: NamedSource::new(
269+
"issue",
270+
"\
271+
if true {
272+
a
273+
} else {
274+
b
275+
}",
276+
),
277+
big: (0, 32).into(),
278+
small: (14, 1).into(),
279+
};
280+
let out = fmt_report(err.into());
281+
println!("Error: {}", out);
282+
283+
let expected = r#" × oops!
284+
╭─[issue:1:1]
285+
1 │ ╭─▶ if true {
286+
2 │ │╭▶ a
287+
· ││ ┬
288+
· ││ ╰── small
289+
3 │ │ } else {
290+
4 │ │ b
291+
5 │ ├─▶ }
292+
· ╰──── big
293+
╰────
294+
"#
295+
.to_string();
296+
297+
assert_eq!(expected, out);
298+
}
299+
254300
#[test]
255301
fn single_line_highlight_span_full_line() {
256302
#[derive(Error, Debug, Diagnostic)]
@@ -725,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
725771
Ok(())
726772
}
727773

774+
#[test]
775+
fn multiline_label() -> Result<(), MietteError> {
776+
#[derive(Debug, Diagnostic, Error)]
777+
#[error("oops!")]
778+
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
779+
struct MyBad {
780+
#[source_code]
781+
src: NamedSource,
782+
#[label("this bit here\nand\nthis\ntoo")]
783+
highlight: SourceSpan,
784+
}
785+
786+
let src = "source\ntext\n here".to_string();
787+
let err = MyBad {
788+
src: NamedSource::new("bad_file.rs", src),
789+
highlight: (7, 4).into(),
790+
};
791+
let out = fmt_report(err.into());
792+
println!("Error: {}", out);
793+
let expected = r#"oops::my::bad
794+
795+
× oops!
796+
╭─[bad_file.rs:2:1]
797+
1 │ source
798+
2 │ text
799+
· ──┬─
800+
· ╰─┤ this bit here
801+
· │ and
802+
· │ this
803+
· │ too
804+
3 │ here
805+
╰────
806+
help: try doing it better next time?
807+
"#
808+
.trim_start()
809+
.to_string();
810+
assert_eq!(expected, out);
811+
Ok(())
812+
}
813+
814+
#[test]
815+
fn multiple_multi_line_labels() -> Result<(), MietteError> {
816+
#[derive(Debug, Diagnostic, Error)]
817+
#[error("oops!")]
818+
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
819+
struct MyBad {
820+
#[source_code]
821+
src: NamedSource,
822+
#[label = "x\ny"]
823+
highlight1: SourceSpan,
824+
#[label = "z\nw"]
825+
highlight2: SourceSpan,
826+
#[label = "a\nb"]
827+
highlight3: SourceSpan,
828+
}
829+
830+
let src = "source\n text text text text text\n here".to_string();
831+
let err = MyBad {
832+
src: NamedSource::new("bad_file.rs", src),
833+
highlight1: (9, 4).into(),
834+
highlight2: (14, 4).into(),
835+
highlight3: (24, 4).into(),
836+
};
837+
let out = fmt_report(err.into());
838+
println!("Error: {}", out);
839+
let expected = r#"oops::my::bad
840+
841+
× oops!
842+
╭─[bad_file.rs:2:3]
843+
1 │ source
844+
2 │ text text text text text
845+
· ──┬─ ──┬─ ──┬─
846+
· │ │ ╰─┤ a
847+
· │ │ │ b
848+
· │ ╰─┤ z
849+
· │ │ w
850+
· ╰─┤ x
851+
· │ y
852+
3 │ here
853+
╰────
854+
help: try doing it better next time?
855+
"#
856+
.trim_start()
857+
.to_string();
858+
assert_eq!(expected, out);
859+
Ok(())
860+
}
861+
728862
#[test]
729863
fn multiple_same_line_highlights() -> Result<(), MietteError> {
730864
#[derive(Debug, Diagnostic, Error)]
@@ -853,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
853987
Ok(())
854988
}
855989

990+
#[test]
991+
fn multiline_highlight_multiline_label() -> Result<(), MietteError> {
992+
#[derive(Debug, Diagnostic, Error)]
993+
#[error("oops!")]
994+
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
995+
struct MyBad {
996+
#[source_code]
997+
src: NamedSource,
998+
#[label = "these two lines\nare the problem"]
999+
highlight: SourceSpan,
1000+
}
1001+
1002+
let src = "source\n text\n here".to_string();
1003+
let err = MyBad {
1004+
src: NamedSource::new("bad_file.rs", src),
1005+
highlight: (9, 11).into(),
1006+
};
1007+
let out = fmt_report(err.into());
1008+
println!("Error: {}", out);
1009+
let expected = r#"oops::my::bad
1010+
1011+
× oops!
1012+
╭─[bad_file.rs:2:3]
1013+
1 │ source
1014+
2 │ ╭─▶ text
1015+
3 │ ├─▶ here
1016+
· ╰──┤ these two lines
1017+
· │ are the problem
1018+
╰────
1019+
help: try doing it better next time?
1020+
"#
1021+
.trim_start()
1022+
.to_string();
1023+
assert_eq!(expected, out);
1024+
Ok(())
1025+
}
1026+
8561027
#[test]
8571028
fn multiline_highlight_flyby() -> Result<(), MietteError> {
8581029
#[derive(Debug, Diagnostic, Error)]

0 commit comments

Comments
 (0)
Please sign in to comment.