Skip to content

Commit a7949be

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 a7949be

File tree

3 files changed

+90
-15
lines changed

3 files changed

+90
-15
lines changed

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();
@@ -350,10 +363,15 @@ impl DisplaySet<'_> {
350363
buffer.puts(line_offset, code_offset, &code, Style::new());
351364
if self.margin.was_cut_left() {
352365
// We have stripped some code/whitespace from the beginning, make it clear.
353-
buffer.puts(line_offset, code_offset, "...", *lineno_color);
366+
buffer.puts(line_offset, code_offset, cut_indicator, *lineno_color);
354367
}
355368
if self.margin.was_cut_right(line_len) {
356-
buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color);
369+
buffer.puts(
370+
line_offset,
371+
code_offset + taken - cut_indicator.width(),
372+
cut_indicator,
373+
*lineno_color,
374+
);
357375
}
358376

359377
let left: usize = text
@@ -725,7 +743,7 @@ impl DisplaySet<'_> {
725743
Ok(())
726744
}
727745
DisplayLine::Fold { inline_marks } => {
728-
buffer.puts(line_offset, 0, "...", *stylesheet.line_no());
746+
buffer.puts(line_offset, 0, cut_indicator, *stylesheet.line_no());
729747
if !inline_marks.is_empty() || 0 < multiline_depth {
730748
format_inline_marks(
731749
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

@@ -1248,7 +1271,7 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
12481271
match unhighlighed_lines.len() {
12491272
0 => {}
12501273
n if n <= INNER_UNFOLD_SIZE => {
1251-
// Rather than render `...`, don't fold
1274+
// Rather than render our cut indicator, don't fold
12521275
lines.append(&mut unhighlighed_lines);
12531276
}
12541277
_ => {
@@ -1287,13 +1310,14 @@ fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
12871310
lines
12881311
}
12891312

1290-
fn format_body(
1291-
snippet: snippet::Snippet<'_>,
1313+
fn format_body<'m>(
1314+
snippet: snippet::Snippet<'m>,
12921315
need_empty_header: bool,
12931316
has_footer: bool,
12941317
term_width: usize,
12951318
anonymized_line_numbers: bool,
1296-
) -> DisplaySet<'_> {
1319+
cut_indicator: &'static str,
1320+
) -> DisplaySet<'m> {
12971321
let source_len = snippet.source.len();
12981322
if let Some(bigger) = snippet.annotations.iter().find_map(|x| {
12991323
// Allow highlighting one past the last character in the source.
@@ -1626,7 +1650,7 @@ fn format_body(
16261650
current_line.to_string().len()
16271651
};
16281652

1629-
let width_offset = 3 + max_line_num_len;
1653+
let width_offset = cut_indicator.len() + max_line_num_len;
16301654

16311655
if span_left_margin == usize::MAX {
16321656
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)