From 84ba4ecaf5532466d85ce463bc6b6560971079b6 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 9 Jan 2025 13:08:35 -0500 Subject: [PATCH] ruff_annotate_snippets: support overriding the "cut indicator" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/rust-lang/annotate-snippets-rs/pull/172 --- .../src/renderer/display_list.rs | 55 ++++++++++++++----- .../src/renderer/mod.rs | 12 ++++ .../ruff_annotate_snippets/tests/formatter.rs | 39 +++++++++++++ crates/ruff_linter/src/message/text.rs | 3 +- crates/ruff_python_parser/tests/fixtures.rs | 2 +- 5 files changed, 94 insertions(+), 17 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/display_list.rs b/crates/ruff_annotate_snippets/src/renderer/display_list.rs index f9c774c2fd..95b0a98639 100644 --- a/crates/ruff_annotate_snippets/src/renderer/display_list.rs +++ b/crates/ruff_annotate_snippets/src/renderer/display_list.rs @@ -38,6 +38,8 @@ use std::fmt::Display; use std::ops::Range; use std::{cmp, fmt}; +use unicode_width::UnicodeWidthStr; + use crate::renderer::styled_buffer::StyledBuffer; use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH}; @@ -53,6 +55,7 @@ pub(crate) struct DisplayList<'a> { pub(crate) body: Vec>, pub(crate) stylesheet: &'a Stylesheet, pub(crate) anonymized_line_numbers: bool, + pub(crate) cut_indicator: &'static str, } impl PartialEq for DisplayList<'_> { @@ -119,13 +122,21 @@ impl<'a> DisplayList<'a> { stylesheet: &'a Stylesheet, anonymized_line_numbers: bool, term_width: usize, + cut_indicator: &'static str, ) -> DisplayList<'a> { - let body = format_message(message, term_width, anonymized_line_numbers, true); + let body = format_message( + message, + term_width, + anonymized_line_numbers, + cut_indicator, + true, + ); Self { body, stylesheet, anonymized_line_numbers, + cut_indicator, } } @@ -143,6 +154,7 @@ impl<'a> DisplayList<'a> { multiline_depth, self.stylesheet, self.anonymized_line_numbers, + self.cut_indicator, buffer, )?; } @@ -270,6 +282,7 @@ impl DisplaySet<'_> { } // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 + #[allow(clippy::too_many_arguments)] #[inline] fn format_line( &self, @@ -278,6 +291,7 @@ impl DisplaySet<'_> { multiline_depth: usize, stylesheet: &Stylesheet, anonymized_line_numbers: bool, + cut_indicator: &'static str, buffer: &mut StyledBuffer, ) -> fmt::Result { let line_offset = buffer.num_lines(); @@ -349,10 +363,15 @@ impl DisplaySet<'_> { buffer.puts(line_offset, code_offset, &code, Style::new()); if self.margin.was_cut_left() { // We have stripped some code/whitespace from the beginning, make it clear. - buffer.puts(line_offset, code_offset, "...", *lineno_color); + buffer.puts(line_offset, code_offset, cut_indicator, *lineno_color); } if was_cut_right { - buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color); + buffer.puts( + line_offset, + code_offset + taken - cut_indicator.width(), + cut_indicator, + *lineno_color, + ); } let left: usize = text @@ -724,7 +743,7 @@ impl DisplaySet<'_> { Ok(()) } DisplayLine::Fold { inline_marks } => { - buffer.puts(line_offset, 0, "...", *stylesheet.line_no()); + buffer.puts(line_offset, 0, cut_indicator, *stylesheet.line_no()); if !inline_marks.is_empty() || 0 < multiline_depth { format_inline_marks( line_offset, @@ -987,12 +1006,13 @@ impl<'a> Iterator for CursorLines<'a> { } } -fn format_message( - message: snippet::Message<'_>, +fn format_message<'m>( + message: snippet::Message<'m>, term_width: usize, anonymized_line_numbers: bool, + cut_indicator: &'static str, primary: bool, -) -> Vec> { +) -> Vec> { let snippet::Message { level, id, @@ -1016,6 +1036,7 @@ fn format_message( !footer.is_empty(), term_width, anonymized_line_numbers, + cut_indicator, )); } @@ -1035,6 +1056,7 @@ fn format_message( annotation, term_width, anonymized_line_numbers, + cut_indicator, false, )); } @@ -1089,13 +1111,14 @@ fn format_label( result } -fn format_snippet( - snippet: snippet::Snippet<'_>, +fn format_snippet<'m>( + snippet: snippet::Snippet<'m>, is_first: bool, has_footer: bool, term_width: usize, anonymized_line_numbers: bool, -) -> DisplaySet<'_> { + cut_indicator: &'static str, +) -> DisplaySet<'m> { let main_range = snippet.annotations.first().map(|x| x.range.start); let origin = snippet.origin; let need_empty_header = origin.is_some() || is_first; @@ -1105,6 +1128,7 @@ fn format_snippet( has_footer, term_width, anonymized_line_numbers, + cut_indicator, ); let header = format_header(origin, main_range, &body.display_lines, is_first); @@ -1241,7 +1265,7 @@ fn fold_body(body: Vec>) -> Vec> { match unhighlighed_lines.len() { 0 => {} n if n <= INNER_UNFOLD_SIZE => { - // Rather than render `...`, don't fold + // Rather than render our cut indicator, don't fold lines.append(&mut unhighlighed_lines); } _ => { @@ -1280,13 +1304,14 @@ fn fold_body(body: Vec>) -> Vec> { lines } -fn format_body( - snippet: snippet::Snippet<'_>, +fn format_body<'m>( + snippet: snippet::Snippet<'m>, need_empty_header: bool, has_footer: bool, term_width: usize, anonymized_line_numbers: bool, -) -> DisplaySet<'_> { + cut_indicator: &'static str, +) -> DisplaySet<'m> { let source_len = snippet.source.len(); if let Some(bigger) = snippet.annotations.iter().find_map(|x| { // Allow highlighting one past the last character in the source. @@ -1617,7 +1642,7 @@ fn format_body( current_line.to_string().len() }; - let width_offset = 3 + max_line_num_len; + let width_offset = cut_indicator.len() + max_line_num_len; if span_left_margin == usize::MAX { span_left_margin = 0; diff --git a/crates/ruff_annotate_snippets/src/renderer/mod.rs b/crates/ruff_annotate_snippets/src/renderer/mod.rs index 52ec9ad331..177ffb9ab1 100644 --- a/crates/ruff_annotate_snippets/src/renderer/mod.rs +++ b/crates/ruff_annotate_snippets/src/renderer/mod.rs @@ -9,6 +9,7 @@ //! //! let renderer = Renderer::styled(); //! println!("{}", renderer.render(snippet)); +//! ``` mod display_list; mod margin; @@ -30,6 +31,7 @@ pub struct Renderer { anonymized_line_numbers: bool, term_width: usize, stylesheet: Stylesheet, + cut_indicator: &'static str, } impl Renderer { @@ -39,6 +41,7 @@ impl Renderer { anonymized_line_numbers: false, term_width: DEFAULT_TERM_WIDTH, stylesheet: Stylesheet::plain(), + cut_indicator: "...", } } @@ -151,6 +154,14 @@ impl Renderer { self } + /// Set the string used for when a long line is cut. + /// + /// The default is `...` (three `U+002E` characters). + pub const fn cut_indicator(mut self, string: &'static str) -> Self { + self.cut_indicator = string; + self + } + /// Render a snippet into a `Display`able object pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a { DisplayList::new( @@ -158,6 +169,7 @@ impl Renderer { &self.stylesheet, self.anonymized_line_numbers, self.term_width, + self.cut_indicator, ) } } diff --git a/crates/ruff_annotate_snippets/tests/formatter.rs b/crates/ruff_annotate_snippets/tests/formatter.rs index e66ad11547..c150aa5438 100644 --- a/crates/ruff_annotate_snippets/tests/formatter.rs +++ b/crates/ruff_annotate_snippets/tests/formatter.rs @@ -990,3 +990,42 @@ error: title let renderer = Renderer::plain(); assert_data_eq!(renderer.render(input).to_string(), expected); } + +#[test] +fn long_line_cut() { + let source = "abcd abcd abcd abcd abcd abcd abcd"; + let input = Level::Error.title("").snippet( + Snippet::source(source) + .line_start(1) + .annotation(Level::Error.span(0..4)), + ); + let expected = str![[r#" +error + | +1 | abcd abcd a... + | ^^^^ + | +"#]]; + let renderer = Renderer::plain().term_width(18); + assert_data_eq!(renderer.render(input).to_string(), expected); +} + +#[test] +fn long_line_cut_custom() { + let source = "abcd abcd abcd abcd abcd abcd abcd"; + let input = Level::Error.title("").snippet( + Snippet::source(source) + .line_start(1) + .annotation(Level::Error.span(0..4)), + ); + // This trims a little less because `…` is visually smaller than `...`. + let expected = str![[r#" +error + | +1 | abcd abcd abc… + | ^^^^ + | +"#]]; + let renderer = Renderer::plain().term_width(18).cut_indicator("…"); + assert_data_eq!(renderer.render(input).to_string(), expected); +} diff --git a/crates/ruff_linter/src/message/text.rs b/crates/ruff_linter/src/message/text.rs index ec2b97e23a..7ea602623b 100644 --- a/crates/ruff_linter/src/message/text.rs +++ b/crates/ruff_linter/src/message/text.rs @@ -277,7 +277,8 @@ impl Display for MessageCodeFrame<'_> { Renderer::styled() } else { Renderer::plain() - }; + } + .cut_indicator("…"); let rendered = renderer.render(message); writeln!(f, "{rendered}") } diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index e8516b6a64..debe45415a 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -210,7 +210,7 @@ impl std::fmt::Display for CodeFrame<'_> { .annotation(annotation) .fold(false); let message = Level::None.title("").snippet(snippet); - let renderer = Renderer::plain(); + let renderer = Renderer::plain().cut_indicator("…"); let rendered = renderer.render(message); writeln!(f, "{rendered}") }