From 9d533f9d305e2ac988461b837c83880db0f2fb13 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 5 Jun 2025 10:09:09 +0300 Subject: [PATCH] Allow to reuse windows in open remote projects dialogue (#32138) Closes https://github.com/zed-industries/zed/issues/26276 Same as other "open window" actions like "open recent", add a `"create_new_window": false` (default `false`) argument into the `projects::OpenRemote` action. Make all menus to use this default; allow users to change this in the keybindings. Same as with other actions, `cmd`/`ctrl` inverts the parameter value. default override Release Notes: - Allowed to reuse windows in open remote projects dialogue --- assets/keymaps/default-linux.json | 6 +- assets/keymaps/default-macos.json | 4 +- crates/recent_projects/src/recent_projects.rs | 5 +- crates/recent_projects/src/remote_servers.rs | 87 +++++++++++++++---- crates/title_bar/src/title_bar.rs | 2 + crates/zed/src/zed/app_menus.rs | 1 + crates/zed_actions/src/lib.rs | 2 + 7 files changed, 82 insertions(+), 25 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 1d0972c92f..db1cd257ae 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -512,14 +512,14 @@ { "context": "Workspace", "bindings": { + "alt-open": ["projects::OpenRecent", { "create_new_window": false }], // Change the default action on `menu::Confirm` by setting the parameter // "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }], - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": false }], - "alt-shift-open": "projects::OpenRemote", - "alt-ctrl-shift-o": "projects::OpenRemote", + "alt-shift-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], // Change to open path modal for existing remote connection by setting the parameter // "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]", + "alt-ctrl-shift-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], "alt-ctrl-shift-b": "branches::OpenRecent", "alt-shift-enter": "toast::RunAction", "ctrl-~": "workspace::NewTerminal", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 833547ea6b..acf024a0a1 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -585,8 +585,8 @@ // 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", { "create_new_window": false }], - "ctrl-cmd-o": "projects::OpenRemote", - "ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }], + "ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], + "ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }], "alt-cmd-b": "branches::OpenRecent", "ctrl-~": "workspace::NewTerminal", "cmd-s": "workspace::Save", diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 1e5361e1e6..2400151324 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -50,6 +50,7 @@ pub fn init(cx: &mut App) { }); cx.on_action(|open_remote: &OpenRemote, cx| { let from_existing_connection = open_remote.from_existing_connection; + let create_new_window = open_remote.create_new_window; with_active_or_new_workspace(cx, move |workspace, window, cx| { if from_existing_connection { cx.propagate(); @@ -58,7 +59,7 @@ pub fn init(cx: &mut App) { let handle = cx.entity().downgrade(); let fs = workspace.project().read(cx).fs().clone(); workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(fs, window, cx, handle) + RemoteServerProjects::new(create_new_window, fs, window, handle, cx) }) }); }); @@ -480,6 +481,7 @@ impl PickerDelegate for RecentProjectsDelegate { .key_binding(KeyBinding::for_action( &OpenRemote { from_existing_connection: false, + create_new_window: false, }, window, cx, @@ -488,6 +490,7 @@ impl PickerDelegate for RecentProjectsDelegate { window.dispatch_action( OpenRemote { from_existing_connection: false, + create_new_window: false, } .boxed_clone(), cx, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index b0ee050b79..1f7c8295a9 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -13,6 +13,7 @@ use futures::FutureExt; use futures::channel::oneshot; use futures::future::Shared; use futures::select; +use gpui::ClickEvent; use gpui::ClipboardItem; use gpui::Subscription; use gpui::Task; @@ -69,6 +70,7 @@ pub struct RemoteServerProjects { retained_connections: Vec>, ssh_config_updates: Task<()>, ssh_config_servers: BTreeSet, + create_new_window: bool, _subscription: Subscription, } @@ -136,6 +138,7 @@ impl Focusable for ProjectPicker { impl ProjectPicker { fn new( + create_new_window: bool, ix: usize, connection: SshConnectionOptions, project: Entity, @@ -167,7 +170,13 @@ impl ProjectPicker { let fs = workspace.project().read(cx).fs().clone(); let weak = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(fs, window, cx, weak) + RemoteServerProjects::new( + create_new_window, + fs, + window, + weak, + cx, + ) }); }) .log_err()?; @@ -361,19 +370,12 @@ impl Mode { } } impl RemoteServerProjects { - pub fn open(workspace: Entity, window: &mut Window, cx: &mut App) { - workspace.update(cx, |workspace, cx| { - let handle = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, window, cx, handle)) - }) - } - pub fn new( + create_new_window: bool, fs: Arc, window: &mut Window, - cx: &mut Context, workspace: WeakEntity, + cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config; @@ -410,11 +412,13 @@ impl RemoteServerProjects { retained_connections: Vec::new(), ssh_config_updates, ssh_config_servers: BTreeSet::new(), + create_new_window, _subscription, } } pub fn project_picker( + create_new_window: bool, ix: usize, connection_options: remote::SshConnectionOptions, project: Entity, @@ -424,8 +428,9 @@ impl RemoteServerProjects { workspace: WeakEntity, ) -> Self { let fs = project.read(cx).fs().clone(); - let mut this = Self::new(fs, window, cx, workspace.clone()); + let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx); this.mode = Mode::ProjectPicker(ProjectPicker::new( + create_new_window, ix, connection_options, project, @@ -541,6 +546,7 @@ impl RemoteServerProjects { return; }; + let create_new_window = self.create_new_window; let connection_options = ssh_connection.into(); workspace.update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { @@ -578,7 +584,7 @@ impl RemoteServerProjects { let weak = cx.entity().downgrade(); let fs = workspace.project().read(cx).fs().clone(); workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(fs, window, cx, weak) + RemoteServerProjects::new(create_new_window, fs, window, weak, cx) }); }); }; @@ -606,6 +612,7 @@ impl RemoteServerProjects { let weak = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { RemoteServerProjects::project_picker( + create_new_window, ix, connection_options, project, @@ -847,6 +854,7 @@ impl RemoteServerProjects { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { + let create_new_window = self.create_new_window; let is_from_zed = server.is_from_zed(); let element_id_base = SharedString::from(format!("remote-project-{server_ix}")); let container_element_id_base = @@ -854,8 +862,11 @@ impl RemoteServerProjects { let callback = Rc::new({ let project = project.clone(); - move |this: &mut Self, window: &mut Window, cx: &mut Context| { - let Some(app_state) = this + move |remote_server_projects: &mut Self, + secondary_confirm: bool, + window: &mut Window, + cx: &mut Context| { + let Some(app_state) = remote_server_projects .workspace .read_with(cx, |workspace, _| workspace.app_state().clone()) .log_err() @@ -865,17 +876,26 @@ impl RemoteServerProjects { let project = project.clone(); let server = server.connection().into_owned(); cx.emit(DismissEvent); + + let replace_window = match (create_new_window, secondary_confirm) { + (true, false) | (false, true) => None, + (true, true) | (false, false) => window.window_handle().downcast::(), + }; + cx.spawn_in(window, async move |_, cx| { let result = open_ssh_project( server.into(), project.paths.into_iter().map(PathBuf::from).collect(), app_state, - OpenOptions::default(), + OpenOptions { + replace_window, + ..OpenOptions::default() + }, cx, ) .await; if let Err(e) = result { - log::error!("Failed to connect: {:?}", e); + log::error!("Failed to connect: {e:#}"); cx.prompt( gpui::PromptLevel::Critical, "Failed to connect", @@ -897,7 +917,13 @@ impl RemoteServerProjects { .on_action(cx.listener({ let callback = callback.clone(); move |this, _: &menu::Confirm, window, cx| { - callback(this, window, cx); + callback(this, false, window, cx); + } + })) + .on_action(cx.listener({ + let callback = callback.clone(); + move |this, _: &menu::SecondaryConfirm, window, cx| { + callback(this, true, window, cx); } })) .child( @@ -911,7 +937,10 @@ impl RemoteServerProjects { .size(IconSize::Small), ) .child(Label::new(project.paths.join(", "))) - .on_click(cx.listener(move |this, _, window, cx| callback(this, window, cx))) + .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { + let secondary_confirm = e.down.modifiers.platform; + callback(this, secondary_confirm, window, cx) + })) .when(is_from_zed, |server_list_item| { server_list_item.end_hover_slot::(Some( div() @@ -1493,10 +1522,30 @@ impl RemoteServerProjects { } let mut modal_section = modal_section.render(window, cx).into_any_element(); + let (create_window, reuse_window) = if self.create_new_window { + ( + window.keystroke_text_for(&menu::Confirm), + window.keystroke_text_for(&menu::SecondaryConfirm), + ) + } else { + ( + window.keystroke_text_for(&menu::SecondaryConfirm), + window.keystroke_text_for(&menu::Confirm), + ) + }; + let placeholder_text = Arc::from(format!( + "{reuse_window} reuses this window, {create_window} opens a new one", + )); + Modal::new("remote-projects", None) .header( ModalHeader::new() - .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)), + .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)) + .child( + Label::new(placeholder_text) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), ) .section( Section::new().padded(false).child( diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b17bb872f9..c96e38a179 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -439,6 +439,7 @@ impl TitleBar { "Remote Project", Some(&OpenRemote { from_existing_connection: false, + create_new_window: false, }), meta.clone(), window, @@ -449,6 +450,7 @@ impl TitleBar { window.dispatch_action( OpenRemote { from_existing_connection: false, + create_new_window: false, } .boxed_clone(), cx, diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index ec98bb9122..01190bd12e 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -73,6 +73,7 @@ pub fn app_menus() -> Vec { MenuItem::action( "Open Remote...", zed_actions::OpenRemote { + create_new_window: false, from_existing_connection: false, }, ), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index aafe458688..afee0e9cfb 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -254,6 +254,8 @@ pub struct OpenRecent { pub struct OpenRemote { #[serde(default)] pub from_existing_connection: bool, + #[serde(default)] + pub create_new_window: bool, } impl_actions!(projects, [OpenRecent, OpenRemote]);