@@ -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| {
@@ -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()),);
+}