From 53564fb2696f0e1ff6cbc2a0e8b8a08802db15a5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 8 Jan 2024 12:29:54 +0100 Subject: [PATCH] Bring back zed.rs tests (#3907) At present 3 tests still fail; 2 are related to keymap issues that (I believe) @maxbrunsfeld is working on. The other one (`test_open_paths_action`) I'll look into. edit: done This PR also fixes workspace unregistration, as we've put the code to do that behind `debug_assert` (https://github.com/zed-industries/zed/pull/3907/files#diff-041673bbd1947a35d45945636c0055429dfc8b5985faf93f8a8a960c9ad31e28L649). Release Notes: - N/A --- crates/gpui/src/app/test_context.rs | 27 + crates/gpui/src/platform/test/platform.rs | 2 +- crates/gpui/src/platform/test/window.rs | 11 +- crates/gpui/src/view.rs | 10 +- crates/workspace/src/workspace.rs | 2 +- crates/zed/Cargo.toml | 5 +- crates/zed/src/zed.rs | 3944 +++++++++++---------- 7 files changed, 2132 insertions(+), 1869 deletions(-) diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 470315f887c6fd5e4c6d3523498dbe496ff041a8..0f71ea61a9ec347b01e601f5e3c1a2237b80e691 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -532,6 +532,33 @@ impl<'a> VisualTestContext { } self.background_executor.run_until_parked(); } + /// Returns true if the window was closed. + pub fn simulate_close(&mut self) -> bool { + let handler = self + .cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .0 + .lock() + .should_close_handler + .take() + }) + .unwrap(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.cx + .update_window(self.window, |_, cx| { + cx.window.platform_window.on_should_close(handler); + }) + .unwrap(); + should_close + } else { + false + } + } } impl Context for VisualTestContext { diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 111fb839211b89c7c47b6d65f7448a92a5997675..695323e9c46b8e2a8f4260a682d8e214f58c43f4 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -266,7 +266,7 @@ impl Platform for TestPlatform { } fn local_timezone(&self) -> time::UtcOffset { - unimplemented!() + time::UtcOffset::UTC } fn path_for_auxiliary_executable(&self, _name: &str) -> Result { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index f089531b0c94b556d48a1c9a8c6777b863649a10..91f965c10ac2987f4f6c8c15cb40a7cd94171370 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -18,7 +18,7 @@ pub struct TestWindowState { pub(crate) edited: bool, platform: Weak, sprite_atlas: Arc, - + pub(crate) should_close_handler: Option bool>>, input_callback: Option bool>>, active_status_change_callback: Option>, resize_callback: Option, f32)>>, @@ -44,7 +44,7 @@ impl TestWindow { sprite_atlas: Arc::new(TestAtlas::new()), title: Default::default(), edited: false, - + should_close_handler: None, input_callback: None, active_status_change_callback: None, resize_callback: None, @@ -117,6 +117,9 @@ impl TestWindow { self.0.lock().input_handler = Some(input_handler); } + pub fn edited(&self) -> bool { + self.0.lock().edited + } } impl PlatformWindow for TestWindow { @@ -235,8 +238,8 @@ impl PlatformWindow for TestWindow { self.0.lock().moved_callback = Some(callback) } - fn on_should_close(&self, _callback: Box bool>) { - unimplemented!() + fn on_should_close(&self, callback: Box bool>) { + self.0.lock().should_close_handler = Some(callback); } fn on_close(&self, _callback: Box) { diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 6e6223cbbea97f74e9ce57b5e207069c1821cc02..4472da02e71fda1bb17d4353056b67ad58639813 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::{Context, Result}; use std::{ - any::TypeId, + any::{type_name, TypeId}, fmt, hash::{Hash, Hasher}, }; @@ -104,6 +104,14 @@ impl Clone for View { } } +impl std::fmt::Debug for View { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(&format!("View<{}>", type_name::())) + .field("entity_id", &self.model.entity_id) + .finish_non_exhaustive() + } +} + impl Hash for View { fn hash(&self, state: &mut H) { self.model.hash(state); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 84b84677d1c13e25be0b3fa1fc63725cb93e5c89..826a6693d7ca350a85efa4589e5c87ba90bbf94c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -658,7 +658,7 @@ impl Workspace { cx.on_release(|this, window, cx| { this.app_state.workspace_store.update(cx, |store, _| { let window = window.downcast::().unwrap(); - debug_assert!(store.workspaces.remove(&window)); + store.workspaces.remove(&window); }) }), ]; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 39ab5e285b77672e31d8b33555f83bd768e4be68..c17d9c781c217641330847b6000c9c4676fb5bd4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -146,8 +146,7 @@ uuid.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } # client = { path = "../client", features = ["test-support"] } -# editor = { path = "../editor", features = ["test-support"] } -# gpui = { path = "../gpui", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } # lsp = { path = "../lsp", features = ["test-support"] } @@ -156,7 +155,7 @@ project = { path = "../project", features = ["test-support"] } # settings = { path = "../settings", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } # util = { path = "../util", features = ["test-support"] } -# workspace = { path = "../workspace", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true [package.metadata.bundle-dev] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c7d30230ea035b29121ceb487e4ea3ce483d5d54..702c815d34600b8502e55f83f60b17c572dc869e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -33,11 +33,11 @@ use util::{ }; use uuid::Uuid; use welcome::BaseKeymap; -use workspace::Pane; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; +use workspace::{dock::Panel, Pane}; use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit}; actions!( @@ -114,9 +114,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .detach(); - // cx.emit(workspace2::Event::PaneAdded( - // workspace.active_pane().clone(), - // )); + // cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); // let collab_titlebar_item = // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); @@ -187,6 +185,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { )?; workspace_handle.update(&mut cx, |workspace, cx| { + let position = project_panel.read(cx).position(cx); workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); workspace.add_panel(assistant_panel, cx); @@ -194,19 +193,18 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace.add_panel(chat_panel, cx); workspace.add_panel(notification_panel, cx); - // if !was_deserialized - // && workspace - // .project() - // .read(cx) - // .visible_worktrees(cx) - // .any(|tree| { - // tree.read(cx) - // .root_entry() - // .map_or(false, |entry| entry.is_dir()) - // }) - // { - // workspace.toggle_dock(project_panel_position, cx); - // } + if workspace + .project() + .read(cx) + .visible_worktrees(cx) + .any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) + }) + { + workspace.toggle_dock(position, cx); + } cx.focus_self(); }) }) @@ -587,7 +585,6 @@ pub fn handle_keymap_file_changes( } } } - cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok(); } }) @@ -770,1844 +767,2073 @@ fn open_bundled_file( } // todo!() -// #[cfg(test)] -// mod tests { -// use super::*; -// use assets::Assets; -// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; -// use fs::{FakeFs, Fs}; -// use gpui::{ -// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle, -// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, -// }; -// use language::LanguageRegistry; -// use project::{project_settings::ProjectSettings, Project, ProjectPath}; -// use serde_json::json; -// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; -// use std::{ -// collections::HashSet, -// path::{Path, PathBuf}, -// }; -// use theme::{ThemeRegistry, ThemeSettings}; -// use workspace::{ -// item::{Item, ItemHandle}, -// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle, -// }; - -// #[gpui::test] -// async fn test_open_paths_action(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "aa": null, -// "ab": null, -// }, -// "b": { -// "ba": null, -// "bb": null, -// }, -// "c": { -// "ca": null, -// "cb": null, -// }, -// "d": { -// "da": null, -// "db": null, -// }, -// }), -// ) -// .await; - -// cx.update(|cx| { -// open_paths( -// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], -// &app_state, -// None, -// cx, -// ) -// }) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); - -// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); -// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); -// workspace_1.update(cx, |workspace, cx| { -// assert_eq!(workspace.worktrees(cx).count(), 2); -// assert!(workspace.left_dock().read(cx).is_open()); -// assert!(workspace.active_pane().is_focused(cx)); -// }); - -// cx.update(|cx| { -// open_paths( -// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], -// &app_state, -// None, -// cx, -// ) -// }) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 2); - -// // Replace existing windows -// let window = cx.windows()[0].downcast::().unwrap(); -// cx.update(|cx| { -// open_paths( -// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], -// &app_state, -// Some(window), -// cx, -// ) -// }) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 2); -// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); -// workspace_1.update(cx, |workspace, cx| { -// assert_eq!( -// workspace -// .worktrees(cx) -// .map(|w| w.read(cx).abs_path()) -// .collect::>(), -// &[Path::new("/root/c").into(), Path::new("/root/d").into()] -// ); -// assert!(workspace.left_dock().read(cx).is_open()); -// assert!(workspace.active_pane().is_focused(cx)); -// }); -// } - -// #[gpui::test] -// async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({"a": "hey"})) -// .await; - -// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); - -// // When opening the workspace, the window is not in a edited state. -// let window = cx.windows()[0].downcast::().unwrap(); -// let workspace = window.root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert!(!window.is_edited(cx)); - -// // Editing a buffer marks the window as edited. -// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); -// assert!(window.is_edited(cx)); - -// // Undoing the edit restores the window's edited state. -// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); -// assert!(!window.is_edited(cx)); - -// // Redoing the edit marks the window as edited again. -// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); -// assert!(window.is_edited(cx)); - -// // Closing the item restores the window's edited state. -// let close = pane.update(cx, |pane, cx| { -// drop(editor); -// pane.close_active_item(&Default::default(), cx).unwrap() -// }); -// executor.run_until_parked(); - -// window.simulate_prompt_answer(1, cx); -// close.await.unwrap(); -// assert!(!window.is_edited(cx)); - -// // Opening the buffer again doesn't impact the window's edited state. -// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) -// .await -// .unwrap(); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert!(!window.is_edited(cx)); - -// // Editing the buffer marks the window as edited. -// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); -// assert!(window.is_edited(cx)); - -// // Ensure closing the window via the mouse gets preempted due to the -// // buffer having unsaved changes. -// assert!(!window.simulate_close(cx)); -// executor.run_until_parked(); -// assert_eq!(cx.windows().len(), 1); - -// // The window is successfully closed after the user dismisses the prompt. -// window.simulate_prompt_answer(1, cx); -// executor.run_until_parked(); -// assert_eq!(cx.windows().len(), 0); -// } - -// #[gpui::test] -// async fn test_new_empty_workspace(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// cx.update(|cx| { -// open_new(&app_state, cx, |workspace, cx| { -// Editor::new_file(workspace, &Default::default(), cx) -// }) -// }) -// .await; - -// let window = cx -// .windows() -// .first() -// .unwrap() -// .downcast::() -// .unwrap(); -// let workspace = window.root(cx); - -// let editor = workspace.update(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// editor.update(cx, |editor, cx| { -// assert!(editor.text(cx).is_empty()); -// assert!(!editor.is_dirty(cx)); -// }); - -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); -// save_task.await.unwrap(); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "the-new-name"); -// }); -// } - -// #[gpui::test] -// async fn test_open_entry(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1", -// "file2": "contents 2", -// "file3": "contents 3", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// 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 -// let entry_1 = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap(); -// 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(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) -// .await -// .unwrap(); -// 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. -// let entry_1b = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap(); -// assert_eq!(entry_1.id(), entry_1b.id()); - -// 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(cx, |w, cx| { -// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx); -// w.open_path(file2.clone(), None, true, cx) -// }) -// .await -// .unwrap(); - -// workspace.read_with(cx, |w, cx| { -// assert_eq!( -// w.active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .project_path(cx), -// Some(file2.clone()) -// ); -// }); - -// // Open the third entry twice concurrently. Only one pane item is added. -// let (t1, t2) = workspace.update(cx, |w, cx| { -// ( -// w.open_path(file3.clone(), None, true, cx), -// w.open_path(file3.clone(), None, true, cx), -// ) -// }); -// t1.await.unwrap(); -// t2.await.unwrap(); -// 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() -// .map(|i| i.project_path(cx).unwrap()) -// .collect::>(); -// assert_eq!(pane_entries, &[file1, file2, file3]); -// }); -// } - -// #[gpui::test] -// async fn test_open_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/", -// json!({ -// "dir1": { -// "a.txt": "" -// }, -// "dir2": { -// "b.txt": "" -// }, -// "dir3": { -// "c.txt": "" -// }, -// "d.txt": "" -// }), -// ) -// .await; - -// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); -// let workspace = cx.windows()[0].downcast::().unwrap().root(cx); - -// #[track_caller] -// fn assert_project_panel_selection( -// workspace: &Workspace, -// expected_worktree_path: &Path, -// expected_entry_path: &Path, -// cx: &AppContext, -// ) { -// let project_panel = [ -// workspace.left_dock().read(cx).panel::(), -// workspace.right_dock().read(cx).panel::(), -// workspace.bottom_dock().read(cx).panel::(), -// ] -// .into_iter() -// .find_map(std::convert::identity) -// .expect("found no project panels") -// .read(cx); -// let (selected_worktree, selected_entry) = project_panel -// .selected_entry(cx) -// .expect("project panel should have a selected entry"); -// assert_eq!( -// selected_worktree.abs_path().as_ref(), -// expected_worktree_path, -// "Unexpected project panel selected worktree path" -// ); -// assert_eq!( -// selected_entry.path.as_ref(), -// expected_entry_path, -// "Unexpected project panel selected entry path" -// ); -// } - -// // Open a file within an existing worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/dir1/a.txt".into()], true, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "a.txt" -// ); -// }); - -// // Open a file outside of any existing worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/dir2/b.txt".into()], true, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); -// let worktree_roots = workspace -// .worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "b.txt" -// ); -// }); - -// // Ensure opening a directory and one of its children only adds one worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); -// let worktree_roots = workspace -// .worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt", "/dir3"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "c.txt" -// ); -// }); - -// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/d.txt".into()], false, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); -// let worktree_roots = workspace -// .worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); - -// let visible_worktree_roots = workspace -// .visible_worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// visible_worktree_roots, -// vec!["/dir1", "/dir2/b.txt", "/dir3"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); - -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "d.txt" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_opening_excluded_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, |project_settings| { -// project_settings.file_scan_exclusions = -// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); -// }); -// }); -// }); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// ".gitignore": "ignored_dir\n", -// ".git": { -// "HEAD": "ref: refs/heads/main", -// }, -// "regular_dir": { -// "file": "regular file contents", -// }, -// "ignored_dir": { -// "ignored_subdir": { -// "file": "ignored subfile contents", -// }, -// "file": "ignored file contents", -// }, -// "excluded_dir": { -// "file": "excluded file contents", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let paths_to_open = [ -// Path::new("/root/excluded_dir/file").to_path_buf(), -// Path::new("/root/.git/HEAD").to_path_buf(), -// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), -// ]; -// let (opened_workspace, new_items) = cx -// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) -// .await -// .unwrap(); - -// assert_eq!( -// opened_workspace.id(), -// workspace.id(), -// "Excluded files in subfolders of a workspace root should be opened in the workspace" -// ); -// let mut opened_paths = cx.read(|cx| { -// assert_eq!( -// new_items.len(), -// paths_to_open.len(), -// "Expect to get the same number of opened items as submitted paths to open" -// ); -// new_items -// .iter() -// .zip(paths_to_open.iter()) -// .map(|(i, path)| { -// match i { -// Some(Ok(i)) => { -// Some(i.project_path(cx).map(|p| p.path.display().to_string())) -// } -// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), -// None => None, -// } -// .flatten() -// }) -// .collect::>() -// }); -// opened_paths.sort(); -// assert_eq!( -// opened_paths, -// vec![ -// None, -// Some(".git/HEAD".to_string()), -// Some("excluded_dir/file".to_string()), -// ], -// "Excluded files should get opened, excluded dir should not get opened" -// ); - -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// assert_eq!( -// initial_entries, entries, -// "Workspace entries should not change after opening excluded files and directories paths" -// ); - -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// let mut opened_buffer_paths = pane -// .items() -// .map(|i| { -// i.project_path(cx) -// .expect("all excluded files that got open should have a path") -// .path -// .display() -// .to_string() -// }) -// .collect::>(); -// opened_buffer_paths.sort(); -// assert_eq!( -// opened_buffer_paths, -// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], -// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_save_conflicting_item(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "a.txt": "" })) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // Open a file within an existing worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx) -// }) -// .await; -// let editor = cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// let item = pane.active_item().unwrap(); -// item.downcast::().unwrap() -// }); - -// editor.update(cx, |editor, cx| editor.handle_input("x", cx)); -// app_state -// .fs -// .as_fake() -// .insert_file("/root/a.txt", "changed".to_string()) -// .await; -// editor -// .condition(cx, |editor, cx| editor.has_conflict(cx)) -// .await; -// cx.read(|cx| assert!(editor.is_dirty(cx))); - -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// cx.foreground().run_until_parked(); -// window.simulate_prompt_answer(0, cx); -// save_task.await.unwrap(); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert!(!editor.has_conflict(cx)); -// }); -// } - -// #[gpui::test] -// async fn test_open_and_save_new_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(rust_lang())); -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); - -// // Create a new untitled buffer -// cx.dispatch_action(window.into(), NewFile); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// editor.update(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "untitled"); -// assert!(Arc::ptr_eq( -// &editor.language_at(0, cx).unwrap(), -// &languages::PLAIN_TEXT -// )); -// editor.handle_input("hi", cx); -// assert!(editor.is_dirty(cx)); -// }); - -// // Save the buffer. This prompts for a filename. -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// cx.foreground().run_until_parked(); -// 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.read(cx).title(cx), "untitled"); -// }); - -// // When the save completes, the buffer's title is updated and the language is assigned based -// // on the path. -// save_task.await.unwrap(); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "the-new-name.rs"); -// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust"); -// }); - -// // Edit the file and save it again. This time, there is no filename prompt. -// editor.update(cx, |editor, cx| { -// editor.handle_input(" there", cx); -// assert!(editor.is_dirty(cx)); -// }); -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// save_task.await.unwrap(); -// assert!(!cx.did_prompt_for_new_path()); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(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.into(), NewFile); -// workspace -// .update(cx, |workspace, cx| { -// workspace.split_and_clone( -// workspace.active_pane().clone(), -// SplitDirection::Right, -// cx, -// ); -// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx) -// }) -// .await -// .unwrap(); -// let editor2 = workspace.update(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// cx.read(|cx| { -// assert_eq!( -// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), -// editor.read(cx).buffer().read(cx).as_singleton().unwrap() -// ); -// }) -// } - -// #[gpui::test] -// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - -// let project = Project::test(app_state.fs.clone(), [], cx).await; -// project.update(cx, |project, _| project.languages().add(rust_lang())); -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // Create a new untitled buffer -// cx.dispatch_action(window.into(), NewFile); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// editor.update(cx, |editor, cx| { -// assert!(Arc::ptr_eq( -// &editor.language_at(0, cx).unwrap(), -// &languages::PLAIN_TEXT -// )); -// editor.handle_input("hi", cx); -// assert!(editor.is_dirty(cx)); -// }); - -// // Save the buffer. This prompts for a filename. -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); -// save_task.await.unwrap(); -// // The buffer is not dirty anymore and the language is assigned based on the path. -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust") -// }); -// } - -// #[gpui::test] -// async fn test_pane_actions(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1", -// "file2": "contents 2", -// "file3": "contents 3", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// 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(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap(); - -// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| { -// let editor = pane_1.active_item().unwrap().downcast::().unwrap(); -// assert_eq!(editor.project_path(cx), Some(file1.clone())); -// let buffer = editor.update(cx, |editor, cx| { -// editor.insert("dirt", cx); -// editor.buffer().downgrade() -// }); -// (editor.downgrade(), buffer) -// }); - -// cx.dispatch_action(window.into(), pane::SplitRight); -// let editor_2 = 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), Some(file1.clone())); - -// pane2_item.downcast::().unwrap().downgrade() -// }); -// cx.dispatch_action( -// window.into(), -// workspace::CloseActiveItem { save_intent: None }, -// ); - -// cx.foreground().run_until_parked(); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.panes().len(), 1); -// assert_eq!(workspace.active_pane(), &pane_1); -// }); - -// cx.dispatch_action( -// window.into(), -// workspace::CloseActiveItem { save_intent: None }, -// ); -// cx.foreground().run_until_parked(); -// window.simulate_prompt_answer(1, cx); -// cx.foreground().run_until_parked(); - -// workspace.read_with(cx, |workspace, cx| { -// assert_eq!(workspace.panes().len(), 1); -// assert!(workspace.active_item(cx).is_none()); -// }); - -// cx.assert_dropped(editor_1); -// cx.assert_dropped(editor_2); -// cx.assert_dropped(buffer); -// } - -// #[gpui::test] -// async fn test_navigation(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1\n".repeat(20), -// "file2": "contents 2\n".repeat(20), -// "file3": "contents 3\n".repeat(20), -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - -// 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(); - -// let editor1 = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// editor1.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { -// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)]) -// }); -// }); -// let editor2 = workspace -// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor3 = workspace -// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// editor3 -// .update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { -// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)]) -// }); -// editor.newline(&Default::default(), cx); -// editor.newline(&Default::default(), cx); -// editor.move_down(&Default::default(), cx); -// editor.move_down(&Default::default(), cx); -// editor.save(project.clone(), cx) -// }) -// .await -// .unwrap(); -// editor3.update(cx, |editor, cx| { -// editor.set_scroll_position(vec2f(0., 12.5), cx) -// }); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(16, 0), 12.5) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file2.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(10, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Go back one more time and ensure we don't navigate past the first item in the history. -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(10, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file2.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Go forward to an item that has been closed, ensuring it gets re-opened at the same -// // location. -// pane.update(cx, |pane, cx| { -// let editor3_id = editor3.id(); -// drop(editor3); -// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(16, 0), 12.5) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. -// pane.update(cx, |pane, cx| { -// let editor2_id = editor2.id(); -// drop(editor2); -// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// app_state -// .fs -// .remove_file(Path::new("/root/a/file2"), Default::default()) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(10, 0), 0.) -// ); -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Modify file to collapse multiple nav history entries into the same location. -// // Ensure we don't visit the same location twice when navigating. -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]) -// }) -// }); - -// for _ in 0..5 { -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) -// }); -// }); -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)]) -// }) -// }); -// } - -// editor1.update(cx, |editor, cx| { -// editor.transact(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)]) -// }); -// editor.insert("", cx); -// }) -// }); - -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) -// }) -// }); -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(2, 0), 0.) -// ); -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(3, 0), 0.) -// ); - -// fn active_location( -// workspace: &ViewHandle, -// cx: &mut TestAppContext, -// ) -> (ProjectPath, DisplayPoint, f32) { -// workspace.update(cx, |workspace, cx| { -// let item = workspace.active_item(cx).unwrap(); -// let editor = item.downcast::().unwrap(); -// let (selections, scroll_position) = editor.update(cx, |editor, cx| { -// ( -// editor.selections.display_ranges(cx), -// editor.scroll_position(cx), -// ) -// }); -// ( -// item.project_path(cx).unwrap(), -// selections[0].start, -// scroll_position.y(), -// ) -// }) -// } -// } - -// #[gpui::test] -// async fn test_reopening_closed_items(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "", -// "file2": "", -// "file3": "", -// "file4": "", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - -// 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(); -// let file4 = entries[3].clone(); - -// let file1_item_id = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// let file2_item_id = workspace -// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// let file3_item_id = workspace -// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// let file4_item_id = workspace -// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// // Close all the pane items in some arbitrary order. -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), None); - -// // Reopen all the closed items, ensuring they are reopened in the same order -// // in which they were closed. -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// // Reopening past the last closed item is a no-op. -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// // Reopening closed items doesn't interfere with navigation history. -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// fn active_path( -// workspace: &ViewHandle, -// cx: &TestAppContext, -// ) -> Option { -// workspace.read_with(cx, |workspace, cx| { -// let item = workspace.active_item(cx)?; -// item.project_path(cx) -// }) -// } -// } - -// #[gpui::test] -// async fn test_base_keymap(cx: &mut gpui::TestAppContext) { -// struct TestView; - -// impl Entity for TestView { -// type Event = (); -// } - -// impl View for TestView { -// fn ui_name() -> &'static str { -// "TestView" -// } - -// fn render(&mut self, _: &mut ViewContext) -> AnyElement { -// Empty::new().into_any() -// } -// } - -// let executor = cx.background(); -// let fs = FakeFs::new(executor.clone()); - -// actions!(test, [A, B]); -// // From the Atom keymap -// actions!(workspace, [ActivatePreviousPane]); -// // From the JetBrains keymap -// actions!(pane, [ActivatePrevItem]); - -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "Atom" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": "test::A" -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(Assets, cx); -// welcome::init(cx); - -// cx.add_global_action(|_: &A, _cx| {}); -// cx.add_global_action(|_: &B, _cx| {}); -// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); -// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); - -// let settings_rx = watch_config_file( -// executor.clone(), -// fs.clone(), -// PathBuf::from("/settings.json"), -// ); -// let keymap_rx = -// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); - -// handle_keymap_file_changes(keymap_rx, cx); -// handle_settings_file_changes(settings_rx, cx); -// }); - -// cx.foreground().run_until_parked(); - -// let window = cx.add_window(|_| TestView); - -// // Test loading the keymap base at all -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &A), ("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test modifying the users keymap, while retaining the base keymap -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": "test::B" -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &B), ("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test modifying the base, while retaining the users keymap -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "JetBrains" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &B), ("[", &ActivatePrevItem)], -// line!(), -// ); - -// #[track_caller] -// fn assert_key_bindings_for<'a>( -// window: AnyWindowHandle, -// cx: &TestAppContext, -// actions: Vec<(&'static str, &'a dyn Action)>, -// line: u32, -// ) { -// for (key, action) in actions { -// // assert that... -// assert!( -// cx.available_actions(window, 0) -// .into_iter() -// .any(|(_, bound_action, b)| { -// // action names match... -// bound_action.name() == action.name() -// && bound_action.namespace() == action.namespace() -// // and key strokes contain the given key -// && b.iter() -// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) -// }), -// "On {} Failed to find {} with key binding {}", -// line, -// action.name(), -// key -// ); -// } -// } -// } - -// #[gpui::test] -// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { -// struct TestView; - -// impl Entity for TestView { -// type Event = (); -// } - -// impl View for TestView { -// fn ui_name() -> &'static str { -// "TestView" -// } - -// fn render(&mut self, _: &mut ViewContext) -> AnyElement { -// Empty::new().into_any() -// } -// } - -// let executor = cx.background(); -// let fs = FakeFs::new(executor.clone()); - -// actions!(test, [A, B]); -// // From the Atom keymap -// actions!(workspace, [ActivatePreviousPane]); -// // From the JetBrains keymap -// actions!(pane, [ActivatePrevItem]); - -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "Atom" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": "test::A" -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(Assets, cx); -// welcome::init(cx); - -// cx.add_global_action(|_: &A, _cx| {}); -// cx.add_global_action(|_: &B, _cx| {}); -// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); -// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); - -// let settings_rx = watch_config_file( -// executor.clone(), -// fs.clone(), -// PathBuf::from("/settings.json"), -// ); -// let keymap_rx = -// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); - -// handle_keymap_file_changes(keymap_rx, cx); -// handle_settings_file_changes(settings_rx, cx); -// }); - -// cx.foreground().run_until_parked(); - -// let window = cx.add_window(|_| TestView); - -// // Test loading the keymap base at all -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &A), ("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test disabling the key binding for the base keymap -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": null -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test modifying the base, while retaining the users keymap -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "JetBrains" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!()); - -// #[track_caller] -// fn assert_key_bindings_for<'a>( -// window: AnyWindowHandle, -// cx: &TestAppContext, -// actions: Vec<(&'static str, &'a dyn Action)>, -// line: u32, -// ) { -// for (key, action) in actions { -// // assert that... -// assert!( -// cx.available_actions(window, 0) -// .into_iter() -// .any(|(_, bound_action, b)| { -// // action names match... -// bound_action.name() == action.name() -// && bound_action.namespace() == action.namespace() -// // and key strokes contain the given key -// && b.iter() -// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) -// }), -// "On {} Failed to find {} with key binding {}", -// line, -// action.name(), -// key -// ); -// } -// } -// } - -// #[gpui::test] -// fn test_bundled_settings_and_themes(cx: &mut AppContext) { -// cx.platform() -// .fonts() -// .add_fonts(&[ -// Assets -// .load("fonts/zed-sans/zed-sans-extended.ttf") -// .unwrap() -// .to_vec() -// .into(), -// Assets -// .load("fonts/zed-mono/zed-mono-extended.ttf") -// .unwrap() -// .to_vec() -// .into(), -// Assets -// .load("fonts/plex/IBMPlexSans-Regular.ttf") -// .unwrap() -// .to_vec() -// .into(), -// ]) -// .unwrap(); -// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); -// let mut settings = SettingsStore::default(); -// settings -// .set_default_settings(&settings::default_settings(), cx) -// .unwrap(); -// cx.set_global(settings); -// theme::init(Assets, cx); - -// let mut has_default_theme = false; -// for theme_name in themes.list(false).map(|meta| meta.name) { -// let theme = themes.get(&theme_name).unwrap(); -// assert_eq!(theme.meta.name, theme_name); -// if theme.meta.name == settings::get::(cx).theme.meta.name { -// has_default_theme = true; -// } -// } -// assert!(has_default_theme); -// } - -// #[gpui::test] -// fn test_bundled_languages(cx: &mut AppContext) { -// cx.set_global(SettingsStore::test(cx)); -// let mut languages = LanguageRegistry::test(); -// languages.set_executor(cx.background().clone()); -// let languages = Arc::new(languages); -// let node_runtime = node_runtime::FakeNodeRuntime::new(); -// languages::init(languages.clone(), node_runtime, cx); -// for name in languages.language_names() { -// languages.language_for_name(&name); -// } -// cx.foreground().run_until_parked(); -// } - -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// let mut app_state = AppState::test(cx); -// let state = Arc::get_mut(&mut app_state).unwrap(); -// state.initialize_workspace = initialize_workspace; -// state.build_window_options = build_window_options; -// theme::init((), cx); -// audio::init((), cx); -// channel::init(&app_state.client, app_state.user_store.clone(), cx); -// call::init(app_state.client.clone(), app_state.user_store.clone(), cx); -// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); -// workspace::init(app_state.clone(), cx); -// Project::init_settings(cx); -// language::init(cx); -// editor::init(cx); -// project_panel::init_settings(cx); -// collab_ui::init(&app_state, cx); -// pane::init(cx); -// project_panel::init((), cx); -// terminal_view::init(cx); -// assistant::init(cx); -// app_state -// }) -// } - -// fn rust_lang() -> Arc { -// Arc::new(language::Language::new( -// language::LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )) -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use assets::Assets; + use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor, EditorEvent}; + use gpui::{ + actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext, + VisualTestContext, WindowHandle, + }; + use language::LanguageRegistry; + use project::{project_settings::ProjectSettings, Project, ProjectPath}; + use serde_json::json; + use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + }; + use theme::{ThemeRegistry, ThemeSettings}; + use workspace::{ + item::{Item, ItemHandle}, + open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection, + WorkspaceHandle, + }; + + #[gpui::test] + async fn test_open_paths_action(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "aa": null, + "ab": null, + }, + "b": { + "ba": null, + "bb": null, + }, + "c": { + "ca": null, + "cb": null, + }, + "d": { + "da": null, + "db": null, + }, + }), + ) + .await; + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + let workspace_1 = cx + .read(|cx| cx.windows()[0].downcast::()) + .unwrap(); + workspace_1 + .update(cx, |workspace, cx| { + assert_eq!(workspace.worktrees(cx).count(), 2); + assert!(workspace.left_dock().read(cx).is_open()); + assert!(workspace + .active_pane() + .read(cx) + .focus_handle(cx) + .is_focused(cx)); + }) + .unwrap(); + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 2); + + // Replace existing windows + let window = cx + .update(|cx| cx.windows()[0].downcast::()) + .unwrap(); + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], + &app_state, + Some(window), + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 2); + let workspace_1 = cx + .update(|cx| cx.windows()[0].downcast::()) + .unwrap(); + workspace_1 + .update(cx, |workspace, cx| { + assert_eq!( + workspace + .worktrees(cx) + .map(|w| w.read(cx).abs_path()) + .collect::>(), + &[Path::new("/root/c").into(), Path::new("/root/d").into()] + ); + assert!(workspace.left_dock().read(cx).is_open()); + assert!(workspace.active_pane().focus_handle(cx).is_focused(cx)); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_window_edit_state(cx: &mut TestAppContext) { + let executor = cx.executor(); + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({"a": "hey"})) + .await; + + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + // When opening the workspace, the window is not in a edited state. + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.test_window(window.into()).edited() + }; + let pane = window + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + + assert!(!window_is_edited(window, cx)); + + // Editing a buffer marks the window as edited. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + + assert!(window_is_edited(window, cx)); + + // Undoing the edit restores the window's edited state. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); + }) + .unwrap(); + assert!(!window_is_edited(window, cx)); + + // Redoing the edit marks the window as edited again. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); + }) + .unwrap(); + assert!(window_is_edited(window, cx)); + + // Closing the item restores the window's edited state. + let close = window + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + drop(editor); + pane.close_active_item(&Default::default(), cx).unwrap() + }) + }) + .unwrap(); + executor.run_until_parked(); + + cx.simulate_prompt_answer(1); + close.await.unwrap(); + assert!(!window_is_edited(window, cx)); + + // Opening the buffer again doesn't impact the window's edited state. + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + assert!(!window_is_edited(window, cx)); + + // Editing the buffer marks the window as edited. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + assert!(window_is_edited(window, cx)); + + // Ensure closing the window via the mouse gets preempted due to the + // buffer having unsaved changes. + assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); + executor.run_until_parked(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + // The window is successfully closed after the user dismisses the prompt. + cx.simulate_prompt_answer(1); + executor.run_until_parked(); + assert_eq!(cx.update(|cx| cx.windows().len()), 0); + } + + #[gpui::test] + async fn test_new_empty_workspace(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + }) + .await; + + let workspace = cx + .update(|cx| cx.windows().first().unwrap().downcast::()) + .unwrap(); + + let editor = workspace + .update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + assert!(editor.text(cx).is_empty()); + assert!(!editor.is_dirty(cx)); + }); + + editor + }) + .unwrap(); + + let save_task = workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); + save_task.await.unwrap(); + workspace + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name"); + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_open_entry(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + 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 + let entry_1 = window + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + 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 + window + .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + 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. + let entry_1b = window + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + assert_eq!(entry_1.item_id(), entry_1b.item_id()); + + 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. + window + .update(cx, |w, cx| { + w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx); + w.open_path(file2.clone(), None, true, cx) + }) + .unwrap() + .await + .unwrap(); + + window + .read_with(cx, |w, cx| { + assert_eq!( + w.active_pane() + .read(cx) + .active_item() + .unwrap() + .project_path(cx), + Some(file2.clone()) + ); + }) + .unwrap(); + + // Open the third entry twice concurrently. Only one pane item is added. + let (t1, t2) = window + .update(cx, |w, cx| { + ( + w.open_path(file3.clone(), None, true, cx), + w.open_path(file3.clone(), None, true, cx), + ) + }) + .unwrap(); + t1.await.unwrap(); + t2.await.unwrap(); + 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() + .map(|i| i.project_path(cx).unwrap()) + .collect::>(); + assert_eq!(pane_entries, &[file1, file2, file3]); + }); + } + + #[gpui::test] + async fn test_open_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/", + json!({ + "dir1": { + "a.txt": "" + }, + "dir2": { + "b.txt": "" + }, + "dir3": { + "c.txt": "" + }, + "d.txt": "" + }), + ) + .await; + + cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let workspace = window.root(cx).unwrap(); + + #[track_caller] + fn assert_project_panel_selection( + workspace: &Workspace, + expected_worktree_path: &Path, + expected_entry_path: &Path, + cx: &AppContext, + ) { + let project_panel = [ + workspace.left_dock().read(cx).panel::(), + workspace.right_dock().read(cx).panel::(), + workspace.bottom_dock().read(cx).panel::(), + ] + .into_iter() + .find_map(std::convert::identity) + .expect("found no project panels") + .read(cx); + let (selected_worktree, selected_entry) = project_panel + .selected_entry(cx) + .expect("project panel should have a selected entry"); + assert_eq!( + selected_worktree.abs_path().as_ref(), + expected_worktree_path, + "Unexpected project panel selected worktree path" + ); + assert_eq!( + selected_entry.path.as_ref(), + expected_entry_path, + "Unexpected project panel selected entry path" + ); + } + + // Open a file within an existing worktree. + window + .update(cx, |view, cx| { + view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "a.txt" + ); + }); + + // Open a file outside of any existing worktree. + window + .update(cx, |view, cx| { + view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); + let worktree_roots = workspace + .worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt"] + .into_iter() + .map(Path::new) + .collect(), + ); + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "b.txt" + ); + }); + + // Ensure opening a directory and one of its children only adds one worktree. + window + .update(cx, |view, cx| { + view.open_paths( + vec!["/dir3".into(), "/dir3/c.txt".into()], + OpenVisible::All, + None, + cx, + ) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); + let worktree_roots = workspace + .worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt", "/dir3"] + .into_iter() + .map(Path::new) + .collect(), + ); + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "c.txt" + ); + }); + + // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. + window + .update(cx, |view, cx| { + view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); + let worktree_roots = workspace + .worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] + .into_iter() + .map(Path::new) + .collect(), + ); + + let visible_worktree_roots = workspace + .visible_worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + visible_worktree_roots, + vec!["/dir1", "/dir2/b.txt", "/dir3"] + .into_iter() + .map(Path::new) + .collect(), + ); + + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "d.txt" + ); + }); + } + + #[gpui::test] + async fn test_opening_excluded_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = + Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); + }); + }); + }); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + ".gitignore": "ignored_dir\n", + ".git": { + "HEAD": "ref: refs/heads/main", + }, + "regular_dir": { + "file": "regular file contents", + }, + "ignored_dir": { + "ignored_subdir": { + "file": "ignored subfile contents", + }, + "file": "ignored file contents", + }, + "excluded_dir": { + "file": "excluded file contents", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); + let paths_to_open = [ + Path::new("/root/excluded_dir/file").to_path_buf(), + Path::new("/root/.git/HEAD").to_path_buf(), + Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), + ]; + let (opened_workspace, new_items) = cx + .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) + .await + .unwrap(); + + assert_eq!( + opened_workspace.root_view(cx).unwrap().entity_id(), + workspace.entity_id(), + "Excluded files in subfolders of a workspace root should be opened in the workspace" + ); + let mut opened_paths = cx.read(|cx| { + assert_eq!( + new_items.len(), + paths_to_open.len(), + "Expect to get the same number of opened items as submitted paths to open" + ); + new_items + .iter() + .zip(paths_to_open.iter()) + .map(|(i, path)| { + match i { + Some(Ok(i)) => { + Some(i.project_path(cx).map(|p| p.path.display().to_string())) + } + Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), + None => None, + } + .flatten() + }) + .collect::>() + }); + opened_paths.sort(); + assert_eq!( + opened_paths, + vec![ + None, + Some(".git/HEAD".to_string()), + Some("excluded_dir/file".to_string()), + ], + "Excluded files should get opened, excluded dir should not get opened" + ); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + assert_eq!( + initial_entries, entries, + "Workspace entries should not change after opening excluded files and directories paths" + ); + + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let mut opened_buffer_paths = pane + .items() + .map(|i| { + i.project_path(cx) + .expect("all excluded files that got open should have a path") + .path + .display() + .to_string() + }) + .collect::>(); + opened_buffer_paths.sort(); + assert_eq!( + opened_buffer_paths, + vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], + "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" + ); + }); + } + + #[gpui::test] + async fn test_save_conflicting_item(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "a.txt": "" })) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + // Open a file within an existing worktree. + window + .update(cx, |view, cx| { + view.open_paths( + vec![PathBuf::from("/root/a.txt")], + OpenVisible::All, + None, + cx, + ) + }) + .unwrap() + .await; + let editor = cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let item = pane.active_item().unwrap(); + item.downcast::().unwrap() + }); + + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.handle_input("x", cx)); + }) + .unwrap(); + + app_state + .fs + .as_fake() + .insert_file("/root/a.txt", "changed".to_string()) + .await; + editor + .condition::(cx, |editor, cx| editor.has_conflict(cx)) + .await; + cx.read(|cx| assert!(editor.is_dirty(cx))); + + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_prompt_answer(0); + save_task.await.unwrap(); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert!(!editor.has_conflict(cx)); + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_open_and_save_new_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(rust_lang())); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap()); + + // Create a new untitled buffer + cx.dispatch_action(window.into(), NewFile); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "untitled"); + assert!(Arc::ptr_eq( + &editor.buffer().read(cx).language_at(0, cx).unwrap(), + &languages::PLAIN_TEXT + )); + editor.handle_input("hi", cx); + assert!(editor.is_dirty(cx)); + }); + }) + .unwrap(); + + // Save the buffer. This prompts for a filename. + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + 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.read(cx).title(cx), "untitled"); + }); + + // When the save completes, the buffer's title is updated and the language is assigned based + // on the path. + save_task.await.unwrap(); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name.rs"); + assert_eq!( + editor + .buffer() + .read(cx) + .language_at(0, cx) + .unwrap() + .name() + .as_ref(), + "Rust" + ); + }); + }) + .unwrap(); + + // Edit the file and save it again. This time, there is no filename prompt. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + editor.handle_input(" there", cx); + assert!(editor.is_dirty(cx)); + }); + }) + .unwrap(); + + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + save_task.await.unwrap(); + // todo!() po + //assert!(!cx.did_prompt_for_new_path()); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name.rs") + }); + }) + .unwrap(); + + // Open the same newly-created file in another pane item. The new editor should reuse + // the same buffer. + cx.dispatch_action(window.into(), NewFile); + window + .update(cx, |workspace, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + cx, + ); + workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap(); + let editor2 = window + .update(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + cx.read(|cx| { + assert_eq!( + editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), + editor.read(cx).buffer().read(cx).as_singleton().unwrap() + ); + }) + } + + #[gpui::test] + async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + + let project = Project::test(app_state.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(rust_lang())); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + + // Create a new untitled buffer + cx.dispatch_action(window.into(), NewFile); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(Arc::ptr_eq( + &editor.buffer().read(cx).language_at(0, cx).unwrap(), + &languages::PLAIN_TEXT + )); + editor.handle_input("hi", cx); + assert!(editor.is_dirty(cx)); + }); + }) + .unwrap(); + + // Save the buffer. This prompts for a filename. + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); + save_task.await.unwrap(); + // The buffer is not dirty anymore and the language is assigned based on the path. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!( + editor + .buffer() + .read(cx) + .language_at(0, cx) + .unwrap() + .name() + .as_ref(), + "Rust" + ) + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_pane_actions(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + 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()); + + window + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + + let (editor_1, buffer) = window + .update(cx, |_, cx| { + pane_1.update(cx, |pane_1, cx| { + let editor = pane_1.active_item().unwrap().downcast::().unwrap(); + assert_eq!(editor.project_path(cx), Some(file1.clone())); + let buffer = editor.update(cx, |editor, cx| { + editor.insert("dirt", cx); + editor.buffer().downgrade() + }); + (editor.downgrade(), buffer) + }) + }) + .unwrap(); + + cx.dispatch_action(window.into(), pane::SplitRight); + let editor_2 = 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), Some(file1.clone())); + + pane2_item.downcast::().unwrap().downgrade() + }); + cx.dispatch_action( + window.into(), + workspace::CloseActiveItem { save_intent: None }, + ); + + cx.background_executor.run_until_parked(); + window + .read_with(cx, |workspace, _| { + assert_eq!(workspace.panes().len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); + }) + .unwrap(); + + cx.dispatch_action( + window.into(), + workspace::CloseActiveItem { save_intent: None }, + ); + cx.background_executor.run_until_parked(); + cx.simulate_prompt_answer(1); + cx.background_executor.run_until_parked(); + + window + .read_with(cx, |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + assert!(workspace.active_item(cx).is_none()); + }) + .unwrap(); + editor_1.assert_dropped(); + editor_2.assert_dropped(); + buffer.assert_dropped(); + } + + #[gpui::test] + async fn test_navigation(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1\n".repeat(20), + "file2": "contents 2\n".repeat(20), + "file3": "contents 3\n".repeat(20), + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + + let editor1 = workspace + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_display_ranges( + [DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], + ) + }); + }); + }) + .unwrap(); + + let editor2 = workspace + .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let editor3 = workspace + .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + workspace + .update(cx, |_, cx| { + editor3.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_display_ranges( + [DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)], + ) + }); + editor.newline(&Default::default(), cx); + editor.newline(&Default::default(), cx); + editor.move_down(&Default::default(), cx); + editor.move_down(&Default::default(), cx); + editor.save(project.clone(), cx) + }) + }) + .unwrap() + .await + .unwrap(); + workspace + .update(cx, |_, cx| { + editor3.update(cx, |editor, cx| { + editor.set_scroll_position(point(0., 12.5), cx) + }); + }) + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(16, 0), 12.5) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file2.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(10, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Go back one more time and ensure we don't navigate past the first item in the history. + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(10, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file2.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Go forward to an item that has been closed, ensuring it gets re-opened at the same + // location. + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + let editor3_id = editor3.entity_id(); + drop(editor3); + pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(16, 0), 12.5) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + let editor2_id = editor2.entity_id(); + drop(editor2); + pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + app_state + .fs + .remove_file(Path::new("/root/a/file2"), Default::default()) + .await + .unwrap(); + cx.background_executor.run_until_parked(); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(10, 0), 0.) + ); + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Modify file to collapse multiple nav history entries into the same location. + // Ensure we don't visit the same location twice when navigating. + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges( + [DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], + ) + }) + }); + }) + .unwrap(); + for _ in 0..5 { + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0) + ]) + }); + }); + }) + .unwrap(); + + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0) + ]) + }) + }); + }) + .unwrap(); + } + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0) + ]) + }); + editor.insert("", cx); + }) + }); + }) + .unwrap(); + + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }) + }); + }) + .unwrap(); + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(2, 0), 0.) + ); + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(3, 0), 0.) + ); + + fn active_location( + workspace: &WindowHandle, + cx: &mut TestAppContext, + ) -> (ProjectPath, DisplayPoint, f32) { + workspace + .update(cx, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + let editor = item.downcast::().unwrap(); + let (selections, scroll_position) = editor.update(cx, |editor, cx| { + ( + editor.selections.display_ranges(cx), + editor.scroll_position(cx), + ) + }); + ( + item.project_path(cx).unwrap(), + selections[0].start, + scroll_position.y, + ) + }) + .unwrap() + } + } + + #[gpui::test] + async fn test_reopening_closed_items(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "", + "file2": "", + "file3": "", + "file4": "", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + let file4 = entries[3].clone(); + + let file1_item_id = workspace + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + let file2_item_id = workspace + .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + let file3_item_id = workspace + .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + let file4_item_id = workspace + .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + // Close all the pane items in some arbitrary order. + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + + assert_eq!(active_path(&workspace, cx), None); + + // Reopen all the closed items, ensuring they are reopened in the same order + // in which they were closed. + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + // Reopening past the last closed item is a no-op. + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + // Reopening closed items doesn't interfere with navigation history. + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + fn active_path( + workspace: &WindowHandle, + cx: &TestAppContext, + ) -> Option { + workspace + .read_with(cx, |workspace, cx| { + let item = workspace.active_item(cx)?; + item.project_path(cx) + }) + .unwrap() + } + } + fn init_keymap_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let app_state = AppState::test(cx); + + theme::init(theme::LoadThemes::JustBase, cx); + client::init(&app_state.client, cx); + language::init(cx); + workspace::init(app_state.clone(), cx); + welcome::init(cx); + Project::init_settings(cx); + app_state + }) + } + #[gpui::test] + async fn test_base_keymap(cx: &mut gpui::TestAppContext) { + let executor = cx.executor(); + let app_state = init_keymap_test(cx); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + actions!(test1, [A, B]); + // From the Atom keymap + use workspace::ActivatePreviousPane; + // From the JetBrains keymap + use workspace::ActivatePrevItem; + + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test1::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + executor.run_until_parked(); + cx.update(|cx| { + let settings_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/keymap.json"), + ); + handle_settings_file_changes(settings_rx, cx); + handle_keymap_file_changes(keymap_rx, cx); + }); + workspace + .update(cx, |workspace, _| { + workspace.register_action(|_, _: &A, _cx| {}); + workspace.register_action(|_, _: &B, _cx| {}); + workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {}); + workspace.register_action(|_, _: &ActivatePrevItem, _cx| {}); + }) + .unwrap(); + executor.run_until_parked(); + // Test loading the keymap base at all + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the users keymap, while retaining the base keymap + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test1::B" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &B), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the base, while retaining the users keymap + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &B), ("[", &ActivatePrevItem)], + line!(), + ); + } + + #[gpui::test] + async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { + let executor = cx.executor(); + let app_state = init_keymap_test(cx); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + actions!(test2, [A, B]); + // From the Atom keymap + use workspace::ActivatePreviousPane; + // From the JetBrains keymap + use pane::ActivatePrevItem; + workspace + .update(cx, |workspace, _| { + workspace + .register_action(|_, _: &A, _| {}) + .register_action(|_, _: &B, _| {}); + }) + .unwrap(); + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test2::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.update(|cx| { + let settings_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/keymap.json"), + ); + + handle_settings_file_changes(settings_rx, cx); + handle_keymap_file_changes(keymap_rx, cx); + }); + + cx.background_executor.run_until_parked(); + + cx.background_executor.run_until_parked(); + // Test loading the keymap base at all + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test disabling the key binding for the base keymap + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": null + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the base, while retaining the users keymap + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("[", &ActivatePrevItem)], + line!(), + ); + } + + #[gpui::test] + fn test_bundled_settings_and_themes(cx: &mut AppContext) { + cx.text_system() + .add_fonts(&[ + Assets + .load("fonts/zed-sans/zed-sans-extended.ttf") + .unwrap() + .to_vec() + .into(), + Assets + .load("fonts/zed-mono/zed-mono-extended.ttf") + .unwrap() + .to_vec() + .into(), + Assets + .load("fonts/plex/IBMPlexSans-Regular.ttf") + .unwrap() + .to_vec() + .into(), + ]) + .unwrap(); + let themes = ThemeRegistry::default(); + let mut settings = SettingsStore::default(); + settings + .set_default_settings(&settings::default_settings(), cx) + .unwrap(); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + + let mut has_default_theme = false; + for theme_name in themes.list(false).map(|meta| meta.name) { + let theme = themes.get(&theme_name).unwrap(); + assert_eq!(theme.name, theme_name); + if theme.name == ThemeSettings::get(None, cx).active_theme.name { + has_default_theme = true; + } + } + assert!(has_default_theme); + } + + #[gpui::test] + fn test_bundled_languages(cx: &mut AppContext) { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.background_executor().clone()); + let languages = Arc::new(languages); + let node_runtime = node_runtime::FakeNodeRuntime::new(); + languages::init(languages.clone(), node_runtime, cx); + for name in languages.language_names() { + languages.language_for_name(&name); + } + cx.background_executor().run_until_parked(); + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let mut app_state = AppState::test(cx); + + let state = Arc::get_mut(&mut app_state).unwrap(); + + state.build_window_options = build_window_options; + theme::init(theme::LoadThemes::JustBase, cx); + audio::init((), cx); + channel::init(&app_state.client, app_state.user_store.clone(), cx); + call::init(app_state.client.clone(), app_state.user_store.clone(), cx); + notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + language::init(cx); + editor::init(cx); + project_panel::init_settings(cx); + collab_ui::init(&app_state, cx); + project_panel::init((), cx); + terminal_view::init(cx); + assistant::init(cx); + initialize_workspace(app_state.clone(), cx); + app_state + }) + } + + fn rust_lang() -> Arc { + Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )) + } + #[track_caller] + fn assert_key_bindings_for<'a>( + window: AnyWindowHandle, + cx: &TestAppContext, + actions: Vec<(&'static str, &'a dyn Action)>, + line: u32, + ) { + let available_actions = cx + .update(|cx| window.update(cx, |_, cx| cx.available_actions())) + .unwrap(); + for (key, action) in actions { + let bindings = cx + .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action))) + .unwrap(); + // assert that... + assert!( + available_actions.iter().any(|bound_action| { + // actions match... + bound_action.partial_eq(action) + }), + "On {} Failed to find {}", + line, + action.name(), + ); + assert!( + // and key strokes contain the given key + bindings + .into_iter() + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), + "On {} Failed to find {} with key binding {}", + line, + action.name(), + key + ); + } + } +}