fix: preserve review comments across unrelated edits

This commit is contained in:
Valdemar Lorenzen
2026-10-20 14:58:56 +02:00
parent 2727425518
commit 31203889bb
10 changed files with 422 additions and 147 deletions
+87 -9
View File
@@ -76,21 +76,99 @@ impl Anchor {
&self.content[offset(self.start)..offset(self.end)]
}
pub fn matches_lines(
/// Map an unchanged selection through edits without changing its saved snapshot.
pub fn mapped_range(
&self,
revision: &str,
path: &Path,
side: Side,
lines: &[String],
eof: bool,
) -> bool {
self.revision == revision
&& self.path == path
&& self.side == side
&& self.content.split('\n').eq(lines
.iter()
.map(String::as_str)
.chain((eof || lines.is_empty()).then_some("")))
) -> Option<std::ops::Range<crate::plugin::TextPosition>> {
use crate::plugin::TextPosition;
if self.path != path
|| self.side != side
|| (self.revision != revision
&& !(self.revision.starts_with("working:") && revision.starts_with("working:")))
{
return None;
}
if self.content.split('\n').eq(lines
.iter()
.map(String::as_str)
.chain((eof || lines.is_empty()).then_some("")))
{
return Some(self.start..self.end);
}
let mut content = lines.join("\n");
if eof && !lines.is_empty() {
content.push('\n');
}
// Split LF explicitly: editor columns count a retained CR separately.
fn tokens(text: &str) -> Vec<&str> {
text.split_inclusive('\n')
.flat_map(|line| {
line.strip_suffix('\n')
.unwrap_or(line)
.graphemes(true)
.chain(line.ends_with('\n').then_some("\n"))
})
.collect()
}
let before = tokens(&self.content);
let after = tokens(&content);
let index = |position: TextPosition| {
self.content
.split('\n')
.take(position.row)
.map(|line| line.graphemes(true).count() + 1)
.sum::<usize>()
+ position.column
};
let start = index(self.start);
let end = index(self.end);
if start == end {
return None;
}
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(10);
let (mut old, mut new) = (0, 0);
for op in similar::capture_diff_slices_deadline(
similar::Algorithm::Myers,
&before,
&after,
Some(deadline),
) {
let (tag, old_range, new_range) = op.as_tag_tuple();
let old_end = old + old_range.len();
if tag == similar::DiffTag::Equal && start >= old && end <= old_end {
// Use the unchanged context, not just potentially repeated selected text.
let byte_start: usize = before[..old].iter().map(|text| text.len()).sum();
let byte_len: usize = before[old..old_end].iter().map(|text| text.len()).sum();
let context = &self.content[byte_start..byte_start + byte_len];
if self.content.match_indices(context).take(2).count() != 1
|| content.match_indices(context).take(2).count() != 1
{
return None;
}
let mapped = new + start - old;
let position = |offset: usize| {
let mut position = TextPosition { row: 0, column: 0 };
for grapheme in &after[..offset] {
if *grapheme == "\n" {
position.row += 1;
position.column = 0;
} else {
position.column += 1;
}
}
position
};
return Some(position(mapped)..position(mapped + end - start));
}
old = old_end;
new += new_range.len();
}
None
}
}