Fix the project diff sometimes missing updates (#40662)
This PR does two related things: - First, it gets rid of the undifferentiated `RepositoryEvent::Updated` in favor of three new events that have clearer definitions: `BranchChanged`, `StashEntriesChanged`, and `StatusesChanged`. An implication of this is that we no longer emit a `RepositoryEvent` unless some git state changed; previously we would emit `RepositoryUpdated` after doing a git status scan even if no statuses changed. - Second, it changes the subscription strategy of the project diff to make it update more robustly. Previously, the project diff only subscribed to the `GitStore`, so it relied on getting a `GitStoreEvent` when some buffer's diff hunks changed, even if the git status of the buffer's file didn't change (e.g. a second hunk in a file that was already modified). After this PR, it also subscribes to the individual `BufferDiff` entities for buffers that have a git status, so the `GitStore` is freed from that responsibility. This also fixes some real cases where the previous strategy was not effective in keeping the project diff up to date (captured in a test). Release Notes: - Fixed some cases where the project diff would fail to update in response to git events.
This commit is contained in:
@@ -6,7 +6,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::HashSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Editor, EditorEvent, SelectionEffects,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
@@ -27,7 +27,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
|
||||
use multi_buffer::{MultiBuffer, PathKey};
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
git_store::{GitStore, GitStoreEvent, Repository},
|
||||
git_store::{GitStore, GitStoreEvent, Repository, RepositoryEvent},
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::any::{Any, TypeId};
|
||||
@@ -57,12 +57,13 @@ pub struct ProjectDiff {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
git_store: Entity<GitStore>,
|
||||
buffer_diff_subscriptions: HashMap<RepoPath, (Entity<BufferDiff>, Subscription)>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
update_needed: postage::watch::Sender<()>,
|
||||
pending_scroll: Option<PathKey>,
|
||||
_task: Task<Result<()>>,
|
||||
_subscription: Subscription,
|
||||
_git_store_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -177,7 +178,11 @@ impl ProjectDiff {
|
||||
window,
|
||||
move |this, _git_store, event, _window, _cx| match event {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_)
|
||||
| GitStoreEvent::RepositoryUpdated(_, _, true)
|
||||
| GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged { full_scan: _ },
|
||||
true,
|
||||
)
|
||||
| GitStoreEvent::ConflictsUpdated => {
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
}
|
||||
@@ -217,10 +222,11 @@ impl ProjectDiff {
|
||||
focus_handle,
|
||||
editor,
|
||||
multibuffer,
|
||||
buffer_diff_subscriptions: Default::default(),
|
||||
pending_scroll: None,
|
||||
update_needed: send,
|
||||
_task: worker,
|
||||
_subscription: git_store_subscription,
|
||||
_git_store_subscription: git_store_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +371,7 @@ impl ProjectDiff {
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
self.buffer_diff_subscriptions.clear();
|
||||
return vec![];
|
||||
};
|
||||
|
||||
@@ -407,6 +414,8 @@ impl ProjectDiff {
|
||||
});
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
for path in previous_paths {
|
||||
self.buffer_diff_subscriptions
|
||||
.remove(&path.path.clone().into());
|
||||
multibuffer.remove_excerpts_for_path(path, cx);
|
||||
}
|
||||
});
|
||||
@@ -419,9 +428,15 @@ impl ProjectDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let path_key = diff_buffer.path_key;
|
||||
let buffer = diff_buffer.buffer;
|
||||
let diff = diff_buffer.diff;
|
||||
let path_key = diff_buffer.path_key.clone();
|
||||
let buffer = diff_buffer.buffer.clone();
|
||||
let diff = diff_buffer.diff.clone();
|
||||
|
||||
let subscription = cx.subscribe(&diff, move |this, _, _, _| {
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
});
|
||||
self.buffer_diff_subscriptions
|
||||
.insert(path_key.path.clone().into(), (diff.clone(), subscription));
|
||||
|
||||
let conflict_addon = self
|
||||
.editor
|
||||
@@ -440,9 +455,10 @@ impl ProjectDiff {
|
||||
.unwrap_or_default();
|
||||
let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
|
||||
|
||||
let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot)
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
let excerpt_ranges =
|
||||
merge_anchor_ranges(diff_hunk_ranges.into_iter(), conflicts, &snapshot)
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let was_empty = multibuffer.is_empty();
|
||||
@@ -519,8 +535,7 @@ impl ProjectDiff {
|
||||
self.multibuffer
|
||||
.read(cx)
|
||||
.excerpt_paths()
|
||||
.map(|key| key.path())
|
||||
.cloned()
|
||||
.map(|key| key.path.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -1621,8 +1636,8 @@ mod tests {
|
||||
cx,
|
||||
&"
|
||||
- original
|
||||
+ different
|
||||
ˇ"
|
||||
+ ˇdifferent
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
@@ -1950,6 +1965,7 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// The project diff updates its excerpts when a new hunk appears in a buffer that already has a diff.
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/project/foo.txt"), cx)
|
||||
@@ -2002,4 +2018,63 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_update_on_uncommit(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"README.md": "# My cool project\n".to_owned()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_head_and_index_for_repo(
|
||||
Path::new(path!("/project/.git")),
|
||||
&[("README.md", "# My cool project\n".to_owned())],
|
||||
);
|
||||
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let _editor = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, rel_path("README.md")), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
cx.focus(&workspace);
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let item = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.read_with(cx, |item, _| item.editor.clone());
|
||||
|
||||
fs.set_head_and_index_for_repo(
|
||||
Path::new(path!("/project/.git")),
|
||||
&[(
|
||||
"README.md",
|
||||
"# My cool project\nDetails to come.\n".to_owned(),
|
||||
)],
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
|
||||
|
||||
cx.assert_excerpts_with_selections("[EXCERPT]\nˇ# My cool project\nDetails to come.\n");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user