From 5eea2536080fa20ef35ac2080414fd611e5fd09c Mon Sep 17 00:00:00 2001 From: jdonszelmann Date: Thu, 9 Nov 2023 00:01:25 +0100 Subject: [PATCH] Fix rendering bug 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. --- src/handlers/graphical.rs | 60 +++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 44d92b77..c15c2033 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -410,7 +410,7 @@ impl GraphicalReportHandler { for line in &lines { let mut num_highlights = 0; for hl in &labels { - if !line.span_line_only(hl) && line.span_applies(hl) { + if !line.span_line_only(hl) && line.span_applies_gutter(hl) { num_highlights += 1; } } @@ -529,7 +529,7 @@ impl GraphicalReportHandler { } let chars = &self.theme.characters; let mut gutter = String::new(); - let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); + let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); let mut arrow = false; for (i, hl) in applicable.enumerate() { if line.span_starts(hl) { @@ -589,26 +589,51 @@ impl GraphicalReportHandler { if max_gutter == 0 { return Ok(()); } + + // keeps track of how many colums wide the gutter is + // important for ansi since simply measuring the size of the final string + // gives the wrong result when the string contains ansi codes. + let mut gutter_cols = 0; + let chars = &self.theme.characters; let mut gutter = String::new(); - let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); + 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) { + let num_repeat = max_gutter.saturating_sub(i) + 2; + gutter.push_str(&chars.lbot.style(hl.style).to_string()); gutter.push_str( &chars .hbar .to_string() - .repeat(max_gutter.saturating_sub(i) + 2) + .repeat(num_repeat) .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; break; } else { gutter.push_str(&chars.vbar.style(hl.style).to_string()); + + // we may push many bytes for the ansi escape codes style adds, + // but we still only add a single character-width to the string in a terminal + gutter_cols += 1; } } - write!(f, "{:width$}", gutter, width = max_gutter + 1)?; + + // now calculate how many spaces to add based on how many columns we just created. + // it's the max width of the gutter, minus how many character-widths we just generated + // capped at 0 (though this should never go below in reality), and then we add 3 to + // account for arrowheads when a gutter line ends + let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols); + // we then write the gutter and as many spaces as we need + write!(f, "{}{:width$}", gutter, "", width = num_spaces)?; Ok(()) } @@ -877,14 +902,31 @@ impl Line { span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length } + /// Returns whether `span` should be visible on this line, either in the gutter or under the + /// text on this line fn span_applies(&self, span: &FancySpan) -> bool { 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) + } + + /// Returns whether `span` should be visible on this line in the gutter (so this excludes spans + /// that are only visible on this line and do not span multiple lines) + fn span_applies_gutter(&self, span: &FancySpan) -> bool { + let spanlen = if span.len() == 0 { 1 } else { span.len() }; + // Span starts in this line + self.span_applies(span) + && !( + // as long as it doesn't start *and* end on this line + (span.offset() >= self.offset && span.offset() < self.offset + self.length) + && (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