Skip to content

Commit 179098f

Browse files
committed
feat: Add Renderer::cut_indicator
This adds a new API for overriding the use of `...` to indicate a cut or trimmed line. In the case of Ruff, we didn't want to use `...` since `...` is valid Python code. It could be rather confusing in some cases where `...` would be ambiguous between "line was cut here" and "this is what the actual line read as." I think this can happen with _any_ indicator of course, but for Python specifically, it's pretty likely to happen with `...`. The new API here is somewhat sub-optimal in that it requires a `&'static str`. I did this because of the constraints imposed by a `Renderer`'s `const` constructor.
1 parent 7132bf3 commit 179098f

File tree

3 files changed

+91
-15
lines changed

3 files changed

+91
-15
lines changed

src/renderer/display_list.rs

+40-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
}
@@ -270,6 +282,7 @@ impl DisplaySet<'_> {
270282
}
271283

272284
// Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211
285+
#[allow(clippy::too_many_arguments)]
273286
#[inline]
274287
fn format_line(
275288
&self,
@@ -278,6 +291,7 @@ impl DisplaySet<'_> {
278291
multiline_depth: usize,
279292
stylesheet: &Stylesheet,
280293
anonymized_line_numbers: bool,
294+
cut_indicator: &'static str,
281295
buffer: &mut StyledBuffer,
282296
) -> fmt::Result {
283297
let line_offset = buffer.num_lines();
@@ -350,10 +364,15 @@ impl DisplaySet<'_> {
350364
buffer.puts(line_offset, code_offset, &code, Style::new());
351365
if self.margin.was_cut_left() {
352366
// We have stripped some code/whitespace from the beginning, make it clear.
353-
buffer.puts(line_offset, code_offset, "...", *lineno_color);
367+
buffer.puts(line_offset, code_offset, cut_indicator, *lineno_color);
354368
}
355369
if self.margin.was_cut_right(line_len) {
356-
buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color);
370+
buffer.puts(
371+
line_offset,
372+
code_offset + taken - cut_indicator.width(),
373+
cut_indicator,
374+
*lineno_color,
375+
);
357376
}
358377

359378
let left: usize = text
@@ -725,7 +744,7 @@ impl DisplaySet<'_> {
725744
Ok(())
726745
}
727746
DisplayLine::Fold { inline_marks } => {
728-
buffer.puts(line_offset, 0, "...", *stylesheet.line_no());
747+
buffer.puts(line_offset, 0, cut_indicator, *stylesheet.line_no());
729748
if !inline_marks.is_empty() || 0 < multiline_depth {
730749
format_inline_marks(
731750
line_offset,
@@ -987,12 +1006,13 @@ impl<'a> Iterator for CursorLines<'a> {
9871006
}
9881007
}
9891008

990-
fn format_message(
991-
message: snippet::Message<'_>,
1009+
fn format_message<'m>(
1010+
message: snippet::Message<'m>,
9921011
term_width: usize,
9931012
anonymized_line_numbers: bool,
1013+
cut_indicator: &'static str,
9941014
primary: bool,
995-
) -> Vec<DisplaySet<'_>> {
1015+
) -> Vec<DisplaySet<'m>> {
9961016
let snippet::Message {
9971017
level,
9981018
id,
@@ -1016,6 +1036,7 @@ fn format_message(
10161036
!footer.is_empty(),
10171037
term_width,
10181038
anonymized_line_numbers,
1039+
cut_indicator,
10191040
));
10201041
}
10211042

@@ -1035,6 +1056,7 @@ fn format_message(
10351056
annotation,
10361057
term_width,
10371058
anonymized_line_numbers,
1059+
cut_indicator,
10381060
false,
10391061
));
10401062
}
@@ -1089,13 +1111,14 @@ fn format_label(
10891111
result
10901112
}
10911113

