Implement worktree interactions for the sidebar (#51421)

Mikayla Maki created

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_panel.rs            |  17 
crates/agent_ui/src/sidebar.rs                | 322 +++++++++++++++++++-
crates/journal/src/journal.rs                 |   7 
crates/recent_projects/src/recent_projects.rs |  21 
crates/workspace/src/multi_workspace.rs       |   4 
crates/workspace/src/workspace.rs             |  88 +++--
crates/zed/src/zed.rs                         |  14 
crates/zed/src/zed/open_listener.rs           |   8 
8 files changed, 399 insertions(+), 82 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -87,7 +87,7 @@ use ui::{
 use util::{ResultExt as _, debug_panic};
 use workspace::{
     CollaboratorId, DraggedSelection, DraggedSidebar, DraggedTab, FocusWorkspaceSidebar,
-    MultiWorkspace, SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, ToggleZoom,
+    MultiWorkspace, OpenResult, SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, ToggleZoom,
     ToolbarItemView, Workspace, WorkspaceId,
     dock::{DockPosition, Panel, PanelEvent},
     multi_workspace_enabled,
@@ -3025,21 +3025,16 @@ impl AgentPanel {
             workspace.set_dock_structure(dock_structure, window, cx);
         }));
 
-        let (new_window_handle, _) = cx
+        let OpenResult {
+            window: new_window_handle,
+            workspace: new_workspace,
+            ..
+        } = cx
             .update(|_window, cx| {
                 Workspace::new_local(all_paths, app_state, window_handle, None, init, false, cx)
             })?
             .await?;
 
-        let new_workspace = new_window_handle.update(cx, |multi_workspace, _window, _cx| {
-            let workspaces = multi_workspace.workspaces();
-            workspaces.last().cloned()
-        })?;
-
-        let Some(new_workspace) = new_workspace else {
-            anyhow::bail!("New workspace was not added to MultiWorkspace");
-        };
-
         let panels_task = new_window_handle.update(cx, |_, _, cx| {
             new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task())
         })?;

crates/agent_ui/src/sidebar.rs 🔗

@@ -99,13 +99,19 @@ impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo {
     }
 }
 
