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:
Cole Miller authored and GitHub committed 2026-02-10 01:19:01 -05:00
1 parent 846b1ae766
commit a19c8b24e5
3 files changed
+484 -52

No files matched your search

+369 -17
View File
@@ -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()
);
}
}
}
}