Skip to content

Commit 366c39d

Browse files
committed
ruff_annotate_snippets: support overriding the "cut indicator"
We do this because `...` is valid Python, which makes it pretty likely that some line trimming will lead to ambiguous output. So we add support for overriding the cut indicator. This also requires changing some of the alignment math, which was previously tightly coupled to `...`. For Ruff, we go with `…` (`U+2026 HORIZONTAL ELLIPSIS`) for our cut indicator. For more details, see the patch sent to upstream: rust-lang/annotate-snippets-rs#172
1 parent dee22d9 commit 366c39d

File tree

5 files changed

+93
-17
lines changed

5 files changed

+93
-17
lines changed

crates/ruff_annotate_snippets/src/renderer/display_list.rs

+39-15
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ use std::fmt::Display;
3838
use std::ops::Range;
3939
use std::{cmp, fmt};
4040

41+
use unicode_width::UnicodeWidthStr;
42+
4143
use crate::renderer::styled_buffer::StyledBuffer;
4244
use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH};
4345

@@ -53,6 +55,7 @@ pub(crate) struct DisplayList<'a> {
5355
pub(crate) body: Vec<DisplaySet<'a>>,
5456
pub(crate) stylesheet: &'a Stylesheet,
5557
pub(crate) anonymized_line_numbers: bool,
58+
pub(crate) cut_indicator: &'static str,
5659
}
5760

5861
impl PartialEq for DisplayList<'_> {
@@ -119,13 +122,21 @@ impl<'a> DisplayList<'a> {
119122
stylesheet: &'a Stylesheet,
120123
anonymized_line_numbers: bool,
121124
term_width: usize,
125+
cut_indicator: &'static str,
122126
) -> DisplayList<'a> {
123-
let body = format_message(message, term_width, anonymized_line_numbers, true);
127+
let body = format_message(
128+
message,
129+
term_width,
130+
anonymized_line_numbers,
131+
cut_indicator,
132+
true,
133+
);
124134

125135
Self {
126136
body,
127137
stylesheet,
128138
anonymized_line_numbers,
139+
cut_indicator,
129140
}
130141
}
131142

@@ -143,6 +154,7 @@ impl<'a> DisplayList<'a> {
143154
multiline_depth,
144155
self.stylesheet,
145156
self.anonymized_line_numbers,
157+
self.cut_indicator,
146158
buffer,
147159
)?;
148160
}
@@ -278,6 +290,7 @@ impl DisplaySet<'_> {
278290
multiline_depth: usize,
279291
stylesheet: &Stylesheet,
280292
anonymized_line_numbers: bool,
293+
cut_indicator: &'static str,
281294
buffer: &mut StyledBuffer,
282295
) -> fmt::Result {
283296
let line_offset = buffer.num_lines();
@@ -349,10 +362,15 @@ impl DisplaySet<'_> {
349362
buffer.puts(line_offset, code_offset, &code, Style::new());
350363
if self.margin.was_cut_left() {
351364
// We have stripped some code/whitespace from the beginning, make it clear.
352-
buffer.puts(line_offset, code_offset, "...", *lineno_color);
365+
buffer.puts(line_offset, code_offset, cut_indicator, *lineno_color);
353366
}
354367
if was_cut_right {
355-
buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color);
368+
buffer.puts(
369+
line_offset,
370+
code_offset + taken - cut_indicator.width(),
371+
cut_indicator,
372+
*lineno_color,
373+
);
356374
}
357375

358376
let left: usize = text
@@ -724,7 +742,7 @@ impl DisplaySet<'_> {
724742
Ok(())
725743
}
726744
DisplayLine::Fold { inline_marks } => {
727-
buffer.puts(line_offset, 0, "...", *stylesheet.line_no());
745+
buffer.puts(line_offset, 0, cut_indicator, *stylesheet.line_no());
728746
if !inline_marks.is_empty() || 0 < multiline_depth {
729747
format_inline_marks(
730748
line_offset,
@@ -987,12 +1005,13 @@ impl<'a> Iterator for CursorLines<'a> {
9871005
}
9881006
}
9891007

990-
fn format_message(
991-
message: snippet::Message<'_>,
1008+
fn format_message<'m>(
1009+
message: snippet::Message<'m>,
9921010
term_width: usize,
9931011
anonymized_line_numbers: bool,
1012+
cut_indicator: &'static str,
9941013
primary: bool,
995-
) -> Vec<DisplaySet<'_>> {
1014+
) -> Vec<DisplaySet<'m>> {
9961015
let snippet::Message {
9971016
level,
9981017
id,
@@ -1016,6 +1035,7 @@ fn format_message(
10161035
!footer.is_empty(),
10171036
term_width,
10181037
anonymized_line_numbers,
1038+
cut_indicator,
10191039
));
10201040
}
10211041

@@ -1035,6 +1055,7 @@ fn format_message(
10351055
annotation,
10361056
term_width,
10371057
anonymized_line_numbers,
1058+
cut_indicator,
10381059
false,
10391060
));
10401061
}
@@ -1089,13 +1110,14 @@ fn format_label(
10891110
result
10901111
}
10911112

1092-
fn format_snippet(
1093-
snippet: snippet::Snippet<'_>,
1113+
fn format_snippet<'m>(
1114+
snippet: snippet::Snippet<'m>,
10941115
is_first: bool,
10951116
has_footer: bool,
10961117
term_width: usize,
10971118
anonymized_line_numbers: bool,
1098-
) -> DisplaySet<'_> {
1119+
cut_indicator: &'static str,
1120+
) -> DisplaySet<'m> {
10991121
let main_range = snippet.annotations.first().map(|x| x.range.start);
11001122
let origin = snippet.origin;
11011123
let need_empty_header = origin.is_some() || is_first;
@@ -1105,6 +1127,7 @@ fn format_snippet(
11051127
has_footer,
11061128
term_width,
11071129
anonymized_line_numbers,
1130+
cut_indicator,
11081131
);
11091132
let header = format_header(origin, main_range, &body.display_lines, is_first);
11101133

