Close the empty project before adding a project to a window (#54450)

This prevents the user from getting into a state where they have unsaved
untitled buffers, but no way to get back to them.

Release Notes:

- N/A
This commit is contained in:
Max Brunsfeld authored and GitHub committed 2026-04-21 13:09:34 -07:00
1 parent dd5cf89e03
commit 7fc65c7db6
2 files changed
+186 -10

No files matched your search

+49 -9
View File
@@ -1930,15 +1930,55 @@ impl MultiWorkspace {
cx: &mut Context<Self>,
) -> Task<Result<Entity<Workspace>>> {
if self.multi_workspace_enabled(cx) {
self.find_or_create_local_workspace(
PathList::new(&paths),
None,
&[],
None,
OpenMode::Activate,
window,
cx,
)
let empty_workspace = if self
.active_workspace
.read(cx)
.project()
.read(cx)
.visible_worktrees(cx)
.next()
.is_none()
{
Some(self.active_workspace.clone())
} else {
None
};
cx.spawn_in(window, async move |this, cx| {
if let Some(empty_workspace) = empty_workspace.as_ref() {
let should_continue = empty_workspace
.update_in(cx, |workspace, window, cx| {
workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
})?
.await?;
if !should_continue {
return Ok(empty_workspace.clone());
}
}
let create_task = this.update_in(cx, |this, window, cx| {
this.find_or_create_local_workspace(
PathList::new(&paths),
None,
empty_workspace.as_slice(),
None,
OpenMode::Activate,
window,
cx,
)
})?;
let new_workspace = create_task.await?;
if let Some(empty_workspace) = empty_workspace {
this.update(cx, |this, cx| {
if this.is_workspace_retained(&empty_workspace) {
this.detach_workspace(&empty_workspace, cx);
}
})?;
}
Ok(new_workspace)
})
} else {
let workspace = self.workspace().clone();
cx.spawn_in(window, async move |_this, cx| {
+137 -1
View File
@@ -1,9 +1,10 @@
use std::path::PathBuf;
use super::*;
use crate::item::test::TestItem;
use client::proto;
use fs::{FakeFs, Fs};
use gpui::TestAppContext;
use gpui::{TestAppContext, VisualTestContext};
use project::DisableAiSettings;
use serde_json::json;
use settings::SettingsStore;
@@ -767,3 +768,138 @@ async fn test_remote_project_root_dir_changes_update_groups(cx: &mut TestAppCont
);
});
}
#[gpui::test]
async fn test_open_project_closes_empty_workspace_but_not_non_empty_ones(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
let fs = app_state.fs.as_fake();
fs.insert_tree(path!("/project_a"), json!({ "file_a.txt": "" }))
.await;
fs.insert_tree(path!("/project_b"), json!({ "file_b.txt": "" }))
.await;
// Start with an empty (no-worktrees) workspace.
let project = Project::test(app_state.fs.clone(), [], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
cx.run_until_parked();
window
.update(cx, |mw, _window, cx| mw.open_sidebar(cx))
.unwrap();
cx.run_until_parked();
let empty_workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
// Add a dirty untitled item to the empty workspace.
let dirty_item = cx.new(|cx| TestItem::new(cx).with_dirty(true));
empty_workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(dirty_item.clone()), None, true, window, cx);
});
// Opening a project while the lone empty workspace has unsaved
// changes prompts the user.
let open_task = window
.update(cx, |mw, window, cx| {
mw.open_project(
vec![PathBuf::from(path!("/project_a"))],
OpenMode::Activate,
window,
cx,
)
})
.unwrap();
cx.run_until_parked();
// Cancelling keeps the empty workspace.
assert!(cx.has_pending_prompt(),);
cx.simulate_prompt_answer("Cancel");
cx.run_until_parked();
assert_eq!(open_task.await.unwrap(), empty_workspace);
window
.read_with(cx, |mw, _cx| {
assert_eq!(mw.workspaces().count(), 1);
assert_eq!(mw.workspace(), &empty_workspace);
assert_eq!(mw.project_group_keys(), vec![]);
})
.unwrap();
// Discarding the unsaved changes closes the empty workspace
// and opens the new project in its place.
let open_task = window
.update(cx, |mw, window, cx| {
mw.open_project(
vec![PathBuf::from(path!("/project_a"))],
OpenMode::Activate,
window,
cx,
)
})
.unwrap();
cx.run_until_parked();
assert!(cx.has_pending_prompt(),);
cx.simulate_prompt_answer("Don't Save");
cx.run_until_parked();
let workspace_a = open_task.await.unwrap();
assert_ne!(workspace_a, empty_workspace);
window
.read_with(cx, |mw, _cx| {
assert_eq!(mw.workspaces().count(), 1);
assert_eq!(mw.workspace(), &workspace_a);
assert_eq!(
mw.project_group_keys(),
vec![ProjectGroupKey::new(
None,
PathList::new(&[path!("/project_a")])
)]
);
})
.unwrap();
assert!(
empty_workspace.read_with(cx, |workspace, _cx| workspace.session_id().is_none()),
"the detached empty workspace should no longer be attached to the session",
);
let dirty_item = cx.new(|cx| TestItem::new(cx).with_dirty(true));
workspace_a.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(dirty_item.clone()), None, true, window, cx);
});
// Opening another project does not close the existing project or prompt.
let workspace_b = window
.update(cx, |mw, window, cx| {
mw.open_project(
vec![PathBuf::from(path!("/project_b"))],
OpenMode::Activate,
window,
cx,
)
})
.unwrap()
.await
.unwrap();
cx.run_until_parked();
assert!(!cx.has_pending_prompt());
assert_ne!(workspace_b, workspace_a);
window
.read_with(cx, |mw, _cx| {
assert_eq!(mw.workspaces().count(), 2);
assert_eq!(mw.workspace(), &workspace_b);
assert_eq!(
mw.project_group_keys(),
vec![
ProjectGroupKey::new(None, PathList::new(&[path!("/project_b")])),
ProjectGroupKey::new(None, PathList::new(&[path!("/project_a")]))
]
);
})
.unwrap();
assert!(workspace_a.read_with(cx, |workspace, _cx| workspace.session_id().is_some()),);
}