Detailed changes
@@ -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<dyn Action>, cx);
});
executor.run_until_parked();
// are you sure you want to leave the call?
@@ -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<Self>) {
+ 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<Workspace>, cx: &mut Context<Self>) {
+ 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);
@@ -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<Self>) {
- 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);
@@ -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);