From 9adff860f7ef9615519a355ef9003f37de31e3ab Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Mon, 13 Apr 2026 18:02:14 +0100 Subject: [PATCH] sidebar: Move to new window, going through sqlite (#53513) Re-adds the `multi_workspace::MoveProjectToNewWindow` action It serializes, then closes, then re-opens the project to avoid issues with pending tasks Release Notes: - N/A or Added/Fixed/Improved ... --- crates/sidebar/src/sidebar.rs | 30 ++++++++++++ crates/workspace/src/multi_workspace.rs | 64 +++++++++++++++++++++++-- crates/workspace/src/workspace.rs | 10 ++-- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index a846d2c2317b2eb890a2a5df33654d61e1555e53..75a1ae961be2028fc8f63acf506412498156985f 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -1972,6 +1972,10 @@ impl Sidebar { let multi_workspace = multi_workspace.clone(); let project_group_key = project_group_key.clone(); + let has_multiple_projects = multi_workspace + .read_with(cx, |mw, _| mw.project_group_keys().len() >= 2) + .unwrap_or(false); + let menu = ContextMenu::build_persistent(window, cx, move |menu, _window, menu_cx| { let weak_menu = menu_cx.weak_entity(); @@ -2031,6 +2035,32 @@ impl Sidebar { }, ); + let menu = if project_group_key.host().is_none() && has_multiple_projects { + menu.entry( + "Open Project in New Window", + Some(Box::new(workspace::MoveProjectToNewWindow)), + { + let project_group_key = project_group_key.clone(); + let multi_workspace = multi_workspace.clone(); + move |window, cx| { + multi_workspace + .update(cx, |multi_workspace, cx| { + multi_workspace + .open_project_group_in_new_window( + &project_group_key, + window, + cx, + ) + .detach_and_log_err(cx); + }) + .ok(); + } + }, + ) + } else { + menu + }; + let project_group_key = project_group_key.clone(); let multi_workspace = multi_workspace.clone(); menu.separator() diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index e3d4f50a482869414f361da870a64aebf0ee9300..081220f4766d5c5ae8a0fd73139dc8b0ffdb6ee6 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -54,6 +54,8 @@ actions!( ShowFewerThreads, /// Creates a new thread in the current workspace. NewThread, + /// Moves the active project to a new window. + MoveProjectToNewWindow, ] ); @@ -1017,6 +1019,50 @@ impl MultiWorkspace { ) } + /// Goes through sqlite: serialize -> close -> open new window + /// This avoids issues with pending tasks having the wrong window + pub fn open_project_group_in_new_window( + &mut self, + key: &ProjectGroupKey, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let paths: Vec = key.path_list().ordered_paths().cloned().collect(); + if paths.is_empty() { + return Task::ready(Ok(())); + } + + let app_state = self.workspace().read(cx).app_state().clone(); + + let workspaces: Vec<_> = self + .workspaces_for_project_group(key, cx) + .unwrap_or_default(); + let mut serialization_tasks = Vec::new(); + for workspace in &workspaces { + serialization_tasks.push(workspace.update(cx, |workspace, inner_cx| { + workspace.flush_serialization(window, inner_cx) + })); + } + + let remove_task = self.remove_project_group(key, window, cx); + + cx.spawn(async move |_this, cx| { + futures::future::join_all(serialization_tasks).await; + + let removed = remove_task.await?; + if !removed { + return Ok(()); + } + + cx.update(|cx| { + Workspace::new_local(paths, app_state, None, None, None, OpenMode::NewWindow, cx) + }) + .await?; + + Ok(()) + }) + } + /// Finds an existing workspace whose root paths and host exactly match. pub fn workspace_for_paths( &self, @@ -1836,13 +1882,23 @@ impl Render for MultiWorkspace { sidebar.cycle_thread(true, window, cx); } })) - .on_action(cx.listener( - |this: &mut Self, _: &PreviousThread, window, cx| { + .on_action( + cx.listener(|this: &mut Self, _: &PreviousThread, window, cx| { if let Some(sidebar) = &this.sidebar { sidebar.cycle_thread(false, window, cx); } - }, - )) + }), + ) + .when(self.project_group_keys().len() >= 2, |el| { + el.on_action(cx.listener( + |this: &mut Self, _: &MoveProjectToNewWindow, window, cx| { + let key = + this.project_group_key_for_workspace(this.workspace(), cx); + this.open_project_group_in_new_window(&key, window, cx) + .detach_and_log_err(cx); + }, + )) + }) }) .when( self.sidebar_open() && self.multi_workspace_enabled(cx), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 967bd539a122202b50d26ee5cb2e7c1a8a74afd0..5e1e680545aa98818bbda6bfa27f05637e75f131 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -31,11 +31,11 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; pub use multi_workspace::{ - CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, - MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject, PreviousThread, - ProjectGroup, ProjectGroupKey, SerializedProjectGroupState, ShowFewerThreads, ShowMoreThreads, - Sidebar, SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar, - sidebar_side_context_menu, + CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow, + MultiWorkspace, MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject, + PreviousThread, ProjectGroup, ProjectGroupKey, SerializedProjectGroupState, ShowFewerThreads, + ShowMoreThreads, Sidebar, SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide, + ToggleWorkspaceSidebar, sidebar_side_context_menu, }; pub use path_list::{PathList, SerializedPathList}; pub use toast_layer::{ToastAction, ToastLayer, ToastView};