From 08d54c3233cc46b8b2eae31ed2ede5b66ac3ef24 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:38:03 -0300 Subject: [PATCH] recent_projects: Add "open in new window" action to opened projects (#55168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a "open in new window" button to the projects that are already open in the current window. This matches a capability that was only available through the threads sidebar. Screenshot 2026-04-29 at 11  28@2x Release Notes: - Added the ability to move a currently open project to a new window through the recent projects modal. --- assets/icons/open_new_window.svg | 7 -- crates/icons/src/icons.rs | 1 - .../src/highlighted_match_with_paths.rs | 5 +- crates/recent_projects/src/recent_projects.rs | 99 ++++++++++++++++++- 4 files changed, 99 insertions(+), 13 deletions(-) delete mode 100644 assets/icons/open_new_window.svg diff --git a/assets/icons/open_new_window.svg b/assets/icons/open_new_window.svg deleted file mode 100644 index c81d49f9ff..0000000000 --- a/assets/icons/open_new_window.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b49ad8e340..d67c4f76e6 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -184,7 +184,6 @@ pub enum IconName { NewThread, Notepad, OpenFolder, - OpenNewWindow, Option, PageDown, PageUp, diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs index 7c88213437..62bae44312 100644 --- a/crates/picker/src/highlighted_match_with_paths.rs +++ b/crates/picker/src/highlighted_match_with_paths.rs @@ -52,7 +52,9 @@ impl HighlightedMatch { } impl RenderOnce for HighlightedMatch { fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions).color(self.color) + HighlightedLabel::new(self.text, self.highlight_positions) + .color(self.color) + .truncate() } } @@ -74,6 +76,7 @@ impl HighlightedMatchWithPaths { impl RenderOnce for HighlightedMatchWithPaths { fn render(mut self, _window: &mut Window, _: &mut App) -> impl IntoElement { v_flex() + .min_w_0() .child( h_flex() .gap_1() diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index b93fa009fc..f5b5f78fd2 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1115,6 +1115,12 @@ impl PickerDelegate for RecentProjectsDelegate { return; }; + if secondary && key.host().is_none() && self.window_project_groups.len() >= 2 { + move_project_group_to_new_window(key, window, cx); + cx.emit(DismissEvent); + return; + } + let key = key.clone(); let path_list = key.path_list().clone(); if let Some(handle) = window.window_handle().downcast::() { @@ -1406,8 +1412,40 @@ impl PickerDelegate for RecentProjectsDelegate { }; let project_group_key = key.clone(); + let is_local = key.host().is_none(); + let has_multiple_groups = self.window_project_groups.len() >= 2; let secondary_actions = h_flex() - .gap_1() + .gap_0p5() + .when(is_local && has_multiple_groups, |this| { + this.child( + IconButton::new("move_to_new_window", IconName::ArrowUpRight) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = self.focus_handle.clone(); + move |_, cx| { + Tooltip::for_action_in( + "Open in New Window", + &menu::SecondaryConfirm, + &focus_handle, + cx, + ) + } + }) + .on_click({ + let project_group_key = project_group_key.clone(); + cx.listener(move |_picker, _, window, cx| { + cx.stop_propagation(); + window.prevent_default(); + move_project_group_to_new_window( + &project_group_key, + window, + cx, + ); + cx.emit(DismissEvent); + }) + }), + ) + }) .when(!is_active, |this| { this.child( IconButton::new("remove_open_project", IconName::Close) @@ -1433,12 +1471,13 @@ impl PickerDelegate for RecentProjectsDelegate { Some( ListItem::new(ix) - .toggle_state(selected) .inset(true) + .toggle_state(selected) .spacing(ListItemSpacing::Sparse) .child( h_flex() .id("open_project_info_container") + .w_full() .gap_2p5() .when(self.has_any_non_local_projects, |this| { this.child(Icon::new(icon).color(Color::Muted)) @@ -1540,7 +1579,7 @@ impl PickerDelegate for RecentProjectsDelegate { ) }) .child( - IconButton::new("open_new_window", IconName::OpenNewWindow) + IconButton::new("open_new_window", IconName::ArrowUpRight) .icon_size(IconSize::Small) .tooltip({ move |_, cx| { @@ -1616,11 +1655,23 @@ impl PickerDelegate for RecentProjectsDelegate { fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { let focus_handle = self.focus_handle.clone(); let popover_style = matches!(self.style, ProjectPickerStyle::Popover); + let is_already_open_entry = matches!( self.filtered_entries.get(self.selected_index), Some(ProjectPickerEntry::OpenFolder { .. } | ProjectPickerEntry::ProjectGroup(_)) ); + let show_move_to_new_window = match self.filtered_entries.get(self.selected_index) { + Some(ProjectPickerEntry::ProjectGroup(hit)) => { + self.window_project_groups.len() >= 2 + && self + .window_project_groups + .get(hit.candidate_id) + .is_some_and(|key| key.host().is_none()) + } + _ => false, + }; + if popover_style { return Some( v_flex() @@ -1753,7 +1804,31 @@ impl PickerDelegate for RecentProjectsDelegate { }) .map(|this| { if is_already_open_entry { - this.child( + this.when(show_move_to_new_window, |this| { + this.child({ + let window_project_groups = self.window_project_groups.clone(); + let selected_index = self.selected_index; + let filtered_entries = self.filtered_entries.clone(); + Button::new("move_to_new_window", "New Window") + .key_binding(KeyBinding::for_action_in( + &menu::SecondaryConfirm, + &focus_handle, + cx, + )) + .on_click(move |_, window, cx| { + let key = match filtered_entries.get(selected_index) { + Some(ProjectPickerEntry::ProjectGroup(hit)) => { + window_project_groups.get(hit.candidate_id).cloned() + } + _ => None, + }; + if let Some(key) = key { + move_project_group_to_new_window(&key, window, cx); + } + }) + }) + }) + .child( Button::new("activate", "Activate") .key_binding(KeyBinding::for_action_in( &menu::Confirm, @@ -1930,6 +2005,22 @@ pub(crate) fn highlights_for_path( }, ) } + +fn move_project_group_to_new_window(key: &ProjectGroupKey, window: &mut Window, cx: &mut App) { + if let Some(handle) = window.window_handle().downcast::() { + let key = key.clone(); + cx.defer(move |cx| { + handle + .update(cx, |multi_workspace, window, cx| { + multi_workspace + .open_project_group_in_new_window(&key, window, cx) + .detach_and_log_err(cx); + }) + .log_err(); + }); + } +} + fn open_local_project( workspace: WeakEntity, create_new_window: bool,