1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//! The gutter: everything drawn to the left of the source text.
//!
//! That is the line-number column ([`write_linum`](GraphicalReportHandler::write_linum) /
//! [`write_no_linum`](GraphicalReportHandler::write_no_linum)) and the vertical
//! "gutter" lines that connect the start and end of a multi-line span
//! ([`render_line_gutter`](GraphicalReportHandler::render_line_gutter) /
//! [`render_highlight_gutter`](GraphicalReportHandler::render_highlight_gutter)).
use std::fmt::{self, Write};
use super::{
handler::GraphicalReportHandler,
label::write_repeated_char,
line::Line,
span::{FancySpan, LabelRenderMode},
};
use crate::handlers::theme::DiagnosticColorize;
impl GraphicalReportHandler {
pub(super) fn write_linum(
&self,
f: &mut impl fmt::Write,
width: usize,
linum: usize,
) -> fmt::Result {
f.write_char(' ')?;
if self.theme.styles.linum.is_plain() {
let mut buffer = itoa::Buffer::new();
let linum = buffer.format(linum);
write_repeated_char(f, ' ', width.saturating_sub(linum.len()))?;
f.write_str(linum)?;
} else {
write!(f, "{:width$}", linum.style(self.theme.styles.linum), width = width)?;
}
f.write_char(' ')?;
f.write_char(self.theme.characters.vbar)?;
f.write_char(' ')
}
pub(super) fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
write_repeated_char(f, ' ', width + 2)?;
f.write_char(self.theme.characters.vbar_break)?;
f.write_char(' ')
}
/// Draws the gutter alongside the source line itself: for each active
/// multi-line span this is the corner/arrow that opens or closes it, or a
/// vertical bar for spans that fly past this line.
pub(super) fn render_line_gutter(
&self,
f: &mut impl fmt::Write,
max_gutter: usize,
line: &Line<'_>,
highlights: &[FancySpan],
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
}
let chars = &self.theme.characters;
let mut gutter = String::new();
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) {
write!(gutter, "{}", chars.ltop.style(hl.style))?;
write!(
gutter,
"{}",
chars.hbar.to_string().repeat(max_gutter.saturating_sub(i)).style(hl.style)
)?;
write!(gutter, "{}", chars.rarrow.style(hl.style))?;
arrow = true;
break;
} else if line.span_ends(hl) {
if hl.has_label() {
write!(gutter, "{}", chars.lcross.style(hl.style))?;
} else {
write!(gutter, "{}", chars.lbot.style(hl.style))?;
}
write!(
gutter,
"{}",
chars.hbar.to_string().repeat(max_gutter.saturating_sub(i)).style(hl.style)
)?;
write!(gutter, "{}", chars.rarrow.style(hl.style))?;
arrow = true;
break;
} else if line.span_flyby(hl) {
write!(gutter, "{}", chars.vbar.style(hl.style))?;
} else {
gutter.push(' ');
}
}
write!(
f,
"{}{}",
gutter,
" ".repeat(
if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
)
)?;
Ok(())
}
/// Draws the gutter alongside a highlight/label line (i.e. the lines below
/// the source text). Unlike [`Self::render_line_gutter`] it must keep an
/// explicit column count because the styled string contains ANSI escapes,
/// so its byte length is not its rendered width.
pub(super) fn render_highlight_gutter(
&self,
f: &mut impl fmt::Write,
max_gutter: usize,
line: &Line<'_>,
highlights: &[FancySpan],
render_mode: LabelRenderMode,
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
}
// keeps track of how many columns 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_gutter(hl));
for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) {
if render_mode == LabelRenderMode::BlockRest {
// 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;
} else {
let num_repeat = max_gutter.saturating_sub(i) + 2;
write!(gutter, "{}", chars.lbot.style(hl.style))?;
write!(
gutter,
"{}",
chars
.hbar
.to_string()
.repeat(
num_repeat
// if we are rendering a multiline label, then leave a bit of space for the
// rcross character
- usize::from(render_mode == LabelRenderMode::BlockFirst),
)
.style(hl.style)
)?;
// 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;
}
write!(gutter, "{}", chars.vbar.style(hl.style))?;
// 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;
}
// 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(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GraphicalTheme;
#[test]
fn direct_plain_line_numbers_match_standard_formatting() {
let handler = GraphicalReportHandler::new_themed(GraphicalTheme::none());
for (width, linum) in [(3, 1), (1, usize::MAX)] {
let mut actual = String::new();
handler.write_linum(&mut actual, width, linum).unwrap();
assert_eq!(actual, format!(" {linum:width$} | "));
}
}
}