+#[derive(Clone)]
+enum ThreadEntryWorkspace {
+    Open(Entity<Workspace>),
+    Closed(PathList),
+}
+
 #[derive(Clone)]
 struct ThreadEntry {
     session_info: acp_thread::AgentSessionInfo,
     icon: IconName,
     icon_from_external_svg: Option<SharedString>,
     status: AgentThreadStatus,
-    workspace: Entity<Workspace>,
+    workspace: ThreadEntryWorkspace,
     is_live: bool,
     is_background: bool,
     highlight_positions: Vec<usize>,
@@ -528,7 +534,8 @@ impl Sidebar {
         // main repo's header instead of getting their own.
         let mut main_repo_workspace: HashMap<Arc<Path>, usize> = HashMap::new();
         let mut absorbed: HashMap<usize, (usize, SharedString)> = HashMap::new();
-        let mut pending: HashMap<Arc<Path>, Vec<(usize, SharedString)>> = HashMap::new();
+        let mut pending: HashMap<Arc<Path>, Vec<(usize, SharedString, Arc<Path>)>> = HashMap::new();
+        let mut absorbed_workspace_by_path: HashMap<Arc<Path>, usize> = HashMap::new();
 
         for (i, workspace) in workspaces.iter().enumerate() {
             for snapshot in root_repository_snapshots(workspace, cx) {
@@ -537,8 +544,9 @@ impl Sidebar {
                         .entry(snapshot.work_directory_abs_path.clone())
                         .or_insert(i);
                     if let Some(waiting) = pending.remove(&snapshot.work_directory_abs_path) {
-                        for (ws_idx, name) in waiting {
+                        for (ws_idx, name, ws_path) in waiting {
                             absorbed.insert(ws_idx, (i, name));
+                            absorbed_workspace_by_path.insert(ws_path, ws_idx);
                         }
                     }
                 } else {
@@ -553,11 +561,13 @@ impl Sidebar {
                         main_repo_workspace.get(&snapshot.original_repo_abs_path)
                     {
                         absorbed.insert(i, (main_idx, name));
+                        absorbed_workspace_by_path
+                            .insert(snapshot.work_directory_abs_path.clone(), i);
                     } else {
                         pending
                             .entry(snapshot.original_repo_abs_path.clone())
                             .or_default()
-                            .push((i, name));
+                            .push((i, name, snapshot.work_directory_abs_path.clone()));
                     }
                 }
             }
@@ -586,7 +596,7 @@ impl Sidebar {
                             icon: IconName::ZedAgent,
                             icon_from_external_svg: None,
                             status: AgentThreadStatus::default(),
-                            workspace: workspace.clone(),
+                            workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                             is_live: false,
                             is_background: false,
                             highlight_positions: Vec::new(),
@@ -599,7 +609,8 @@ impl Sidebar {
 
                 // Load threads from linked git worktrees of this workspace's repos.
                 if let Some(ref thread_store) = thread_store {
-                    let mut linked_worktree_queries: Vec<(PathList, SharedString)> = Vec::new();
+                    let mut linked_worktree_queries: Vec<(PathList, SharedString, Arc<Path>)> =
+                        Vec::new();
                     for snapshot in root_repository_snapshots(workspace, cx) {
                         if snapshot.work_directory_abs_path != snapshot.original_repo_abs_path {
                             continue;
@@ -614,11 +625,20 @@ impl Sidebar {
                             linked_worktree_queries.push((
                                 PathList::new(std::slice::from_ref(&git_worktree.path)),
                                 name.into(),
+                                Arc::from(git_worktree.path.as_path()),
                             ));
                         }
                     }
 
-                    for (worktree_path_list, worktree_name) in &linked_worktree_queries {
+                    for (worktree_path_list, worktree_name, worktree_path) in
+                        &linked_worktree_queries
+                    {
+                        let target_workspace =
+                            match absorbed_workspace_by_path.get(worktree_path.as_ref()) {
+                                Some(&idx) => ThreadEntryWorkspace::Open(workspaces[idx].clone()),
+                                None => ThreadEntryWorkspace::Closed(worktree_path_list.clone()),
+                            };
+
                         for meta in thread_store.read(cx).threads_for_paths(worktree_path_list) {
                             if !seen_session_ids.insert(meta.id.clone()) {
                                 continue;
@@ -628,7 +648,7 @@ impl Sidebar {
                                 icon: IconName::ZedAgent,
                                 icon_from_external_svg: None,
                                 status: AgentThreadStatus::default(),
-                                workspace: workspace.clone(),
+                                workspace: target_workspace.clone(),
                                 is_live: false,
                                 is_background: false,
                                 highlight_positions: Vec::new(),
@@ -1347,8 +1367,20 @@ impl Sidebar {
             }
             ListEntry::Thread(thread) => {
                 let session_info = thread.session_info.clone();
-                let workspace = thread.workspace.clone();
-                self.activate_thread(session_info, &workspace, window, cx);
+                match &thread.workspace {
+                    ThreadEntryWorkspace::Open(workspace) => {
+                        let workspace = workspace.clone();
+                        self.activate_thread(session_info, &workspace, window, cx);
+                    }
+                    ThreadEntryWorkspace::Closed(path_list) => {
+                        self.open_workspace_and_activate_thread(
+                            session_info,
+                            path_list.clone(),
+                            window,
+                            cx,
+                        );
+                    }
+                }
             }
             ListEntry::ViewMore {
                 path_list,
@@ -1403,6 +1435,32 @@ impl Sidebar {
         }
     }
 
+    fn open_workspace_and_activate_thread(
+        &mut self,
+        session_info: acp_thread::AgentSessionInfo,
+        path_list: PathList,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
+            return;
+        };
+
+        let paths: Vec<std::path::PathBuf> =
+            path_list.paths().iter().map(|p| p.to_path_buf()).collect();
+
+        let open_task = multi_workspace.update(cx, |mw, cx| mw.open_project(paths, window, cx));
+
+        cx.spawn_in(window, async move |this, cx| {
+            let workspace = open_task.await?;
+            this.update_in(cx, |this, window, cx| {
+                this.activate_thread(session_info, &workspace, window, cx);
+            })?;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
     fn expand_selected_entry(
         &mut self,
         _: &ExpandSelectedEntry,
@@ -1480,7 +1538,7 @@ impl Sidebar {
             .clone()
             .unwrap_or_else(|| "Untitled".into());
         let session_info = thread.session_info.clone();
-        let workspace = thread.workspace.clone();
+        let thread_workspace = thread.workspace.clone();
 
         let id = SharedString::from(format!("thread-entry-{}", ix));
 
@@ -1533,7 +1591,19 @@ impl Sidebar {
             .docked_right(docked_right)
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selection = None;
-                this.activate_thread(session_info.clone(), &workspace, window, cx);
+                match &thread_workspace {
+                    ThreadEntryWorkspace::Open(workspace) => {
+                        this.activate_thread(session_info.clone(), workspace, window, cx);
+                    }
+                    ThreadEntryWorkspace::Closed(path_list) => {
+                        this.open_workspace_and_activate_thread(
+                            session_info.clone(),
+                            path_list.clone(),
+                            window,
+                            cx,
+                        );
+                    }
+                }
             }))
             .into_any_element()
     }
@@ -2447,7 +2517,7 @@ mod tests {
                     icon: IconName::ZedAgent,
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Completed,
-                    workspace: workspace.clone(),
+                    workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: false,
                     is_background: false,
                     highlight_positions: Vec::new(),
@@ -2468,7 +2538,7 @@ mod tests {
                     icon: IconName::ZedAgent,
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Running,
-                    workspace: workspace.clone(),
+                    workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: false,
                     highlight_positions: Vec::new(),
@@ -2489,7 +2559,7 @@ mod tests {
                     icon: IconName::ZedAgent,
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Error,
-                    workspace: workspace.clone(),
+                    workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: false,
                     highlight_positions: Vec::new(),
@@ -2510,7 +2580,7 @@ mod tests {
                     icon: IconName::ZedAgent,
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::WaitingForConfirmation,
-                    workspace: workspace.clone(),
+                    workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: false,
                     is_background: false,
                     highlight_positions: Vec::new(),
@@ -2531,7 +2601,7 @@ mod tests {
                     icon: IconName::ZedAgent,
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Completed,
-                    workspace: workspace.clone(),
+                    workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: true,
                     highlight_positions: Vec::new(),
@@ -4305,4 +4375,222 @@ mod tests {
             vec!["v [project]", "  Thread A {wt-feature-a}",]
         );
     }
+
+    #[gpui::test]
+    async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(
+        cx: &mut TestAppContext,
+    ) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        fs.insert_tree(
+            "/project",
+            serde_json::json!({
+                ".git": {
+                    "worktrees": {
+                        "feature-a": {
+                            "commondir": "../../",
+                            "HEAD": "ref: refs/heads/feature-a",
+                        },
+                    },
+                },
+                "src": {},
+            }),
+        )
+        .await;
+
+        fs.insert_tree(
+            "/wt-feature-a",
+            serde_json::json!({
+                ".git": "gitdir: /project/.git/worktrees/feature-a",
+                "src": {},
+            }),
+        )
+        .await;
+
+        fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
+            state.worktrees.push(git::repository::Worktree {
+                path: std::path::PathBuf::from("/wt-feature-a"),
+                ref_name: "refs/heads/feature-a".into(),
+                sha: "aaa".into(),
+            });
+        })
+        .unwrap();
+
+        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+        // Only open the main repo — no workspace for the worktree.
+        let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
+        main_project
+            .update(cx, |p, cx| p.git_scans_complete(cx))
+            .await;
+
+        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
+            MultiWorkspace::test_new(main_project.clone(), window, cx)
+        });
+        let sidebar = setup_sidebar(&multi_workspace, cx);
+
+        // Save a thread for the worktree path (no workspace for it).
+        let paths_wt = PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]);
+        save_named_thread("thread-wt", "WT Thread", &paths_wt, cx).await;
+
+        multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
+        cx.run_until_parked();
+
+        // Thread should appear under the main repo with a worktree chip.
+        assert_eq!(
+            visible_entries_as_strings(&sidebar, cx),
+            vec!["v [project]", "  WT Thread {wt-feature-a}"],
+        );
+
+        // Only 1 workspace should exist.
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.workspaces().len()),
+            1,
+        );
+
+        // Focus the sidebar and select the worktree thread.
+        open_and_focus_sidebar(&sidebar, cx);
+        sidebar.update_in(cx, |sidebar, _window, _cx| {
+            sidebar.selection = Some(1); // index 0 is header, 1 is the thread
+        });
+
+        // Confirm to open the worktree thread.
+        cx.dispatch_action(Confirm);
+        cx.run_until_parked();
+
+        // A new workspace should have been created for the worktree path.
+        let new_workspace = multi_workspace.read_with(cx, |mw, _| {
+            assert_eq!(
+                mw.workspaces().len(),
+                2,
+                "confirming a worktree thread without a workspace should open one",
+            );
+            mw.workspaces()[1].clone()
+        });
+
+        let (new_path_list, _) = new_workspace.read_with(cx, |_, cx| {
+            workspace_path_list_and_label(&new_workspace, cx)
+        });
+        assert_eq!(
+            new_path_list,
+            PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]),
+            "the new workspace should have been opened for the worktree path",
+        );
+    }
+
+    #[gpui::test]
+    async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
+        cx: &mut TestAppContext,
+    ) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        fs.insert_tree(
+            "/project",
+            serde_json::json!({
+                ".git": {
+                    "worktrees": {
+                        "feature-a": {
+                            "commondir": "../../",
+                            "HEAD": "ref: refs/heads/feature-a",
+                        },
+                    },
+                },
+                "src": {},
+            }),
+        )
+        .await;
+
+        fs.insert_tree(
+            "/wt-feature-a",
+            serde_json::json!({
+                ".git": "gitdir: /project/.git/worktrees/feature-a",
+                "src": {},
+            }),
+        )
+        .await;
+
+        fs.with_git_state(std::path::Path::new("/project/.git"), false, |state| {
+            state.worktrees.push(git::repository::Worktree {
+                path: std::path::PathBuf::from("/wt-feature-a"),
+                ref_name: "refs/heads/feature-a".into(),
+                sha: "aaa".into(),
+            });
+        })
+        .unwrap();
+
+        cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+
+        let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
+        let worktree_project =
+            project::Project::test(fs.clone(), ["/wt-feature-a".as_ref()], cx).await;
+
+        main_project
+            .update(cx, |p, cx| p.git_scans_complete(cx))
+            .await;
+        worktree_project
+            .update(cx, |p, cx| p.git_scans_complete(cx))
+            .await;
+
+        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
+            MultiWorkspace::test_new(main_project.clone(), window, cx)
+        });
+
+        let worktree_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.test_add_workspace(worktree_project.clone(), window, cx)
+        });
+
+        // Activate the main workspace before setting up the sidebar.
+        multi_workspace.update_in(cx, |mw, window, cx| {
+            mw.activate_index(0, window, cx);
+        });
+
+        let sidebar = setup_sidebar(&multi_workspace, cx);
+
+        let paths_main = PathList::new(&[std::path::PathBuf::from("/project")]);
+        let paths_wt = PathList::new(&[std::path::PathBuf::from("/wt-feature-a")]);
+        save_named_thread("thread-main", "Main Thread", &paths_main, cx).await;
+        save_named_thread("thread-wt", "WT Thread", &paths_wt, cx).await;
+
+        multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
+        cx.run_until_parked();
+
+        // The worktree workspace should be absorbed under the main repo.
+        let entries = visible_entries_as_strings(&sidebar, cx);
+        assert_eq!(entries.len(), 3);
+        assert_eq!(entries[0], "v [project]");
+        assert!(entries.contains(&"  Main Thread".to_string()));
+        assert!(entries.contains(&"  WT Thread {wt-feature-a}".to_string()));
+
+        let wt_thread_index = entries
+            .iter()
+            .position(|e| e.contains("WT Thread"))
+            .expect("should find the worktree thread entry");
+
+        assert_eq!(
+            multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+            0,
+            "main workspace should be active initially"
+        );
+
+        // Focus the sidebar and select the absorbed worktree thread.
+        open_and_focus_sidebar(&sidebar, cx);
+        sidebar.update_in(cx, |sidebar, _window, _cx| {
+            sidebar.selection = Some(wt_thread_index);
+        });
+
+        // Confirm to activate the worktree thread.
+        cx.dispatch_action(Confirm);
+        cx.run_until_parked();
+
+        // The worktree workspace should now be active, not the main one.
+        let active_workspace = multi_workspace.read_with(cx, |mw, _| {
+            mw.workspaces()[mw.active_workspace_index()].clone()
+        });
+        assert_eq!(
+            active_workspace, worktree_workspace,
+            "clicking an absorbed worktree thread should activate the worktree workspace"
+        );
+    }
 }

crates/journal/src/journal.rs 🔗

@@ -9,7 +9,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use workspace::{AppState, OpenVisible, Workspace};
+use workspace::{AppState, OpenResult, OpenVisible, Workspace};
 
 actions!(
     journal,
@@ -107,7 +107,10 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
         .spawn(cx, async move |cx| {
             let (journal_dir, entry_path) = create_entry.await?;
             let opened = if open_new_workspace {
-                let (new_workspace, _) = cx
+                let OpenResult {
+                    window: new_workspace,
+                    ..
+                } = cx
                     .update(|_window, cx| {
                         workspace::open_paths(
                             &[journal_dir],

crates/recent_projects/src/recent_projects.rs 🔗

@@ -935,7 +935,14 @@ impl PickerDelegate for RecentProjectsDelegate {
                                 }
                                 return;
                             } else {
-                                workspace.open_workspace_for_paths(false, paths, window, cx)
+                                workspace
+                                    .open_workspace_for_paths(false, paths, window, cx)
+                                    .detach_and_prompt_err(
+                                        "Failed to open project",
+                                        window,
+                                        cx,
+                                        |_, _, _| None,
+                                    );
                             }
                         }
                         SerializedWorkspaceLocation::Remote(mut connection) => {
@@ -964,14 +971,14 @@ impl PickerDelegate for RecentProjectsDelegate {
                                 )
                                 .await
                             })
+                            .detach_and_prompt_err(
+                                "Failed to open project",
+                                window,
+                                cx,
+                                |_, _, _| None,
+                            );
                         }
                     }
-                    .detach_and_prompt_err(
-                        "Failed to open project",
-                        window,
-                        cx,
-                        |_, _, _| None,
-                    );
                 });
                 cx.emit(DismissEvent);
             }

