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

@ -979,6 +979,7 @@ impl<'a> Iterator for WrapRows<'a> {
Some(if soft_wrapped {
RowInfo {
buffer_id: None,
buffer_row: None,
multibuffer_row: None,
diff_status,

View file

@ -10137,12 +10137,12 @@ impl Editor {
let mut diagnostics;
if direction == Direction::Prev {
diagnostics = buffer
.diagnostics_in_range::<_, usize>(0..search_start)
.diagnostics_in_range::<usize>(0..search_start)
.collect::<Vec<_>>();
diagnostics.reverse();
} else {
diagnostics = buffer
.diagnostics_in_range::<_, usize>(search_start..buffer.len())
.diagnostics_in_range::<usize>(search_start..buffer.len())
.collect::<Vec<_>>();
};
let group = diagnostics
@ -11333,8 +11333,9 @@ impl Editor {
if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
let buffer = self.buffer.read(cx).snapshot(cx);
let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
let primary_range_end = active_diagnostics.primary_range.end.to_offset(&buffer);
let is_valid = buffer
.diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone())
.diagnostics_in_range::<usize>(primary_range_start..primary_range_end)
.any(|entry| {
entry.diagnostic.is_primary
&& !entry.range.is_empty()

View file

@ -12431,8 +12431,8 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
(buffer_2.clone(), base_text_2),
(buffer_3.clone(), base_text_3),
] {
let change_set = cx
.new(|cx| BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx));
let change_set =
cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx));
editor
.buffer
.update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
@ -13125,9 +13125,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
(buffer_2.clone(), file_2_old),
(buffer_3.clone(), file_3_old),
] {
let change_set = cx.new(|cx| {
BufferChangeSet::new_with_base_text(diff_base.to_string(), &buffer, cx)
});
let change_set =
cx.new(|cx| BufferChangeSet::new_with_base_text(&diff_base, &buffer, cx));
editor
.buffer
.update(cx, |buffer, cx| buffer.add_change_set(change_set, cx));
@ -13212,7 +13211,7 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
init_test(cx, |_| {});
let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\n";
let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
let multi_buffer = cx.new(|cx| {
@ -13225,7 +13224,11 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
context: Point::new(4, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 0),
primary: None,
},
],
@ -13239,8 +13242,7 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
});
editor
.update(cx, |editor, _window, cx| {
let change_set =
cx.new(|cx| BufferChangeSet::new_with_base_text(base.to_string(), &buffer, cx));
let change_set = cx.new(|cx| BufferChangeSet::new_with_base_text(base, &buffer, cx));
editor
.buffer
.update(cx, |buffer, cx| buffer.add_change_set(change_set, cx))
@ -13255,14 +13257,22 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext
});
cx.executor().run_until_parked();
// When the start of a hunk coincides with the start of its excerpt,
// the hunk is expanded. When the start of a a hunk is earlier than
// the start of its excerpt, the hunk is not expanded.
cx.assert_state_with_diff(
"
ˇaaa
- bbb
+ BBB
- ddd
- eee
+ DDD
+ EEE
fff
iii
"
.unindent(),
);
@ -13500,8 +13510,8 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
cx.set_state(indoc! { "
one
TWO
ˇthree
ˇTWO
three
four
five
"});
@ -13514,15 +13524,14 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
indoc! { "
one
- two
+ TWO
ˇthree
+ ˇTWO
three
four
five
"}
.to_string(),
);
cx.update_editor(|editor, window, cx| {
editor.move_up(&Default::default(), window, cx);
editor.move_up(&Default::default(), window, cx);
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
});
@ -14402,12 +14411,8 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut gpui::TestAppContex
editor.buffer().update(cx, |multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
let change_set = cx.new(|cx| {
let mut change_set = BufferChangeSet::new(&buffer, cx);
let _ =
change_set.set_base_text(base_text.into(), buffer.read(cx).text_snapshot(), cx);
change_set
});
let change_set =
cx.new(|cx| BufferChangeSet::new_with_base_text(base_text, &buffer, cx));
multibuffer.set_all_diff_hunks_expanded(cx);
multibuffer.add_change_set(change_set, cx);

View file

@ -5295,7 +5295,7 @@ impl EditorElement {
if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
let diagnostics = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, Point>(Point::zero()..max_point)
.diagnostics_in_range::<Point>(Point::zero()..max_point)
// Don't show diagnostics the user doesn't care about
.filter(|diagnostic| {
match (

View file

@ -697,7 +697,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new("/my-repo/.git"),
vec![(
Path::new("file.txt"),
"file.txt".into(),
Blame {
entries: vec![
blame_entry("1b1b1b", 0..1),
@ -809,7 +809,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new("/my-repo/.git"),
vec![(
Path::new("file.txt"),
"file.txt".into(),
Blame {
entries: vec![blame_entry("1b1b1b", 0..4)],
..Default::default()
@ -958,7 +958,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new("/my-repo/.git"),
vec![(
Path::new("file.txt"),
"file.txt".into(),
Blame {
entries: blame_entries,
..Default::default()
@ -1000,7 +1000,7 @@ mod tests {
fs.set_blame_for_repo(
Path::new("/my-repo/.git"),
vec![(
Path::new("file.txt"),
"file.txt".into(),
Blame {
entries: blame_entries,
..Default::default()

File diff suppressed because it is too large Load diff

View file

@ -279,9 +279,10 @@ fn show_hover(
delay.await;
}
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
let local_diagnostic = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, usize>(anchor..anchor)
.diagnostics_in_range::<usize>(offset..offset)
// Find the entry with the most specific range
.min_by_key(|entry| entry.range.len());

View file

@ -111,11 +111,7 @@ impl ProposedChangesEditor {
.read(cx)
.change_set_for(buffer.remote_id())?;
Some(change_set.update(cx, |change_set, cx| {
change_set.set_base_text(
base_buffer.read(cx).text(),
buffer,
cx,
)
change_set.set_base_text(base_buffer.clone(), buffer, cx)
}))
})
.collect::<Vec<_>>()
@ -192,7 +188,7 @@ impl ProposedChangesEditor {
new_change_sets.push(cx.new(|cx| {
let mut change_set = BufferChangeSet::new(&branch_buffer, cx);
let _ = change_set.set_base_text(
location.buffer.read(cx).text(),
location.buffer.clone(),
branch_buffer.read(cx).text_snapshot(),
cx,
);

View file

@ -292,7 +292,7 @@ impl EditorTestContext {
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo(
&Self::root_path().join(".git"),
&[(path.as_ref(), diff_base.to_string())],
&[(path.into(), diff_base.to_string())],
);
self.cx.run_until_parked();
}