fix: preserve review comments across unrelated edits
This commit is contained in:
+87
-9
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user