@@ -1241,7 +1264,7 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
12411264
match unhighlighed_lines.len() {
12421265
0 => {}
12431266
n if n <= INNER_UNFOLD_SIZE => {
1244-
// Rather than render `...`, don't fold
1267+
// Rather than render our cut indicator, don't fold
12451268
lines.append(&mut unhighlighed_lines);
12461269
}
12471270
_ => {
@@ -1280,13 +1303,14 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
12801303
lines
12811304
}
12821305

1283-
fn format_body(
1284-
snippet: snippet::Snippet<'_>,
1306+
fn format_body<'m>(
1307+
snippet: snippet::Snippet<'m>,
12851308
need_empty_header: bool,
12861309
has_footer: bool,
12871310
term_width: usize,
12881311
anonymized_line_numbers: bool,
1289-
) -> DisplaySet<'_> {
1312+
cut_indicator: &'static str,
1313+
) -> DisplaySet<'m> {
12901314
let source_len = snippet.source.len();
12911315
if let Some(bigger) = snippet.annotations.iter().find_map(|x| {
12921316
// Allow highlighting one past the last character in the source.
@@ -1617,7 +1641,7 @@ fn format_body(
16171641
current_line.to_string().len()
16181642
};
16191643

1620-
let width_offset = 3 + max_line_num_len;
1644+
let width_offset = cut_indicator.len() + max_line_num_len;
16211645

16221646
if span_left_margin == usize::MAX {
16231647
span_left_margin = 0;

crates/ruff_annotate_snippets/src/renderer/mod.rs

+12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//!
1010
//! let renderer = Renderer::styled();
1111
//! println!("{}", renderer.render(snippet));
12+
//! ```
1213
1314
mod display_list;
1415
mod margin;
@@ -30,6 +31,7 @@ pub struct Renderer {
3031
anonymized_line_numbers: bool,
3132
term_width: usize,
3233
stylesheet: Stylesheet,
34+
cut_indicator: &'static str,
3335
}
3436

3537
impl Renderer {
@@ -39,6 +41,7 @@ impl Renderer {
3941
anonymized_line_numbers: false,
4042
term_width: DEFAULT_TERM_WIDTH,
4143
stylesheet: Stylesheet::plain(),
44+
cut_indicator: "...",
4245
}
4346
}
4447

@@ -151,13 +154,22 @@ impl Renderer {
151154
self
152155
}
153156

157+
/// Set the string used for when a long line is cut.
158+
///
159+
/// The default is `...` (three `U+002E` characters).
160+
pub const fn cut_indicator(mut self, string: &'static str) -> Self {
161+
self.cut_indicator = string;
162+
self
163+
}
164+
154165
/// Render a snippet into a `Display`able object
155166
pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a {
156167
DisplayList::new(
157168
msg,
158169
&self.stylesheet,
159170
self.anonymized_line_numbers,
160171
self.term_width,
172+
self.cut_indicator,
161173
)
162174
}
163175
}

crates/ruff_annotate_snippets/tests/formatter.rs

+39
Original file line numberDiff line numberDiff line change
@@ -990,3 +990,42 @@ error: title
990990
let renderer = Renderer::plain();
991991
assert_data_eq!(renderer.render(input).to_string(), expected);
992992
}
993+
994+
#[test]
995+
fn long_line_cut() {
996+
let source = "abcd abcd abcd abcd abcd abcd abcd";
997+
let input = Level::Error.title("").snippet(
998+
Snippet::source(source)
999+
.line_start(1)
1000+
.annotation(Level::Error.span(0..4)),
1001+
);
1002+
let expected = str![[r#"
1003+
error
1004+
|
1005+
1 | abcd abcd a...
1006+
| ^^^^
1007+
|
1008+
"#]];
1009+
let renderer = Renderer::plain().term_width(18);
1010+
assert_data_eq!(renderer.render(input).to_string(), expected);
1011+
}
1012+
1013+
#[test]
1014+
fn long_line_cut_custom() {
1015+
let source = "abcd abcd abcd abcd abcd abcd abcd";
1016+
let input = Level::Error.title("").snippet(
1017+
Snippet::source(source)
1018+
.line_start(1)
1019+
.annotation(Level::Error.span(0..4)),
1020+
);
1021+
// This trims a little less because `…` is visually smaller than `...`.
1022+
let expected = str![[r#"
1023+
error
1024+
|
1025+
1 | abcd abcd abc…
1026+
| ^^^^
1027+
|
1028+
"#]];
1029+
let renderer = Renderer::plain().term_width(18).cut_indicator("…");
1030+
assert_data_eq!(renderer.render(input).to_string(), expected);
1031+
}

crates/ruff_linter/src/message/text.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ impl Display for MessageCodeFrame<'_> {
277277
Renderer::styled()
278278
} else {
279279
Renderer::plain()
280-
};
280+
}
281+
.cut_indicator("…");
281282
let rendered = renderer.render(message);
282283
writeln!(f, "{rendered}")
283284
}

crates/ruff_python_parser/tests/fixtures.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ impl std::fmt::Display for CodeFrame<'_> {
210210
.annotation(annotation)
211211
.fold(false);
212212
let message = Level::None.title("").snippet(snippet);
213-
let renderer = Renderer::plain();
213+
let renderer = Renderer::plain().cut_indicator("…");
214214
let rendered = renderer.render(message);
215215
writeln!(f, "{rendered}")
216216
}

0 commit comments

Comments
 (0)