Add a way to change what `menu::Confirm` does in the recent projects modal (#8688)

Kirill Bulatov created

Follow-up of
https://github.com/zed-industries/zed/issues/8651#issuecomment-1973411072

Zed current default is still to reuse the current window, but now it's
possible to do
```json
"alt-cmd-o": [
  "projects::OpenRecent",
  {
    "create_new_window": true
  }
]
```
and change this.

menu::Secondary confirm does the action with opposite window creation
strategy.

Release Notes:

- Improved open recent projects flexibility: settings can change whether
`menu::Confirm` opens a new window or reuses the old one

Change summary

Cargo.lock                                    |  1 
assets/keymaps/default-linux.json             |  7 ++
assets/keymaps/default-macos.json             |  7 ++
crates/recent_projects/Cargo.toml             |  1 
crates/recent_projects/src/recent_projects.rs | 62 ++++++++++++++++----
crates/zed/src/app_menus.rs                   |  7 ++
6 files changed, 72 insertions(+), 13 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -7151,6 +7151,7 @@ dependencies = [
  "ordered-float 2.10.0",
  "picker",
  "project",
+ "serde",
  "serde_json",
  "smol",
  "ui",

assets/keymaps/default-linux.json πŸ”—

@@ -341,6 +341,13 @@
   {
     "context": "Workspace",
     "bindings": {
+      // Change the default action on `menu::Confirm` by setting the parameter
+      // "alt-cmd-o": [
+      //     "projects::OpenRecent",
+      //     {
+      //         "create_new_window": true
+      //     }
+      // ]
       "ctrl-alt-o": "projects::OpenRecent",
       "ctrl-alt-b": "branches::OpenRecent",
       "ctrl-~": "workspace::NewTerminal",

assets/keymaps/default-macos.json πŸ”—

@@ -383,6 +383,13 @@
   {
     "context": "Workspace",
     "bindings": {
+      // Change the default action on `menu::Confirm` by setting the parameter
+      // "alt-cmd-o": [
+      //     "projects::OpenRecent",
+      //     {
+      //         "create_new_window": true
+      //     }
+      // ]
       "alt-cmd-o": "projects::OpenRecent",
       "alt-cmd-b": "branches::OpenRecent",
       "ctrl-~": "workspace::NewTerminal",

crates/recent_projects/Cargo.toml πŸ”—

@@ -15,6 +15,7 @@ gpui.workspace = true
 menu.workspace = true
 ordered-float.workspace = true
 picker.workspace = true
+serde.workspace = true
 smol.workspace = true
 ui.workspace = true
 util.workspace = true

crates/recent_projects/src/recent_projects.rs πŸ”—

@@ -8,12 +8,19 @@ use gpui::{
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
+use serde::Deserialize;
 use std::sync::Arc;
 use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip};
 use util::paths::PathExt;
 use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
 
-gpui::actions!(projects, [OpenRecent]);
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct OpenRecent {
+    #[serde(default)]
+    pub create_new_window: bool,
+}
+
+gpui::impl_actions!(projects, [OpenRecent]);
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(RecentProjects::register).detach();
@@ -63,9 +70,9 @@ impl RecentProjects {
     }
 
     fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
-        workspace.register_action(|workspace, _: &OpenRecent, cx| {
+        workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
             let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
-                if let Some(handler) = Self::open(workspace, cx) {
+                if let Some(handler) = Self::open(workspace, open_recent.create_new_window, cx) {
                     handler.detach_and_log_err(cx);
                 }
                 return;
@@ -79,12 +86,17 @@ impl RecentProjects {
         });
     }
 
-    fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
+    fn open(
+        _: &mut Workspace,
+        create_new_window: bool,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
         Some(cx.spawn(|workspace, mut cx| async move {
             workspace.update(&mut cx, |workspace, cx| {
                 let weak_workspace = cx.view().downgrade();
                 workspace.toggle_modal(cx, |cx| {
-                    let delegate = RecentProjectsDelegate::new(weak_workspace, true);
+                    let delegate =
+                        RecentProjectsDelegate::new(weak_workspace, create_new_window, true);
 
                     let modal = Self::new(delegate, 34., cx);
                     modal
@@ -95,7 +107,13 @@ impl RecentProjects {
     }
 
     pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
-        cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
+        cx.new_view(|cx| {
+            Self::new(
+                RecentProjectsDelegate::new(workspace, false, false),
+                20.,
+                cx,
+            )
+        })
     }
 }
 
@@ -126,17 +144,19 @@ pub struct RecentProjectsDelegate {
     selected_match_index: usize,
     matches: Vec<StringMatch>,
     render_paths: bool,
+    create_new_window: bool,
     // Flag to reset index when there is a new query vs not reset index when user delete an item
     reset_selected_match_index: bool,
 }
 
 impl RecentProjectsDelegate {
-    fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
+    fn new(workspace: WeakView<Workspace>, create_new_window: bool, render_paths: bool) -> Self {
         Self {
             workspace,
             workspaces: vec![],
             selected_match_index: 0,
             matches: Default::default(),
+            create_new_window,
             render_paths,
             reset_selected_match_index: true,
         }
@@ -147,10 +167,19 @@ impl PickerDelegate for RecentProjectsDelegate {
     type ListItem = ListItem;
 
     fn placeholder_text(&self, cx: &mut WindowContext) -> Arc<str> {
+        let (create_window, reuse_window) = if self.create_new_window {
+            (
+                cx.keystroke_text_for(&menu::Confirm),
+                cx.keystroke_text_for(&menu::SecondaryConfirm),
+            )
+        } else {
+            (
+                cx.keystroke_text_for(&menu::SecondaryConfirm),
+                cx.keystroke_text_for(&menu::Confirm),
+            )
+        };
         Arc::from(format!(
-            "{} reuses the window, {} opens a new one",
-            cx.keystroke_text_for(&menu::Confirm),
-            cx.keystroke_text_for(&menu::SecondaryConfirm),
+            "{reuse_window} reuses the window, {create_window} opens a new one",
         ))
     }
 
@@ -219,7 +248,11 @@ impl PickerDelegate for RecentProjectsDelegate {
         {
             let (candidate_workspace_id, candidate_workspace_location) =
                 &self.workspaces[selected_match.candidate_id];
-            let replace_current_window = !secondary;
+            let replace_current_window = if self.create_new_window {
+                secondary
+            } else {
+                !secondary
+            };
             workspace
                 .update(cx, |workspace, cx| {
                     if workspace.database_id() != *candidate_workspace_id {
@@ -492,7 +525,12 @@ mod tests {
         workspace: &WindowHandle<Workspace>,
         cx: &mut TestAppContext,
     ) -> View<Picker<RecentProjectsDelegate>> {
-        cx.dispatch_action((*workspace).into(), OpenRecent);
+        cx.dispatch_action(
+            (*workspace).into(),
+            OpenRecent {
+                create_new_window: false,
+            },
+        );
         workspace
             .update(cx, |workspace, cx| {
                 workspace

crates/zed/src/app_menus.rs πŸ”—

@@ -37,7 +37,12 @@ pub fn app_menus() -> Vec<Menu<'static>> {
                 MenuItem::action("New Window", workspace::NewWindow),
                 MenuItem::separator(),
                 MenuItem::action("Open…", workspace::Open),
-                MenuItem::action("Open Recent...", recent_projects::OpenRecent),
+                MenuItem::action(
+                    "Open Recent...",
+                    recent_projects::OpenRecent {
+                        create_new_window: false,
+                    },
+                ),
                 MenuItem::separator(),
                 MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
                 MenuItem::action("Save", workspace::Save { save_intent: None }),