git: Compute and synchronize diffs from HEAD (#23626)

This PR builds on #21258 to make it possible to use HEAD as a diff base.
The buffer store is extended to support holding multiple change sets,
and collab gains support for synchronizing the committed text of files
when any collaborator requires it.

Not implemented in this PR:

- Exposing the diff from HEAD to the user
- Decorating the diff from HEAD with information about which hunks are
staged

`test_random_multibuffer` now fails first at `SEED=13277`, similar to
the previous high-water mark, but with various bugs in the multibuffer
logic now shaken out.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Cole Miller 2025-02-04 15:29:10 -05:00 committed by GitHub
parent 871f98bc4d
commit 5704b50fb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2799 additions and 603 deletions

View file

@ -28,7 +28,7 @@ use smol::future::yield_now;
use std::{
any::type_name,
borrow::Cow,
cell::{Ref, RefCell, RefMut},
cell::{Ref, RefCell},
cmp, fmt,
future::Future,
io,
@ -290,6 +290,7 @@ impl ExcerptBoundary {
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct RowInfo {
pub buffer_id: Option<BufferId>,
pub buffer_row: Option<u32>,
pub multibuffer_row: Option<MultiBufferRow>,
pub diff_status: Option<git::diff::DiffHunkStatus>,
@ -1742,7 +1743,7 @@ impl MultiBuffer {
}
self.sync_diff_transforms(
snapshot,
&mut snapshot,
vec![Edit {
old: edit_start..edit_start,
new: edit_start..edit_end,
@ -1775,7 +1776,7 @@ impl MultiBuffer {
snapshot.has_conflict = false;
self.sync_diff_transforms(
snapshot,
&mut snapshot,
vec![Edit {
old: start..prev_len,
new: start..start,
@ -2053,7 +2054,7 @@ impl MultiBuffer {
snapshot.trailing_excerpt_update_count += 1;
}
self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
@ -2218,7 +2219,7 @@ impl MultiBuffer {
}
self.sync_diff_transforms(
snapshot,
&mut snapshot,
excerpt_edits,
DiffChangeKind::DiffUpdated {
base_changed: base_text_changed,
@ -2388,7 +2389,7 @@ impl MultiBuffer {
cx: &mut Context<Self>,
) {
self.sync(cx);
let snapshot = self.snapshot.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut();
let mut excerpt_edits = Vec::new();
for range in ranges.iter() {
let end_excerpt_id = range.end.excerpt_id;
@ -2422,7 +2423,7 @@ impl MultiBuffer {
}
self.sync_diff_transforms(
snapshot,
&mut snapshot,
excerpt_edits,
DiffChangeKind::ExpandOrCollapseHunks { expand },
);
@ -2491,7 +2492,7 @@ impl MultiBuffer {
drop(cursor);
snapshot.excerpts = new_excerpts;
self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
@ -2592,7 +2593,7 @@ impl MultiBuffer {
drop(cursor);
snapshot.excerpts = new_excerpts;
self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited);
cx.emit(Event::Edited {
singleton_buffer_edited: false,
edited_buffer: None,
@ -2705,12 +2706,12 @@ impl MultiBuffer {
drop(cursor);
snapshot.excerpts = new_excerpts;
self.sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited);
self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited);
}
fn sync_diff_transforms(
&self,
mut snapshot: RefMut<MultiBufferSnapshot>,
snapshot: &mut MultiBufferSnapshot,
excerpt_edits: Vec<text::Edit<ExcerptOffset>>,
change_kind: DiffChangeKind,
) {
@ -2791,11 +2792,23 @@ impl MultiBuffer {
if excerpt_edits.peek().map_or(true, |next_edit| {
next_edit.old.start >= old_diff_transforms.end(&()).0
}) {
let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end)
&& match old_diff_transforms.item() {
Some(DiffTransform::BufferContent {
inserted_hunk_anchor: Some(hunk_anchor),
..
}) => excerpts
.item()
.is_some_and(|excerpt| hunk_anchor.1.is_valid(&excerpt.buffer)),
_ => true,
};
let mut excerpt_offset = edit.new.end;
if old_diff_transforms.start().0 < edit.old.end {
if !keep_next_old_transform {
excerpt_offset += old_diff_transforms.end(&()).0 - edit.old.end;
old_diff_transforms.next(&());
}
old_expanded_hunks.clear();
self.push_buffer_content_transform(
&snapshot,
@ -2894,12 +2907,14 @@ impl MultiBuffer {
buffer.anchor_before(edit_buffer_start)..buffer.anchor_after(edit_buffer_end);
for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) {
let hunk_buffer_range = hunk.buffer_range.to_offset(buffer);
let hunk_anchor = (excerpt.id, hunk.buffer_range.start);
if !hunk_anchor.1.is_valid(buffer) {
if hunk_buffer_range.start < excerpt_buffer_start {
log::trace!("skipping hunk that starts before excerpt");
continue;
}
let hunk_buffer_range = hunk.buffer_range.to_offset(buffer);
let hunk_excerpt_start = excerpt_start
+ ExcerptOffset::new(
hunk_buffer_range.start.saturating_sub(excerpt_buffer_start),
@ -2941,8 +2956,9 @@ impl MultiBuffer {
if should_expand_hunk {
did_expand_hunks = true;
log::trace!(
"expanding hunk {:?}",
"expanding hunk {:?}, excerpt:{:?}",
hunk_excerpt_start.value..hunk_excerpt_end.value,
excerpt.id
);
if !hunk.diff_base_byte_range.is_empty()
@ -3389,12 +3405,12 @@ impl MultiBufferSnapshot {
self.diff_hunks_in_range(Anchor::min()..Anchor::max())
}
pub fn diff_hunks_in_range<T: ToOffset>(
pub fn diff_hunks_in_range<T: ToPoint>(
&self,
range: Range<T>,
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.lift_buffer_metadata(range.clone(), move |buffer, buffer_range| {
let query_range = range.start.to_point(self)..range.end.to_point(self);
self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| {
let diff = self.diffs.get(&buffer.remote_id())?;
let buffer_start = buffer.anchor_before(buffer_range.start);
let buffer_end = buffer.anchor_after(buffer_range.end);
@ -3409,19 +3425,25 @@ impl MultiBufferSnapshot {
}),
)
})
.map(|(range, hunk, excerpt)| {
.filter_map(move |(range, hunk, excerpt)| {
if range.start != range.end
&& range.end == query_range.start
&& !hunk.row_range.is_empty()
{
return None;
}
let end_row = if range.end.column == 0 {
range.end.row
} else {
range.end.row + 1
};
MultiBufferDiffHunk {
Some(MultiBufferDiffHunk {
row_range: MultiBufferRow(range.start.row)..MultiBufferRow(end_row),
buffer_id: excerpt.buffer_id,
excerpt_id: excerpt.id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
})
})
}
@ -3560,8 +3582,8 @@ impl MultiBufferSnapshot {
/// multi-buffer coordinates.
fn lift_buffer_metadata<'a, D, M, I>(
&'a self,
range: Range<usize>,
get_buffer_metadata: impl 'a + Fn(&'a BufferSnapshot, Range<usize>) -> Option<I>,
query_range: Range<D>,
get_buffer_metadata: impl 'a + Fn(&'a BufferSnapshot, Range<D>) -> Option<I>,
) -> impl Iterator<Item = (Range<D>, M, &'a Excerpt)> + 'a
where
I: Iterator<Item = (Range<D>, M)> + 'a,
@ -3569,18 +3591,19 @@ impl MultiBufferSnapshot {
{
let max_position = D::from_text_summary(&self.text_summary());
let mut current_excerpt_metadata: Option<(ExcerptId, I)> = None;
let mut cursor = self.cursor::<DimensionPair<usize, D>>();
let mut cursor = self.cursor::<D>();
// Find the excerpt and buffer offset where the given range ends.
cursor.seek(&DimensionPair {
key: range.end,
value: None,
});
cursor.seek(&query_range.end);
let mut range_end = None;
while let Some(region) = cursor.region() {
if region.is_main_buffer {
let mut buffer_end = region.buffer_range.start.key;
let overshoot = range.end.saturating_sub(region.range.start.key);
let mut buffer_end = region.buffer_range.start;
let overshoot = if query_range.end > region.range.start {
query_range.end - region.range.start
} else {
D::default()
};
buffer_end.add_assign(&overshoot);
range_end = Some((region.excerpt.id, buffer_end));
break;
@ -3588,13 +3611,10 @@ impl MultiBufferSnapshot {
cursor.next();
}
cursor.seek(&DimensionPair {
key: range.start,
value: None,
});
cursor.seek(&query_range.start);
if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) {
if region.range.start.key > 0 {
if region.range.start > D::zero(&()) {
cursor.prev()
}
}
@ -3613,14 +3633,18 @@ impl MultiBufferSnapshot {
// and retrieve the metadata for the resulting range.
else {
let region = cursor.region()?;
let buffer_start = if region.is_main_buffer {
let start_overshoot = range.start.saturating_sub(region.range.start.key);
(region.buffer_range.start.key + start_overshoot)
.min(region.buffer_range.end.key)
let mut buffer_start;
if region.is_main_buffer {
buffer_start = region.buffer_range.start;
if query_range.start > region.range.start {
let overshoot = query_range.start - region.range.start;
buffer_start.add_assign(&overshoot);
}
buffer_start = buffer_start.min(region.buffer_range.end);
} else {
cursor.main_buffer_position()?.key
buffer_start = cursor.main_buffer_position()?;
};
let mut buffer_end = excerpt.range.context.end.to_offset(&excerpt.buffer);
let mut buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
if let Some((end_excerpt_id, end_buffer_offset)) = range_end {
if excerpt.id == end_excerpt_id {
buffer_end = buffer_end.min(end_buffer_offset);
@ -3637,53 +3661,56 @@ impl MultiBufferSnapshot {
};
// Visit each metadata item.
if let Some((range, metadata)) = metadata_iter.and_then(Iterator::next) {
if let Some((metadata_buffer_range, metadata)) = metadata_iter.and_then(Iterator::next)
{
// Find the multibuffer regions that contain the start and end of
// the metadata item's range.
if range.start > D::default() {
if metadata_buffer_range.start > D::default() {
while let Some(region) = cursor.region() {
if !region.is_main_buffer
|| region.buffer.remote_id() == excerpt.buffer_id
&& region.buffer_range.end.value.unwrap() < range.start
if region.is_main_buffer
&& (region.buffer_range.end >= metadata_buffer_range.start
|| cursor.is_at_end_of_excerpt())
{
cursor.next();
} else {
break;
}
cursor.next();
}
}
let start_region = cursor.region()?;
while let Some(region) = cursor.region() {
if !region.is_main_buffer
|| region.buffer.remote_id() == excerpt.buffer_id
&& region.buffer_range.end.value.unwrap() <= range.end
if region.is_main_buffer
&& (region.buffer_range.end > metadata_buffer_range.end
|| cursor.is_at_end_of_excerpt())
{
cursor.next();
} else {
break;
}
cursor.next();
}
let end_region = cursor
.region()
.filter(|region| region.buffer.remote_id() == excerpt.buffer_id);
let end_region = cursor.region();
// Convert the metadata item's range into multibuffer coordinates.
let mut start = start_region.range.start.value.unwrap();
let region_buffer_start = start_region.buffer_range.start.value.unwrap();
if start_region.is_main_buffer && range.start > region_buffer_start {
start.add_assign(&(range.start - region_buffer_start));
}
let mut end = max_position;
if let Some(end_region) = end_region {
end = end_region.range.start.value.unwrap();
debug_assert!(end_region.is_main_buffer);
let region_buffer_start = end_region.buffer_range.start.value.unwrap();
if range.end > region_buffer_start {
end.add_assign(&(range.end - region_buffer_start));
}
let mut start_position = start_region.range.start;
let region_buffer_start = start_region.buffer_range.start;
if start_region.is_main_buffer && metadata_buffer_range.start > region_buffer_start
{
start_position.add_assign(&(metadata_buffer_range.start - region_buffer_start));
start_position = start_position.min(start_region.range.end);
}
return Some((start..end, metadata, excerpt));
let mut end_position = max_position;
if let Some(end_region) = &end_region {
end_position = end_region.range.start;
debug_assert!(end_region.is_main_buffer);
let region_buffer_start = end_region.buffer_range.start;
if metadata_buffer_range.end > region_buffer_start {
end_position.add_assign(&(metadata_buffer_range.end - region_buffer_start));
}
end_position = end_position.min(end_region.range.end);
}
if start_position <= query_range.end && end_position >= query_range.start {
return Some((start_position..end_position, metadata, excerpt));
}
}
// When there are no more metadata items for this excerpt, move to the next excerpt.
else {
@ -4509,7 +4536,16 @@ impl MultiBufferSnapshot {
}
let excerpt_start_position = D::from_text_summary(&cursor.start().text);
if let Some(excerpt) = cursor.item().filter(|excerpt| excerpt.id == excerpt_id) {
if let Some(excerpt) = cursor.item() {
if excerpt.id != excerpt_id {
let position = self.resolve_summary_for_anchor(
&Anchor::min(),
excerpt_start_position,
&mut diff_transforms_cursor,
);
summaries.extend(excerpt_anchors.map(|_| position));
continue;
}
let excerpt_buffer_start =
excerpt.range.context.start.summary::<D>(&excerpt.buffer);
let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
@ -5525,7 +5561,7 @@ impl MultiBufferSnapshot {
buffer_id: BufferId,
group_id: usize,
) -> impl Iterator<Item = DiagnosticEntry<Point>> + '_ {
self.lift_buffer_metadata(0..self.len(), move |buffer, _| {
self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, _| {
if buffer.remote_id() != buffer_id {
return None;
};
@ -5538,15 +5574,19 @@ impl MultiBufferSnapshot {
.map(|(range, diagnostic, _)| DiagnosticEntry { diagnostic, range })
}
pub fn diagnostics_in_range<'a, T, O>(
pub fn diagnostics_in_range<'a, T>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
) -> impl Iterator<Item = DiagnosticEntry<T>> + 'a
where
T: 'a + ToOffset,
O: 'a + text::FromAnchor + Copy + TextDimension + Ord + Sub<O, Output = O> + fmt::Debug,
T: 'a
+ text::ToOffset
+ text::FromAnchor
+ TextDimension
+ Ord
+ Sub<T, Output = T>
+ fmt::Debug,
{
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.lift_buffer_metadata(range, move |buffer, buffer_range| {
Some(
buffer
@ -6036,6 +6076,24 @@ where
self.cached_region.clone()
}
fn is_at_end_of_excerpt(&mut self) -> bool {
if self.diff_transforms.end(&()).1 < self.excerpts.end(&()) {
return false;
} else if self.diff_transforms.end(&()).1 > self.excerpts.end(&())
|| self.diff_transforms.item().is_none()
{
return true;
}
self.diff_transforms.next(&());
let next_transform = self.diff_transforms.item();
self.diff_transforms.prev(&());
next_transform.map_or(true, |next_transform| {
matches!(next_transform, DiffTransform::BufferContent { .. })
})
}
fn main_buffer_position(&self) -> Option<D> {
let excerpt = self.excerpts.item()?;
let buffer = &excerpt.buffer;
@ -6879,6 +6937,7 @@ impl<'a> Iterator for MultiBufferRows<'a> {
if self.is_empty && self.point.row == 0 {
self.point += Point::new(1, 0);
return Some(RowInfo {
buffer_id: None,
buffer_row: Some(0),
multibuffer_row: Some(MultiBufferRow(0)),
diff_status: None,
@ -6906,6 +6965,7 @@ impl<'a> Iterator for MultiBufferRows<'a> {
.to_point(&last_excerpt.buffer)
.row;
return Some(RowInfo {
buffer_id: Some(last_excerpt.buffer_id),
buffer_row: Some(last_row),
multibuffer_row: Some(multibuffer_row),
diff_status: None,
@ -6919,6 +6979,7 @@ impl<'a> Iterator for MultiBufferRows<'a> {
let overshoot = self.point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
let result = Some(RowInfo {
buffer_id: Some(region.buffer.remote_id()),
buffer_row: Some(buffer_point.row),
multibuffer_row: Some(MultiBufferRow(self.point.row)),
diff_status: if region.is_inserted_hunk && self.point < region.range.end {