crates/workspace/src/multi_workspace.rs 🔗

@@ -498,7 +498,7 @@ impl MultiWorkspace {
         paths: Vec<PathBuf>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<Entity<Workspace>>> {
         let workspace = self.workspace().clone();
 
         if multi_workspace_enabled(cx) {
@@ -519,7 +519,7 @@ impl MultiWorkspace {
                         })?
                         .await
                 } else {
-                    Ok(())
+                    Ok(workspace)
                 }
             })
         }

crates/workspace/src/workspace.rs 🔗

@@ -659,7 +659,7 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
     } else {
         let task = Workspace::new_local(Vec::new(), app_state.clone(), None, None, None, true, cx);
         cx.spawn(async move |cx| {
-            let (window, _) = task.await?;
+            let OpenResult { window, .. } = task.await?;
             window.update(cx, |multi_workspace, window, cx| {
                 window.activate_window();
                 let workspace = multi_workspace.workspace().clone();
@@ -1752,12 +1752,7 @@ impl Workspace {
         init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
         activate: bool,
         cx: &mut App,
-    ) -> Task<
-        anyhow::Result<(
-            WindowHandle<MultiWorkspace>,
-            Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
-        )>,
-    > {
+    ) -> Task<anyhow::Result<OpenResult>> {
         let project_handle = Project::local(
             app_state.client.clone(),
             app_state.node_runtime.clone(),
@@ -1997,7 +1992,11 @@ impl Workspace {
                     });
                 })
                 .log_err();
-            Ok((window, opened_items))
+            Ok(OpenResult {
+                window,
+                workspace,
+                opened_items,
+            })
         })
     }
 
@@ -2685,7 +2684,10 @@ impl Workspace {
                 cx,
             );
             cx.spawn_in(window, async move |_vh, cx| {
-                let (multi_workspace_window, _) = task.await?;
+                let OpenResult {
+                    window: multi_workspace_window,
+                    ..
+                } = task.await?;
                 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
                     let workspace = multi_workspace.workspace().clone();
                     workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
@@ -2723,7 +2725,10 @@ impl Workspace {
                 cx,
             );
             cx.spawn_in(window, async move |_vh, cx| {
-                let (multi_workspace_window, _) = task.await?;
+                let OpenResult {
+                    window: multi_workspace_window,
+                    ..
+                } = task.await?;
                 multi_workspace_window.update(cx, |multi_workspace, window, cx| {
                     let workspace = multi_workspace.workspace().clone();
                     workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
@@ -3102,7 +3107,7 @@ impl Workspace {
         paths: Vec<PathBuf>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<Entity<Workspace>>> {
         let window_handle = window.window_handle().downcast::<MultiWorkspace>();
         let is_remote = self.project.read(cx).is_via_collab();
         let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
@@ -3118,19 +3123,20 @@ impl Workspace {
         let app_state = self.app_state.clone();
 
         cx.spawn(async move |_, cx| {
-            cx.update(|cx| {
-                open_paths(
-                    &paths,
-                    app_state,
-                    OpenOptions {
-                        replace_window: window_to_replace,
-                        ..Default::default()
-                    },
-                    cx,
-                )
-            })
-            .await?;
-            Ok(())
+            let OpenResult { workspace, .. } = cx
+                .update(|cx| {
+                    open_paths(
+                        &paths,
+                        app_state,
+                        OpenOptions {
+                            replace_window: window_to_replace,
+                            ..Default::default()
+                        },
+                        cx,
+                    )
+                })
+                .await?;
+            Ok(workspace)
         })
     }
 
@@ -8210,7 +8216,7 @@ pub async fn restore_multiworkspace(
         cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
             .await?
     } else {
-        let (window, _items) = cx
+        let OpenResult { window, .. } = cx
             .update(|cx| {
                 Workspace::new_local(
                     first.paths.paths().to_vec(),
@@ -8503,7 +8509,10 @@ pub fn join_channel(
         let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
         if active_window.is_none() {
             // no open workspaces, make one to show the error in (blergh)
-            let (window_handle, _) = cx
+            let OpenResult {
+                window: window_handle,
+                ..
+            } = cx
                 .update(|cx| {
                     Workspace::new_local(
                         vec![],
@@ -8759,6 +8768,14 @@ pub struct OpenOptions {
     pub env: Option<HashMap<String, String>>,
 }
 
+/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
+/// or [`Workspace::open_workspace_for_paths`].
+pub struct OpenResult {
+    pub window: WindowHandle<MultiWorkspace>,
+    pub workspace: Entity<Workspace>,
+    pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
+}
+
 /// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
 pub fn open_workspace_by_id(
     workspace_id: WorkspaceId,
@@ -8878,12 +8895,7 @@ pub fn open_paths(
     app_state: Arc<AppState>,
     open_options: OpenOptions,
     cx: &mut App,
-) -> Task<
-    anyhow::Result<(
-        WindowHandle<MultiWorkspace>,
-        Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
-    )>,
-> {
+) -> Task<anyhow::Result<OpenResult>> {
     let abs_paths = abs_paths.to_vec();
     #[cfg(target_os = "windows")]
     let wsl_path = abs_paths
@@ -8962,7 +8974,7 @@ pub fn open_paths(
                 });
             });
 
-            Ok((existing, open_task))
+            Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
         } else {
             let result = cx
                 .update(move |cx| {
@@ -8978,8 +8990,8 @@ pub fn open_paths(
                 })
                 .await;
 
-            if let Ok((ref window_handle, _)) = result {
-                window_handle
+            if let Ok(ref result) = result {
+                result.window
                     .update(cx, |_, window, _cx| {
                         window.activate_window();
                     })
@@ -8991,9 +9003,9 @@ pub fn open_paths(
 
         #[cfg(target_os = "windows")]
         if let Some(util::paths::WslPath{distro, path}) = wsl_path
-            && let Ok((multi_workspace_window, _)) = &result
+            && let Ok(ref result) = result
         {
-            multi_workspace_window
+            result.window
                 .update(cx, move |multi_workspace, _window, cx| {
                     struct OpenInWsl;
                     let workspace = multi_workspace.workspace().clone();
@@ -9040,7 +9052,7 @@ pub fn open_new(
         cx,
     );
     cx.spawn(async move |cx| {
-        let (window, _opened_paths) = task.await?;
+        let OpenResult { window, .. } = task.await?;
         window
             .update(cx, |_, window, _cx| {
                 window.activate_window();

crates/zed/src/zed.rs 🔗

@@ -3442,7 +3442,11 @@ mod tests {
             PathBuf::from(path!("/root/.git/HEAD")),
             PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
         ];
-        let (opened_workspace, new_items) = cx
+        let workspace::OpenResult {
+            window: opened_workspace,
+            opened_items: new_items,
+            ..
+        } = cx
             .update(|cx| {
                 workspace::open_paths(
                     &paths_to_open,
@@ -5866,7 +5870,9 @@ mod tests {
         //
         //   Window A: workspace for dir1, workspace for dir2
         //   Window B: workspace for dir3
-        let (window_a, _) = cx
+        let workspace::OpenResult {
+            window: window_a, ..
+        } = cx
             .update(|cx| {
                 Workspace::new_local(
                     vec![dir1.into()],
@@ -5890,7 +5896,9 @@ mod tests {
             .expect("failed to open second workspace into window A");
         cx.run_until_parked();
 
-        let (window_b, _) = cx
+        let workspace::OpenResult {
+            window: window_b, ..
+        } = cx
             .update(|cx| {
                 Workspace::new_local(
                     vec![dir3.into()],

crates/zed/src/zed/open_listener.rs 🔗

@@ -29,7 +29,7 @@ use util::ResultExt;
 use util::paths::PathWithPosition;
 use workspace::PathList;
 use workspace::item::ItemHandle;
-use workspace::{AppState, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation};
+use workspace::{AppState, MultiWorkspace, OpenOptions, OpenResult, SerializedWorkspaceLocation};
 
 #[derive(Default, Debug)]
 pub struct OpenRequest {
@@ -345,7 +345,11 @@ pub async fn open_paths_with_positions(
         .map(|path_with_position| path_with_position.path.clone())
         .collect::<Vec<_>>();
 
-    let (multi_workspace, mut items) = cx
+    let OpenResult {
+        window: multi_workspace,
+        opened_items: mut items,
+        ..
+    } = cx
         .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))
         .await?;