Restructure git diff state management to allow viewing buffers with different diff bases (#21258)

This is a pure refactor of our Git diff state management. Buffers are no
longer are associated with one single diff (the unstaged changes).
Instead, there is an explicit project API for retrieving a buffer's
unstaged changes, and the `Editor` view layer is responsible for
choosing what diff to associate with a buffer.

The reason for this change is that we'll soon want to add multiple "git
diff views" to Zed, one of which will show the *uncommitted* changes for
a buffer. But that view will need to co-exist with other views of the
same buffer, which may want to show the unstaged changes.

### Todo

* [x] Get git gutter and git hunks working with new structure
* [x] Update editor tests to use new APIs
* [x] Update buffer tests
* [x] Restructure remoting/collab protocol
* [x] Update assertions about staged text in
`random_project_collaboration_tests`
* [x] Move buffer tests for git diff management to a new spot, using the
new APIs

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-12-04 15:02:33 -08:00 committed by GitHub
parent 31796171de
commit a2115e7242
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1832 additions and 1651 deletions

View file

@ -25,7 +25,7 @@ pub mod search_history;
mod yarn;
use anyhow::{anyhow, Context as _, Result};
use buffer_store::{BufferStore, BufferStoreEvent};
use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent};
use client::{proto, Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{BTreeSet, HashMap, HashSet};
@ -1821,6 +1821,20 @@ impl Project {
})
}
pub fn open_unstaged_changes(
&mut self,
buffer: Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<BufferChangeSet>>> {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
self.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.open_unstaged_changes(buffer, cx)
})
}
pub fn open_buffer_by_id(
&mut self,
id: BufferId,
@ -2269,10 +2283,7 @@ impl Project {
event: &BufferEvent,
cx: &mut ModelContext<Self>,
) -> Option<()> {
if matches!(
event,
BufferEvent::Edited { .. } | BufferEvent::Reloaded | BufferEvent::DiffBaseChanged
) {
if matches!(event, BufferEvent::Edited { .. } | BufferEvent::Reloaded) {
self.request_buffer_diff_recalculation(&buffer, cx);
}
@ -2369,34 +2380,32 @@ impl Project {
}
fn recalculate_buffer_diffs(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
let buffers = self.buffers_needing_diff.drain().collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let tasks: Vec<_> = buffers
.iter()
.filter_map(|buffer| {
let buffer = buffer.upgrade()?;
buffer
.update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
.ok()
.flatten()
})
.collect();
futures::future::join_all(tasks).await;
this.update(&mut cx, |this, cx| {
if this.buffers_needing_diff.is_empty() {
// TODO: Would a `ModelContext<Project>.notify()` suffice here?
for buffer in buffers {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, |_, cx| cx.notify());
loop {
let task = this
.update(&mut cx, |this, cx| {
let buffers = this
.buffers_needing_diff
.drain()
.filter_map(|buffer| buffer.upgrade())
.collect::<Vec<_>>();
if buffers.is_empty() {
None
} else {
Some(this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.recalculate_buffer_diffs(buffers, cx)
}))
}
}
})
.ok()
.flatten();
if let Some(task) = task {
task.await;
} else {
this.recalculate_buffer_diffs(cx).detach();
break;
}
})
.ok();
}
})
}
@ -4149,6 +4158,10 @@ impl Project {
.read(cx)
.language_servers_for_buffer(buffer, cx)
}
pub fn buffer_store(&self) -> &Model<BufferStore> {
&self.buffer_store
}
}
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {