@@ -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."
+ );
+ }
}
@@ -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<Arc<Path>> {