diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml index 5d637bb49d0922039930caa12611b8a53757fd8d..5b897f4d55c702263fa9098f3d0a987c08b0baa6 100644 --- a/crates/sidebar/Cargo.toml +++ b/crates/sidebar/Cargo.toml @@ -45,6 +45,7 @@ editor.workspace = true language_model = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true prompt_store.workspace = true +recent_projects = { workspace = true, features = ["test-support"] } serde_json.workspace = true feature_flags.workspace = true fs = { workspace = true, features = ["test-support"] } diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index da5123af210cdb6bc7e824dc8d956b56a5c53b70..0f230fb3d165440e71bedef4e01f6f0e4bb2e058 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -2904,7 +2904,7 @@ mod tests { // Add a second workspace multi_workspace.update_in(cx, |mw, window, cx| { - mw.create_workspace(window, cx); + mw.create_test_workspace(window, cx).detach(); }); cx.run_until_parked(); @@ -3974,7 +3974,7 @@ mod tests { // Add a second workspace. multi_workspace.update_in(cx, |mw, window, cx| { - mw.create_workspace(window, cx); + mw.create_test_workspace(window, cx).detach(); }); cx.run_until_parked(); @@ -4066,7 +4066,7 @@ mod tests { // Add a second workspace. multi_workspace.update_in(cx, |mw, window, cx| { - mw.create_workspace(window, cx); + mw.create_test_workspace(window, cx).detach(); }); cx.run_until_parked(); @@ -4316,7 +4316,7 @@ mod tests { let sidebar = setup_sidebar(&multi_workspace, cx); multi_workspace.update_in(cx, |mw, window, cx| { - mw.create_workspace(window, cx); + mw.create_test_workspace(window, cx).detach(); }); cx.run_until_parked(); @@ -4628,7 +4628,7 @@ mod tests { }); multi_workspace.update_in(cx, |mw, window, cx| { - mw.activate_next_workspace(window, cx); + mw.activate_index(0, window, cx); }); cx.run_until_parked(); diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 85432a448f4139fea48178ababd502e8412a7dfd..ee26041c50389a7375d96f40003ec59e91adea12 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -5,7 +5,9 @@ use gpui::{ ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId, actions, deferred, px, }; -use project::{DisableAiSettings, Project}; +use project::DisableAiSettings; +#[cfg(any(test, feature = "test-support"))] +use project::Project; use settings::Settings; use std::future::Future; use std::path::PathBuf; @@ -15,19 +17,13 @@ use util::ResultExt; const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0); use crate::{ - CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast, - Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId, + CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, + Workspace, WorkspaceId, client_side_decorations, }; actions!( multi_workspace, [ - /// Creates a new workspace within the current window. - NewWorkspaceInWindow, - /// Switches to the next workspace within the current window. - NextWorkspaceInWindow, - /// Switches to the previous workspace within the current window. - PreviousWorkspaceInWindow, /// Toggles the workspace switcher sidebar. ToggleWorkspaceSidebar, /// Moves focus to or from the workspace sidebar without closing it. @@ -126,7 +122,6 @@ pub struct MultiWorkspace { sidebar_open: bool, pending_removal_tasks: Vec>, _serialize_task: Option>, - _create_task: Option>, _subscriptions: Vec, } @@ -138,9 +133,6 @@ impl MultiWorkspace { 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(); } @@ -161,7 +153,6 @@ impl MultiWorkspace { sidebar_open: false, pending_removal_tasks: Vec::new(), _serialize_task: None, - _create_task: None, _subscriptions: vec![ release_subscription, quit_subscription, @@ -385,24 +376,6 @@ impl MultiWorkspace { cx.notify(); } - pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context) { - if self.workspaces.len() > 1 { - let next_index = (self.active_workspace_index + 1) % self.workspaces.len(); - self.activate_index(next_index, window, cx); - } - } - - pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context) { - if self.workspaces.len() > 1 { - let prev_index = if self.active_workspace_index == 0 { - self.workspaces.len() - 1 - } else { - self.active_workspace_index - 1 - }; - self.activate_index(prev_index, window, cx); - } - } - fn serialize(&mut self, cx: &mut App) { let window_id = self.window_id; let state = crate::persistence::model::MultiWorkspaceState { @@ -426,9 +399,6 @@ impl MultiWorkspace { 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 { @@ -531,15 +501,10 @@ impl MultiWorkspace { } pub fn take_pending_removal_tasks(&mut self) -> Vec> { - let mut tasks: Vec> = std::mem::take(&mut self.pending_removal_tasks) + let 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 } @@ -568,10 +533,12 @@ impl MultiWorkspace { workspace } - pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context) { - if !self.multi_workspace_enabled(cx) { - return; - } + #[cfg(any(test, feature = "test-support"))] + pub fn create_test_workspace( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Task<()> { let app_state = self.workspace().read(cx).app_state().clone(); let project = Project::local( app_state.client.clone(), @@ -588,53 +555,27 @@ impl MultiWorkspace { 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, - ); + cx.spawn_in(window, async move |this, cx| { + let workspace_id = crate::persistence::DB.next_id().await.unwrap(); + let workspace = weak_workspace.upgrade().unwrap(); + let task: Task<()> = this + .update_in(cx, |this, window, cx| { + 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); }); - } - }) - .log_err(); - })); + this.serialize(cx); + cx.background_spawn(async move { + crate::persistence::DB + .set_session_binding(workspace_id, session_id, Some(window_id)) + .await + .log_err(); + }) + }) + .unwrap(); + task.await + }) } pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context) { @@ -772,21 +713,6 @@ impl Render for MultiWorkspace { .font(ui_font) .text_color(text_color) .on_action(cx.listener(Self::close_window)) - .on_action( - cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| { - this.create_workspace(window, cx); - }), - ) - .on_action( - cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| { - this.activate_next_workspace(window, cx); - }), - ) - .on_action(cx.listener( - |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| { - this.activate_previous_workspace(window, cx); - }, - )) .when(self.multi_workspace_enabled(cx), |this| { this.on_action(cx.listener( |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 29346e47d9b21dade0bddb0cb0882188c32a19c2..7202a216cb7da94637c2bd24a76c61bb6c36c73b 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -2401,6 +2401,14 @@ mod tests { use serde_json::json; use std::{thread, time::Duration}; + /// Creates a unique directory in a FakeFs, returning the path. + /// Uses a UUID suffix to avoid collisions with other tests sharing the global DB. + async fn unique_test_dir(fs: &fs::FakeFs, prefix: &str) -> PathBuf { + let dir = PathBuf::from(format!("/test-dirs/{}-{}", prefix, uuid::Uuid::new_v4())); + fs.insert_tree(&dir, json!({})).await; + dir + } + #[gpui::test] async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) { use crate::multi_workspace::MultiWorkspace; @@ -4000,9 +4008,7 @@ mod tests { } #[gpui::test] - async fn test_create_workspace_serializes_active_workspace_id_after_db_id_assigned( - cx: &mut gpui::TestAppContext, - ) { + async fn test_create_workspace_serialization(cx: &mut gpui::TestAppContext) { use crate::multi_workspace::MultiWorkspace; use crate::persistence::read_multi_workspace_state; use feature_flags::FeatureFlagAppExt; @@ -4032,72 +4038,30 @@ mod tests { // Create a new workspace via the MultiWorkspace API (triggers next_id()). multi_workspace.update_in(cx, |mw, window, cx| { - mw.create_workspace(window, cx); + mw.create_test_workspace(window, cx).detach(); }); // 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. + // The new workspace should now have a database_id. 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" ); + + // The multi-workspace state should record it as the active workspace. + let state = read_multi_workspace_state(window_id); 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 + // The individual workspace row should exist with real data // (not just the bare DEFAULT VALUES row from next_id). + let workspace_id = new_workspace_db_id.unwrap(); let serialized = DB.workspace_for_id(workspace_id); assert!( serialized.is_some(), @@ -4120,6 +4084,7 @@ mod tests { }); let fs = fs::FakeFs::new(cx.executor()); + let dir = unique_test_dir(&fs, "remove").await; let project1 = Project::test(fs.clone(), [], cx).await; let project2 = Project::test(fs.clone(), [], cx).await; @@ -4142,16 +4107,17 @@ mod tests { }); // Save a full workspace row to the DB directly. + let session_id = format!("remove-test-session-{}", Uuid::new_v4()); DB.save_workspace(SerializedWorkspace { id: workspace2_db_id, - paths: PathList::new(&["/tmp/remove_test"]), + paths: PathList::new(&[&dir]), 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()), + session_id: Some(session_id.clone()), breakpoints: Default::default(), window_id: Some(99), user_toolchains: Default::default(), @@ -4311,6 +4277,7 @@ mod tests { }); let fs = fs::FakeFs::new(cx.executor()); + let dir = unique_test_dir(&fs, "pending-removal").await; let project1 = Project::test(fs.clone(), [], cx).await; let project2 = Project::test(fs.clone(), [], cx).await; @@ -4333,16 +4300,17 @@ mod tests { }); // Save a full workspace row to the DB directly and let it settle. + let session_id = format!("pending-removal-session-{}", Uuid::new_v4()); DB.save_workspace(SerializedWorkspace { id: workspace2_db_id, - paths: PathList::new(&["/tmp/pending_removal_test"]), + paths: PathList::new(&[&dir]), 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()), + session_id: Some(session_id.clone()), breakpoints: Default::default(), window_id: Some(88), user_toolchains: Default::default(), @@ -4420,11 +4388,9 @@ mod tests { mw.set_random_database_id(cx); }); - multi_workspace.update_in(cx, |mw, window, cx| { - mw.create_workspace(window, cx); - }); - - cx.run_until_parked(); + let task = + multi_workspace.update_in(cx, |mw, window, cx| mw.create_test_workspace(window, cx)); + task.await; let new_workspace_db_id = multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id()); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 27493941b206c871ed1bbbab0a4d51e6b11791d8..1e9358b80b8346c6cc0a794820e51b04e7ffde96 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,9 +27,8 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; pub use multi_workspace::{ - DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, - NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarHandle, - ToggleWorkspaceSidebar, + DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Sidebar, + SidebarHandle, ToggleWorkspaceSidebar, }; pub use path_list::{PathList, SerializedPathList}; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; @@ -5979,6 +5978,7 @@ impl Workspace { self.database_id } + #[cfg(any(test, feature = "test-support"))] pub(crate) fn set_database_id(&mut self, id: WorkspaceId) { self.database_id = Some(id); } @@ -8260,7 +8260,7 @@ pub async fn restore_multiworkspace( Some(window_handle), None, None, - true, + false, cx, ) })