Fix remote projects not appearing in the sidebar after adding them to the window (#54198)

Max Brunsfeld created

Release Notes:

- N/A

Change summary

crates/project/src/worktree_store.rs          |  6 --
crates/workspace/src/multi_workspace.rs       | 20 +++++++
crates/workspace/src/multi_workspace_tests.rs | 58 ++++++++++++--------
crates/workspace/src/workspace.rs             | 10 ++
4 files changed, 63 insertions(+), 31 deletions(-)

Detailed changes

crates/project/src/worktree_store.rs 🔗

@@ -1383,12 +1383,6 @@ impl WorktreeStore {
     pub fn paths(&self, cx: &App) -> WorktreePaths {
         let (mains, folders): (Vec<PathBuf>, Vec<PathBuf>) = self
             .visible_worktrees(cx)
-            .filter(|worktree| {
-                let worktree = worktree.read(cx);
-                // Remote worktrees that haven't received their first update
-                // don't have enough data to contribute yet.
-                !worktree.is_remote() || worktree.root_entry().is_some()
-            })
             .map(|worktree| {
                 let snapshot = worktree.read(cx).snapshot();
                 let folder_path = snapshot.abs_path().to_path_buf();

crates/workspace/src/multi_workspace.rs 🔗

@@ -714,6 +714,26 @@ impl MultiWorkspace {
         cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
     }
 
+    pub(crate) fn activate_provisional_workspace(
+        &mut self,
+        workspace: Entity<Workspace>,
+        provisional_key: ProjectGroupKey,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if workspace != self.active_workspace {
+            self.register_workspace(&workspace, window, cx);
+        }
+
+        self.ensure_project_group_state(provisional_key);
+        if !self.is_workspace_retained(&workspace) {
+            self.retained_workspaces.push(workspace.clone());
+        }
+
+        self.activate(workspace.clone(), window, cx);
+        cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
+    }
+
     fn register_workspace(
         &mut self,
         workspace: &Entity<Workspace>,

crates/workspace/src/multi_workspace_tests.rs 🔗

@@ -667,44 +667,52 @@ async fn test_switching_projects_with_sidebar_closed_detaches_old_active_workspa
 }
 
 #[gpui::test]
-async fn test_remote_worktree_without_git_updates_project_group(cx: &mut TestAppContext) {
+async fn test_remote_project_root_dir_changes_update_groups(cx: &mut TestAppContext) {
     init_test(cx);
     let fs = FakeFs::new(cx.executor());
-    fs.insert_tree("/local", json!({ "file.txt": "" })).await;
-    let project = Project::test(fs.clone(), ["/local".as_ref()], cx).await;
+    fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
+    fs.insert_tree("/local_b", json!({ "file.txt": "" })).await;
+    let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
+    let project_b = Project::test(fs.clone(), ["/local_b".as_ref()], cx).await;
 
     let (multi_workspace, cx) =
-        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
 
     multi_workspace.update(cx, |mw, cx| {
         mw.open_sidebar(cx);
     });
     cx.run_until_parked();
 
-    let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
+    let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+        let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx));
+        let key = workspace.read(cx).project_group_key(cx);
+        mw.activate_provisional_workspace(workspace.clone(), key, window, cx);
+        workspace
+    });
+    cx.run_until_parked();
+
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert_eq!(
+            mw.workspace().entity_id(),
+            workspace_b.entity_id(),
+            "registered workspace should become active"
+        );
+    });
+
+    let initial_key = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
     multi_workspace.read_with(cx, |mw, _cx| {
         let keys = mw.project_group_keys();
-        assert_eq!(keys.len(), 1);
-        assert_eq!(keys[0], initial_key);
+        assert!(
+            keys.contains(&initial_key),
+            "project groups should contain the initial key for the registered workspace"
+        );
     });
 
-    // Add a remote worktree without git repo info.
-    let remote_worktree = project.update(cx, |project, cx| {
+    let remote_worktree = project_b.update(cx, |project, cx| {
         project.add_test_remote_worktree("/remote/project", cx)
     });
     cx.run_until_parked();
 
-    // The remote worktree has no entries yet, so project_group_key should
-    // still exclude it.
-    let key_after_add = project.read_with(cx, |p, cx| p.project_group_key(cx));
-    assert_eq!(
-        key_after_add, initial_key,
-        "remote worktree without entries should not affect the group key"
-    );
-
-    // Send an UpdateWorktree to the remote worktree with entries but no repo.
-    // This triggers UpdatedRootRepoCommonDir on the first update (the fix),
-    // which propagates through WorktreeStore → Project → MultiWorkspace.
     let worktree_id = remote_worktree.read_with(cx, |wt, _| wt.id().to_proto());
     remote_worktree.update(cx, |worktree, _cx| {
         worktree
@@ -741,17 +749,21 @@ async fn test_remote_worktree_without_git_updates_project_group(cx: &mut TestApp
     });
     cx.run_until_parked();
 
-    let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
+    let updated_key = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
     assert_ne!(
         initial_key, updated_key,
-        "adding a remote worktree should change the project group key"
+        "remote worktree update should change the project group key"
     );
 
     multi_workspace.read_with(cx, |mw, _cx| {
         let keys = mw.project_group_keys();
         assert!(
             keys.contains(&updated_key),
-            "should contain the updated key; got {keys:?}"
+            "project groups should contain the updated key after remote change; got {keys:?}"
+        );
+        assert!(
+            !keys.contains(&initial_key),
+            "project groups should no longer contain the stale initial key; got {keys:?}"
         );
     });
 }

crates/workspace/src/workspace.rs 🔗

@@ -9959,9 +9959,15 @@ async fn open_remote_project_inner(
         });
 
         if let Some(project_group_key) = provisional_project_group_key.clone() {
-            multi_workspace.retain_workspace(new_workspace.clone(), project_group_key, cx);
+            multi_workspace.activate_provisional_workspace(
+                new_workspace.clone(),
+                project_group_key,
+                window,
+                cx,
+            );
+        } else {
+            multi_workspace.activate(new_workspace.clone(), window, cx);
         }
-        multi_workspace.activate(new_workspace.clone(), window, cx);
         new_workspace
     })?;