diff --git a/Cargo.lock b/Cargo.lock index 9a402fbe74..1333284229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7171,11 +7171,15 @@ dependencies = [ name = "recent_projects" version = "0.1.0" dependencies = [ + "editor", "fuzzy", "gpui", + "language", "menu", "ordered-float 2.10.0", "picker", + "project", + "serde_json", "smol", "ui", "util", diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 6d9e3d6f41..eb5638d53e 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -19,3 +19,10 @@ smol.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true + +[dev-dependencies] +editor = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +serde_json.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index ef3451b970..41379a247b 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -224,11 +224,35 @@ impl PickerDelegate for RecentProjectsDelegate { workspace .update(cx, |workspace, cx| { if workspace.database_id() != *candidate_workspace_id { - workspace.open_workspace_for_paths( - replace_current_window, - candidate_workspace_location.paths().as_ref().clone(), - cx, - ) + let candidate_paths = candidate_workspace_location.paths().as_ref().clone(); + if replace_current_window { + cx.spawn(move |workspace, mut cx| async move { + let continue_replacing = workspace + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + })? + .await?; + if continue_replacing { + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_workspace_for_paths( + replace_current_window, + candidate_paths, + cx, + ) + })? + .await + } else { + Ok(()) + } + }) + } else { + workspace.open_workspace_for_paths( + replace_current_window, + candidate_paths, + cx, + ) + } } else { Task::ready(Ok(())) } @@ -407,3 +431,136 @@ impl Render for MatchTooltip { }) } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use editor::Editor; + use gpui::{TestAppContext, WindowHandle}; + use project::Project; + use serde_json::json; + use workspace::{open_paths, AppState}; + + use super::*; + + #[gpui::test] + async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/dir", + json!({ + "main.ts": "a" + }), + ) + .await; + cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + let workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + workspace + .update(cx, |workspace, _| assert!(!workspace.is_edited())) + .unwrap(); + + let editor = workspace + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + workspace + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + workspace + .update(cx, |workspace, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project")) + .unwrap(); + + let recent_projects_picker = open_recent_projects(&workspace, cx); + workspace + .update(cx, |_, cx| { + recent_projects_picker.update(cx, |picker, cx| { + assert_eq!(picker.query(cx), ""); + let delegate = &mut picker.delegate; + delegate.matches = vec![StringMatch { + candidate_id: 0, + score: 1.0, + positions: Vec::new(), + string: "fake candidate".to_string(), + }]; + delegate.workspaces = vec![(0, WorkspaceLocation::new(vec!["/test/path/"]))]; + }); + }) + .unwrap(); + + assert!( + !cx.has_pending_prompt(), + "Should have no pending prompt on dirty project before opening the new recent project" + ); + cx.dispatch_action((*workspace).into(), menu::Confirm); + workspace + .update(cx, |workspace, cx| { + assert!( + workspace.active_modal::(cx).is_none(), + "Should remove the modal after selecting new recent project" + ) + }) + .unwrap(); + assert!( + cx.has_pending_prompt(), + "Dirty workspace should prompt before opening the new recent project" + ); + // Cancel + cx.simulate_prompt_answer(0); + assert!( + !cx.has_pending_prompt(), + "Should have no pending prompt after cancelling" + ); + workspace + .update(cx, |workspace, _| { + assert!( + workspace.is_edited(), + "Should be in the same dirty project after cancelling" + ) + }) + .unwrap(); + } + + fn open_recent_projects( + workspace: &WindowHandle, + cx: &mut TestAppContext, + ) -> View> { + cx.dispatch_action((*workspace).into(), OpenRecent); + workspace + .update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }) + .unwrap() + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + language::init(cx); + crate::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } +} diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 6431145e08..dc308de1b2 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -284,7 +284,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_name(cx: &mut TestAppContext) { + async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 73ae0f9b7e..6c917b495d 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -22,6 +22,16 @@ impl WorkspaceLocation { pub fn paths(&self) -> Arc> { self.0.clone() } + + #[cfg(any(test, feature = "test-support"))] + pub fn new>(paths: Vec

) -> Self { + Self(Arc::new( + paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect(), + )) + } } impl, T: IntoIterator> From for WorkspaceLocation {