Skip to content

Commit 7edacbb

Browse files
authored
Merge pull request #321 from Muscraft/fix-trimmed-multiline-suggestions
fix: Use correct line num with trimmed suggestions
2 parents 5a632cd + 142438e commit 7edacbb

File tree

4 files changed

+309
-69
lines changed

4 files changed

+309
-69
lines changed

src/renderer/render.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,8 @@ fn emit_suggestion_default(
15111511
}
15121512

15131513
let file_lines = sm.span_to_lines(parts[0].span.clone());
1514-
let (line_start, line_end) = sm.span_to_locations(parts[0].span.clone());
1514+
// We use the original span to get original line_start
1515+
let (line_start, line_end) = sm.span_to_locations(parts[0].original_span.clone());
15151516
let mut lines = complete.lines();
15161517
if lines.clone().next().is_none() {
15171518
// Account for a suggestion to completely remove a line(s) with whitespace (#94192).

src/renderer/source_map.rs

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

src/snippet.rs

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Structures used as an input for the library.
22
3-
use crate::renderer::source_map::SourceMap;
3+
use crate::renderer::source_map::{as_substr, SourceMap, TrimmedPatch};
44
use crate::Level;
55
use std::borrow::Cow;
66
use std::ops::Range;
@@ -439,51 +439,28 @@ impl<'a> Patch<'a> {
439439
}
440440
}
441441

442-
pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
443-
!self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
444-
}
445-
446-
pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
447-
self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
448-
}
449-
450-
pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
451-
!self.replacement.is_empty() && self.replaces_meaningful_content(sm)
452-
}
453-
454-
/// Whether this is a replacement that overwrites source with a snippet
455-
/// in a way that isn't a superset of the original string. For example,
456-
/// replacing "abc" with "abcde" is not destructive, but replacing it
457-
/// it with "abx" is, since the "c" character is lost.
458-
pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
459-
self.is_replacement(sm)
460-
&& !sm
461-
.span_to_snippet(self.span.clone())
462-
// This should use `is_some_and` when our MSRV is >= 1.70
463-
.map_or(false, |s| {
464-
as_substr(s.trim(), self.replacement.trim()).is_some()
465-
})
466-
}
467-
468-
fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
469-
sm.span_to_snippet(self.span.clone())
470-
.map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
471-
}
472-
473442
/// Try to turn a replacement into an addition when the span that is being
474443
/// overwritten matches either the prefix or suffix of the replacement.
475-
pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
476-
if self.replacement.is_empty() {
477-
return;
444+
pub(crate) fn trim_trivial_replacements(self, sm: &'a SourceMap<'a>) -> TrimmedPatch<'a> {
445+
let mut trimmed = TrimmedPatch {
446+
original_span: self.span.clone(),
447+
span: self.span,
448+
replacement: self.replacement,
449+
};
450+
451+
if trimmed.replacement.is_empty() {
452+
return trimmed;
478453
}
479-
let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
480-
return;
454+
let Some(snippet) = sm.span_to_snippet(trimmed.original_span.clone()) else {
455+
return trimmed;
481456
};
482457

483-
if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
484-
self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
485-
self.replacement = Cow::Owned(substr.to_owned());
458+
if let Some((prefix, substr, suffix)) = as_substr(snippet, &trimmed.replacement) {
459+
trimmed.span = trimmed.original_span.start + prefix
460+
..trimmed.original_span.end.saturating_sub(suffix);
461+
trimmed.replacement = Cow::Owned(substr.to_owned());
486462
}
463+
trimmed
487464
}
488465
}
489466

@@ -587,24 +564,3 @@ impl<'a> From<&'a String> for OptionCow<'a> {
587564
Self(Some(Cow::Borrowed(value.as_str())))
588565
}
589566
}
590-
591-
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
592-
/// the case where a substring of the suggestion is "sandwiched" in the original, like
593-
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
594-
/// of the suffix.
595-
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
596-
let common_prefix = original
597-
.chars()
598-
.zip(suggestion.chars())
599-
.take_while(|(c1, c2)| c1 == c2)
600-
.map(|(c, _)| c.len_utf8())
601-
.sum();
602-
let original = &original[common_prefix..];
603-
let suggestion = &suggestion[common_prefix..];
604-
if let Some(stripped) = suggestion.strip_suffix(original) {
605-
let common_suffix = original.len();
606-
Some((common_prefix, stripped, common_suffix))
607-
} else {
608-
None
609-
}
610-
}

0 commit comments

Comments
 (0)