diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index ba6ee548348cf1dc3da612e112d497c1da011559..492b7a8f385730feaa06dfe3b5e8b4cc0a20bb59 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -4359,4 +4359,114 @@ mod tests { "Pending removal task should have deleted the workspace row when awaited" ); } + + #[gpui::test] + async fn test_create_workspace_bounds_observer_uses_fresh_id(cx: &mut gpui::TestAppContext) { + use crate::multi_workspace::MultiWorkspace; + use feature_flags::FeatureFlagAppExt; + use project::Project; + + crate::tests::init_test(cx); + + cx.update(|cx| { + cx.set_staff(true); + cx.update_flags(true, vec!["agent-v2".to_string()]); + }); + + let fs = fs::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + multi_workspace.update_in(cx, |mw, _, cx| { + mw.set_random_database_id(cx); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + mw.create_workspace(window, cx); + }); + + cx.run_until_parked(); + + let new_workspace_db_id = + multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id()); + assert!( + new_workspace_db_id.is_some(), + "After run_until_parked, the workspace should have a database_id" + ); + + let workspace_id = new_workspace_db_id.unwrap(); + + assert!( + DB.workspace_for_id(workspace_id).is_some(), + "The workspace row should exist in the DB" + ); + + cx.simulate_resize(gpui::size(px(1024.0), px(768.0))); + + // Advance the clock past the 100ms debounce timer so the bounds + // observer task fires + cx.executor().advance_clock(Duration::from_millis(200)); + cx.run_until_parked(); + + let serialized = DB + .workspace_for_id(workspace_id) + .expect("workspace row should still exist"); + assert!( + serialized.window_bounds.is_some(), + "The bounds observer should write bounds for the workspace's real DB ID, \ + even when the workspace was created via create_workspace (where the ID \ + is assigned asynchronously after construction)." + ); + } + + #[gpui::test] + async fn test_flush_serialization_writes_bounds(cx: &mut gpui::TestAppContext) { + use crate::multi_workspace::MultiWorkspace; + use feature_flags::FeatureFlagAppExt; + use project::Project; + + crate::tests::init_test(cx); + + cx.update(|cx| { + cx.set_staff(true); + cx.update_flags(true, vec!["agent-v2".to_string()]); + }); + + let fs = fs::FakeFs::new(cx.executor()); + let dir = tempfile::TempDir::with_prefix("flush_bounds_test").unwrap(); + fs.insert_tree(dir.path(), json!({})).await; + + let project = Project::test(fs.clone(), [dir.path()], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace_id = DB.next_id().await.unwrap(); + multi_workspace.update_in(cx, |mw, _, cx| { + mw.workspace().update(cx, |ws, _cx| { + ws.set_database_id(workspace_id); + }); + }); + + let task = multi_workspace.update_in(cx, |mw, window, cx| { + mw.workspace() + .update(cx, |ws, cx| ws.flush_serialization(window, cx)) + }); + task.await; + + let after = DB + .workspace_for_id(workspace_id) + .expect("workspace row should exist after flush_serialization"); + assert!( + !after.paths.is_empty(), + "flush_serialization should have written paths via save_workspace" + ); + assert!( + after.window_bounds.is_some(), + "flush_serialization should ensure window bounds are persisted to the DB \ + before the process exits." + ); + } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 975f00d589b76e56bcf6d819798c23bd8844a2d6..cab4b4974f15f4b68da7c4a5abd0fca34a4af00c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1601,36 +1601,7 @@ impl Workspace { .timer(Duration::from_millis(100)) .await; this.update_in(cx, |this, window, cx| { - if let Some(display) = window.display(cx) - && let Ok(display_uuid) = display.uuid() - { - let window_bounds = window.inner_window_bounds(); - let has_paths = !this.root_paths(cx).is_empty(); - if !has_paths { - cx.background_executor() - .spawn(persistence::write_default_window_bounds( - window_bounds, - display_uuid, - )) - .detach_and_log_err(cx); - } - if let Some(database_id) = workspace_id { - cx.background_executor() - .spawn(DB.set_window_open_status( - database_id, - SerializedWindowBounds(window_bounds), - display_uuid, - )) - .detach_and_log_err(cx); - } else { - cx.background_executor() - .spawn(persistence::write_default_window_bounds( - window_bounds, - display_uuid, - )) - .detach_and_log_err(cx); - } - } + this.save_window_bounds(window, cx).detach(); this.bounds_save_task_queued.take(); }) .ok(); @@ -5857,6 +5828,40 @@ impl Workspace { self.session_id.clone() } + fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> { + let Some(display) = window.display(cx) else { + return Task::ready(()); + }; + let Ok(display_uuid) = display.uuid() else { + return Task::ready(()); + }; + + let window_bounds = window.inner_window_bounds(); + let database_id = self.database_id; + let has_paths = !self.root_paths(cx).is_empty(); + + cx.background_executor().spawn(async move { + if !has_paths { + persistence::write_default_window_bounds(window_bounds, display_uuid) + .await + .log_err(); + } + if let Some(database_id) = database_id { + DB.set_window_open_status( + database_id, + SerializedWindowBounds(window_bounds), + display_uuid, + ) + .await + .log_err(); + } else { + persistence::write_default_window_bounds(window_bounds, display_uuid) + .await + .log_err(); + } + }) + } + /// Bypass the 200ms serialization throttle and write workspace state to /// the DB immediately. Returns a task the caller can await to ensure the /// write completes. Used by the quit handler so the most recent state @@ -5864,7 +5869,14 @@ impl Workspace { pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> { self._schedule_serialize_workspace.take(); self._serialize_workspace_task.take(); - self.serialize_workspace_internal(window, cx) + self.bounds_save_task_queued.take(); + + let bounds_task = self.save_window_bounds(window, cx); + let serialize_task = self.serialize_workspace_internal(window, cx); + cx.spawn(async move |_| { + bounds_task.await; + serialize_task.await; + }) } pub fn root_paths(&self, cx: &App) -> Vec> {