From 6a16e226d5cba049b02c4c6c476664661ff0a700 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Mon, 20 Oct 2025 03:32:43 -0600 Subject: [PATCH 1/2] test: Add tests for no folding suggestion --- tests/formatter.rs | 157 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/formatter.rs b/tests/formatter.rs index e715f63..74de330 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -4541,3 +4541,160 @@ help: add a `;` here let renderer_unicode = renderer_ascii.decor_style(DecorStyle::Unicode); assert_data_eq!(renderer_unicode.render(input), expected_unicode); } + +#[test] +fn suggestion_no_fold() { + let source = r#"fn main() { + let variable_name = 42; + function_with_lots_of_arguments( + variable_name, + variable_name, + variable_name, + variable_name, + ); +}"#; + let path = "$DIR/trimmed_multiline_suggestion.rs"; + + let input = &[ + Group::with_title( + Level::ERROR + .primary_title("this function takes 6 arguments but 5 arguments were supplied") + .id("E0061"), + ) + .element( + Snippet::source(source) + .path(path) + .annotation( + AnnotationKind::Context + .span(108..121) + .label("argument #2 of type `char` is missing"), + ) + .annotation(AnnotationKind::Primary.span(44..75)), + ), + Group::with_title(Level::HELP.secondary_title("provide the argument")).element( + Snippet::source(source) + .path(path) + .fold(false) + .patch(Patch::new( + 75..174, + "( + variable_name, + /* char */, + variable_name, + variable_name, + variable_name, + )", + )), + ), + ]; + + let expected_ascii = str![[r#" +error[E0061]: this function takes 6 arguments but 5 arguments were supplied + --> $DIR/trimmed_multiline_suggestion.rs:3:5 + | +3 | function_with_lots_of_arguments( + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4 | variable_name, +5 | variable_name, + | ------------- argument #2 of type `char` is missing + | +help: provide the argument + | +3 | function_with_lots_of_arguments( +4 | variable_name, +5 ~ /* char */, +6 ~ variable_name, + | +"#]]; + let renderer_ascii = Renderer::plain(); + assert_data_eq!(renderer_ascii.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error[E0061]: this function takes 6 arguments but 5 arguments were supplied + ╭▸ $DIR/trimmed_multiline_suggestion.rs:3:5 + │ +3 │ function_with_lots_of_arguments( + │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +4 │ variable_name, +5 │ variable_name, + │ ───────────── argument #2 of type `char` is missing + ╰╴ +help: provide the argument + ╭╴ +3 │ function_with_lots_of_arguments( +4 │ variable_name, +5 ± /* char */, +6 ± variable_name, + ╰╴ +"#]]; + let renderer_unicode = renderer_ascii.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer_unicode.render(input), expected_unicode); +} + +#[test] +fn suggestion_no_fold_replacement_ends_with_newline() { + let source = r#" +use st::cell::Cell; + +mod bar { + pub fn bar() { bar::baz(); } + + fn baz() {} +} + +use bas::bar; + +struct Foo { + bar: st::cell::Cell +} + +fn main() {}"#; + + let input = &[ + Level::ERROR + .primary_title("failed to resolve: use of undeclared crate or module `st`") + .id("E0433") + .element( + Snippet::source(source).line_start(1).annotation( + AnnotationKind::Primary + .span(122..124) + .label("use of undeclared crate or module `st`"), + ), + ), + Level::HELP + .secondary_title("consider importing this module") + .element( + Snippet::source(source) + .fold(false) + .patch(Patch::new(1..1, "use std::cell;\n")), + ), + ]; + let expected_ascii = str![[r#" +error[E0433]: failed to resolve: use of undeclared crate or module `st` + | +13 | bar: st::cell::Cell + | ^^ use of undeclared crate or module `st` + | +help: consider importing this module + | + 2 + use std::cell; + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error[E0433]: failed to resolve: use of undeclared crate or module `st` + ╭▸ +13 │ bar: st::cell::Cell + │ ━━ use of undeclared crate or module `st` + ╰╴ +help: consider importing this module + ╭╴ + 2 + use std::cell; + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} From ee6e975b6d0aa3ea1e55dc04e7f57d10257d85d5 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Mon, 20 Oct 2025 03:35:31 -0600 Subject: [PATCH 2/2] fix: Don't fold suggestions with fold = false --- src/renderer/render.rs | 12 +++++++---- src/renderer/source_map.rs | 32 ++++++++++++++++++++------- tests/formatter.rs | 44 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/renderer/render.rs b/src/renderer/render.rs index 451e036..035c815 100644 --- a/src/renderer/render.rs +++ b/src/renderer/render.rs @@ -1450,7 +1450,7 @@ fn emit_suggestion_default( is_first: bool, is_cont: bool, ) { - let suggestions = sm.splice_lines(suggestion.markers.clone()); + let suggestions = sm.splice_lines(suggestion.markers.clone(), suggestion.fold); let buffer_offset = buffer.num_lines(); let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion); @@ -1511,8 +1511,12 @@ fn emit_suggestion_default( } let file_lines = sm.span_to_lines(parts[0].span.clone()); - // We use the original span to get original line_start - let (line_start, line_end) = sm.span_to_locations(parts[0].original_span.clone()); + let (line_start, line_end) = if suggestion.fold { + // We use the original span to get original line_start + sm.span_to_locations(parts[0].original_span.clone()) + } else { + sm.span_to_locations(0..sm.source.len()) + }; let mut lines = complete.lines(); if lines.clone().next().is_none() { // Account for a suggestion to completely remove a line(s) with whitespace (#94192). @@ -1545,7 +1549,7 @@ fn emit_suggestion_default( last_pos = line_pos; // Remember lines that are not highlighted to hide them if needed - if highlight_parts.is_empty() { + if highlight_parts.is_empty() && suggestion.fold { unhighlighted_lines.push((line_pos, line)); continue; } diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs index 0018d1b..a59e304 100644 --- a/src/renderer/source_map.rs +++ b/src/renderer/source_map.rs @@ -370,6 +370,7 @@ impl<'a> SourceMap<'a> { pub(crate) fn splice_lines<'b>( &'b self, mut patches: Vec>, + fold: bool, ) -> Vec<( String, Vec>, @@ -430,11 +431,16 @@ impl<'a> SourceMap<'a> { patches.sort_by_key(|p| p.span.start); // Find the bounding span. - let Some(lo) = patches.iter().map(|p| p.span.start).min() else { - return Vec::new(); - }; - let Some(hi) = patches.iter().map(|p| p.span.end).max() else { - return Vec::new(); + let (lo, hi) = if fold { + let Some(lo) = patches.iter().map(|p| p.span.start).min() else { + return Vec::new(); + }; + let Some(hi) = patches.iter().map(|p| p.span.end).max() else { + return Vec::new(); + }; + (lo, hi) + } else { + (0, source_len) }; let lines = self.span_to_lines(lo..hi); @@ -536,9 +542,19 @@ impl<'a> SourceMap<'a> { } } highlights.push(std::mem::take(&mut line_highlight)); - // if the replacement already ends with a newline, don't print the next line - if !buf.ends_with('\n') { - push_trailing(&mut buf, prev_line, &prev_hi, None); + if fold { + // if the replacement already ends with a newline, don't print the next line + if !buf.ends_with('\n') { + push_trailing(&mut buf, prev_line, &prev_hi, None); + } + } else { + // Add the trailing part of the source after the last patch + if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) { + buf.push_str(snippet); + for _ in snippet.matches('\n') { + highlights.push(std::mem::take(&mut line_highlight)); + } + } } // remove trailing newlines while buf.ends_with('\n') { diff --git a/tests/formatter.rs b/tests/formatter.rs index 74de330..471aea9 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -4600,10 +4600,16 @@ error[E0061]: this function takes 6 arguments but 5 arguments were supplied | help: provide the argument | +1 | fn main() { +2 | let variable_name = 42; 3 | function_with_lots_of_arguments( 4 | variable_name, 5 ~ /* char */, 6 ~ variable_name, +7 | variable_name, +8 | variable_name, +9 | ); +10| } | "#]]; let renderer_ascii = Renderer::plain(); @@ -4621,10 +4627,16 @@ error[E0061]: this function takes 6 arguments but 5 arguments were supplied ╰╴ help: provide the argument ╭╴ +1 │ fn main() { +2 │ let variable_name = 42; 3 │ function_with_lots_of_arguments( 4 │ variable_name, 5 ± /* char */, 6 ± variable_name, +7 │ variable_name, +8 │ variable_name, +9 │ ); +10│ } ╰╴ "#]]; let renderer_unicode = renderer_ascii.decor_style(DecorStyle::Unicode); @@ -4677,7 +4689,23 @@ error[E0433]: failed to resolve: use of undeclared crate or module `st` | help: consider importing this module | + 1 | 2 + use std::cell; + 3 ~ use st::cell::Cell; + 4 | + 5 | mod bar { + 6 | pub fn bar() { bar::baz(); } + 7 | + 8 | fn baz() {} + 9 | } +10 | +11 | use bas::bar; +12 | +13 | struct Foo { +14 | bar: st::cell::Cell +15 | } +16 | +17 | fn main() {} | "#]]; @@ -4692,7 +4720,23 @@ error[E0433]: failed to resolve: use of undeclared crate or module `st` ╰╴ help: consider importing this module ╭╴ + 1 │ 2 + use std::cell; + 3 ± use st::cell::Cell; + 4 │ + 5 │ mod bar { + 6 │ pub fn bar() { bar::baz(); } + 7 │ + 8 │ fn baz() {} + 9 │ } +10 │ +11 │ use bas::bar; +12 │ +13 │ struct Foo { +14 │ bar: st::cell::Cell +15 │ } +16 │ +17 │ fn main() {} ╰╴ "#]]; let renderer = renderer.decor_style(DecorStyle::Unicode);