diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 638ed33891..a68825fa77 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1448,6 +1448,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 13296887cb..c460cac252 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -32,9 +32,9 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - dbg!("yay"); + dbg!("REGISTERING"); workspace.register_action(|workspace, _: &Toggle, cx| { - dbg!("yayer"); + dbg!("CALLING ACTION"); let Some(file_finder) = workspace.current_modal::(cx) else { Self::open(workspace, cx); return; @@ -738,1459 +738,1236 @@ impl PickerDelegate for FileFinderDelegate { } } -// #[cfg(test)] -// mod tests { -// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - -// use super::*; -// use editor::Editor; -// use gpui::{TestAppContext, ViewHandle}; -// use menu::{Confirm, SelectNext}; -// use serde_json::json; -// use workspace::{AppState, Workspace}; - -// #[ctor::ctor] -// fn init_logger() { -// if std::env::var("RUST_LOG").is_ok() { -// env_logger::init(); -// } -// } - -// #[gpui::test] -// async fn test_matching_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "banana": "", -// "bandana": "", -// } -// }), -// ) -// .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); -// cx.dispatch_action(window.into(), Toggle); - -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches("bna".to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// assert_eq!(finder.delegate().matches.len(), 2); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window.into(), SelectNext); -// cx.dispatch_action(window.into(), Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// cx.read(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// assert_eq!( -// active_item -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "bandana" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// let first_file_name = "first.rs"; -// let first_file_contents = "// First Rust file"; -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// first_file_name: first_file_contents, -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// cx.dispatch_action(window.into(), Toggle); -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - -// let file_query = &first_file_name[..3]; -// let file_row = 1; -// let file_column = 3; -// assert!(file_column <= first_file_contents.len()); -// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(query_inside_file.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let finder = finder.delegate(); -// assert_eq!(finder.matches.len(), 1); -// let latest_search_query = finder -// .latest_search_query -// .as_ref() -// .expect("Finder should have a query after the update_matches call"); -// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); -// assert_eq!( -// latest_search_query.path_like.file_query_end, -// Some(file_query.len()) -// ); -// assert_eq!(latest_search_query.row, Some(file_row)); -// assert_eq!(latest_search_query.column, Some(file_column as u32)); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window.into(), SelectNext); -// cx.dispatch_action(window.into(), Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// let editor = cx.update(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// active_item.downcast::().unwrap() -// }); -// cx.foreground().advance_clock(Duration::from_secs(2)); -// cx.foreground().start_waiting(); -// cx.foreground().finish_waiting(); -// editor.update(cx, |editor, cx| { -// let all_selections = editor.selections.all_adjusted(cx); -// assert_eq!( -// all_selections.len(), -// 1, -// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" -// ); -// let caret_selection = all_selections.into_iter().next().unwrap(); -// assert_eq!(caret_selection.start, caret_selection.end, -// "Caret selection should have its start and end at the same position"); -// assert_eq!(file_row, caret_selection.start.row + 1, -// "Query inside file should get caret with the same focus row"); -// assert_eq!(file_column, caret_selection.start.column as usize + 1, -// "Query inside file should get caret with the same focus column"); -// }); -// } - -// #[gpui::test] -// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// let first_file_name = "first.rs"; -// let first_file_contents = "// First Rust file"; -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// first_file_name: first_file_contents, -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// cx.dispatch_action(window.into(), Toggle); -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - -// let file_query = &first_file_name[..3]; -// let file_row = 200; -// let file_column = 300; -// assert!(file_column > first_file_contents.len()); -// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(query_outside_file.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let finder = finder.delegate(); -// assert_eq!(finder.matches.len(), 1); -// let latest_search_query = finder -// .latest_search_query -// .as_ref() -// .expect("Finder should have a query after the update_matches call"); -// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); -// assert_eq!( -// latest_search_query.path_like.file_query_end, -// Some(file_query.len()) -// ); -// assert_eq!(latest_search_query.row, Some(file_row)); -// assert_eq!(latest_search_query.column, Some(file_column as u32)); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window.into(), SelectNext); -// cx.dispatch_action(window.into(), Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// let editor = cx.update(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// active_item.downcast::().unwrap() -// }); -// cx.foreground().advance_clock(Duration::from_secs(2)); -// cx.foreground().start_waiting(); -// cx.foreground().finish_waiting(); -// editor.update(cx, |editor, cx| { -// let all_selections = editor.selections.all_adjusted(cx); -// assert_eq!( -// all_selections.len(), -// 1, -// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" -// ); -// let caret_selection = all_selections.into_iter().next().unwrap(); -// assert_eq!(caret_selection.start, caret_selection.end, -// "Caret selection should have its start and end at the same position"); -// assert_eq!(0, caret_selection.start.row, -// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); -// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, -// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); -// }); -// } - -// #[gpui::test] -// async fn test_matching_cancellation(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/dir", -// json!({ -// "hello": "", -// "goodbye": "", -// "halogen-light": "", -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// let query = test_path_like("hi"); -// finder -// .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) -// .await; -// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); - -// finder.update(cx, |finder, cx| { -// let delegate = finder.delegate_mut(); -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); - -// // Simulate a search being cancelled after the time limit, -// // returning only a subset of the matches that would have been found. -// drop(delegate.spawn_search(query.clone(), cx)); -// delegate.set_search_matches( -// delegate.latest_search_id, -// true, // did-cancel -// query.clone(), -// vec![matches[1].clone(), matches[3].clone()], -// cx, -// ); - -// // Simulate another cancellation. -// drop(delegate.spawn_search(query.clone(), cx)); -// delegate.set_search_matches( -// delegate.latest_search_id, -// true, // did-cancel -// query.clone(), -// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], -// cx, -// ); - -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); -// }); -// } - -// #[gpui::test] -// async fn test_ignored_files(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/ancestor", -// json!({ -// ".gitignore": "ignored-root", -// "ignored-root": { -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }, -// "tracked-root": { -// ".gitignore": "height", -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }, -// }), -// ) -// .await; - -// let project = Project::test( -// app_state.fs.clone(), -// [ -// "/ancestor/tracked-root".as_ref(), -// "/ancestor/ignored-root".as_ref(), -// ], -// cx, -// ) -// .await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("hi"), cx) -// }) -// .await; -// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); -// } - -// #[gpui::test] -// async fn test_single_file_worktrees(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) -// .await; - -// let project = Project::test( -// app_state.fs.clone(), -// ["/root/the-parent-dir/the-file".as_ref()], -// cx, -// ) -// .await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// // Even though there is only one worktree, that worktree's filename -// // is included in the matching, because the worktree is a single file. -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("thf"), cx) -// }) -// .await; -// cx.read(|cx| { -// let finder = finder.read(cx); -// let delegate = finder.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); -// assert_eq!(matches.len(), 1); - -// let (file_name, file_name_positions, full_path, full_path_positions) = -// delegate.labels_for_path_match(&matches[0]); -// assert_eq!(file_name, "the-file"); -// assert_eq!(file_name_positions, &[0, 1, 4]); -// assert_eq!(full_path, "the-file"); -// assert_eq!(full_path_positions, &[0, 1, 4]); -// }); - -// // Since the worktree root is a file, searching for its name followed by a slash does -// // not match anything. -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("thf/"), cx) -// }) -// .await; -// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); -// } - -// #[gpui::test] -// async fn test_path_distance_ordering(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "dir1": { "a.txt": "" }, -// "dir2": { -// "a.txt": "", -// "b.txt": "" -// } -// }), -// ) -// .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 worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // When workspace has an active item, sort items which are closer to that item -// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt -// // so that one should be sorted earlier -// let b_path = Some(dummy_found_path(ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("/root/dir2/b.txt")), -// })); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// b_path, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) -// }) -// .await; - -// finder.read_with(cx, |f, _| { -// let delegate = f.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); -// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); -// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); -// }); -// } - -// #[gpui::test] -// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "dir1": {}, -// "dir2": { -// "dir3": {} -// } -// }), -// ) -// .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 finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); -// finder -// .update(cx, |f, cx| { -// f.delegate_mut().spawn_search(test_path_like("dir"), cx) -// }) -// .await; -// cx.read(|cx| { -// let finder = finder.read(cx); -// assert_eq!(finder.delegate().matches.len(), 0); -// }); -// } - -// #[gpui::test] -// async fn test_query_history( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // Open and close panels, getting their history items afterwards. -// // Ensure history items get populated with opened items, and items are kept in a certain order. -// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. -// // -// // TODO: without closing, the opened items do not propagate their history changes for some reason -// // it does work in real app though, only tests do not propagate. - -// let initial_history = open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert!( -// initial_history.is_empty(), -// "Should have no history before opening any files" -// ); - -// let history_after_first = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_first, -// vec![FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )], -// "Should show 1st opened item in the history when opening the 2nd item" -// ); - -// let history_after_second = open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_second, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ -// 2nd item should be the first in the history, as the last opened." -// ); - -// let history_after_third = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_third, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/third.rs")), -// }, -// Some(PathBuf::from("/src/test/third.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ -// 3rd item should be the first in the history, as the last opened." -// ); - -// let history_after_second_again = open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// history_after_second_again, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/third.rs")), -// }, -// Some(PathBuf::from("/src/test/third.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ -// 2nd item, as the last opened, 3rd item should go next as it was opened right before." -// ); -// } - -// #[gpui::test] -// async fn test_external_files_history( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/external-src", -// json!({ -// "test": { -// "third.rs": "// Third Rust file", -// "fourth.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// cx.update(|cx| { -// project.update(cx, |project, cx| { -// project.find_or_create_local_worktree("/external-src", false, cx) -// }) -// }) -// .detach(); -// deterministic.run_until_parked(); - -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1,); - -// WorktreeId::from_usize(worktrees[0].id()) -// }); -// workspace -// .update(cx, |workspace, cx| { -// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) -// }) -// .detach(); -// deterministic.run_until_parked(); -// let external_worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!( -// worktrees.len(), -// 2, -// "External file should get opened in a new worktree" -// ); - -// WorktreeId::from_usize( -// worktrees -// .into_iter() -// .find(|worktree| worktree.id() != worktree_id.to_usize()) -// .expect("New worktree should have a different id") -// .id(), -// ) -// }); -// close_active_item(&workspace, &deterministic, cx).await; - -// let initial_history_items = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// initial_history_items, -// vec![FoundPath::new( -// ProjectPath { -// worktree_id: external_worktree_id, -// path: Arc::from(Path::new("")), -// }, -// Some(PathBuf::from("/external-src/test/third.rs")) -// )], -// "Should show external file with its full path in the history after it was open" -// ); - -// let updated_history_items = open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// assert_eq!( -// updated_history_items, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id: external_worktree_id, -// path: Arc::from(Path::new("")), -// }, -// Some(PathBuf::from("/external-src/test/third.rs")) -// ), -// ], -// "Should keep external file with history updates", -// ); -// } - -// #[gpui::test] -// async fn test_toggle_panel_new_selections( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// let current_history = open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// for expected_selected_index in 0..current_history.len() { -// cx.dispatch_action(window.into(), Toggle); -// let selected_index = cx.read(|cx| { -// workspace -// .read(cx) -// .modal::() -// .unwrap() -// .read(cx) -// .delegate() -// .selected_index() -// }); -// assert_eq!( -// selected_index, expected_selected_index, -// "Should select the next item in the history" -// ); -// } - -// cx.dispatch_action(window.into(), Toggle); -// let selected_index = cx.read(|cx| { -// workspace -// .read(cx) -// .modal::() -// .unwrap() -// .read(cx) -// .delegate() -// .selected_index() -// }); -// assert_eq!( -// selected_index, 0, -// "Should wrap around the history and start all over" -// ); -// } - -// #[gpui::test] -// async fn test_search_preserves_history_items( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// "fourth.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1,); - -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// cx.dispatch_action(window.into(), Toggle); -// let first_query = "f"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(first_query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); -// let history_match = delegate.matches.history.first().unwrap(); -// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); -// assert_eq!(history_match.0, FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )); -// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); -// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); -// }); - -// let second_query = "fsdasdsa"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(second_query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "No history entries should match {second_query}" -// ); -// assert!( -// delegate.matches.search.is_empty(), -// "No search entries should match {second_query}" -// ); -// }); - -// let first_query_again = first_query; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate_mut() -// .update_matches(first_query_again.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); -// let history_match = delegate.matches.history.first().unwrap(); -// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); -// assert_eq!(history_match.0, FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )); -// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); -// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); -// }); -// } - -// #[gpui::test] -// async fn test_history_items_vs_very_good_external_match( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "collab_ui": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// "collab_ui.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "sec", -// 1, -// "second.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// cx.dispatch_action(window.into(), Toggle); -// let query = "collab_ui"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches(query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// assert!( -// delegate.matches.history.is_empty(), -// "History items should not math query {query}, they should be matched by name only" -// ); - -// let search_entries = delegate -// .matches -// .search -// .iter() -// .map(|path_match| path_match.path.to_path_buf()) -// .collect::>(); -// assert_eq!( -// search_entries, -// vec![ -// PathBuf::from("collab_ui/collab_ui.rs"), -// PathBuf::from("collab_ui/third.rs"), -// PathBuf::from("collab_ui/first.rs"), -// PathBuf::from("collab_ui/second.rs"), -// ], -// "Despite all search results having the same directory name, the most matching one should be on top" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_nonexistent_history_items_not_shown( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "nonexistent.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// // generate some history to select from -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "non", -// 1, -// "nonexistent.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "thi", -// 1, -// "third.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; -// open_close_queried_buffer( -// "fir", -// 1, -// "first.rs", -// window.into(), -// &workspace, -// &deterministic, -// cx, -// ) -// .await; - -// cx.dispatch_action(window.into(), Toggle); -// let query = "rs"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches(query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = finder.delegate(); -// let history_entries = delegate -// .matches -// .history -// .iter() -// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) -// .collect::>(); -// assert_eq!( -// history_entries, -// vec![ -// PathBuf::from("test/first.rs"), -// PathBuf::from("test/third.rs"), -// ], -// "Should have all opened files in the history, except the ones that do not exist on disk" -// ); -// }); -// } - -// async fn open_close_queried_buffer( -// input: &str, -// expected_matches: usize, -// expected_editor_title: &str, -// window: gpui::AnyWindowHandle, -// workspace: &ViewHandle, -// deterministic: &gpui::executor::Deterministic, -// cx: &mut gpui::TestAppContext, -// ) -> Vec { -// cx.dispatch_action(window, Toggle); -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate_mut().update_matches(input.to_string(), cx) -// }) -// .await; -// let history_items = finder.read_with(cx, |finder, _| { -// assert_eq!( -// finder.delegate().matches.len(), -// expected_matches, -// "Unexpected number of matches found for query {input}" -// ); -// finder.delegate().history_items.clone() -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(window, SelectNext); -// cx.dispatch_action(window, Confirm); -// deterministic.run_until_parked(); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// cx.read(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// let active_editor_title = active_item -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx); -// assert_eq!( -// expected_editor_title, active_editor_title, -// "Unexpected editor title for query {input}" -// ); -// }); - -// close_active_item(workspace, deterministic, cx).await; - -// history_items -// } - -// async fn close_active_item( -// workspace: &ViewHandle, -// deterministic: &gpui::executor::Deterministic, -// cx: &mut TestAppContext, -// ) { -// let mut original_items = HashMap::new(); -// cx.read(|cx| { -// for pane in workspace.read(cx).panes() { -// let pane_id = pane.id(); -// let pane = pane.read(cx); -// let insertion_result = original_items.insert(pane_id, pane.items().count()); -// assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); -// } -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// active_pane -// .update(cx, |pane, cx| { -// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) -// .unwrap() -// }) -// .await -// .unwrap(); -// deterministic.run_until_parked(); -// cx.read(|cx| { -// for pane in workspace.read(cx).panes() { -// let pane_id = pane.id(); -// let pane = pane.read(cx); -// match original_items.remove(&pane_id) { -// Some(original_items) => { -// assert_eq!( -// pane.items().count(), -// original_items.saturating_sub(1), -// "Pane id {pane_id} should have item closed" -// ); -// } -// None => panic!("Pane id {pane_id} not found in original items"), -// } -// } -// }); -// assert!( -// original_items.len() <= 1, -// "At most one panel should got closed" -// ); -// } - -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.foreground_executor().forbid_parking(); -// cx.update(|cx| { -// let state = AppState::test(cx); -// theme::init(cx); -// language::init(cx); -// super::init(cx); -// editor::init(cx); -// workspace::init_settings(cx); -// Project::init_settings(cx); -// state -// }) -// } - -// fn test_path_like(test_str: &str) -> PathLikeWithPosition { -// PathLikeWithPosition::parse_str(test_str, |path_like_str| { -// Ok::<_, std::convert::Infallible>(FileSearchQuery { -// raw_query: test_str.to_owned(), -// file_query_end: if path_like_str == test_str { -// None -// } else { -// Some(path_like_str.len()) -// }, -// }) -// }) -// .unwrap() -// } - -// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { -// FoundPath { -// project: project_path, -// absolute: None, -// } -// } -// } +#[cfg(test)] +mod tests { + use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + + use super::*; + use editor::Editor; + use gpui::{Entity, TestAppContext, VisualTestContext}; + use menu::{Confirm, SelectNext}; + use serde_json::json; + use workspace::{AppState, Workspace}; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[gpui::test] + async fn test_matching_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "banana": "", + "bandana": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.update_matches("bna".to_string(), cx) + }) + .await; + + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + assert_eq!( + active_item + .to_any() + .downcast::() + .unwrap() + .read(cx) + .title(cx), + "bandana" + ); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let file_query = &first_file_name[..3]; + let file_row = 1; + let file_column = 3; + assert!(file_column <= first_file_contents.len()); + let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); + picker + .update(cx, |finder, cx| { + finder + .delegate + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let finder = &finder.delegate; + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.executor().advance_clock(Duration::from_secs(2)); + + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(file_row, caret_selection.start.row + 1, + "Query inside file should get caret with the same focus row"); + assert_eq!(file_column, caret_selection.start.column as usize + 1, + "Query inside file should get caret with the same focus column"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let file_query = &first_file_name[..3]; + let file_row = 200; + let file_column = 300; + assert!(file_column > first_file_contents.len()); + let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert_eq!(delegate.matches.len(), 1); + let latest_search_query = delegate + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.executor().advance_clock(Duration::from_secs(2)); + + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(0, caret_selection.start.row, + "Excessive rows (as in query outside file borders) should get trimmed to last file row"); + assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, + "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/dir", + json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let query = test_path_like("hi"); + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(query.clone(), cx) + }) + .await; + + picker.update(cx, |picker, _cx| { + assert_eq!(picker.delegate.matches.len(), 5) + }); + + picker.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + + // Simulate a search being cancelled after the time limit, + // returning only a subset of the matches that would have been found. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], + cx, + ); + + // Simulate another cancellation. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + cx, + ); + + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); + }); + } + + #[gpui::test] + async fn test_ignored_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/ancestor", + json!({ + ".gitignore": "ignored-root", + "ignored-root": { + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + "tracked-root": { + ".gitignore": "height", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + "/ancestor/tracked-root".as_ref(), + "/ancestor/ignored-root".as_ref(), + ], + cx, + ) + .await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(test_path_like("hi"), cx) + }) + .await; + picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); + } + + // #[gpui::test] + // async fn test_single_file_worktrees(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) + // .await; + + // let project = Project::test( + // app_state.fs.clone(), + // ["/root/the-parent-dir/the-file".as_ref()], + // cx, + // ) + // .await; + + // let (picker, _, mut cx) = build_find_picker(project, cx); + // let cx = &mut cx; + + // // Even though there is only one worktree, that worktree's filename + // // is included in the matching, because the worktree is a single file. + // picker + // .update(cx, |picker, cx| { + // picker.delegate.spawn_search(test_path_like("thf"), cx) + // }) + // .await; + // cx.read(|cx| { + // let picker = picker.read(cx); + // let delegate = &picker.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "Search matches expected" + // ); + // let matches = delegate.matches.search.clone(); + // assert_eq!(matches.len(), 1); + + // let (file_name, file_name_positions, full_path, full_path_positions) = + // delegate.labels_for_path_match(&matches[0]); + // assert_eq!(file_name, "the-file"); + // assert_eq!(file_name_positions, &[0, 1, 4]); + // assert_eq!(full_path, "the-file"); + // assert_eq!(full_path_positions, &[0, 1, 4]); + // }); + + // // Since the worktree root is a file, searching for its name followed by a slash does + // // not match anything. + // picker + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("thf/"), cx) + // }) + // .await; + // picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); + // } + + // #[gpui::test] + // async fn test_path_distance_ordering(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/root", + // json!({ + // "dir1": { "a.txt": "" }, + // "dir2": { + // "a.txt": "", + // "b.txt": "" + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1); + // WorktreeId::from_usize(worktrees[0].id()) + // }); + + // // When workspace has an active item, sort items which are closer to that item + // // first when they have the same name. In this case, b.txt is closer to dir2's a.txt + // // so that one should be sorted earlier + // let b_path = Some(dummy_found_path(ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("/root/dir2/b.txt")), + // })); + // cx.dispatch_action(Toggle); + + // let finder = cx + // .add_window(|cx| { + // Picker::new( + // FileFinderDelegate::new( + // workspace.downgrade(), + // workspace.read(cx).project().clone(), + // b_path, + // Vec::new(), + // cx, + // ), + // cx, + // ) + // }) + // .root(cx); + + // finder + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("a.txt"), cx) + // }) + // .await; + + // finder.read_with(cx, |f, _| { + // let delegate = &f.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "Search matches expected" + // ); + // let matches = delegate.matches.search.clone(); + // assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); + // assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); + // }); + // } + + // #[gpui::test] + // async fn test_search_worktree_without_files(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/root", + // json!({ + // "dir1": {}, + // "dir2": { + // "dir3": {} + // } + // }), + // ) + // .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 finder = cx + // .add_window(|cx| { + // Picker::new( + // FileFinderDelegate::new( + // workspace.downgrade(), + // workspace.read(cx).project().clone(), + // None, + // Vec::new(), + // cx, + // ), + // cx, + // ) + // }) + // .root(cx); + // finder + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("dir"), cx) + // }) + // .await; + // cx.read(|cx| { + // let finder = finder.read(cx); + // assert_eq!(finder.delegate.matches.len(), 0); + // }); + // } + + // #[gpui::test] + // async fn test_query_history(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1); + // WorktreeId::from_usize(worktrees[0].id()) + // }); + + // // Open and close panels, getting their history items afterwards. + // // Ensure history items get populated with opened items, and items are kept in a certain order. + // // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. + // // + // // TODO: without closing, the opened items do not propagate their history changes for some reason + // // it does work in real app though, only tests do not propagate. + + // let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // assert!( + // initial_history.is_empty(), + // "Should have no history before opening any files" + // ); + + // let history_after_first = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // assert_eq!( + // history_after_first, + // vec![FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // )], + // "Should show 1st opened item in the history when opening the 2nd item" + // ); + + // let history_after_second = + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // assert_eq!( + // history_after_second, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // ), + // ], + // "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ + // 2nd item should be the first in the history, as the last opened." + // ); + + // let history_after_third = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // assert_eq!( + // history_after_third, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/third.rs")), + // }, + // Some(PathBuf::from("/src/test/third.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // ), + // ], + // "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ + // 3rd item should be the first in the history, as the last opened." + // ); + + // let history_after_second_again = + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // assert_eq!( + // history_after_second_again, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/third.rs")), + // }, + // Some(PathBuf::from("/src/test/third.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // ), + // ], + // "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ + // 2nd item, as the last opened, 3rd item should go next as it was opened right before." + // ); + // } + + // #[gpui::test] + // async fn test_external_files_history(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // } + // }), + // ) + // .await; + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/external-src", + // json!({ + // "test": { + // "third.rs": "// Third Rust file", + // "fourth.rs": "// Fourth Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // cx.update(|cx| { + // project.update(cx, |project, cx| { + // project.find_or_create_local_worktree("/external-src", false, cx) + // }) + // }) + // .detach(); + // cx.background_executor.run_until_parked(); + + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1,); + + // WorktreeId::from_usize(worktrees[0].id()) + // }); + // workspace + // .update(cx, |workspace, cx| { + // workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) + // }) + // .detach(); + // cx.background_executor.run_until_parked(); + // let external_worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!( + // worktrees.len(), + // 2, + // "External file should get opened in a new worktree" + // ); + + // WorktreeId::from_usize( + // worktrees + // .into_iter() + // .find(|worktree| worktree.entity_id() != worktree_id.to_usize()) + // .expect("New worktree should have a different id") + // .id(), + // ) + // }); + // close_active_item(&workspace, cx).await; + + // let initial_history_items = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // assert_eq!( + // initial_history_items, + // vec![FoundPath::new( + // ProjectPath { + // worktree_id: external_worktree_id, + // path: Arc::from(Path::new("")), + // }, + // Some(PathBuf::from("/external-src/test/third.rs")) + // )], + // "Should show external file with its full path in the history after it was open" + // ); + + // let updated_history_items = + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // assert_eq!( + // updated_history_items, + // vec![ + // FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/second.rs")), + // }, + // Some(PathBuf::from("/src/test/second.rs")) + // ), + // FoundPath::new( + // ProjectPath { + // worktree_id: external_worktree_id, + // path: Arc::from(Path::new("")), + // }, + // Some(PathBuf::from("/external-src/test/third.rs")) + // ), + // ], + // "Should keep external file with history updates", + // ); + // } + + #[gpui::test] + async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + let current_history = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + for expected_selected_index in 0..current_history.len() { + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .read(cx) + .delegate + .selected_index() + }); + assert_eq!( + selected_index, expected_selected_index, + "Should select the next item in the history" + ); + } + + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .read(cx) + .delegate + .selected_index() + }); + assert_eq!( + selected_index, 0, + "Should wrap around the history and start all over" + ); + } + + // #[gpui::test] + // async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // "fourth.rs": "// Fourth Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // let worktree_id = cx.read(|cx| { + // let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + // assert_eq!(worktrees.len(), 1,); + + // WorktreeId::from_usize(worktrees[0].entity_id()) + // }); + + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let first_query = "f"; + // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(first_query.to_string(), cx) + // }) + // .await; + // finder.read_with(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); + // let history_match = delegate.matches.history.first().unwrap(); + // assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + // assert_eq!(history_match.0, FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // )); + // assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); + // assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + // }); + + // let second_query = "fsdasdsa"; + // let finder = workspace.update(cx, |workspace, cx| { + // workspace + // .current_modal::(cx) + // .unwrap() + // .read(cx) + // .picker + // }); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(second_query.to_string(), cx) + // }) + // .await; + // finder.update(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "No history entries should match {second_query}" + // ); + // assert!( + // delegate.matches.search.is_empty(), + // "No search entries should match {second_query}" + // ); + // }); + + // let first_query_again = first_query; + + // let finder = workspace.update(cx, |workspace, cx| { + // workspace + // .current_modal::(cx) + // .unwrap() + // .read(cx) + // .picker + // }); + // finder + // .update(cx, |finder, cx| { + // finder + // .delegate + // .update_matches(first_query_again.to_string(), cx) + // }) + // .await; + // finder.read_with(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); + // let history_match = delegate.matches.history.first().unwrap(); + // assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + // assert_eq!(history_match.0, FoundPath::new( + // ProjectPath { + // worktree_id, + // path: Arc::from(Path::new("test/first.rs")), + // }, + // Some(PathBuf::from("/src/test/first.rs")) + // )); + // assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); + // assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + // }); + // } + + // #[gpui::test] + // async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "collab_ui": { + // "first.rs": "// First Rust file", + // "second.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // "collab_ui.rs": "// Fourth Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let query = "collab_ui"; + // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(query.to_string(), cx) + // }) + // .await; + // finder.read_with(cx, |finder, _| { + // let delegate = &finder.delegate; + // assert!( + // delegate.matches.history.is_empty(), + // "History items should not math query {query}, they should be matched by name only" + // ); + + // let search_entries = delegate + // .matches + // .search + // .iter() + // .map(|path_match| path_match.path.to_path_buf()) + // .collect::>(); + // assert_eq!( + // search_entries, + // vec![ + // PathBuf::from("collab_ui/collab_ui.rs"), + // PathBuf::from("collab_ui/third.rs"), + // PathBuf::from("collab_ui/first.rs"), + // PathBuf::from("collab_ui/second.rs"), + // ], + // "Despite all search results having the same directory name, the most matching one should be on top" + // ); + // }); + // } + + // #[gpui::test] + // async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { + // let app_state = init_test(cx); + + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/src", + // json!({ + // "test": { + // "first.rs": "// First Rust file", + // "nonexistent.rs": "// Second Rust file", + // "third.rs": "// Third Rust file", + // } + // }), + // ) + // .await; + + // let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let query = "rs"; + // let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.picker.update(cx, |picker, cx| { + // picker.delegate.update_matches(query.to_string(), cx) + // }) + // }) + // .await; + // finder.update(cx, |finder, _| { + // let history_entries = finder.delegate + // .matches + // .history + // .iter() + // .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + // .collect::>(); + // assert_eq!( + // history_entries, + // vec![ + // PathBuf::from("test/first.rs"), + // PathBuf::from("test/third.rs"), + // ], + // "Should have all opened files in the history, except the ones that do not exist on disk" + // ); + // }); + // } + + async fn open_close_queried_buffer( + input: &str, + expected_matches: usize, + expected_editor_title: &str, + workspace: &View, + cx: &mut gpui::VisualTestContext<'_>, + ) -> Vec { + cx.dispatch_action(Toggle); + let picker = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches(input.to_string(), cx) + }) + .await; + let history_items = picker.update(cx, |finder, _| { + assert_eq!( + finder.delegate.matches.len(), + expected_matches, + "Unexpected number of matches found for query {input}" + ); + finder.delegate.history_items.clone() + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + cx.background_executor.run_until_parked(); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + let active_editor_title = active_item + .to_any() + .downcast::() + .unwrap() + .read(cx) + .title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + close_active_item(workspace, cx).await; + + history_items + } + + async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { + let mut original_items = HashMap::new(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.entity_id(); + let pane = pane.read(cx); + let insertion_result = original_items.insert(pane_id, pane.items().count()); + assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); + } + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + active_pane + .update(cx, |pane, cx| { + pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) + .unwrap() + }) + .await + .unwrap(); + cx.background_executor.run_until_parked(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.entity_id(); + let pane = pane.read(cx); + match original_items.remove(&pane_id) { + Some(original_items) => { + assert_eq!( + pane.items().count(), + original_items.saturating_sub(1), + "Pane id {pane_id} should have item closed" + ); + } + None => panic!("Pane id {pane_id} not found in original items"), + } + } + }); + assert!( + original_items.len() <= 1, + "At most one panel should got closed" + ); + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + theme::init(cx); + language::init(cx); + super::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } + + fn test_path_like(test_str: &str) -> PathLikeWithPosition { + PathLikeWithPosition::parse_str(test_str, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: test_str.to_owned(), + file_query_end: if path_like_str == test_str { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .unwrap() + } + + fn dummy_found_path(project_path: ProjectPath) -> FoundPath { + FoundPath { + project: project_path, + absolute: None, + } + } + + fn build_find_picker( + project: Model, + cx: &mut TestAppContext, + ) -> ( + View>, + View, + VisualTestContext, + ) { + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(Toggle); + let picker = workspace.update(&mut cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + (picker, workspace, cx) + } +} diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 44c31bbd69..850ddd6c9a 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ - div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, - Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, - ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, - VisualContext, WindowContext, WindowHandle, WindowOptions, + div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, + BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TextStyle, + View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -83,8 +83,16 @@ impl TestAppContext { )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); + let cx = AppContext::new(platform, asset_source, http_client); + let lock = cx.borrow_mut(); + lock.push_text_style(crate::TextStyleRefinement { + font_family: "Helvetica".into(), + ..Default::default() + }); + drop(lock); + Self { - app: AppContext::new(platform, asset_source, http_client), + app: cx, background_executor, foreground_executor, dispatcher: dispatcher.clone(), @@ -199,6 +207,15 @@ impl TestAppContext { } } + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .unwrap() + } + pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -376,6 +393,13 @@ impl<'a> VisualTestContext<'a> { pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } + + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index efb586fe03..acbe851b4d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -422,8 +422,11 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { + dbg!("BEFORE FOCUS"); if let Some(focus_handle) = self.focused() { + dbg!("BEFORE DEFER", focus_handle.id); self.defer(move |cx| { + dbg!("AFTER DEFER"); if let Some(node_id) = cx .window .current_frame diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index db012da38b..247c738161 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + FocusHandle, FocusableKeyDispatch, GlobalPixels, KeyContext, Model, ModelContext, + ParentElement, Point, Render, Size, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3409,10 +3409,6 @@ impl Workspace { // }); } - // todo!() - // #[cfg(any(test, feature = "test-support"))] - // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { - // use node_runtime::FakeNodeRuntime; #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { use gpui::Context; @@ -3432,7 +3428,10 @@ impl Workspace { initialize_workspace: |_, _, _, _| Task::ready(Ok(())), node_runtime: FakeNodeRuntime::new(), }); - Self::new(0, project, app_state, cx) + let workspace = Self::new(0, project, app_state, cx); + dbg!(&workspace.focus_handle); + workspace.focus_handle.focus(cx); + workspace } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { @@ -3710,13 +3709,14 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); self.add_workspace_actions_listeners(div()) + .track_focus(&self.focus_handle) .context(context) .relative() .size_full()