1092-
fn format_snippet(
1093-
snippet: snippet::Snippet<'_>,
1114+
fn format_snippet<'m>(
1115+
snippet: snippet::Snippet<'m>,
10941116
is_first: bool,
10951117
has_footer: bool,
10961118
term_width: usize,
10971119
anonymized_line_numbers: bool,
1098-
) -> DisplaySet<'_> {
1120+
cut_indicator: &'static str,
1121+
) -> DisplaySet<'m> {
10991122
let main_range = snippet.annotations.first().map(|x| x.range.start);
11001123
let origin = snippet.origin;
11011124
let need_empty_header = origin.is_some() || is_first;
@@ -1105,6 +1128,7 @@ fn format_snippet(
11051128
has_footer,
11061129
term_width,
11071130
anonymized_line_numbers,
1131+
cut_indicator,
11081132
);
11091133
let header = format_header(origin, main_range, &body.display_lines, is_first);
11101134

@@ -1248,7 +1272,7 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
12481272
match unhighlighed_lines.len() {
12491273
0 => {}
12501274
n if n <= INNER_UNFOLD_SIZE => {
1251-
// Rather than render `...`, don't fold
1275+
// Rather than render our cut indicator, don't fold
12521276
lines.append(&mut unhighlighed_lines);
12531277
}
12541278
_ => {
@@ -1287,13 +1311,14 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
12871311
lines
12881312
}
12891313

1290-
fn format_body(
1291-
snippet: snippet::Snippet<'_>,
1314+
fn format_body<'m>(
1315+
snippet: snippet::Snippet<'m>,
12921316
need_empty_header: bool,
12931317
has_footer: bool,
12941318
term_width: usize,
12951319
anonymized_line_numbers: bool,
1296-
) -> DisplaySet<'_> {
1320+
cut_indicator: &'static str,
1321+
) -> DisplaySet<'m> {
12971322
let source_len = snippet.source.len();
12981323
if let Some(bigger) = snippet.annotations.iter().find_map(|x| {
12991324
// Allow highlighting one past the last character in the source.
@@ -1626,7 +1651,7 @@ fn format_body(
16261651
current_line.to_string().len()
16271652
};
16281653

1629-
let width_offset = 3 + max_line_num_len;
1654+
let width_offset = cut_indicator.len() + max_line_num_len;
16301655

16311656
if span_left_margin == usize::MAX {
16321657
span_left_margin = 0;

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
}

tests/formatter.rs

+39
Original file line numberDiff line numberDiff line change
@@ -955,3 +955,42 @@ error: title
955955
let renderer = Renderer::plain();
956956
assert_data_eq!(renderer.render(input).to_string(), expected);
957957
}
958+
959+
#[test]
960+
fn long_line_cut() {
961+
let source = "abcd abcd abcd abcd abcd abcd abcd";
962+
let input = Level::Error.title("").snippet(
963+
Snippet::source(source)
964+
.line_start(1)
965+
.annotation(Level::Error.span(0..4)),
966+
);
967+
let expected = str![[r#"
968+
error
969+
|
970+
1 | abcd abcd a...
971+
| ^^^^
972+
|
973+
"#]];
974+
let renderer = Renderer::plain().term_width(18);
975+
assert_data_eq!(renderer.render(input).to_string(), expected);
976+
}
977+
978+
#[test]
979+
fn long_line_cut_custom() {
980+
let source = "abcd abcd abcd abcd abcd abcd abcd";
981+
let input = Level::Error.title("").snippet(
982+
Snippet::source(source)
983+
.line_start(1)
984+
.annotation(Level::Error.span(0..4)),
985+
);
986+
// This trims a little less because `…` is visually smaller than `...`.
987+
let expected = str![[r#"
988+
error
989+
|
990+
1 | abcd abcd abc…
991+
| ^^^^
992+
|
993+
"#]];
994+
let renderer = Renderer::plain().term_width(18).cut_indicator("…");
995+
assert_data_eq!(renderer.render(input).to_string(), expected);
996+
}

0 commit comments

Comments
 (0)