Skip to content

Commit e364000

Browse files
committed
Add strip code to the left and right for long lines
1 parent 26fb6e1 commit e364000

File tree

10 files changed

+299
-6
lines changed

10 files changed

+299
-6
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ coveralls = { repository = "rust-lang/annotate-snippets-rs", branch = "master",
1616
maintenance = { status = "actively-developed" }
1717

1818
[dependencies]
19+
unicode-width = "0.1"
1920
yansi-term = { version = "0.1", optional = true }
2021

2122
[dev-dependencies]

src/display_list/from_snippet.rs

+22-4
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,13 @@ fn format_slice(
107107
slice: snippet::Slice<'_>,
108108
is_first: bool,
109109
has_footer: bool,
110+
margin: Option<Margin>,
110111
) -> Vec<DisplayLine<'_>> {
111112
let main_range = slice.annotations.get(0).map(|x| x.range.0);
112113
let origin = slice.origin;
113114
let line_start = slice.line_start;
114115
let need_empty_header = origin.is_some() || is_first;
115-
let mut body = format_body(slice, need_empty_header, has_footer);
116+
let mut body = format_body(slice, need_empty_header, has_footer, margin);
116117
let header = format_header(origin, main_range, line_start, &body, is_first);
117118
let mut result = vec![];
118119

@@ -273,6 +274,7 @@ fn format_body(
273274
slice: snippet::Slice<'_>,
274275
need_empty_header: bool,
275276
has_footer: bool,
277+
margin: Option<Margin>,
276278
) -> Vec<DisplayLine<'_>> {
277279
let source_len = slice.source.chars().count();
278280
if let Some(bigger) = slice.annotations.iter().find_map(|x| {
@@ -312,6 +314,9 @@ fn format_body(
312314
let mut annotation_line_count = 0;
313315
let mut annotations = slice.annotations;
314316
for (idx, (line_start, line_end)) in line_index_ranges.into_iter().enumerate() {
317+
let margin_left = margin
318+
.map(|m| m.left(line_end - line_start))
319+
.unwrap_or_default();
315320
// It would be nice to use filter_drain here once it's stable.
316321
annotations = annotations
317322
.into_iter()
@@ -328,7 +333,10 @@ fn format_body(
328333
if start >= line_start && end <= line_end
329334
|| start == line_end && end - start <= 1 =>
330335
{
331-
let range = (start - line_start, end - line_start);
336+
let range = (
337+
(start - line_start) - margin_left,
338+
(end - line_start) - margin_left,
339+
);
332340
body.insert(
333341
body_idx + 1,
334342
DisplayLine::Source {
@@ -419,7 +427,10 @@ fn format_body(
419427
});
420428
}
421429

422-
let range = (end - line_start, end - line_start + 1);
430+
let range = (
431+
(end - line_start) - margin_left,
432+
(end - line_start + 1) - margin_left,
433+
);
423434
body.insert(
424435
body_idx + 1,
425436
DisplayLine::Source {
@@ -499,7 +510,12 @@ impl<'a> From<snippet::Snippet<'a>> for DisplayList<'a> {
499510
}
500511

501512
for (idx, slice) in slices.into_iter().enumerate() {
502-
body.append(&mut format_slice(slice, idx == 0, !footer.is_empty()));
513+
body.append(&mut format_slice(
514+
slice,
515+
idx == 0,
516+
!footer.is_empty(),
517+
opt.margin,
518+
));
503519
}
504520

505521
for annotation in footer {
@@ -509,12 +525,14 @@ impl<'a> From<snippet::Snippet<'a>> for DisplayList<'a> {
509525
let FormatOptions {
510526
color,
511527
anonymized_line_numbers,
528+
margin,
512529
} = opt;
513530

514531
Self {
515532
body,
516533
stylesheet: get_term_style(color),
517534
anonymized_line_numbers,
535+
margin,
518536
}
519537
}
520538
}

src/display_list/structs.rs

+118
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::cmp::{max, min};
12
use std::fmt;
23

34
use crate::formatter::{get_term_style, style::Stylesheet};
@@ -7,6 +8,7 @@ pub struct DisplayList<'a> {
78
pub body: Vec<DisplayLine<'a>>,
89
pub stylesheet: Box<dyn Stylesheet>,
910
pub anonymized_line_numbers: bool,
11+
pub margin: Option<Margin>,
1012
}
1113

1214
impl<'a> From<Vec<DisplayLine<'a>>> for DisplayList<'a> {
@@ -15,6 +17,7 @@ impl<'a> From<Vec<DisplayLine<'a>>> for DisplayList<'a> {
1517
body,
1618
anonymized_line_numbers: false,
1719
stylesheet: get_term_style(false),
20+
margin: None,
1821
}
1922
}
2023
}
@@ -38,6 +41,121 @@ impl<'a> fmt::Debug for DisplayList<'a> {
3841
pub struct FormatOptions {
3942
pub color: bool,
4043
pub anonymized_line_numbers: bool,
44+
pub margin: Option<Margin>,
45+
}
46+
47+
#[derive(Clone, Copy, Debug)]
48+
pub struct Margin {
49+
/// The available whitespace in the left that can be consumed when centering.
50+
pub whitespace_left: usize,
51+
/// The column of the beginning of left-most span.
52+
pub span_left: usize,
53+
/// The column of the end of right-most span.
54+
pub span_right: usize,
55+
/// The beginning of the line to be displayed.
56+
pub computed_left: usize,
57+
/// The end of the line to be displayed.
58+
pub computed_right: usize,
59+
/// The current width of the terminal. 140 by default and in tests.
60+
pub column_width: usize,
61+
/// The end column of a span label, including the span. Doesn't account for labels not in the
62+
/// same line as the span.
63+
pub label_right: usize,
64+
}
65+
66+
impl Margin {
67+
pub fn new(
68+
whitespace_left: usize,
69+
span_left: usize,
70+
span_right: usize,
71+
label_right: usize,
72+
column_width: usize,
73+
max_line_len: usize,
74+
) -> Self {
75+
// The 6 is padding to give a bit of room for `...` when displaying:
76+
// ```
77+
// error: message
78+
// --> file.rs:16:58
79+
// |
80+
// 16 | ... fn foo(self) -> Self::Bar {
81+
// | ^^^^^^^^^
82+
// ```
83+
84+
let mut m = Margin {
85+
whitespace_left: whitespace_left.saturating_sub(6),
86+
span_left: span_left.saturating_sub(6),
87+
span_right: span_right + 6,
88+
computed_left: 0,
89+
computed_right: 0,
90+
column_width,
91+
label_right: label_right + 6,
92+
};
93+
m.compute(max_line_len);
94+
m
95+
}
96+
97+
pub(crate) fn was_cut_left(&self) -> bool {
98+
self.computed_left > 0
99+
}
100+
101+
pub(crate) fn was_cut_right(&self, line_len: usize) -> bool {
102+
let right =
103+
if self.computed_right == self.span_right || self.computed_right == self.label_right {
104+
// Account for the "..." padding given above. Otherwise we end up with code lines that
105+
// do fit but end in "..." as if they were trimmed.
106+
self.computed_right - 6
107+
} else {
108+
self.computed_right
109+
};
110+
right < line_len && self.computed_left + self.column_width < line_len
111+
}
112+
113+
fn compute(&mut self, max_line_len: usize) {
114+
// When there's a lot of whitespace (>20), we want to trim it as it is useless.
115+
self.computed_left = if self.whitespace_left > 20 {
116+
self.whitespace_left - 16 // We want some padding.
117+
} else {
118+
0
119+
};
120+
// We want to show as much as possible, max_line_len is the right-most boundary for the
121+
// relevant code.
122+
self.computed_right = max(max_line_len, self.computed_left);
123+
124+
if self.computed_right - self.computed_left > self.column_width {
125+
// Trimming only whitespace isn't enough, let's get craftier.
126+
if self.label_right - self.whitespace_left <= self.column_width {
127+
// Attempt to fit the code window only trimming whitespace.
128+
self.computed_left = self.whitespace_left;
129+
self.computed_right = self.computed_left + self.column_width;
130+
} else if self.label_right - self.span_left <= self.column_width {
131+
// Attempt to fit the code window considering only the spans and labels.
132+
let padding_left = (self.column_width - (self.label_right - self.span_left)) / 2;
133+
self.computed_left = self.span_left.saturating_sub(padding_left);
134+
self.computed_right = self.computed_left + self.column_width;
135+
} else if self.span_right - self.span_left <= self.column_width {
136+
// Attempt to fit the code window considering the spans and labels plus padding.
137+
let padding_left = (self.column_width - (self.span_right - self.span_left)) / 5 * 2;
138+
self.computed_left = self.span_left.saturating_sub(padding_left);
139+
self.computed_right = self.computed_left + self.column_width;
140+
} else {
141+
// Mostly give up but still don't show the full line.
142+
self.computed_left = self.span_left;
143+
self.computed_right = self.span_right;
144+
}
145+
}
146+
}
147+
148+
pub(crate) fn left(&self, line_len: usize) -> usize {
149+
min(self.computed_left, line_len)
150+
}
151+
152+
pub(crate) fn right(&self, line_len: usize) -> usize {
153+
if line_len.saturating_sub(self.computed_left) <= self.column_width {
154+
line_len
155+
} else {
156+
min(line_len, self.computed_right)
157+
}
158+
}
41159
}
42160

43161
/// Inline annotation which can be used in either Raw or Source line.

src/formatter/mod.rs

+50-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,56 @@ impl<'a> DisplayList<'a> {
198198
DisplaySourceLine::Empty => Ok(()),
199199
DisplaySourceLine::Content { text, .. } => {
200200
f.write_char(' ')?;
201-
text.fmt(f)
201+
if let Some(margin) = self.margin {
202+
let line_len = text.chars().count();
203+
let mut left = margin.left(line_len);
204+
let right = margin.right(line_len);
205+
206+
if margin.was_cut_left() {
207+
// We have stripped some code/whitespace from the beginning, make it clear.
208+
"...".fmt(f)?;
209+
left += 3;
210+
}
211+
212+
// On long lines, we strip the source line, accounting for unicode.
213+
let mut taken = 0;
214+
let cut_right = if margin.was_cut_right(line_len) {
215+
taken += 3;
216+
true
217+
} else {
218+
false
219+
};
220+
let range = text
221+
.char_indices()
222+
.skip(left)
223+
.take_while(|(_, ch)| {
224+
// Make sure that the trimming on the right will fall within the terminal width.
225+
// FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is.
226+
// For now, just accept that sometimes the code line will be longer than desired.
227+
taken += unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1);
228+
if taken > right - left {
229+
return false;
230+
}
231+
true
232+
})
233+
.fold((None, 0), |acc, (i, _)| {
234+
if acc.0.is_some() {
235+
(acc.0, i)
236+
} else {
237+
(Some(i), i)
238+
}
239+
});
240+
241+
text[range.0.expect("One character at line")..=range.1].fmt(f)?;
242+
243+
if cut_right {
244+
// We have stripped some code after the right-most span end, make it clear we did so.
245+
"...".fmt(f)?;
246+
}
247+
Ok(())
248+
} else {
249+
text.fmt(f)
250+
}
202251
}
203252
DisplaySourceLine::Annotation {
204253
range,

tests/dl_from_snippet.rs

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ fn test_format_title() {
2828
})],
2929
stylesheet: get_term_style(input.opt.color),
3030
anonymized_line_numbers: input.opt.anonymized_line_numbers,
31+
margin: None,
3132
};
3233
assert_eq!(dl::DisplayList::from(input), output);
3334
}
@@ -80,6 +81,7 @@ fn test_format_slice() {
8081
],
8182
stylesheet: get_term_style(input.opt.color),
8283
anonymized_line_numbers: input.opt.anonymized_line_numbers,
84+
margin: None,
8385
};
8486
assert_eq!(dl::DisplayList::from(input), output);
8587
}
@@ -162,6 +164,7 @@ fn test_format_slices_continuation() {
162164
],
163165
stylesheet: get_term_style(input.opt.color),
164166
anonymized_line_numbers: input.opt.anonymized_line_numbers,
167+
margin: None,
165168
};
166169
assert_eq!(dl::DisplayList::from(input), output);
167170
}
@@ -237,6 +240,7 @@ fn test_format_slice_annotation_standalone() {
237240
],
238241
stylesheet: get_term_style(input.opt.color),
239242
anonymized_line_numbers: input.opt.anonymized_line_numbers,
243+
margin: None,
240244
};
241245
assert_eq!(dl::DisplayList::from(input), output);
242246
}
@@ -278,6 +282,7 @@ fn test_format_label() {
278282
})],
279283
stylesheet: get_term_style(input.opt.color),
280284
anonymized_line_numbers: input.opt.anonymized_line_numbers,
285+
margin: None,
281286
};
282287
assert_eq!(dl::DisplayList::from(input), output);
283288
}
@@ -395,6 +400,7 @@ fn test_i_29() {
395400
],
396401
stylesheet: get_term_style(false),
397402
anonymized_line_numbers: false,
403+
margin: None,
398404
};
399405

400406
assert_eq!(DisplayList::from(snippets), expected);
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[title]
2+
id = "E0308"
3+
label = "mismatched types"
4+
annotation_type = "Error"
5+
6+
[[slices]]
7+
source = " let _: () = 42;"
8+
line_start = 4
9+
origin = "$DIR/whitespace-trimming.rs"
10+
11+
[[slices.annotations]]
12+
label = "expected (), found integer"
13+
annotation_type = "Error"
14+
range = [192, 194]
15+
16+
[opt]
17+
color = false
18+
anonymized_line_numbers = true
19+
[opt.margin]
20+
whitespace_left = 180
21+
span_left = 192
22+
span_right = 194
23+
label_right = 221
24+
column_width = 140
25+
max_line_len = 195
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error[E0308]: mismatched types
2+
--> $DIR/whitespace-trimming.rs:4:193
3+
|
4+
LL | ... let _: () = 42;
5+
| ^^ expected (), found integer
6+
|

0 commit comments

Comments
 (0)