Skip to content

Commit 7ff4f87

Browse files
authoredNov 15, 2023
fix(graphical): rendering bug on small spans in large spans (#316)
Fixes: #317 It turned out there were two really. One related to how many characters were added for the arrowheads in the gutter, and one where the gutter was extended to a number of characters, including ansi escape codes. However, because ansi escape codes are rather big, there would never be any extension since the system thought the string was already long enough, even though you don't actually see the width of those codes.
1 parent 865d67c commit 7ff4f87

File tree

2 files changed

+63
-12
lines changed

2 files changed

+63
-12
lines changed
 

‎src/handlers/graphical.rs

+60-9
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,
@@ -468,7 +468,7 @@ impl GraphicalReportHandler {
468468
for line in &lines {
469469
let mut num_highlights = 0;
470470
for hl in &labels {
471-
if !line.span_line_only(hl) && line.span_applies(hl) {
471+
if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
472472
num_highlights += 1;
473473
}
474474
}
@@ -674,7 +674,7 @@ impl GraphicalReportHandler {
674674
}
675675
let chars = &self.theme.characters;
676676
let mut gutter = String::new();
677-
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
677+
let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
678678
let mut arrow = false;
679679
for (i, hl) in applicable.enumerate() {
680680
if line.span_starts(hl) {
@@ -735,44 +735,78 @@ impl GraphicalReportHandler {
735735
if max_gutter == 0 {
736736
return Ok(());
737737
}
738+
739+
// keeps track of how many colums wide the gutter is
740+
// important for ansi since simply measuring the size of the final string
741+
// gives the wrong result when the string contains ansi codes.
742+
let mut gutter_cols = 0;
743+
738744
let chars = &self.theme.characters;
739745
let mut gutter = String::new();
740-
let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
746+
let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
741747
for (i, hl) in applicable.enumerate() {
742748
if !line.span_line_only(hl) && line.span_ends(hl) {
743749
if render_mode == LabelRenderMode::MultiLineRest {
744750
// this is to make multiline labels work. We want to make the right amount
745751
// of horizontal space for them, but not actually draw the lines
746-
for _ in 0..max_gutter.saturating_sub(i) + 2 {
752+
let horizontal_space = max_gutter.saturating_sub(i) + 2;
753+
for _ in 0..horizontal_space {
747754
gutter.push(' ');
748755
}
756+
// account for one more horizontal space, since in multiline mode
757+
// we also add in the vertical line before the label like this:
758+
// 2 │ ╭─▶ text
759+
// 3 │ ├─▶ here
760+
// · ╰──┤ these two lines
761+
// · │ are the problem
762+
// ^this
763+
gutter_cols += horizontal_space + 1;
749764
} else {
765+
let num_repeat = max_gutter.saturating_sub(i) + 2;
766+
750767
gutter.push_str(&chars.lbot.style(hl.style).to_string());
751768

752769
gutter.push_str(
753770
&chars
754771
.hbar
755772
.to_string()
756773
.repeat(
757-
max_gutter.saturating_sub(i)
774+
num_repeat
758775
// if we are rendering a multiline label, then leave a bit of space for the
759776
// rcross character
760-
+ if render_mode == LabelRenderMode::MultiLineFirst {
777+
- if render_mode == LabelRenderMode::MultiLineFirst {
761778
1
762779
} else {
763-
2
780+
0
764781
},
765782
)
766783
.style(hl.style)
767784
.to_string(),
768785
);
786+
787+
// we count 1 for the lbot char, and then a few more, the same number
788+
// as we just repeated for. For each repeat we only add 1, even though
789+
// due to ansi escape codes the number of bytes in the string could grow
790+
// a lot each time.
791+
gutter_cols += num_repeat + 1;
769792
}
770793
break;
771794
} else {
772795
gutter.push_str(&chars.vbar.style(hl.style).to_string());
796+
797+
// we may push many bytes for the ansi escape codes style adds,
798+
// but we still only add a single character-width to the string in a terminal
799+
gutter_cols += 1;
773800
}
774801
}
775-
write!(f, "{:width$}", gutter, width = max_gutter + 1)?;
802+
803+
// now calculate how many spaces to add based on how many columns we just created.
804+
// it's the max width of the gutter, minus how many character-widths we just generated
805+
// capped at 0 (though this should never go below in reality), and then we add 3 to
806+
// account for arrowheads when a gutter line ends
807+
let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
808+
// we then write the gutter and as many spaces as we need
809+
write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
776810
Ok(())
777811
}
778812

@@ -1133,16 +1167,33 @@ impl Line {
11331167
span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
11341168
}
11351169

1170+
/// Returns whether `span` should be visible on this line, either in the gutter or under the
1171+
/// text on this line
11361172
fn span_applies(&self, span: &FancySpan) -> bool {
11371173
let spanlen = if span.len() == 0 { 1 } else { span.len() };
11381174
// Span starts in this line
1175+
11391176
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
11401177
// Span passes through this line
11411178
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
11421179
// Span ends on this line
11431180
|| (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
11441181
}
11451182

1183+
/// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
1184+
/// that are only visible on this line and do not span multiple lines)
1185+
fn span_applies_gutter(&self, span: &FancySpan) -> bool {
1186+
let spanlen = if span.len() == 0 { 1 } else { span.len() };
1187+
// Span starts in this line
1188+
self.span_applies(span)
1189+
&& !(
1190+
// as long as it doesn't start *and* end on this line
1191+
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
1192+
&& (span.offset() + spanlen > self.offset
1193+
&& span.offset() + spanlen <= self.offset + self.length)
1194+
)
1195+
}
1196+
11461197
// A 'flyby' is a multi-line span that technically covers this line, but
11471198
// does not begin or end within the line itself. This method is used to
11481199
// calculate gutters.

‎tests/graphical.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,9 @@ if true {
283283
let expected = r#" × oops!
284284
╭─[issue:1:1]
285285
1 │ ╭─▶ if true {
286-
2 │ │╭▶ a
287-
· │
288-
· │ ╰── small
286+
2 │ │ a
287+
· │
288+
· │ ╰── small
289289
3 │ │ } else {
290290
4 │ │ b
291291
5 │ ├─▶ }

0 commit comments

Comments
 (0)
Please sign in to comment.