Skip to content

Commit ff009fb

Browse files
authored
Merge pull request #36 from botika/master
Add strip code to the left and right for long lines
2 parents 26fb6e1 + dae1a97 commit ff009fb

File tree

14 files changed

+332
-43
lines changed

14 files changed

+332
-43
lines changed

Diff for: 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]

Diff for: README.md

+28-30
Original file line numberDiff line numberDiff line change
@@ -35,49 +35,47 @@ Usage
3535

3636
```rust
3737
use annotate_snippets::{
38-
display_list::DisplayList,
39-
formatter::DisplayListFormatter,
38+
display_list::{DisplayList, FormatOptions},
4039
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
4140
};
4241

4342
fn main() {
4443
let snippet = Snippet {
4544
title: Some(Annotation {
46-
label: Some("expected type, found `22`".to_string()),
45+
label: Some("expected type, found `22`"),
4746
id: None,
4847
annotation_type: AnnotationType::Error,
4948
}),
5049
footer: vec![],
51-
slices: vec![
52-
Slice {
53-
source: r#"
54-
This is an example
55-
content of the slice
56-
which will be annotated
57-
with the list of annotations below.
58-
"#.to_string(),
59-
line_start: 26,
60-
origin: Some("examples/example.txt".to_string()),
61-
fold: false,
62-
annotations: vec![
63-
SourceAnnotation {
64-
label: "Example error annotation".to_string(),
65-
annotation_type: AnnotationType::Error,
66-
range: (13, 18),
67-
},
68-
SourceAnnotation {
69-
label: "and here's a warning".to_string(),
70-
annotation_type: AnnotationType::Warning,
71-
range: (34, 50),
72-
},
73-
],
74-
},
75-
],
50+
slices: vec![Slice {
51+
source: r#" annotations: vec![SourceAnnotation {
52+
label: "expected struct `annotate_snippets::snippet::Slice`, found reference"
53+
,
54+
range: <22, 25>,"#,
55+
line_start: 26,
56+
origin: Some("examples/footer.rs"),
57+
fold: true,
58+
annotations: vec![
59+
SourceAnnotation {
60+
label: "",
61+
annotation_type: AnnotationType::Error,
62+
range: (205, 207),
63+
},
64+
SourceAnnotation {
65+
label: "while parsing this struct",
66+
annotation_type: AnnotationType::Info,
67+
range: (34, 50),
68+
},
69+
],
70+
}],
71+
opt: FormatOptions {
72+
color: true,
73+
..Default::default()
74+
},
7675
};
7776

7877
let dl = DisplayList::from(snippet);
79-
let dlf = DisplayListFormatter::new(true, false);
80-
println!("{}", dlf.format(&dl));
78+
println!("{}", dl);
8179
}
8280
```
8381

Diff for: 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
}

Diff for: src/display_list/structs.rs

+119-2
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+
whitespace_left: usize,
51+
/// The column of the beginning of left-most span.
52+
span_left: usize,
53+
/// The column of the end of right-most span.
54+
span_right: usize,
55+
/// The beginning of the line to be displayed.
56+
computed_left: usize,
57+
/// The end of the line to be displayed.
58+
computed_right: usize,
59+
/// The current width of the terminal. 140 by default and in tests.
60+
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+
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.
@@ -162,8 +280,7 @@ pub enum DisplayMarkType {
162280

163281
/// A type of the `Annotation` which may impact the sigils, style or text displayed.
164282
///
165-
/// There are several ways in which the `DisplayListFormatter` uses this information
166-
/// when formatting the `DisplayList`:
283+
/// There are several ways to uses this information when formatting the `DisplayList`:
167284
///
168285
/// * An annotation may display the name of the type like `error` or `info`.
169286
/// * An underline for `Error` may be `^^^` while for `Warning` it coule be `---`.

Diff for: 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,

Diff for: src/formatter/style.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ pub trait Style {
3939
c: Box<dyn FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result + 'a>,
4040
f: &mut fmt::Formatter<'_>,
4141
) -> fmt::Result;
42-
/// The method used by the DisplayListFormatter to display the message
43-
/// in bold font.
42+
/// The method used by the `Formatter` to display the message in bold font.
4443
fn bold(&self) -> Box<dyn Style>;
4544
}
4645

0 commit comments

Comments
 (0)