diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index a682693a49f08d6626bfe2d4f7f02af926c3aa10..180ea078e1c090d664f1cda6fb95e5ba249b526d 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -11,4 +11,6 @@ workspace = { path = "../workspace" } postage = { version = "0.4.1", features = ["futures-traits"] } [dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 64463f27230de1f4a17be1d94aa069a2dbfe4795..f35bbb00e6185efdc0589d93be11ed3410c31f7a 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -948,7 +948,8 @@ mod tests { lsp, people_panel::JoinWorktree, project::{ProjectPath, Worktree}, - workspace::{Workspace, WorkspaceParams}, + test::test_app_state, + workspace::Workspace, }; #[gpui::test] @@ -1059,15 +1060,17 @@ mod tests { #[gpui::test] async fn test_unshare_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_b.update(zed::people_panel::init); - let lang_registry = Arc::new(LanguageRegistry::new()); + let mut app_state_a = cx_a.update(test_app_state); + let mut app_state_b = cx_b.update(test_app_state); // Connect to a server as 2 clients. let mut server = TestServer::start().await; - let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; - let mut workspace_b_params = cx_b.update(WorkspaceParams::test); - workspace_b_params.client = client_b; - workspace_b_params.user_store = user_store_b; + Arc::get_mut(&mut app_state_a).unwrap().client = client_a; + Arc::get_mut(&mut app_state_a).unwrap().user_store = user_store_a; + Arc::get_mut(&mut app_state_b).unwrap().client = client_b; + Arc::get_mut(&mut app_state_b).unwrap().user_store = user_store_b; cx_a.foreground().forbid_parking(); @@ -1083,10 +1086,10 @@ mod tests { ) .await; let worktree_a = Worktree::open_local( - client_a.clone(), + app_state_a.client.clone(), "/a".as_ref(), fs, - lang_registry.clone(), + app_state_a.languages.clone(), &mut cx_a.to_async(), ) .await @@ -1100,7 +1103,8 @@ mod tests { .await .unwrap(); - let (window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&workspace_b_params, cx)); + let (window_b, workspace_b) = + cx_b.add_window(|cx| Workspace::new(&app_state_b.as_ref().into(), cx)); cx_b.update(|cx| { cx.dispatch_action( window_b, diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 0c3d3c0ba2de6edf236fd32454686b98ebbace70..4ec16f45454b4707ad42e9334ac136629387ed47 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -298,16 +298,7 @@ pub struct WorkspaceParams { impl WorkspaceParams { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Self { - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(language::Language::new( - language::LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - tree_sitter_rust::language(), - ))); - + let languages = LanguageRegistry::new(); let client = Client::new(); let http_client = client::test::FakeHttpClient::new(|_| async move { Ok(client::http::ServerResponse::new(404)) @@ -702,7 +693,7 @@ impl Workspace { } } - pub fn active_item(&self, cx: &ViewContext) -> Option> { + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } @@ -884,7 +875,7 @@ impl Workspace { } } - fn split_pane( + pub fn split_pane( &mut self, pane: ViewHandle, direction: SplitDirection, @@ -911,6 +902,10 @@ impl Workspace { } } + pub fn panes(&self) -> &[ViewHandle] { + &self.panes + } + fn pane(&self, pane_id: usize) -> Option> { self.panes.iter().find(|pane| pane.id() == pane_id).cloned() } @@ -1085,12 +1080,10 @@ impl View for Workspace { } } -#[cfg(test)] pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; } -#[cfg(test)] impl WorkspaceHandle for ViewHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec { self.read(cx) @@ -1106,448 +1099,3 @@ impl WorkspaceHandle for ViewHandle { .collect::>() } } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use editor::{Editor, Input}; -// use serde_json::json; -// use std::collections::HashSet; - -// #[gpui::test] -// async fn test_open_entry(mut cx: gpui::TestAppContext) { -// let params = cx.update(WorkspaceParams::test); -// params -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1", -// "file2": "contents 2", -// "file3": "contents 3", -// }, -// }), -// ) -// .await; - -// let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); -// workspace -// .update(&mut cx, |workspace, cx| { -// workspace.add_worktree(Path::new("/root"), cx) -// }) -// .await -// .unwrap(); - -// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) -// .await; -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let file1 = entries[0].clone(); -// let file2 = entries[1].clone(); -// let file3 = entries[2].clone(); - -// // Open the first entry -// workspace -// .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) -// .unwrap() -// .await; -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file1.clone()) -// ); -// assert_eq!(pane.items().len(), 1); -// }); - -// // Open the second entry -// workspace -// .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx)) -// .unwrap() -// .await; -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file2.clone()) -// ); -// assert_eq!(pane.items().len(), 2); -// }); - -// // Open the first entry again. The existing pane item is activated. -// workspace.update(&mut cx, |w, cx| { -// assert!(w.open_entry(file1.clone(), cx).is_none()) -// }); -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file1.clone()) -// ); -// assert_eq!(pane.items().len(), 2); -// }); - -// // Split the pane with the first entry, then open the second entry again. -// workspace.update(&mut cx, |w, cx| { -// w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx); -// assert!(w.open_entry(file2.clone(), cx).is_none()); -// assert_eq!( -// w.active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .project_path(cx.as_ref()), -// Some(file2.clone()) -// ); -// }); - -// // Open the third entry twice concurrently. Only one pane item is added. -// let (t1, t2) = workspace.update(&mut cx, |w, cx| { -// ( -// w.open_entry(file3.clone(), cx).unwrap(), -// w.open_entry(file3.clone(), cx).unwrap(), -// ) -// }); -// t1.await; -// t2.await; -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file3.clone()) -// ); -// let pane_entries = pane -// .items() -// .iter() -// .map(|i| i.project_path(cx).unwrap()) -// .collect::>(); -// assert_eq!(pane_entries, &[file1, file2, file3]); -// }); -// } - -// #[gpui::test] -// async fn test_open_paths(mut cx: gpui::TestAppContext) { -// let params = cx.update(WorkspaceParams::test); -// let fs = params.fs.as_fake(); -// fs.insert_dir("/dir1").await.unwrap(); -// fs.insert_dir("/dir2").await.unwrap(); -// fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); -// fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); - -// let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); -// workspace -// .update(&mut cx, |workspace, cx| { -// workspace.add_worktree("/dir1".as_ref(), cx) -// }) -// .await -// .unwrap(); -// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) -// .await; - -// // Open a file within an existing worktree. -// cx.update(|cx| { -// workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx)) -// }) -// .await; -// cx.read(|cx| { -// assert_eq!( -// workspace -// .read(cx) -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .title(cx), -// "a.txt" -// ); -// }); - -// // Open a file outside of any existing worktree. -// cx.update(|cx| { -// workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx)) -// }) -// .await; -// cx.read(|cx| { -// let worktree_roots = workspace -// .read(cx) -// .worktrees(cx) -// .iter() -// .map(|w| w.read(cx).as_local().unwrap().abs_path()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); -// assert_eq!( -// workspace -// .read(cx) -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .title(cx), -// "b.txt" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) { -// let params = cx.update(WorkspaceParams::test); -// let fs = params.fs.as_fake(); -// fs.insert_tree("/root", json!({ "a.txt": "" })).await; - -// let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); -// workspace -// .update(&mut cx, |workspace, cx| { -// workspace.add_worktree(Path::new("/root"), cx) -// }) -// .await -// .unwrap(); - -// // Open a file within an existing worktree. -// cx.update(|cx| { -// workspace.update(cx, |view, cx| { -// view.open_paths(&[PathBuf::from("/root/a.txt")], cx) -// }) -// }) -// .await; -// let editor = cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// let item = pane.active_item().unwrap(); -// item.to_any().downcast::().unwrap() -// }); - -// cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input(&Input("x".into()), cx))); -// fs.insert_file("/root/a.txt", "changed".to_string()) -// .await -// .unwrap(); -// editor -// .condition(&cx, |editor, cx| editor.has_conflict(cx)) -// .await; -// cx.read(|cx| assert!(editor.is_dirty(cx))); - -// cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx))); -// cx.simulate_prompt_answer(window_id, 0); -// editor -// .condition(&cx, |editor, cx| !editor.is_dirty(cx)) -// .await; -// cx.read(|cx| assert!(!editor.has_conflict(cx))); -// } - -// #[gpui::test] -// async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) { -// let params = cx.update(WorkspaceParams::test); -// params.fs.as_fake().insert_dir("/root").await.unwrap(); -// let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); -// workspace -// .update(&mut cx, |workspace, cx| { -// workspace.add_worktree(Path::new("/root"), cx) -// }) -// .await -// .unwrap(); -// let worktree = cx.read(|cx| { -// workspace -// .read(cx) -// .worktrees(cx) -// .iter() -// .next() -// .unwrap() -// .clone() -// }); - -// // Create a new untitled buffer -// let editor = workspace.update(&mut cx, |workspace, cx| { -// workspace.open_new_file(&OpenNew(params.clone()), cx); -// workspace -// .active_item(cx) -// .unwrap() -// .to_any() -// .downcast::() -// .unwrap() -// }); - -// editor.update(&mut cx, |editor, cx| { -// assert!(!editor.is_dirty(cx.as_ref())); -// assert_eq!(editor.title(cx.as_ref()), "untitled"); -// assert!(editor.language(cx).is_none()); -// editor.handle_input(&Input("hi".into()), cx); -// assert!(editor.is_dirty(cx.as_ref())); -// }); - -// // Save the buffer. This prompts for a filename. -// workspace.update(&mut cx, |workspace, cx| { -// workspace.save_active_item(&Save, cx) -// }); -// cx.simulate_new_path_selection(|parent_dir| { -// assert_eq!(parent_dir, Path::new("/root")); -// Some(parent_dir.join("the-new-name.rs")) -// }); -// cx.read(|cx| { -// assert!(editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "untitled"); -// }); - -// // When the save completes, the buffer's title is updated. -// editor -// .condition(&cx, |editor, cx| !editor.is_dirty(cx)) -// .await; -// cx.read(|cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "the-new-name.rs"); -// }); -// // The language is assigned based on the path -// editor.read_with(&cx, |editor, cx| { -// assert_eq!(editor.language(cx).unwrap().name(), "Rust") -// }); - -// // Edit the file and save it again. This time, there is no filename prompt. -// editor.update(&mut cx, |editor, cx| { -// editor.handle_input(&Input(" there".into()), cx); -// assert_eq!(editor.is_dirty(cx.as_ref()), true); -// }); -// workspace.update(&mut cx, |workspace, cx| { -// workspace.save_active_item(&Save, cx) -// }); -// assert!(!cx.did_prompt_for_new_path()); -// editor -// .condition(&cx, |editor, cx| !editor.is_dirty(cx)) -// .await; -// cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs")); - -// // Open the same newly-created file in another pane item. The new editor should reuse -// // the same buffer. -// workspace.update(&mut cx, |workspace, cx| { -// workspace.open_new_file(&OpenNew(params.clone()), cx); -// workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); -// assert!(workspace -// .open_entry( -// ProjectPath { -// worktree_id: worktree.id(), -// path: Path::new("the-new-name.rs").into() -// }, -// cx -// ) -// .is_none()); -// }); -// let editor2 = workspace.update(&mut cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .to_any() -// .downcast::() -// .unwrap() -// }); -// cx.read(|cx| { -// assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer()); -// }) -// } - -// #[gpui::test] -// async fn test_setting_language_when_saving_as_single_file_worktree( -// mut cx: gpui::TestAppContext, -// ) { -// let params = cx.update(WorkspaceParams::test); -// params.fs.as_fake().insert_dir("/root").await.unwrap(); -// let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - -// // Create a new untitled buffer -// let editor = workspace.update(&mut cx, |workspace, cx| { -// workspace.open_new_file(&OpenNew(params.clone()), cx); -// workspace -// .active_item(cx) -// .unwrap() -// .to_any() -// .downcast::() -// .unwrap() -// }); - -// editor.update(&mut cx, |editor, cx| { -// assert!(editor.language(cx).is_none()); -// editor.handle_input(&Input("hi".into()), cx); -// assert!(editor.is_dirty(cx.as_ref())); -// }); - -// // Save the buffer. This prompts for a filename. -// workspace.update(&mut cx, |workspace, cx| { -// workspace.save_active_item(&Save, cx) -// }); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); - -// editor -// .condition(&cx, |editor, cx| !editor.is_dirty(cx)) -// .await; - -// // The language is assigned based on the path -// editor.read_with(&cx, |editor, cx| { -// assert_eq!(editor.language(cx).unwrap().name(), "Rust") -// }); -// } - -// #[gpui::test] -// async fn test_pane_actions(mut cx: gpui::TestAppContext) { -// cx.update(|cx| pane::init(cx)); -// let params = cx.update(WorkspaceParams::test); -// params -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1", -// "file2": "contents 2", -// "file3": "contents 3", -// }, -// }), -// ) -// .await; - -// let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); -// workspace -// .update(&mut cx, |workspace, cx| { -// workspace.add_worktree(Path::new("/root"), cx) -// }) -// .await -// .unwrap(); -// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) -// .await; -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let file1 = entries[0].clone(); - -// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); - -// workspace -// .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) -// .unwrap() -// .await; -// cx.read(|cx| { -// assert_eq!( -// pane_1.read(cx).active_item().unwrap().project_path(cx), -// Some(file1.clone()) -// ); -// }); - -// cx.dispatch_action( -// window_id, -// vec![pane_1.id()], -// pane::Split(SplitDirection::Right), -// ); -// cx.update(|cx| { -// let pane_2 = workspace.read(cx).active_pane().clone(); -// assert_ne!(pane_1, pane_2); - -// let pane2_item = pane_2.read(cx).active_item().unwrap(); -// assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone())); - -// cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem); -// let workspace = workspace.read(cx); -// assert_eq!(workspace.panes.len(), 1); -// assert_eq!(workspace.active_pane(), &pane_1); -// }); -// } -// } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ebbc73c3b908d83b2e1bf5d2da058202e05902d6..a13602016abbede33230ee18d791f936606fc314 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -95,7 +95,6 @@ impl Pane { item_idx } - #[cfg(test)] pub fn items(&self) -> &[Box] { &self.items } diff --git a/crates/zed/src/lib.rs b/crates/zed/src/lib.rs index 0f0b240085eb482fb35c8066d0e603efd64654bc..cace44a81605ce23505846475fe44809dc9ce01c 100644 --- a/crates/zed/src/lib.rs +++ b/crates/zed/src/lib.rs @@ -26,7 +26,7 @@ use std::{path::PathBuf, sync::Arc}; use theme::ThemeRegistry; use theme_selector::ThemeSelectorParams; pub use workspace; -use workspace::{Settings, Workspace, WorkspaceParams}; +use workspace::{OpenNew, Settings, Workspace, WorkspaceParams}; action!(About); action!(Open, Arc); @@ -129,7 +129,7 @@ fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> { }) } -fn open_new(action: &workspace::OpenNew, cx: &mut MutableAppContext) { +fn open_new(action: &OpenNew, cx: &mut MutableAppContext) { let (window_id, workspace) = cx.add_window(window_options(), |cx| build_workspace(&action.0, cx)); cx.dispatch_action(window_id, vec![workspace.id()], action); @@ -212,11 +212,14 @@ impl<'a> From<&'a AppState> for ThemeSelectorParams { #[cfg(test)] mod tests { use super::*; + use editor::Editor; + use project::ProjectPath; use serde_json::json; + use std::{collections::HashSet, path::Path}; use test::test_app_state; use theme::DEFAULT_THEME_NAME; use util::test::temp_tree; - use workspace::ItemView; + use workspace::{pane, ItemView, ItemViewHandle, SplitDirection, WorkspaceHandle}; #[gpui::test] async fn test_open_paths_action(mut cx: gpui::TestAppContext) { @@ -318,6 +321,451 @@ mod tests { }); } + #[gpui::test] + async fn test_open_entry(mut cx: gpui::TestAppContext) { + let app_state = cx.update(test_app_state); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + }), + ) + .await; + + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(Path::new("/root"), cx) + }) + .await + .unwrap(); + + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + + // Open the first entry + workspace + .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) + .unwrap() + .await; + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 1); + }); + + // Open the second entry + workspace + .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx)) + .unwrap() + .await; + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file2.clone()) + ); + assert_eq!(pane.items().len(), 2); + }); + + // Open the first entry again. The existing pane item is activated. + workspace.update(&mut cx, |w, cx| { + assert!(w.open_entry(file1.clone(), cx).is_none()) + }); + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 2); + }); + + // Split the pane with the first entry, then open the second entry again. + workspace.update(&mut cx, |w, cx| { + w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx); + assert!(w.open_entry(file2.clone(), cx).is_none()); + assert_eq!( + w.active_pane() + .read(cx) + .active_item() + .unwrap() + .project_path(cx.as_ref()), + Some(file2.clone()) + ); + }); + + // Open the third entry twice concurrently. Only one pane item is added. + let (t1, t2) = workspace.update(&mut cx, |w, cx| { + ( + w.open_entry(file3.clone(), cx).unwrap(), + w.open_entry(file3.clone(), cx).unwrap(), + ) + }); + t1.await; + t2.await; + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file3.clone()) + ); + let pane_entries = pane + .items() + .iter() + .map(|i| i.project_path(cx).unwrap()) + .collect::>(); + assert_eq!(pane_entries, &[file1, file2, file3]); + }); + } + + #[gpui::test] + async fn test_open_paths(mut cx: gpui::TestAppContext) { + let app_state = cx.update(test_app_state); + let fs = app_state.fs.as_fake(); + fs.insert_dir("/dir1").await.unwrap(); + fs.insert_dir("/dir2").await.unwrap(); + fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); + fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); + + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree("/dir1".as_ref(), cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + + // Open a file within an existing worktree. + cx.update(|cx| { + workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx)) + }) + .await; + cx.read(|cx| { + assert_eq!( + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .unwrap() + .title(cx), + "a.txt" + ); + }); + + // Open a file outside of any existing worktree. + cx.update(|cx| { + workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx)) + }) + .await; + cx.read(|cx| { + let worktree_roots = workspace + .read(cx) + .worktrees(cx) + .iter() + .map(|w| w.read(cx).as_local().unwrap().abs_path()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt"] + .into_iter() + .map(Path::new) + .collect(), + ); + assert_eq!( + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .unwrap() + .title(cx), + "b.txt" + ); + }); + } + + #[gpui::test] + async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) { + let app_state = cx.update(test_app_state); + let fs = app_state.fs.as_fake(); + fs.insert_tree("/root", json!({ "a.txt": "" })).await; + + let (window_id, workspace) = + cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(Path::new("/root"), cx) + }) + .await + .unwrap(); + + // Open a file within an existing worktree. + cx.update(|cx| { + workspace.update(cx, |view, cx| { + view.open_paths(&[PathBuf::from("/root/a.txt")], cx) + }) + }) + .await; + let editor = cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let item = pane.active_item().unwrap(); + item.to_any().downcast::().unwrap() + }); + + cx.update(|cx| { + editor.update(cx, |editor, cx| { + editor.handle_input(&editor::Input("x".into()), cx) + }) + }); + fs.insert_file("/root/a.txt", "changed".to_string()) + .await + .unwrap(); + editor + .condition(&cx, |editor, cx| editor.has_conflict(cx)) + .await; + cx.read(|cx| assert!(editor.is_dirty(cx))); + + cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&workspace::Save, cx))); + cx.simulate_prompt_answer(window_id, 0); + editor + .condition(&cx, |editor, cx| !editor.is_dirty(cx)) + .await; + cx.read(|cx| assert!(!editor.has_conflict(cx))); + } + + #[gpui::test] + async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) { + let app_state = cx.update(test_app_state); + app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + let params = app_state.as_ref().into(); + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(Path::new("/root"), cx) + }) + .await + .unwrap(); + let worktree = cx.read(|cx| { + workspace + .read(cx) + .worktrees(cx) + .iter() + .next() + .unwrap() + .clone() + }); + + // Create a new untitled buffer + cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone())); + let editor = workspace.read_with(&cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + + editor.update(&mut cx, |editor, cx| { + assert!(!editor.is_dirty(cx.as_ref())); + assert_eq!(editor.title(cx.as_ref()), "untitled"); + assert!(editor.language(cx).is_none()); + editor.handle_input(&editor::Input("hi".into()), cx); + assert!(editor.is_dirty(cx.as_ref())); + }); + + // Save the buffer. This prompts for a filename. + workspace.update(&mut cx, |workspace, cx| { + workspace.save_active_item(&workspace::Save, cx) + }); + cx.simulate_new_path_selection(|parent_dir| { + assert_eq!(parent_dir, Path::new("/root")); + Some(parent_dir.join("the-new-name.rs")) + }); + cx.read(|cx| { + assert!(editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "untitled"); + }); + + // When the save completes, the buffer's title is updated. + editor + .condition(&cx, |editor, cx| !editor.is_dirty(cx)) + .await; + cx.read(|cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name.rs"); + }); + // The language is assigned based on the path + editor.read_with(&cx, |editor, cx| { + assert_eq!(editor.language(cx).unwrap().name(), "Rust") + }); + + // Edit the file and save it again. This time, there is no filename prompt. + editor.update(&mut cx, |editor, cx| { + editor.handle_input(&editor::Input(" there".into()), cx); + assert_eq!(editor.is_dirty(cx.as_ref()), true); + }); + workspace.update(&mut cx, |workspace, cx| { + workspace.save_active_item(&workspace::Save, cx) + }); + assert!(!cx.did_prompt_for_new_path()); + editor + .condition(&cx, |editor, cx| !editor.is_dirty(cx)) + .await; + cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs")); + + // Open the same newly-created file in another pane item. The new editor should reuse + // the same buffer. + cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone())); + workspace.update(&mut cx, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + assert!(workspace + .open_entry( + ProjectPath { + worktree_id: worktree.id(), + path: Path::new("the-new-name.rs").into() + }, + cx + ) + .is_none()); + }); + let editor2 = workspace.update(&mut cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + cx.read(|cx| { + assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer()); + }) + } + + #[gpui::test] + async fn test_setting_language_when_saving_as_single_file_worktree( + mut cx: gpui::TestAppContext, + ) { + let app_state = cx.update(test_app_state); + app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + let params = app_state.as_ref().into(); + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + + // Create a new untitled buffer + cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone())); + let editor = workspace.read_with(&cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .to_any() + .downcast::() + .unwrap() + }); + + editor.update(&mut cx, |editor, cx| { + assert!(editor.language(cx).is_none()); + editor.handle_input(&editor::Input("hi".into()), cx); + assert!(editor.is_dirty(cx.as_ref())); + }); + + // Save the buffer. This prompts for a filename. + workspace.update(&mut cx, |workspace, cx| { + workspace.save_active_item(&workspace::Save, cx) + }); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); + + editor + .condition(&cx, |editor, cx| !editor.is_dirty(cx)) + .await; + + // The language is assigned based on the path + editor.read_with(&cx, |editor, cx| { + assert_eq!(editor.language(cx).unwrap().name(), "Rust") + }); + } + + #[gpui::test] + async fn test_pane_actions(mut cx: gpui::TestAppContext) { + cx.update(|cx| pane::init(cx)); + let app_state = cx.update(test_app_state); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + }), + ) + .await; + + let (window_id, workspace) = + cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(Path::new("/root"), cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + let file1 = entries[0].clone(); + + let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); + + workspace + .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx)) + .unwrap() + .await; + cx.read(|cx| { + assert_eq!( + pane_1.read(cx).active_item().unwrap().project_path(cx), + Some(file1.clone()) + ); + }); + + cx.dispatch_action( + window_id, + vec![pane_1.id()], + pane::Split(SplitDirection::Right), + ); + cx.update(|cx| { + let pane_2 = workspace.read(cx).active_pane().clone(); + assert_ne!(pane_1, pane_2); + + let pane2_item = pane_2.read(cx).active_item().unwrap(); + assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone())); + + cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem); + let workspace = workspace.read(cx); + assert_eq!(workspace.panes().len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); + }); + } + #[gpui::test] fn test_bundled_themes(cx: &mut MutableAppContext) { let app_state = test_app_state(cx); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index f868ab704b47e1cd6eb9540ba2d2f44bd52fbf6b..5cb9b5f0e8944e0d1c9cdf0067b625729254eb25 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -16,21 +16,32 @@ fn init_logger() { } pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { + let mut entry_openers = Vec::new(); + editor::init(cx, &mut entry_openers); let (settings_tx, settings) = watch::channel_with(build_settings(cx)); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let client = Client::new(); let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + let mut languages = LanguageRegistry::new(); + languages.add(Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + tree_sitter_rust::language(), + ))); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, - languages: Arc::new(LanguageRegistry::new()), + languages: Arc::new(languages), channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)), client, user_store, fs: Arc::new(FakeFs::new()), - entry_openers: Arc::from([]), + entry_openers: Arc::from(entry_openers), }) }