diff --git a/crates/collab/tests/integration/following_tests.rs b/crates/collab/tests/integration/following_tests.rs index b761bef9ec3be679d55d1c82e3cb5cce0ac7f14e..c4031788c87f747c3125f4dbc509d68ea3720b43 100644 --- a/crates/collab/tests/integration/following_tests.rs +++ b/crates/collab/tests/integration/following_tests.rs @@ -8,8 +8,8 @@ use collab_ui::{ }; use editor::{Editor, MultiBuffer, MultiBufferOffset, PathKey, SelectionEffects}; use gpui::{ - AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext, - VisualContext, VisualTestContext, point, + Action, AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, + TestAppContext, VisualContext, VisualTestContext, point, }; use language::Capability; use rpc::proto::PeerId; @@ -18,7 +18,7 @@ use settings::SettingsStore; use text::{Point, ToPoint}; use util::{path, rel_path::rel_path, test::sample_text}; use workspace::{ - CollaboratorId, MultiWorkspace, ParticipantLocation, SplitDirection, Workspace, + CloseWindow, CollaboratorId, MultiWorkspace, ParticipantLocation, SplitDirection, Workspace, item::ItemHandle as _, }; @@ -259,8 +259,8 @@ async fn test_basic_following( // Client C closes the project. let weak_workspace_c = workspace_c.downgrade(); - workspace_c.update_in(cx_c, |workspace, window, cx| { - workspace.close_window(&Default::default(), window, cx); + workspace_c.update_in(cx_c, |_, window, cx| { + window.dispatch_action(Box::new(CloseWindow) as Box, cx); }); executor.run_until_parked(); // are you sure you want to leave the call? diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 567c24f5d73887289445fb8367bdd950097ba073..e5d529556be690298b57fbb864a7010729e8c170 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -14,8 +14,8 @@ use util::ResultExt; const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); use crate::{ - DockPosition, Item, ModalView, Panel, Toast, Workspace, WorkspaceId, client_side_decorations, - notifications::NotificationId, + CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast, + Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId, }; actions!( @@ -122,6 +122,7 @@ impl MultiWorkspace { } }); let quit_subscription = cx.on_app_quit(Self::app_will_quit); + Self::subscribe_to_workspace(&workspace, cx); Self { window_id: window.window_handle().window_id(), workspaces: vec![workspace], @@ -237,6 +238,41 @@ impl MultiWorkspace { cx.notify(); } + pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) { + cx.spawn_in(window, async move |this, cx| { + let workspaces = this.update(cx, |multi_workspace, _cx| { + multi_workspace.workspaces().to_vec() + })?; + + for workspace in workspaces { + let should_continue = workspace + .update_in(cx, |workspace, window, cx| { + workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx) + })? + .await?; + if !should_continue { + return anyhow::Ok(()); + } + } + + cx.update(|window, _cx| { + window.remove_window(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn subscribe_to_workspace(workspace: &Entity, cx: &mut Context) { + cx.subscribe(workspace, |this, workspace, event, cx| { + if let WorkspaceEvent::Activate = event { + this.activate(workspace, cx); + } + }) + .detach(); + } + pub fn is_sidebar_open(&self) -> bool { self.sidebar_open } @@ -290,6 +326,7 @@ impl MultiWorkspace { workspace.set_workspace_sidebar_open(true, cx); }); } + Self::subscribe_to_workspace(&workspace, cx); self.workspaces.push(workspace); cx.notify(); self.workspaces.len() - 1 @@ -679,6 +716,7 @@ impl Render for MultiWorkspace { .key_context("Workspace") .relative() .size_full() + .on_action(cx.listener(Self::close_window)) .on_action( cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| { this.create_workspace(window, cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cab4b4974f15f4b68da7c4a5abd0fca34a4af00c..f680007924f5061756a864f8c4330345a69403f4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1182,6 +1182,7 @@ pub enum Event { }, ZoomChanged, ModalOpened, + Activate, } #[derive(Debug, Clone)] @@ -2629,17 +2630,6 @@ impl Workspace { }); } - pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) { - let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx); - cx.spawn_in(window, async move |_, cx| { - if prepare.await? { - cx.update(|window, _cx| window.remove_window())?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - pub fn move_focused_panel_to_next_position( &mut self, _: &MoveFocusedPanelToNextPosition, @@ -2717,6 +2707,7 @@ impl Workspace { .unwrap_or(false) { if close_intent == CloseIntent::CloseWindow { + this.update(cx, |_, cx| cx.emit(Event::Activate))?; let answer = cx.update(|window, cx| { window.prompt( PromptLevel::Warning, @@ -2905,6 +2896,10 @@ impl Workspace { futures::future::try_join_all(serialize_tasks).await?; + if !remaining_dirty_items.is_empty() { + workspace.update(cx, |_, cx| cx.emit(Event::Activate))?; + } + if remaining_dirty_items.len() > 1 { let answer = workspace.update_in(cx, |_, window, cx| { let detail = Pane::file_names_for_prompt( @@ -6354,7 +6349,6 @@ impl Workspace { .on_action(cx.listener(Self::send_keystrokes)) .on_action(cx.listener(Self::add_folder_to_project)) .on_action(cx.listener(Self::follow_next_collaborator)) - .on_action(cx.listener(Self::close_window)) .on_action(cx.listener(Self::activate_pane_at_index)) .on_action(cx.listener(Self::move_item_to_pane_at_index)) .on_action(cx.listener(Self::move_focused_panel_to_next_position)) @@ -10052,6 +10046,86 @@ mod tests { assert!(!task.await.unwrap()); } + #[gpui::test] + async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({ "one": "" })).await; + + let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await; + let project_b = Project::test(fs, ["root".as_ref()], cx).await; + let multi_workspace_handle = + cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); + + let workspace_a = multi_workspace_handle + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + + let workspace_b = multi_workspace_handle + .update(cx, |mw, window, cx| { + mw.test_add_workspace(project_b, window, cx) + }) + .unwrap(); + + // Activate workspace A + multi_workspace_handle + .update(cx, |mw, window, cx| { + mw.activate_index(0, window, cx); + }) + .unwrap(); + + let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx); + + // Workspace A has a clean item + let item_a = cx.new(TestItem::new); + workspace_a.update_in(cx, |w, window, cx| { + w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx) + }); + + // Workspace B has a dirty item + let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true)); + workspace_b.update_in(cx, |w, window, cx| { + w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx) + }); + + // Verify workspace A is active + multi_workspace_handle + .read_with(cx, |mw, _| { + assert_eq!(mw.active_workspace_index(), 0); + }) + .unwrap(); + + // Dispatch CloseWindow — workspace A will pass, workspace B will prompt + multi_workspace_handle + .update(cx, |mw, window, cx| { + mw.close_window(&CloseWindow, window, cx); + }) + .unwrap(); + cx.run_until_parked(); + + // Workspace B should now be active since it has dirty items that need attention + multi_workspace_handle + .read_with(cx, |mw, _| { + assert_eq!( + mw.active_workspace_index(), + 1, + "workspace B should be activated when it prompts" + ); + }) + .unwrap(); + + // User cancels the save prompt from workspace B + cx.simulate_prompt_answer("Cancel"); + cx.run_until_parked(); + + // Window should still exist because workspace B's close was cancelled + assert!( + multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(), + "window should still exist after cancelling one workspace's close" + ); + } + #[gpui::test] async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 83d504ea8f1cfbb13b5f0ea97cea6508a04126aa..bbbce4986607aa5b64453e8bceb61375a49a7122 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -376,8 +376,19 @@ pub fn initialize_workspace( return; }; let multi_workspace_handle = cx.entity(); - let sidebar = cx.new(|cx| Sidebar::new(multi_workspace_handle, window, cx)); + let sidebar = cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx)); multi_workspace.register_sidebar(sidebar, window, cx); + + let multi_workspace_handle = multi_workspace_handle.downgrade(); + window.on_window_should_close(cx, move |window, cx| { + multi_workspace_handle + .update(cx, |multi_workspace, cx| { + // We'll handle closing asynchronously + multi_workspace.close_window(&CloseWindow, window, cx); + false + }) + .unwrap_or(true) + }); }) .detach(); @@ -485,17 +496,6 @@ pub fn initialize_workspace( status_bar.add_right_item(image_info, window, cx); }); - let handle = cx.entity().downgrade(); - window.on_window_should_close(cx, move |window, cx| { - handle - .update(cx, |workspace, cx| { - // We'll handle closing asynchronously - workspace.close_window(&CloseWindow, window, cx); - false - }) - .unwrap_or(true) - }); - initialize_panels(prompt_builder.clone(), window, cx); register_actions(app_state.clone(), workspace, window, cx);