diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 75c794cb6f53ba884b9879192118e1f8bea0870e..478e33418c68efa1cf5da0868fe16d6dec693447 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -6,13 +6,16 @@ use gpui::{ actions, deferred, px, }; use project::Project; +use std::future::Future; use std::path::PathBuf; use ui::prelude::*; +use util::ResultExt; const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); use crate::{ - DockPosition, Item, ModalView, Panel, Workspace, WorkspaceId, client_side_decorations, + DockPosition, Item, ModalView, Panel, Toast, Workspace, WorkspaceId, client_side_decorations, + notifications::NotificationId, }; actions!( @@ -99,10 +102,26 @@ pub struct MultiWorkspace { sidebar: Option>, sidebar_open: bool, _sidebar_subscription: Option, + pending_removal_tasks: Vec>, + _serialize_task: Option>, + _create_task: Option>, + _subscriptions: Vec, } impl MultiWorkspace { - pub fn new(workspace: Entity, window: &mut Window, _cx: &mut Context) -> Self { + pub fn new(workspace: Entity, window: &mut Window, cx: &mut Context) -> Self { + let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| { + if let Some(task) = this._serialize_task.take() { + task.detach(); + } + if let Some(task) = this._create_task.take() { + task.detach(); + } + for task in std::mem::take(&mut this.pending_removal_tasks) { + task.detach(); + } + }); + let quit_subscription = cx.on_app_quit(Self::app_will_quit); Self { window_id: window.window_handle().window_id(), workspaces: vec![workspace], @@ -110,6 +129,10 @@ impl MultiWorkspace { sidebar: None, sidebar_open: false, _sidebar_subscription: None, + pending_removal_tasks: Vec::new(), + _serialize_task: None, + _create_task: None, + _subscriptions: vec![release_subscription, quit_subscription], } } @@ -238,14 +261,24 @@ impl MultiWorkspace { return; } - let index = self.add_workspace(workspace, cx); - if self.active_workspace_index != index { - self.active_workspace_index = index; + let old_index = self.active_workspace_index; + let new_index = self.set_active_workspace(workspace, cx); + if old_index != new_index { self.serialize(cx); - cx.notify(); } } + fn set_active_workspace( + &mut self, + workspace: Entity, + cx: &mut Context, + ) -> usize { + let index = self.add_workspace(workspace, cx); + self.active_workspace_index = index; + cx.notify(); + index + } + /// Adds a workspace to this window without changing which workspace is active. /// Returns the index of the workspace (existing or newly inserted). pub fn add_workspace(&mut self, workspace: Entity, cx: &mut Context) -> usize { @@ -292,16 +325,37 @@ impl MultiWorkspace { } } - fn serialize(&self, cx: &mut App) { + fn serialize(&mut self, cx: &mut App) { let window_id = self.window_id; let state = crate::persistence::model::MultiWorkspaceState { active_workspace_id: self.workspace().read(cx).database_id(), sidebar_open: self.sidebar_open, }; - cx.background_spawn(async move { + self._serialize_task = Some(cx.background_spawn(async move { crate::persistence::write_multi_workspace_state(window_id, state).await; - }) - .detach(); + })); + } + + /// Returns the in-flight serialization task (if any) so the caller can + /// await it. Used by the quit handler to ensure pending DB writes + /// complete before the process exits. + pub fn flush_serialization(&mut self) -> Task<()> { + self._serialize_task.take().unwrap_or(Task::ready(())) + } + + fn app_will_quit(&mut self, _cx: &mut Context) -> impl Future + use<> { + let mut tasks: Vec> = Vec::new(); + if let Some(task) = self._serialize_task.take() { + tasks.push(task); + } + if let Some(task) = self._create_task.take() { + tasks.push(task); + } + tasks.extend(std::mem::take(&mut self.pending_removal_tasks)); + + async move { + futures::future::join_all(tasks).await; + } } fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) { @@ -397,6 +451,19 @@ impl MultiWorkspace { self.workspace().read(cx).database_id() } + pub fn take_pending_removal_tasks(&mut self) -> Vec> { + let mut tasks: Vec> = std::mem::take(&mut self.pending_removal_tasks) + .into_iter() + .filter(|task| !task.is_ready()) + .collect(); + if let Some(task) = self._create_task.take() { + if !task.is_ready() { + tasks.push(task); + } + } + tasks + } + #[cfg(any(test, feature = "test-support"))] pub fn set_random_database_id(&mut self, cx: &mut Context) { self.workspace().update(cx, |workspace, _cx| { @@ -438,8 +505,57 @@ impl MultiWorkspace { cx, ); let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx)); - self.activate(new_workspace, cx); + self.set_active_workspace(new_workspace.clone(), cx); self.focus_active_workspace(window, cx); + + let weak_workspace = new_workspace.downgrade(); + self._create_task = Some(cx.spawn_in(window, async move |this, cx| { + let result = crate::persistence::DB.next_id().await; + this.update_in(cx, |this, window, cx| match result { + Ok(workspace_id) => { + if let Some(workspace) = weak_workspace.upgrade() { + let session_id = workspace.read(cx).session_id(); + let window_id = window.window_handle().window_id().as_u64(); + workspace.update(cx, |workspace, _cx| { + workspace.set_database_id(workspace_id); + }); + cx.background_spawn(async move { + crate::persistence::DB + .set_session_binding(workspace_id, session_id, Some(window_id)) + .await + .log_err(); + }) + .detach(); + } else { + cx.background_spawn(async move { + crate::persistence::DB + .delete_workspace_by_id(workspace_id) + .await + .log_err(); + }) + .detach(); + } + this.serialize(cx); + } + Err(error) => { + log::error!("Failed to create workspace: {error:#}"); + if let Some(index) = weak_workspace + .upgrade() + .and_then(|w| this.workspaces.iter().position(|ws| *ws == w)) + { + this.remove_workspace(index, window, cx); + } + this.workspace().update(cx, |workspace, cx| { + let id = NotificationId::unique::(); + workspace.show_toast( + Toast::new(id, format!("Failed to create workspace: {error}")), + cx, + ); + }); + } + }) + .log_err(); + })); } pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context) { @@ -447,7 +563,7 @@ impl MultiWorkspace { return; } - self.workspaces.remove(index); + let removed_workspace = self.workspaces.remove(index); if self.active_workspace_index >= self.workspaces.len() { self.active_workspace_index = self.workspaces.len() - 1; @@ -455,8 +571,19 @@ impl MultiWorkspace { self.active_workspace_index -= 1; } - self.focus_active_workspace(window, cx); + if let Some(workspace_id) = removed_workspace.read(cx).database_id() { + self.pending_removal_tasks.retain(|task| !task.is_ready()); + self.pending_removal_tasks + .push(cx.background_spawn(async move { + crate::persistence::DB + .delete_workspace_by_id(workspace_id) + .await + .log_err(); + })); + } + self.serialize(cx); + self.focus_active_workspace(window, cx); cx.notify(); } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index e0ad046e8d75507225b63c68d309d48ad3415ebb..ba6ee548348cf1dc3da612e112d497c1da011559 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -2153,6 +2153,14 @@ impl WorkspaceDb { } } + query! { + pub(crate) async fn set_session_binding(workspace_id: WorkspaceId, session_id: Option, window_id: Option) -> Result<()> { + UPDATE workspaces + SET session_id = ?2, window_id = ?3 + WHERE workspace_id = ?1 + } + } + pub(crate) async fn toolchains( &self, workspace_id: WorkspaceId, @@ -3934,4 +3942,421 @@ mod tests { assert_eq!(group_none.state.active_workspace_id, None); assert_eq!(group_none.state.sidebar_open, false); } + + #[gpui::test] + async fn test_flush_serialization_completes_before_quit(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)); + + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); + + // Assign a database_id so serialization will actually persist. + let workspace_id = DB.next_id().await.unwrap(); + workspace.update(cx, |ws, _cx| { + ws.set_database_id(workspace_id); + }); + + // Mutate some workspace state. + DB.set_centered_layout(workspace_id, true).await.unwrap(); + + // Call flush_serialization and await the returned task directly + // (without run_until_parked — the point is that awaiting the task + // alone is sufficient). + let task = multi_workspace.update_in(cx, |mw, window, cx| { + mw.workspace() + .update(cx, |ws, cx| ws.flush_serialization(window, cx)) + }); + task.await; + + // Read the workspace back from the DB and verify serialization happened. + let serialized = DB.workspace_for_id(workspace_id); + assert!( + serialized.is_some(), + "flush_serialization should have persisted the workspace to DB" + ); + } + + #[gpui::test] + async fn test_create_workspace_serializes_active_workspace_id_after_db_id_assigned( + cx: &mut gpui::TestAppContext, + ) { + use crate::multi_workspace::MultiWorkspace; + use crate::persistence::read_multi_workspace_state; + 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)); + + // Give the first workspace a database_id. + multi_workspace.update_in(cx, |mw, _, cx| { + mw.set_random_database_id(cx); + }); + + let window_id = + multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id()); + + // Create a new workspace via the MultiWorkspace API (triggers next_id()). + multi_workspace.update_in(cx, |mw, window, cx| { + mw.create_workspace(window, cx); + }); + + // Let the async next_id() and re-serialization tasks complete. + cx.run_until_parked(); + + // Read back the multi-workspace state. + let state = read_multi_workspace_state(window_id); + + // The new workspace should now have a database_id, and the multi-workspace + // state should record it as the active workspace. + 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(), + "New workspace should have a database_id after run_until_parked" + ); + assert_eq!( + state.active_workspace_id, new_workspace_db_id, + "Serialized active_workspace_id should match the new workspace's database_id" + ); + } + + #[gpui::test] + async fn test_create_workspace_individual_serialization(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); + }); + + // Create a new workspace. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.create_workspace(window, cx); + }); + + cx.run_until_parked(); + + // Get the new workspace's database_id. + let new_db_id = + multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id()); + assert!( + new_db_id.is_some(), + "New workspace should have a database_id" + ); + + let workspace_id = new_db_id.unwrap(); + + // The workspace should have been serialized to the DB with real data + // (not just the bare DEFAULT VALUES row from next_id). + let serialized = DB.workspace_for_id(workspace_id); + assert!( + serialized.is_some(), + "Newly created workspace should be fully serialized in the DB after database_id assignment" + ); + } + + #[gpui::test] + async fn test_remove_workspace_deletes_db_row(cx: &mut gpui::TestAppContext) { + use crate::multi_workspace::MultiWorkspace; + use feature_flags::FeatureFlagAppExt; + use gpui::AppContext as _; + 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 project1 = Project::test(fs.clone(), [], cx).await; + let project2 = Project::test(fs.clone(), [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx)); + + multi_workspace.update_in(cx, |mw, _, cx| { + mw.set_random_database_id(cx); + }); + + // Get a real DB id for workspace2 so the row actually exists. + let workspace2_db_id = DB.next_id().await.unwrap(); + + multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx)); + workspace.update(cx, |ws: &mut crate::Workspace, _cx| { + ws.set_database_id(workspace2_db_id) + }); + mw.activate(workspace.clone(), cx); + }); + + // Save a full workspace row to the DB directly. + DB.save_workspace(SerializedWorkspace { + id: workspace2_db_id, + paths: PathList::new(&["/tmp/remove_test"]), + location: SerializedWorkspaceLocation::Local, + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: Some("remove-test-session".to_owned()), + breakpoints: Default::default(), + window_id: Some(99), + user_toolchains: Default::default(), + }) + .await; + + assert!( + DB.workspace_for_id(workspace2_db_id).is_some(), + "Workspace2 should exist in DB before removal" + ); + + // Remove workspace at index 1 (the second workspace). + multi_workspace.update_in(cx, |mw, window, cx| { + mw.remove_workspace(1, window, cx); + }); + + cx.run_until_parked(); + + // The row should be deleted, not just have session_id cleared. + assert!( + DB.workspace_for_id(workspace2_db_id).is_none(), + "Removed workspace's DB row should be deleted entirely" + ); + } + + #[gpui::test] + async fn test_remove_workspace_not_restored_as_zombie(cx: &mut gpui::TestAppContext) { + use crate::multi_workspace::MultiWorkspace; + use feature_flags::FeatureFlagAppExt; + use gpui::AppContext as _; + 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 dir1 = tempfile::TempDir::with_prefix("zombie_test1").unwrap(); + let dir2 = tempfile::TempDir::with_prefix("zombie_test2").unwrap(); + fs.insert_tree(dir1.path(), json!({})).await; + fs.insert_tree(dir2.path(), json!({})).await; + + let project1 = Project::test(fs.clone(), [], cx).await; + let project2 = Project::test(fs.clone(), [], cx).await; + + // Get real DB ids so the rows actually exist. + let ws1_id = DB.next_id().await.unwrap(); + let ws2_id = DB.next_id().await.unwrap(); + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx)); + + multi_workspace.update_in(cx, |mw, _, cx| { + mw.workspace().update(cx, |ws, _cx| { + ws.set_database_id(ws1_id); + }); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx)); + workspace.update(cx, |ws: &mut crate::Workspace, _cx| { + ws.set_database_id(ws2_id) + }); + mw.activate(workspace.clone(), cx); + }); + + let session_id = "test-zombie-session"; + let window_id_val: u64 = 42; + + DB.save_workspace(SerializedWorkspace { + id: ws1_id, + paths: PathList::new(&[dir1.path()]), + location: SerializedWorkspaceLocation::Local, + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: Some(session_id.to_owned()), + breakpoints: Default::default(), + window_id: Some(window_id_val), + user_toolchains: Default::default(), + }) + .await; + + DB.save_workspace(SerializedWorkspace { + id: ws2_id, + paths: PathList::new(&[dir2.path()]), + location: SerializedWorkspaceLocation::Local, + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: Some(session_id.to_owned()), + breakpoints: Default::default(), + window_id: Some(window_id_val), + user_toolchains: Default::default(), + }) + .await; + + // Remove workspace2 (index 1). + multi_workspace.update_in(cx, |mw, window, cx| { + mw.remove_workspace(1, window, cx); + }); + + cx.run_until_parked(); + + // The removed workspace should NOT appear in session restoration. + let locations = DB + .last_session_workspace_locations(session_id, None, fs.as_ref()) + .await + .unwrap(); + + let restored_ids: Vec = locations.iter().map(|sw| sw.workspace_id).collect(); + assert!( + !restored_ids.contains(&ws2_id), + "Removed workspace should not appear in session restoration list. Found: {:?}", + restored_ids + ); + assert!( + restored_ids.contains(&ws1_id), + "Remaining workspace should still appear in session restoration list" + ); + } + + #[gpui::test] + async fn test_pending_removal_tasks_drained_on_flush(cx: &mut gpui::TestAppContext) { + use crate::multi_workspace::MultiWorkspace; + use feature_flags::FeatureFlagAppExt; + use gpui::AppContext as _; + 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 project1 = Project::test(fs.clone(), [], cx).await; + let project2 = Project::test(fs.clone(), [], cx).await; + + // Get a real DB id for workspace2 so the row actually exists. + let workspace2_db_id = DB.next_id().await.unwrap(); + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx)); + + multi_workspace.update_in(cx, |mw, _, cx| { + mw.set_random_database_id(cx); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx)); + workspace.update(cx, |ws: &mut crate::Workspace, _cx| { + ws.set_database_id(workspace2_db_id) + }); + mw.activate(workspace.clone(), cx); + }); + + // Save a full workspace row to the DB directly and let it settle. + DB.save_workspace(SerializedWorkspace { + id: workspace2_db_id, + paths: PathList::new(&["/tmp/pending_removal_test"]), + location: SerializedWorkspaceLocation::Local, + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: Some("pending-removal-session".to_owned()), + breakpoints: Default::default(), + window_id: Some(88), + user_toolchains: Default::default(), + }) + .await; + cx.run_until_parked(); + + // Remove workspace2 — this pushes a task to pending_removal_tasks. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.remove_workspace(1, window, cx); + }); + + // Simulate the quit handler pattern: collect flush tasks + pending + // removal tasks and await them all. + let all_tasks = multi_workspace.update_in(cx, |mw, window, cx| { + let mut tasks: Vec> = mw + .workspaces() + .iter() + .map(|workspace| { + workspace.update(cx, |workspace, cx| { + workspace.flush_serialization(window, cx) + }) + }) + .collect(); + let mut removal_tasks = mw.take_pending_removal_tasks(); + // Note: removal_tasks may be empty if the background task already + // completed (take_pending_removal_tasks filters out ready tasks). + tasks.append(&mut removal_tasks); + tasks.push(mw.flush_serialization()); + tasks + }); + futures::future::join_all(all_tasks).await; + + // After awaiting, the DB row should be deleted. + assert!( + DB.workspace_for_id(workspace2_db_id).is_none(), + "Pending removal task should have deleted the workspace row when awaited" + ); + } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4141952cc788c4fb6a701bf79bc0d57615573442..9711c7d034c452c863dd2876ac82c1bfd9ab3f45 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1244,6 +1244,7 @@ pub struct Workspace { _apply_leader_updates: Task>, _observe_current_user: Task>, _schedule_serialize_workspace: Option>, + _serialize_workspace_task: Option>, _schedule_serialize_ssh_paths: Option>, pane_history_timestamp: Arc, bounds: Bounds, @@ -1667,6 +1668,7 @@ impl Workspace { _observe_current_user, _apply_leader_updates, _schedule_serialize_workspace: None, + _serialize_workspace_task: None, _schedule_serialize_ssh_paths: None, leader_updates_tx, _subscriptions: subscriptions, @@ -5823,10 +5825,24 @@ impl Workspace { self.database_id } + pub(crate) fn set_database_id(&mut self, id: WorkspaceId) { + self.database_id = Some(id); + } + pub fn session_id(&self) -> Option { self.session_id.clone() } + /// 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 + /// isn't lost to a pending throttle timer when the process exits. + 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) + } + pub fn root_paths(&self, cx: &App) -> Vec> { let project = self.project().read(cx); project @@ -5883,7 +5899,8 @@ impl Workspace { .timer(SERIALIZATION_THROTTLE_TIME) .await; this.update_in(cx, |this, window, cx| { - this.serialize_workspace_internal(window, cx).detach(); + this._serialize_workspace_task = + Some(this.serialize_workspace_internal(window, cx)); this._schedule_serialize_workspace.take(); }) .log_err(); @@ -7906,11 +7923,16 @@ pub async fn last_session_workspace_locations( .log_err() } +pub struct MultiWorkspaceRestoreResult { + pub window_handle: WindowHandle, + pub errors: Vec, +} + pub async fn restore_multiworkspace( multi_workspace: SerializedMultiWorkspace, app_state: Arc, cx: &mut AsyncApp, -) -> anyhow::Result> { +) -> anyhow::Result { let SerializedMultiWorkspace { workspaces, state } = multi_workspace; let mut group_iter = workspaces.into_iter(); let first = group_iter @@ -7936,8 +7958,10 @@ pub async fn restore_multiworkspace( window }; + let mut errors = Vec::new(); + for session_workspace in group_iter { - if session_workspace.paths.is_empty() { + let error = if session_workspace.paths.is_empty() { cx.update(|cx| { open_workspace_by_id( session_workspace.workspace_id, @@ -7946,7 +7970,8 @@ pub async fn restore_multiworkspace( cx, ) }) - .await?; + .await + .err() } else { cx.update(|cx| { Workspace::new_local( @@ -7958,7 +7983,12 @@ pub async fn restore_multiworkspace( cx, ) }) - .await?; + .await + .err() + }; + + if let Some(error) = error { + errors.push(error); } } @@ -8000,7 +8030,10 @@ pub async fn restore_multiworkspace( }) .ok(); - Ok(window_handle) + Ok(MultiWorkspaceRestoreResult { + window_handle, + errors, + }) } actions!( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 09264e7799d25f23a91bb014ea4dff0a3283ab74..68b0a78fca658a2dccd8f6e1e667bf8631cad1c7 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1290,14 +1290,18 @@ pub(crate) async fn restore_or_create_workspace( let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); - let mut local_results = Vec::new(); for multi_workspace in multi_workspaces { - local_results - .push(restore_multiworkspace(multi_workspace, app_state.clone(), cx).await); - } - - for result in local_results { - results.push(result.map(|_| ())); + match restore_multiworkspace(multi_workspace, app_state.clone(), cx).await { + Ok(result) => { + for error in result.errors { + log::error!("Failed to restore workspace in group: {error:#}"); + results.push(Err(error)); + } + } + Err(e) => { + results.push(Err(e)); + } + } } for session_workspace in remote_workspaces { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ed94725511b7fb5eaf31af0829979f2f9a05807a..4089abf5fece50f32c85b02fed7a3d08a73a2a64 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1322,7 +1322,8 @@ fn quit(_: &Quit, cx: &mut App) { } // If the user cancels any save prompt, then keep the app open. - for window in workspace_windows { + for window in &workspace_windows { + let window = *window; let workspaces = window .update(cx, |multi_workspace, _, _| { multi_workspace.workspaces().to_vec() @@ -1350,6 +1351,24 @@ fn quit(_: &Quit, cx: &mut App) { } } } + // Flush all pending workspace serialization before quitting so that + // session_id/window_id are up-to-date in the database. + let mut flush_tasks = Vec::new(); + for window in &workspace_windows { + window + .update(cx, |multi_workspace, window, cx| { + for workspace in multi_workspace.workspaces() { + flush_tasks.push(workspace.update(cx, |workspace, cx| { + workspace.flush_serialization(window, cx) + })); + } + flush_tasks.append(&mut multi_workspace.take_pending_removal_tasks()); + flush_tasks.push(multi_workspace.flush_serialization()); + }) + .log_err(); + } + futures::future::join_all(flush_tasks).await; + cx.update(|cx| cx.quit()); anyhow::Ok(()) })