From b1b7041cbf8fc35bc6ab796a1f45f2da45153bef Mon Sep 17 00:00:00 2001 From: Akshit Joshi Date: Tue, 14 Jul 2026 04:15:49 +0530 Subject: [PATCH] Fix CloseProject not switching to neighbor in multi-project workspace (#60524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #54772 When "Workspace: close project" was used in a multi-project window, the handler pre-created an empty "get started" workspace and activated it before removing the old project group. This prevented `remove_project_group` from detecting that the active workspace was being removed, so the neighbor-fallback never ran — leaving the user on the empty workspace even though other projects were still open. The fix delegates entirely to `remove_project_group`, which already handles: - Computing a neighboring project group - Prompting for unsaved changes via `prepare_to_close` - Activating the neighbor (or creating an empty workspace if none remains) This matches the behavior of the sidebar "Remove Project" action, which uses the same `remove_project_group` path. Release Notes: - Fixed "Workspace: close project" action not switching to another project in multi-project workspace --------- Co-authored-by: Eric Holk --- crates/zed/src/zed.rs | 142 ++++++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 139066d2fa..55cf315227 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1291,59 +1291,17 @@ fn register_actions( } }) .register_action({ - let app_state = app_state.clone(); move |workspace, _: &CloseProject, window, cx| { let Some(window_handle) = window.window_handle().downcast::() else { return; }; - let app_state = app_state.clone(); let old_group_key = workspace.project_group_key(cx); - cx.spawn_in(window, async move |this, cx| { - let should_continue = this - .update_in(cx, |workspace, window, cx| { - workspace.prepare_to_close( - CloseIntent::ReplaceWindow, - window, - cx, - ) - })? - .await?; - if should_continue { - let task = cx.update(|_window, cx| { - open_new( - workspace::OpenOptions { - requesting_window: Some(window_handle), - ..Default::default() - }, - app_state, - cx, - |workspace, window, cx| { - cx.activate(true); - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("", None, true, cx) - }); - let editor = cx.new(|cx| { - Editor::for_buffer(buffer, Some(project), window, cx) - }); - workspace.add_item_to_active_pane( - Box::new(editor), - None, - true, - window, - cx, - ); - }, - ) - })?; - task.await?; - window_handle.update(cx, |mw, window, cx| { - mw.remove_project_group(&old_group_key, window, cx) - })?.await.log_err(); - Ok::<(), anyhow::Error>(()) - } else { - Ok(()) - } + cx.spawn_in(window, async move |_, cx| { + let task = window_handle.update(cx, |multi_workspace, window, cx| { + multi_workspace.remove_project_group(&old_group_key, window, cx) + })?; + task.await?; + anyhow::Ok(()) }) .detach_and_log_err(cx); } @@ -7092,4 +7050,92 @@ mod tests { "project group should be removed after CloseProject: {keys:?}" ); } + + #[gpui::test] + async fn test_close_project_switches_to_neighbor_in_multi_project(cx: &mut TestAppContext) { + use workspace::OpenMode; + + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree(path!("/project-a"), json!({})) + .await; + app_state + .fs + .as_fake() + .insert_tree(path!("/project-b"), json!({})) + .await; + + let workspace::OpenResult { + window, + workspace: workspace_a, + .. + } = cx + .update(|cx| { + workspace::Workspace::new_local( + vec![path!("/project-a").into()], + app_state.clone(), + None, + None, + None, + OpenMode::Activate, + cx, + ) + }) + .await + .unwrap(); + + let project_b = Project::test(app_state.fs.clone(), [Path::new("/project-b")], cx).await; + + window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.test_add_workspace(project_b, window, cx); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + + // Reactivate workspace A so we close it via CloseProject. + window + .update(cx, |multi_workspace, window, cx| { + multi_workspace.activate(workspace_a, None, window, cx); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + + let keys_before = window + .read_with(cx, |multi_workspace, _| { + multi_workspace.project_group_keys() + }) + .unwrap(); + assert_eq!( + keys_before.len(), + 2, + "should have 2 project groups before CloseProject: {keys_before:?}" + ); + + cx.dispatch_action(window.into(), CloseProject); + cx.background_executor.run_until_parked(); + + let keys_after = window + .read_with(cx, |multi_workspace, _| { + multi_workspace.project_group_keys() + }) + .unwrap(); + assert_eq!( + keys_after.len(), + 1, + "one project group should remain after CloseProject: {keys_after:?}" + ); + + let active_paths = window + .read_with(cx, |multi_workspace, cx| { + multi_workspace.workspace().read(cx).root_paths(cx) + }) + .unwrap(); + assert!( + !active_paths.is_empty(), + "active workspace should contain the remaining project, not be empty: {active_paths:?}" + ); + } }