git: Optimize patch_for_range functions (#48852)
These are heavily used by the side-by-side diff. Previously, we were iterating over all hunks for each call. Now we skip hunks that can't affect the provided range. Release Notes: - N/A
This commit is contained in:
1 parent
846b1ae766
commit
a19c8b24e5
3 files changed
+484
-52
No files matched your search
@@ -362,7 +362,7 @@ impl BufferDiffSnapshot {
|
||||
(new_id == old_id && new_version == old_version) || (new_empty && old_empty)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
/// Returns the last hunk whose start is less than or equal to the given position.
|
||||
fn hunk_before_base_text_offset<'a>(
|
||||
&self,
|
||||
target: usize,
|
||||
@@ -375,16 +375,11 @@ impl BufferDiffSnapshot {
|
||||
{
|
||||
cursor.prev();
|
||||
}
|
||||
let result = cursor
|
||||
cursor
|
||||
.item()
|
||||
.filter(|hunk| target >= hunk.diff_base_byte_range.start);
|
||||
if cursor.item().is_none() {
|
||||
cursor.reset();
|
||||
}
|
||||
result
|
||||
.filter(|hunk| target >= hunk.diff_base_byte_range.start)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn hunk_before_buffer_anchor<'a>(
|
||||
&self,
|
||||
target: Anchor,
|
||||
@@ -398,13 +393,9 @@ impl BufferDiffSnapshot {
|
||||
{
|
||||
cursor.prev();
|
||||
}
|
||||
let result = cursor
|
||||
cursor
|
||||
.item()
|
||||
.filter(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_ge());
|
||||
if cursor.item().is_none() {
|
||||
cursor.reset();
|
||||
}
|
||||
result
|
||||
.filter(|hunk| target.cmp(&hunk.buffer_range.start, buffer).is_ge())
|
||||
}
|
||||
|
||||
/// Returns a patch mapping the provided main buffer snapshot to the base text of this diff.
|
||||
@@ -413,7 +404,79 @@ impl BufferDiffSnapshot {
|
||||
/// but not necessarily for points outside that range.
|
||||
pub fn patch_for_buffer_range<'a>(
|
||||
&'a self,
|
||||
_range: RangeInclusive<Point>,
|
||||
range: RangeInclusive<Point>,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> Patch<Point> {
|
||||
if !self.inner.base_text_exists {
|
||||
return Patch::new(vec![Edit {
|
||||
old: Point::zero()..buffer.max_point(),
|
||||
new: Point::zero()..Point::zero(),
|
||||
}]);
|
||||
}
|
||||
|
||||
let mut edits_since_diff = Patch::new(
|
||||
buffer
|
||||
.edits_since::<Point>(&self.inner.buffer_snapshot.version)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
edits_since_diff.invert();
|
||||
|
||||
let mut start_point = edits_since_diff.old_to_new(*range.start());
|
||||
if let Some(first_edit) = edits_since_diff.edits().first() {
|
||||
start_point = start_point.min(first_edit.new.start);
|
||||
}
|
||||
|
||||
let original_snapshot = self.original_buffer_snapshot();
|
||||
let base_text = self.base_text();
|
||||
|
||||
let mut cursor = self.inner.hunks.cursor(original_snapshot);
|
||||
self.hunk_before_buffer_anchor(
|
||||
original_snapshot.anchor_before(start_point),
|
||||
&mut cursor,
|
||||
original_snapshot,
|
||||
);
|
||||
if cursor.item().is_none() {
|
||||
cursor.next();
|
||||
}
|
||||
|
||||
let mut prefix_edit = cursor.prev_item().map(|prev_hunk| Edit {
|
||||
old: Point::zero()..prev_hunk.buffer_range.end.to_point(original_snapshot),
|
||||
new: Point::zero()..prev_hunk.diff_base_byte_range.end.to_point(base_text),
|
||||
});
|
||||
|
||||
let mut range_end = edits_since_diff.old_to_new(*range.end());
|
||||
if let Some(last_edit) = edits_since_diff.edits().last() {
|
||||
range_end = range_end.max(last_edit.new.end);
|
||||
}
|
||||
let range_end = original_snapshot.anchor_before(range_end);
|
||||
|
||||
let hunk_iter = std::iter::from_fn(move || {
|
||||
if let Some(edit) = prefix_edit.take() {
|
||||
return Some(edit);
|
||||
}
|
||||
let hunk = cursor.item()?;
|
||||
if hunk
|
||||
.buffer_range
|
||||
.start
|
||||
.cmp(&range_end, original_snapshot)
|
||||
.is_gt()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let edit = Edit {
|
||||
old: hunk.buffer_range.to_point(original_snapshot),
|
||||
new: hunk.diff_base_byte_range.to_point(base_text),
|
||||
};
|
||||
cursor.next();
|
||||
Some(edit)
|
||||
});
|
||||
|
||||
edits_since_diff.compose(hunk_iter)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn patch_for_buffer_range_naive<'a>(
|
||||
&'a self,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> Patch<Point> {
|
||||
let original_snapshot = self.original_buffer_snapshot();
|
||||
@@ -455,13 +518,87 @@ impl BufferDiffSnapshot {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a patch mapping the base text of this diff to the provided buffer snapshot.
|
||||
/// Returns a patch mapping the base text of this diff to the provided main buffer snapshot.
|
||||
///
|
||||
/// The returned patch is guaranteed to be accurate for all base text points in the provided range,
|
||||
/// but not necessarily for points outside that range.
|
||||
pub fn patch_for_base_text_range<'a>(
|
||||
&'a self,
|
||||
_range: RangeInclusive<Point>,
|
||||
range: RangeInclusive<Point>,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> Patch<Point> {
|
||||
if !self.inner.base_text_exists {
|
||||
return Patch::new(vec![Edit {
|
||||
old: Point::zero()..Point::zero(),
|
||||
new: Point::zero()..buffer.max_point(),
|
||||
}]);
|
||||
}
|
||||
|
||||
let edits_since_diff = buffer
|
||||
.edits_since::<Point>(&self.inner.buffer_snapshot.version)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut hunk_patch = Vec::new();
|
||||
let mut cursor = self.inner.hunks.cursor(self.original_buffer_snapshot());
|
||||
let hunk_before = self
|
||||
.hunk_before_base_text_offset(range.start().to_offset(self.base_text()), &mut cursor);
|
||||
|
||||
if let Some(hunk) = hunk_before
|
||||
&& let Some(first_edit) = edits_since_diff.first()
|
||||
&& hunk
|
||||
.buffer_range
|
||||
.start
|
||||
.to_point(self.original_buffer_snapshot())
|
||||
> first_edit.old.start
|
||||
{
|
||||
cursor.reset();
|
||||
self.hunk_before_buffer_anchor(
|
||||
self.original_buffer_snapshot()
|
||||
.anchor_before(first_edit.old.start),
|
||||
&mut cursor,
|
||||
self.original_buffer_snapshot(),
|
||||
);
|
||||
}
|
||||
if cursor.item().is_none() {
|
||||
cursor.next();
|
||||
}
|
||||
if let Some(prev_hunk) = cursor.prev_item() {
|
||||
hunk_patch.push(Edit {
|
||||
old: Point::zero()
|
||||
..prev_hunk
|
||||
.diff_base_byte_range
|
||||
.end
|
||||
.to_point(self.base_text()),
|
||||
new: Point::zero()
|
||||
..prev_hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.to_point(self.original_buffer_snapshot()),
|
||||
})
|
||||
}
|
||||
let range_end = range.end().to_offset(self.base_text());
|
||||
while let Some(hunk) = cursor.item()
|
||||
&& (hunk.diff_base_byte_range.start <= range_end
|
||||
|| edits_since_diff.last().is_some_and(|last_edit| {
|
||||
hunk.buffer_range
|
||||
.start
|
||||
.to_point(self.original_buffer_snapshot())
|
||||
<= last_edit.old.end
|
||||
}))
|
||||
{
|
||||
hunk_patch.push(Edit {
|
||||
old: hunk.diff_base_byte_range.to_point(self.base_text()),
|
||||
new: hunk.buffer_range.to_point(self.original_buffer_snapshot()),
|
||||
});
|
||||
cursor.next();
|
||||
}
|
||||
|
||||
Patch::new(hunk_patch).compose(edits_since_diff)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn patch_for_base_text_range_naive<'a>(
|
||||
&'a self,
|
||||
buffer: &'a text::BufferSnapshot,
|
||||
) -> Patch<Point> {
|
||||
let original_snapshot = self.original_buffer_snapshot();
|
||||
@@ -3543,4 +3680,219 @@ mod tests {
|
||||
Point::new(1, 4)..Point::new(4, 0),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_patch_for_range_random(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
if rng.random_bool(0.2) {
|
||||
"\n".to_owned()
|
||||
} else {
|
||||
let c = rng.random_range('A'..='Z');
|
||||
format!("{c}{c}{c}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_text(rng: &mut StdRng, line_count: usize) -> String {
|
||||
(0..line_count).map(|_| gen_line(rng)).collect()
|
||||
}
|
||||
|
||||
fn gen_edits_from(rng: &mut StdRng, base: &str) -> String {
|
||||
let mut old_lines: Vec<&str> = base.lines().collect();
|
||||
let mut result = String::new();
|
||||
|
||||
while !old_lines.is_empty() {
|
||||
let unchanged_count = rng.random_range(0..=old_lines.len());
|
||||
for _ in 0..unchanged_count {
|
||||
if old_lines.is_empty() {
|
||||
break;
|
||||
}
|
||||
result.push_str(old_lines.remove(0));
|
||||
result.push('\n');
|
||||
}
|
||||
|
||||
if old_lines.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let deleted_count = rng.random_range(0..=old_lines.len().min(3));
|
||||
for _ in 0..deleted_count {
|
||||
if old_lines.is_empty() {
|
||||
break;
|
||||
}
|
||||
old_lines.remove(0);
|
||||
}
|
||||
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.random_range(minimum_added..=3);
|
||||
for _ in 0..added_count {
|
||||
result.push_str(&gen_line(rng));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn random_point_in_text(rng: &mut StdRng, lines: &[&str]) -> Point {
|
||||
if lines.is_empty() {
|
||||
return Point::zero();
|
||||
}
|
||||
let row = rng.random_range(0..lines.len() as u32);
|
||||
let line = lines[row as usize];
|
||||
let col = if line.is_empty() {
|
||||
0
|
||||
} else {
|
||||
rng.random_range(0..=line.len() as u32)
|
||||
};
|
||||
Point::new(row, col)
|
||||
}
|
||||
|
||||
fn random_range_in_text(rng: &mut StdRng, lines: &[&str]) -> RangeInclusive<Point> {
|
||||
let start = random_point_in_text(rng, lines);
|
||||
let end = random_point_in_text(rng, lines);
|
||||
if start <= end {
|
||||
start..=end
|
||||
} else {
|
||||
end..=start
|
||||
}
|
||||
}
|
||||
|
||||
fn points_in_range(range: &RangeInclusive<Point>, lines: &[&str]) -> Vec<Point> {
|
||||
let mut points = Vec::new();
|
||||
for row in range.start().row..=range.end().row {
|
||||
if row as usize >= lines.len() {
|
||||
points.push(Point::new(row, 0));
|
||||
continue;
|
||||
}
|
||||
let line = lines[row as usize];
|
||||
let start_col = if row == range.start().row {
|
||||
range.start().column
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end_col = if row == range.end().row {
|
||||
range.end().column
|
||||
} else {
|
||||
line.len() as u32
|
||||
};
|
||||
for col in start_col..=end_col {
|
||||
points.push(Point::new(row, col));
|
||||
}
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
let rng = &mut rng;
|
||||
|
||||
let line_count = rng.random_range(5..20);
|
||||
let base_text = gen_text(rng, line_count);
|
||||
let initial_buffer_text = gen_edits_from(rng, &base_text);
|
||||
|
||||
let mut buffer = Buffer::new(
|
||||
ReplicaId::LOCAL,
|
||||
BufferId::new(1).unwrap(),
|
||||
initial_buffer_text.clone(),
|
||||
);
|
||||
|
||||
let diff = BufferDiffSnapshot::new_sync(&buffer, base_text.clone(), cx);
|
||||
|
||||
let edit_count = rng.random_range(1..=5);
|
||||
for _ in 0..edit_count {
|
||||
let buffer_text = buffer.text();
|
||||
if buffer_text.is_empty() {
|
||||
buffer.edit([(0..0, gen_line(rng))]);
|
||||
} else {
|
||||
let lines: Vec<&str> = buffer_text.lines().collect();
|
||||
let start_row = rng.random_range(0..lines.len());
|
||||
let end_row = rng.random_range(start_row..=lines.len().min(start_row + 3));
|
||||
|
||||
let start_col = if start_row < lines.len() {
|
||||
rng.random_range(0..=lines[start_row].len())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end_col = if end_row < lines.len() {
|
||||
rng.random_range(0..=lines[end_row].len())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let start_offset = buffer
|
||||
.point_to_offset(Point::new(start_row as u32, start_col as u32))
|
||||
.min(buffer.len());
|
||||
let end_offset = buffer
|
||||
.point_to_offset(Point::new(end_row as u32, end_col as u32))
|
||||
.min(buffer.len());
|
||||
|
||||
let (start, end) = if start_offset <= end_offset {
|
||||
(start_offset, end_offset)
|
||||
} else {
|
||||
(end_offset, start_offset)
|
||||
};
|
||||
|
||||
let new_text = if rng.random_bool(0.3) {
|
||||
String::new()
|
||||
} else {
|
||||
let line_count = rng.random_range(0..=2);
|
||||
gen_text(rng, line_count)
|
||||
};
|
||||
|
||||
buffer.edit([(start..end, new_text)]);
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
|
||||
let buffer_text = buffer_snapshot.text();
|
||||
let buffer_lines: Vec<&str> = buffer_text.lines().collect();
|
||||
let base_lines: Vec<&str> = base_text.lines().collect();
|
||||
|
||||
let test_count = 10;
|
||||
for _ in 0..test_count {
|
||||
let range = random_range_in_text(rng, &buffer_lines);
|
||||
let points = points_in_range(&range, &buffer_lines);
|
||||
|
||||
let optimized_patch = diff.patch_for_buffer_range(range.clone(), &buffer_snapshot);
|
||||
let naive_patch = diff.patch_for_buffer_range_naive(&buffer_snapshot);
|
||||
|
||||
for point in points {
|
||||
let optimized_edit = optimized_patch.edit_for_old_position(point);
|
||||
let naive_edit = naive_patch.edit_for_old_position(point);
|
||||
|
||||
assert_eq!(
|
||||
optimized_edit,
|
||||
naive_edit,
|
||||
"patch_for_buffer_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
|
||||
point,
|
||||
range,
|
||||
base_text,
|
||||
initial_buffer_text,
|
||||
buffer_snapshot.text()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..test_count {
|
||||
let range = random_range_in_text(rng, &base_lines);
|
||||
let points = points_in_range(&range, &base_lines);
|
||||
|
||||
let optimized_patch = diff.patch_for_base_text_range(range.clone(), &buffer_snapshot);
|
||||
let naive_patch = diff.patch_for_base_text_range_naive(&buffer_snapshot);
|
||||
|
||||
for point in points {
|
||||
let optimized_edit = optimized_patch.edit_for_old_position(point);
|
||||
let naive_edit = naive_patch.edit_for_old_position(point);
|
||||
|
||||
assert_eq!(
|
||||
optimized_edit,
|
||||
naive_edit,
|
||||
"patch_for_base_text_range mismatch at point {:?} in range {:?}\nbase_text: {:?}\ninitial_buffer: {:?}\ncurrent_buffer: {:?}",
|
||||
point,
|
||||
range,
|
||||
base_text,
|
||||
initial_buffer_text,
|
||||
buffer_snapshot.text()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user