Handle changing root paths without splitting in the sidebar (#53566)

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Eric Holk <eric@zed.dev>
This commit is contained in:
Mikayla MakiandEric Holk authored and GitHub committed 2026-04-09 18:21:19 -07:00
1 parent 3436e51042
commit 4a1fb25f67
6 files changed
+1347 -317

No files matched your search

@@ -477,6 +477,36 @@ impl ThreadMetadataStore {
}
}
pub fn update_main_worktree_paths(
&mut self,
old_paths: &PathList,
new_paths: PathList,
cx: &mut Context<Self>,
) {
let session_ids = match self.threads_by_main_paths.remove(old_paths) {
Some(ids) if !ids.is_empty() => ids,
_ => return,
};
let new_index = self
.threads_by_main_paths
.entry(new_paths.clone())
.or_default();
for session_id in &session_ids {
new_index.insert(session_id.clone());
if let Some(thread) = self.threads.get_mut(session_id) {
thread.main_worktree_paths = new_paths.clone();
self.pending_thread_ops_tx
.try_send(DbOperation::Upsert(thread.clone()))
.log_err();
}
}
cx.notify();
}
pub fn create_archived_worktree(
&self,
worktree_path: String,
+132 -47
View File
@@ -283,10 +283,8 @@ impl ListEntry {
}
}
ListEntry::ProjectHeader { key, .. } => multi_workspace
.workspaces()
.find(|ws| PathList::new(&ws.read(cx).root_paths(cx)) == *key.path_list())
.workspaces_for_project_group(key, cx)
.cloned()
.into_iter()
.collect(),
ListEntry::ViewMore { .. } => Vec::new(),
}
@@ -365,35 +363,81 @@ fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
///
/// For each path in the thread's `folder_paths`, produces a
/// [`WorktreeInfo`] with a short display name, full path, and whether
/// the worktree is the main checkout or a linked git worktree.
/// the worktree is the main checkout or a linked git worktree. When
/// multiple main paths exist and a linked worktree's short name alone
/// wouldn't identify which main project it belongs to, the main project
/// name is prefixed for disambiguation (e.g. `project:feature`).
///
/// `linked_to_main` maps linked worktree abs paths to their main repo
/// abs path, used to pick the correct prefix. Falls back to a heuristic
/// when no mapping is available.
fn worktree_info_from_thread_paths(
folder_paths: &PathList,
group_key: &project::ProjectGroupKey,
) -> impl Iterator<Item = WorktreeInfo> {
let main_paths = group_key.path_list().paths();
folder_paths.paths().iter().filter_map(|path| {
main_worktree_paths: &PathList,
linked_to_main: &HashMap<PathBuf, PathBuf>,
) -> Vec<WorktreeInfo> {
let main_paths = main_worktree_paths.paths();
let mut infos: Vec<WorktreeInfo> = Vec::new();
let mut linked_short_names: Vec<(SharedString, SharedString)> = Vec::new();
for path in folder_paths.paths().iter() {
let is_main = main_paths.iter().any(|mp| mp.as_path() == path.as_path());
if is_main {
let name = path.file_name()?.to_string_lossy().to_string();
Some(WorktreeInfo {
name: SharedString::from(name),
let Some(name) = path.file_name() else {
continue;
};
infos.push(WorktreeInfo {
name: SharedString::from(name.to_string_lossy().to_string()),
full_path: SharedString::from(path.display().to_string()),
highlight_positions: Vec::new(),
kind: ui::WorktreeKind::Main,
})
});
} else {
let main_path = main_paths
.iter()
.find(|mp| mp.file_name() == path.file_name())
.or(main_paths.first())?;
Some(WorktreeInfo {
name: linked_worktree_short_name(main_path, path).unwrap_or_default(),
let Some(main_path) = linked_to_main
.get(&**path)
.and_then(|main| main_paths.iter().find(|mp| mp.as_path() == main.as_path()))
.or_else(|| {
main_paths
.iter()
.find(|mp| mp.file_name() == path.file_name())
.or(main_paths.first())
})
else {
continue;
};
let short_name = linked_worktree_short_name(main_path, path).unwrap_or_default();
let project_name = main_path
.file_name()
.map(|n| SharedString::from(n.to_string_lossy().to_string()))
.unwrap_or_default();
linked_short_names.push((short_name.clone(), project_name));
infos.push(WorktreeInfo {
name: short_name,
full_path: SharedString::from(path.display().to_string()),
highlight_positions: Vec::new(),
kind: ui::WorktreeKind::Linked,
})
});
}
})
}
// When the group has multiple main worktree paths and the thread's
// folder paths don't all share the same short name, prefix each
// linked worktree chip with its main project name so the user knows
// which project it belongs to.
let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);
if main_paths.len() > 1 && !all_same_name {
for (info, (_short_name, project_name)) in infos
.iter_mut()
.filter(|i| i.kind == ui::WorktreeKind::Linked)
.zip(linked_short_names.iter())
{
info.name = SharedString::from(format!("{}:{}", project_name, info.name));
}
}
infos
}
/// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
@@ -480,6 +524,16 @@ impl Sidebar {
MultiWorkspaceEvent::WorkspaceRemoved(_) => {
this.update_entries(cx);
}
MultiWorkspaceEvent::ProjectGroupKeyChanged { old_key, new_key } => {
ThreadMetadataStore::global(cx).update(cx, |store, cx| {
store.update_main_worktree_paths(
old_key.path_list(),
new_key.path_list().clone(),
cx,
);
});
this.update_entries(cx);
}
},
)
.detach();
@@ -912,6 +966,21 @@ impl Sidebar {
.as_ref()
.is_some_and(|active| group_workspaces.contains(active));
// Build a mapping from linked worktree paths to their main
// repo path, used to correctly attribute chips.
let linked_to_main: HashMap<PathBuf, PathBuf> = group_workspaces
.iter()
.flat_map(|ws| root_repository_snapshots(ws, cx))
.flat_map(|snapshot| {
let main_path = snapshot.original_repo_abs_path.to_path_buf();
snapshot
.linked_worktrees()
.iter()
.map(move |wt| (wt.path.clone(), main_path.clone()))
.collect::<Vec<_>>()
})
.collect();
// Collect live thread infos from all workspaces in this group.
let live_infos: Vec<_> = group_workspaces
.iter()
@@ -949,26 +1018,28 @@ impl Sidebar {
};
// Build a ThreadEntry from a metadata row.
let make_thread_entry = |row: ThreadMetadata,
workspace: ThreadEntryWorkspace|
-> ThreadEntry {
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
let worktrees: Vec<WorktreeInfo> =
worktree_info_from_thread_paths(&row.folder_paths, &group_key).collect();
ThreadEntry {
metadata: row,
icon,
icon_from_external_svg,
status: AgentThreadStatus::default(),
workspace,
is_live: false,
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
worktrees,
diff_stats: DiffStats::default(),
}
};
let make_thread_entry =
|row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> ThreadEntry {
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
let worktrees = worktree_info_from_thread_paths(
&row.folder_paths,
&row.main_worktree_paths,
&linked_to_main,
);
ThreadEntry {
metadata: row,
icon,
icon_from_external_svg,
status: AgentThreadStatus::default(),
workspace,
is_live: false,
is_background: false,
is_title_generating: false,
highlight_positions: Vec::new(),
worktrees,
diff_stats: DiffStats::default(),
}
};
// Main code path: one query per group via main_worktree_paths.
// The main_worktree_paths column is set on all new threads and
@@ -1184,11 +1255,17 @@ impl Sidebar {
if is_draft_for_group {
if let Some(ActiveEntry::Draft(draft_ws)) = &self.active_entry {
let ws_path_list = workspace_path_list(draft_ws, cx);
let worktrees = worktree_info_from_thread_paths(&ws_path_list, &group_key);
let main_worktree_paths =
draft_ws.read(cx).project_group_key(cx).path_list().clone();
let worktrees = worktree_info_from_thread_paths(
&ws_path_list,
&main_worktree_paths,
&linked_to_main,
);
entries.push(ListEntry::DraftThread {
key: group_key.clone(),
workspace: None,
worktrees: worktrees.collect(),
worktrees,
});
}
}
@@ -1212,9 +1289,14 @@ impl Sidebar {
continue;
}
let ws_path_list = workspace_path_list(ws, cx);
let has_linked_worktrees =
worktree_info_from_thread_paths(&ws_path_list, &group_key)
.any(|wt| wt.kind == ui::WorktreeKind::Linked);
let ws_main_paths = ws.read(cx).project_group_key(cx).path_list().clone();
let has_linked_worktrees = worktree_info_from_thread_paths(
&ws_path_list,
&ws_main_paths,
&linked_to_main,
)
.iter()
.any(|wt| wt.kind == ui::WorktreeKind::Linked);
if !has_linked_worktrees {
continue;
}
@@ -1227,8 +1309,11 @@ impl Sidebar {
if has_threads {
continue;
}
let worktrees: Vec<WorktreeInfo> =
worktree_info_from_thread_paths(&ws_path_list, &group_key).collect();
let worktrees = worktree_info_from_thread_paths(
&ws_path_list,
&ws_main_paths,
&linked_to_main,
);
entries.push(ListEntry::DraftThread {
key: group_key.clone(),
File diff suppressed because it is too large. Load diff
+166 -43
View File
@@ -101,6 +101,10 @@ pub enum MultiWorkspaceEvent {
ActiveWorkspaceChanged,
WorkspaceAdded(Entity<Workspace>),
WorkspaceRemoved(EntityId),
ProjectGroupKeyChanged {
old_key: ProjectGroupKey,
new_key: ProjectGroupKey,
},
}
pub enum SidebarEvent {
@@ -302,7 +306,7 @@ pub struct MultiWorkspace {
workspaces: Vec<Entity<Workspace>>,
active_workspace: ActiveWorkspace,
project_group_keys: Vec<ProjectGroupKey>,
provisional_project_group_keys: HashMap<EntityId, ProjectGroupKey>,
workspace_group_keys: HashMap<EntityId, ProjectGroupKey>,
sidebar: Option<Box<dyn SidebarHandle>>,
sidebar_open: bool,
sidebar_overlay: Option<AnyView>,
@@ -355,7 +359,7 @@ impl MultiWorkspace {
Self {
window_id: window.window_handle().window_id(),
project_group_keys: Vec::new(),
provisional_project_group_keys: HashMap::default(),
workspace_group_keys: HashMap::default(),
workspaces: Vec::new(),
active_workspace: ActiveWorkspace::Transient(workspace),
sidebar: None,
@@ -559,19 +563,11 @@ impl MultiWorkspace {
cx.subscribe_in(&project, window, {
let workspace = workspace.downgrade();
move |this, _project, event, _window, cx| match event {
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
project::Event::WorktreeAdded(_)
| project::Event::WorktreeRemoved(_)
| project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
if let Some(workspace) = workspace.upgrade() {
this.add_project_group_key(workspace.read(cx).project_group_key(cx));
}
}
project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
if let Some(workspace) = workspace.upgrade() {
this.maybe_clear_provisional_project_group_key(&workspace, cx);
this.add_project_group_key(
this.project_group_key_for_workspace(&workspace, cx),
);
this.remove_stale_project_group_keys(cx);
cx.notify();
this.handle_workspace_key_change(&workspace, cx);
}
}
_ => {}
@@ -587,7 +583,111 @@ impl MultiWorkspace {
.detach();
}
pub fn add_project_group_key(&mut self, project_group_key: ProjectGroupKey) {
fn handle_workspace_key_change(
&mut self,
workspace: &Entity<Workspace>,
cx: &mut Context<Self>,
) {
let workspace_id = workspace.entity_id();
let old_key = self.project_group_key_for_workspace(workspace, cx);
let new_key = workspace.read(cx).project_group_key(cx);
if new_key.path_list().paths().is_empty() || old_key == new_key {
return;
}
let active_workspace = self.workspace().clone();
self.set_workspace_group_key(workspace, new_key.clone());
let changed_root_paths = workspace.read(cx).root_paths(cx);
let old_paths = old_key.path_list().paths();
let new_paths = new_key.path_list().paths();
// Remove workspaces that already had the new key and have the same
// root paths (true duplicates that this workspace is replacing).
//
// NOTE: These are dropped without prompting for unsaved changes because
// the user explicitly added a folder that makes this workspace
// identical to the duplicate — they are intentionally overwriting it.
let duplicate_workspaces: Vec<Entity<Workspace>> = self
.workspaces
.iter()
.filter(|ws| {
ws.entity_id() != workspace_id
&& self.project_group_key_for_workspace(ws, cx) == new_key
&& ws.read(cx).root_paths(cx) == changed_root_paths
})
.cloned()
.collect();
if duplicate_workspaces.contains(&active_workspace) {
// The active workspace is among the duplicates — drop the
// incoming workspace instead so the user stays where they are.
self.detach_workspace(workspace, cx);
self.workspaces.retain(|w| w != workspace);
} else {
for ws in &duplicate_workspaces {
self.detach_workspace(ws, cx);
self.workspaces.retain(|w| w != ws);
}
}
// Propagate folder adds/removes to linked worktree siblings
// (different root paths, same old key) so they stay in the group.
let group_workspaces: Vec<Entity<Workspace>> = self
.workspaces
.iter()
.filter(|ws| {
ws.entity_id() != workspace_id
&& self.project_group_key_for_workspace(ws, cx) == old_key
})
.cloned()
.collect();
for workspace in &group_workspaces {
// Pre-set this to stop later WorktreeAdded events from triggering
self.set_workspace_group_key(&workspace, new_key.clone());
let project = workspace.read(cx).project().clone();
for added_path in new_paths.iter().filter(|p| !old_paths.contains(p)) {
project
.update(cx, |project, cx| {
project.find_or_create_worktree(added_path, true, cx)
})
.detach_and_log_err(cx);
}
for removed_path in old_paths.iter().filter(|p| !new_paths.contains(p)) {
project.update(cx, |project, cx| {
project.remove_worktree_for_main_worktree_path(removed_path, cx);
});
}
}
// Restore the active workspace after removals may have shifted
// the index. If the previously active workspace was removed,
// fall back to the workspace whose key just changed.
if let ActiveWorkspace::Persistent(_) = &self.active_workspace {
let target = if self.workspaces.contains(&active_workspace) {
&active_workspace
} else {
workspace
};
if let Some(new_index) = self.workspaces.iter().position(|ws| ws == target) {
self.active_workspace = ActiveWorkspace::Persistent(new_index);
}
}
self.remove_stale_project_group_keys(cx);
cx.emit(MultiWorkspaceEvent::ProjectGroupKeyChanged { old_key, new_key });
self.serialize(cx);
cx.notify();
}
fn add_project_group_key(&mut self, project_group_key: ProjectGroupKey) {
if project_group_key.path_list().paths().is_empty() {
return;
}
@@ -598,12 +698,12 @@ impl MultiWorkspace {
self.project_group_keys.insert(0, project_group_key);
}
pub fn set_provisional_project_group_key(
pub(crate) fn set_workspace_group_key(
&mut self,
workspace: &Entity<Workspace>,
project_group_key: ProjectGroupKey,
) {
self.provisional_project_group_keys
self.workspace_group_keys
.insert(workspace.entity_id(), project_group_key.clone());
self.add_project_group_key(project_group_key);
}
@@ -613,28 +713,12 @@ impl MultiWorkspace {
workspace: &Entity<Workspace>,
cx: &App,
) -> ProjectGroupKey {
self.provisional_project_group_keys
self.workspace_group_keys
.get(&workspace.entity_id())
.cloned()
.unwrap_or_else(|| workspace.read(cx).project_group_key(cx))
}
fn maybe_clear_provisional_project_group_key(
&mut self,
workspace: &Entity<Workspace>,
cx: &App,
) {
let live_key = workspace.read(cx).project_group_key(cx);
if self
.provisional_project_group_keys
.get(&workspace.entity_id())
.is_some_and(|key| *key == live_key)
{
self.provisional_project_group_keys
.remove(&workspace.entity_id());
}
}
fn remove_stale_project_group_keys(&mut self, cx: &App) {
let workspace_keys: HashSet<ProjectGroupKey> = self
.workspaces
@@ -1045,7 +1129,6 @@ impl MultiWorkspace {
self.promote_transient(old, cx);
} else {
self.detach_workspace(&old, cx);
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
}
}
} else {
@@ -1056,7 +1139,6 @@ impl MultiWorkspace {
});
if let Some(old) = self.active_workspace.set_transient(workspace) {
self.detach_workspace(&old, cx);
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
}
}
@@ -1083,7 +1165,7 @@ impl MultiWorkspace {
/// Returns the index of the newly inserted workspace.
fn promote_transient(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
let project_group_key = self.project_group_key_for_workspace(&workspace, cx);
self.add_project_group_key(project_group_key);
self.set_workspace_group_key(&workspace, project_group_key);
self.workspaces.push(workspace.clone());
cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
self.workspaces.len() - 1
@@ -1099,10 +1181,10 @@ impl MultiWorkspace {
for workspace in std::mem::take(&mut self.workspaces) {
if workspace != active {
self.detach_workspace(&workspace, cx);
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
}
}
self.project_group_keys.clear();
self.workspace_group_keys.clear();
self.active_workspace = ActiveWorkspace::Transient(active);
cx.notify();
}
@@ -1128,7 +1210,7 @@ impl MultiWorkspace {
workspace.set_multi_workspace(weak_self, cx);
});
self.add_project_group_key(project_group_key);
self.set_workspace_group_key(&workspace, project_group_key);
self.workspaces.push(workspace.clone());
cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
cx.notify();
@@ -1136,10 +1218,12 @@ impl MultiWorkspace {
}
}
/// Clears session state and DB binding for a workspace that is being
/// removed or replaced. The DB row is preserved so the workspace still
/// appears in the recent-projects list.
/// Detaches a workspace: clears session state, DB binding, cached
/// group key, and emits `WorkspaceRemoved`. The DB row is preserved
/// so the workspace still appears in the recent-projects list.
fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
self.workspace_group_keys.remove(&workspace.entity_id());
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
workspace.update(cx, |workspace, _cx| {
workspace.session_id.take();
workspace._schedule_serialize_workspace.take();
@@ -1313,6 +1397,46 @@ impl MultiWorkspace {
tasks
}
#[cfg(any(test, feature = "test-support"))]
pub fn assert_project_group_key_integrity(&self, cx: &App) -> anyhow::Result<()> {
let stored_keys: HashSet<&ProjectGroupKey> = self.project_group_keys().collect();
let workspace_group_keys: HashSet<&ProjectGroupKey> =
self.workspace_group_keys.values().collect();
let extra_keys = &workspace_group_keys - &stored_keys;
anyhow::ensure!(
extra_keys.is_empty(),
"workspace_group_keys values not in project_group_keys: {:?}",
extra_keys,
);
let cached_ids: HashSet<EntityId> = self.workspace_group_keys.keys().copied().collect();
let workspace_ids: HashSet<EntityId> =
self.workspaces.iter().map(|ws| ws.entity_id()).collect();
anyhow::ensure!(
cached_ids == workspace_ids,
"workspace_group_keys entity IDs don't match workspaces.\n\
only in cache: {:?}\n\
only in workspaces: {:?}",
&cached_ids - &workspace_ids,
&workspace_ids - &cached_ids,
);
for workspace in self.workspaces() {
let live_key = workspace.read(cx).project_group_key(cx);
let cached_key = &self.workspace_group_keys[&workspace.entity_id()];
anyhow::ensure!(
*cached_key == live_key,
"workspace {:?} has live key {:?} but cached key {:?}",
workspace.entity_id(),
live_key,
cached_key,
);
}
Ok(())
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
self.workspace().update(cx, |workspace, _cx| {
@@ -1471,7 +1595,6 @@ impl MultiWorkspace {
for workspace in &removed_workspaces {
this.detach_workspace(workspace, cx);
cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
}
let removed_any = !removed_workspaces.is_empty();
@@ -185,157 +185,3 @@ async fn test_project_group_keys_duplicate_not_added(cx: &mut TestAppContext) {
);
});
}
#[gpui::test]
async fn test_project_group_keys_on_worktree_added(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
let project = Project::test(fs, ["/root_a".as_ref()], cx).await;
let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
multi_workspace.update(cx, |mw, cx| {
mw.open_sidebar(cx);
});
// Add a second worktree to the same project.
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/root_b", true, cx)
})
.await
.unwrap();
worktree
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
cx.run_until_parked();
let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
assert_ne!(
initial_key, updated_key,
"key should change after adding a worktree"
);
multi_workspace.read_with(cx, |mw, _cx| {
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
assert_eq!(
keys.len(),
2,
"should have both the original and updated key"
);
assert_eq!(*keys[0], updated_key);
assert_eq!(*keys[1], initial_key);
});
}
#[gpui::test]
async fn test_project_group_keys_on_worktree_removed(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
let project = Project::test(fs, ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
multi_workspace.update(cx, |mw, cx| {
mw.open_sidebar(cx);
});
// Remove one worktree.
let worktree_b_id = project.read_with(cx, |project, cx| {
project
.worktrees(cx)
.find(|wt| wt.read(cx).root_name().as_unix_str() == "root_b")
.unwrap()
.read(cx)
.id()
});
project.update(cx, |project, cx| {
project.remove_worktree(worktree_b_id, cx);
});
cx.run_until_parked();
let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
assert_ne!(
initial_key, updated_key,
"key should change after removing a worktree"
);
multi_workspace.read_with(cx, |mw, _cx| {
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
assert_eq!(
keys.len(),
2,
"should accumulate both the original and post-removal key"
);
assert_eq!(*keys[0], updated_key);
assert_eq!(*keys[1], initial_key);
});
}
#[gpui::test]
async fn test_project_group_keys_across_multiple_workspaces_and_worktree_changes(
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
fs.insert_tree("/root_c", json!({ "file.txt": "" })).await;
let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
let project_b = Project::test(fs.clone(), ["/root_b".as_ref()], cx).await;
let key_a = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
let key_b = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
multi_workspace.update(cx, |mw, cx| {
mw.open_sidebar(cx);
});
multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_b, window, cx);
});
multi_workspace.read_with(cx, |mw, _cx| {
assert_eq!(mw.project_group_keys().count(), 2);
});
// Now add a worktree to project_a. This should produce a third key.
let (worktree, _) = project_a
.update(cx, |project, cx| {
project.find_or_create_worktree("/root_c", true, cx)
})
.await
.unwrap();
worktree
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
cx.run_until_parked();
let key_a_updated = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
assert_ne!(key_a, key_a_updated);
multi_workspace.read_with(cx, |mw, _cx| {
let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
assert_eq!(
keys.len(),
3,
"should have key_a, key_b, and the updated key_a with root_c"
);
assert_eq!(*keys[0], key_a_updated);
assert_eq!(*keys[1], key_b);
assert_eq!(*keys[2], key_a);
});
}
+1 -1
View File
@@ -9886,7 +9886,7 @@ async fn open_remote_project_inner(
});
if let Some(project_group_key) = provisional_project_group_key.clone() {
multi_workspace.set_provisional_project_group_key(&new_workspace, project_group_key);
multi_workspace.set_workspace_group_key(&new_workspace, project_group_key);
}
multi_workspace.activate(new_workspace.clone(), window, cx);
new_workspace