Fix CloseProject not switching to neighbor in multi-project workspace (#60524)

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 <eric@zed.dev>
This commit is contained in:
Akshit JoshiandEric Holk authored and GitHub committed 2026-07-13 22:45:49 +00:00
1 parent f7e2380479
commit b1b7041cbf
1 file changed
+94 -48
+94 -48
View File
@@ -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::<MultiWorkspace>() 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:?}"
);
}
}