From 4d72a3e37c8e9ff9f349f248f9c09feee57d892f Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 22 Oct 2025 05:29:08 -0600 Subject: [PATCH 1/2] test: Highlighting of strings with shared prefix/suffix --- tests/formatter.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/formatter.rs b/tests/formatter.rs index f06d2e9..7536cad 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -5015,3 +5015,52 @@ help: consider importing this module let renderer = renderer.decor_style(DecorStyle::Unicode); assert_data_eq!(renderer.render(input), expected_unicode); } + +#[test] +fn original_matches_replacement_suffix() { + let source = r#"use sync;"#; + let input = &[ + Group::with_level(Level::ERROR).element( + Snippet::source(source).path("/tmp/test.rs").annotation( + AnnotationKind::Primary + .span(4..8) + .label("no `sync` in the root"), + ), + ), + Level::HELP + .secondary_title("consider importing this module instead") + .element( + Snippet::source(source) + .path("/tmp/test.rs") + .patch(Patch::new(4..8, "std::sync")), + ), + ]; + + let expected_ascii = str![[r#" + --> /tmp/test.rs:1:5 + | +1 | use sync; + | ^^^^ no `sync` in the root + | +help: consider importing this module instead + | +1 | use std::sync; + | +++++ +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" + ╭▸ /tmp/test.rs:1:5 + │ +1 │ use sync; + │ ━━━━ no `sync` in the root + ╰╴ +help: consider importing this module instead + ╭╴ +1 │ use std::sync; + ╰╴ +++++ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} From f33da7fd78ba639d86ad5c9306c54d7746c79284 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 22 Oct 2025 05:29:08 -0600 Subject: [PATCH 2/2] fix: Prefer exact prefix/suffix matches when trimming replacements --- src/renderer/source_map.rs | 30 ++++++++++++++++++------------ tests/formatter.rs | 4 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs index 95b9030..88e2e2a 100644 --- a/src/renderer/source_map.rs +++ b/src/renderer/source_map.rs @@ -779,18 +779,24 @@ pub(crate) fn as_substr<'a>( original: &'a str, suggestion: &'a str, ) -> Option<(usize, &'a str, usize)> { - let common_prefix = original - .chars() - .zip(suggestion.chars()) - .take_while(|(c1, c2)| c1 == c2) - .map(|(c, _)| c.len_utf8()) - .sum(); - let original = &original[common_prefix..]; - let suggestion = &suggestion[common_prefix..]; - if let Some(stripped) = suggestion.strip_suffix(original) { - let common_suffix = original.len(); - Some((common_prefix, stripped, common_suffix)) + if let Some(stripped) = suggestion.strip_prefix(original) { + Some((original.len(), stripped, 0)) + } else if let Some(stripped) = suggestion.strip_suffix(original) { + Some((0, stripped, original.len())) } else { - None + let common_prefix = original + .chars() + .zip(suggestion.chars()) + .take_while(|(c1, c2)| c1 == c2) + .map(|(c, _)| c.len_utf8()) + .sum(); + let original = &original[common_prefix..]; + let suggestion = &suggestion[common_prefix..]; + if let Some(stripped) = suggestion.strip_suffix(original) { + let common_suffix = original.len(); + Some((common_prefix, stripped, common_suffix)) + } else { + None + } } } diff --git a/tests/formatter.rs b/tests/formatter.rs index 7536cad..4ba45dd 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -5045,7 +5045,7 @@ fn original_matches_replacement_suffix() { help: consider importing this module instead | 1 | use std::sync; - | +++++ + | +++++ "#]]; let renderer = Renderer::plain(); assert_data_eq!(renderer.render(input), expected_ascii); @@ -5059,7 +5059,7 @@ help: consider importing this module instead help: consider importing this module instead ╭╴ 1 │ use std::sync; - ╰╴ +++++ + ╰╴ +++++ "#]]; let renderer = renderer.decor_style(DecorStyle::Unicode); assert_data_eq!(renderer.render(input), expected_unicode);