Adjust "recent projects" modal behavior to allow opening projects in both current and new window (#8267)

Kirill Bulatov created

![image](https://github.com/zed-industries/zed/assets/2690773/7a0927e8-f32a-4502-8a8a-c7f8e5f325bb)

Fixes https://github.com/zed-industries/zed/issues/7419 by changing the
way "recent projects" modal confirm actions work:
* `menu::Confirm` now reuses the current window when opening a recent
project
* `menu::SecondaryConfirm` now opens a recent project in the new window 
* neither confirm tries to open the current project anymore
* modal's placeholder is adjusted to emphasize this behavior

Release Notes:

- Added a way to open recent projects in the new window

Change summary

Cargo.lock                                    |  1 
crates/recent_projects/Cargo.toml             |  1 
crates/recent_projects/src/projects.rs        |  1 
crates/recent_projects/src/recent_projects.rs | 27 +++++++++++++++-----
crates/workspace/src/workspace.rs             | 10 ++++++-
5 files changed, 30 insertions(+), 10 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7343,6 +7343,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "language",
+ "menu",
  "ordered-float 2.10.0",
  "picker",
  "postage",

crates/recent_projects/Cargo.toml 🔗

@@ -15,6 +15,7 @@ futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 language.workspace = true
+menu.workspace = true
 ordered-float.workspace = true
 picker.workspace = true
 postage.workspace = true

crates/recent_projects/src/recent_projects.rs 🔗

@@ -1,5 +1,4 @@
 mod highlighted_workspace_location;
-mod projects;
 
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -14,7 +13,7 @@ use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpac
 use util::paths::PathExt;
 use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
 
-pub use projects::OpenRecent;
+gpui::actions!(projects, [OpenRecent]);
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(RecentProjects::register).detach();
@@ -94,6 +93,7 @@ impl RecentProjects {
             Ok(())
         }))
     }
+
     pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
         cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
     }
@@ -147,7 +147,11 @@ impl PickerDelegate for RecentProjectsDelegate {
     type ListItem = ListItem;
 
     fn placeholder_text(&self) -> Arc<str> {
-        "Search recent projects...".into()
+        Arc::from(format!(
+            "`{:?}` reuses the window, `{:?}` opens in new",
+            menu::Confirm,
+            menu::SecondaryConfirm,
+        ))
     }
 
     fn match_count(&self) -> usize {
@@ -207,17 +211,26 @@ impl PickerDelegate for RecentProjectsDelegate {
         Task::ready(())
     }
 
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some((selected_match, workspace)) = self
             .matches
             .get(self.selected_index())
             .zip(self.workspace.upgrade())
         {
-            let (_, workspace_location) = &self.workspaces[selected_match.candidate_id];
+            let (candidate_workspace_id, candidate_workspace_location) =
+                &self.workspaces[selected_match.candidate_id];
+            let replace_current_window = !secondary;
             workspace
                 .update(cx, |workspace, cx| {
-                    workspace
-                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
+                    if workspace.database_id() != *candidate_workspace_id {
+                        workspace.open_workspace_for_paths(
+                            replace_current_window,
+                            candidate_workspace_location.paths().as_ref().clone(),
+                            cx,
+                        )
+                    } else {
+                        Task::ready(Ok(()))
+                    }
                 })
                 .detach_and_log_err(cx);
             cx.emit(DismissEvent);

crates/workspace/src/workspace.rs 🔗

@@ -1387,7 +1387,9 @@ impl Workspace {
             };
 
             if let Some(task) = this
-                .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
+                .update(&mut cx, |this, cx| {
+                    this.open_workspace_for_paths(false, paths, cx)
+                })
                 .log_err()
             {
                 task.await.log_err();
@@ -1398,6 +1400,7 @@ impl Workspace {
 
     pub fn open_workspace_for_paths(
         &mut self,
+        replace_current_window: bool,
         paths: Vec<PathBuf>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
@@ -1405,7 +1408,10 @@ impl Workspace {
         let is_remote = self.project.read(cx).is_remote();
         let has_worktree = self.project.read(cx).worktrees().next().is_some();
         let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
-        let window_to_replace = if is_remote || has_worktree || has_dirty_items {
+
+        let window_to_replace = if replace_current_window {
+            window
+        } else if is_remote || has_worktree || has_dirty_items {
             None
         } else {
             window