@@ -370,7 +370,11 @@ impl<'a> SourceMap<'a> {
370370 pub ( crate ) fn splice_lines < ' b > (
371371 & ' b self ,
372372 mut patches : Vec < Patch < ' b > > ,
373- ) -> Vec < ( String , Vec < Patch < ' b > > , Vec < Vec < SubstitutionHighlight > > ) > {
373+ ) -> Vec < (
374+ String ,
375+ Vec < TrimmedPatch < ' b > > ,
376+ Vec < Vec < SubstitutionHighlight > > ,
377+ ) > {
374378 fn push_trailing (
375379 buf : & mut String ,
376380 line_opt : Option < & str > ,
@@ -450,15 +454,18 @@ impl<'a> SourceMap<'a> {
450454 let mut prev_line = lines. first ( ) . map ( |line| line. line ) ;
451455 let mut buf = String :: new ( ) ;
452456
457+ let trimmed_patches = patches
458+ . into_iter ( )
459+ // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
460+ // suggestion and snippet to look as if we just suggested to add
461+ // `"b"`, which is typically much easier for the user to understand.
462+ . map ( |part| part. trim_trivial_replacements ( self ) )
463+ . collect :: < Vec < _ > > ( ) ;
453464 let mut line_highlight = vec ! [ ] ;
454465 // We need to keep track of the difference between the existing code and the added
455466 // or deleted code in order to point at the correct column *after* substitution.
456467 let mut acc = 0 ;
457- for part in & mut patches {
458- // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
459- // suggestion and snippet to look as if we just suggested to add
460- // `"b"`, which is typically much easier for the user to understand.
461- part. trim_trivial_replacements ( self ) ;
468+ for part in & trimmed_patches {
462469 let ( cur_lo, cur_hi) = self . span_to_locations ( part. span . clone ( ) ) ;
463470 if prev_hi. line == cur_lo. line {
464471 let mut count = push_trailing ( & mut buf, prev_line, & prev_hi, Some ( & cur_lo) ) ;
@@ -540,7 +547,7 @@ impl<'a> SourceMap<'a> {
540547 if highlights. iter ( ) . all ( |parts| parts. is_empty ( ) ) {
541548 Vec :: new ( )
542549 } else {
543- vec ! [ ( buf, patches , highlights) ]
550+ vec ! [ ( buf, trimmed_patches , highlights) ]
544551 }
545552 }
546553}
@@ -704,3 +711,67 @@ pub(crate) struct SubstitutionHighlight {
704711 pub ( crate ) start : usize ,
705712 pub ( crate ) end : usize ,
706713}
714+
715+ #[ derive( Clone , Debug ) ]
716+ pub ( crate ) struct TrimmedPatch < ' a > {
717+ pub ( crate ) original_span : Range < usize > ,
718+ pub ( crate ) span : Range < usize > ,
719+ pub ( crate ) replacement : Cow < ' a , str > ,
720+ }
721+
722+ impl < ' a > TrimmedPatch < ' a > {
723+ pub ( crate ) fn is_addition ( & self , sm : & SourceMap < ' _ > ) -> bool {
724+ !self . replacement . is_empty ( ) && !self . replaces_meaningful_content ( sm)
725+ }
726+
727+ pub ( crate ) fn is_deletion ( & self , sm : & SourceMap < ' _ > ) -> bool {
728+ self . replacement . trim ( ) . is_empty ( ) && self . replaces_meaningful_content ( sm)
729+ }
730+
731+ pub ( crate ) fn is_replacement ( & self , sm : & SourceMap < ' _ > ) -> bool {
732+ !self . replacement . is_empty ( ) && self . replaces_meaningful_content ( sm)
733+ }
734+
735+ /// Whether this is a replacement that overwrites source with a snippet
736+ /// in a way that isn't a superset of the original string. For example,
737+ /// replacing "abc" with "abcde" is not destructive, but replacing it
738+ /// it with "abx" is, since the "c" character is lost.
739+ pub ( crate ) fn is_destructive_replacement ( & self , sm : & SourceMap < ' _ > ) -> bool {
740+ self . is_replacement ( sm)
741+ && !sm
742+ . span_to_snippet ( self . span . clone ( ) )
743+ // This should use `is_some_and` when our MSRV is >= 1.70
744+ . map_or ( false , |s| {
745+ as_substr ( s. trim ( ) , self . replacement . trim ( ) ) . is_some ( )
746+ } )
747+ }
748+
749+ fn replaces_meaningful_content ( & self , sm : & SourceMap < ' _ > ) -> bool {
750+ sm. span_to_snippet ( self . span . clone ( ) )
751+ . map_or ( !self . span . is_empty ( ) , |snippet| !snippet. trim ( ) . is_empty ( ) )
752+ }
753+ }
754+
755+ /// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
756+ /// the case where a substring of the suggestion is "sandwiched" in the original, like
757+ /// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
758+ /// of the suffix.
759+ pub ( crate ) fn as_substr < ' a > (
760+ original : & ' a str ,
761+ suggestion : & ' a str ,
762+ ) -> Option < ( usize , & ' a str , usize ) > {
763+ let common_prefix = original
764+ . chars ( )
765+ . zip ( suggestion. chars ( ) )
766+ . take_while ( |( c1, c2) | c1 == c2)
767+ . map ( |( c, _) | c. len_utf8 ( ) )
768+ . sum ( ) ;
769+ let original = & original[ common_prefix..] ;
770+ let suggestion = & suggestion[ common_prefix..] ;
771+ if let Some ( stripped) = suggestion. strip_suffix ( original) {
772+ let common_suffix = original. len ( ) ;
773+ Some ( ( common_prefix, stripped, common_suffix) )
774+ } else {
775+ None
776+ }
777+ }
0 commit comments