diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index a3594c0818..05bee6bd78 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -403,7 +403,7 @@ mod tests { let palette = workspace.update(cx, |workspace, cx| { workspace - .current_modal::(cx) + .active_modal::(cx) .unwrap() .read(cx) .picker @@ -426,7 +426,7 @@ mod tests { cx.simulate_keystrokes("enter"); workspace.update(cx, |workspace, cx| { - assert!(workspace.current_modal::(cx).is_none()); + assert!(workspace.active_modal::(cx).is_none()); assert_eq!(editor.read(cx).text(cx), "ab") }); @@ -443,7 +443,7 @@ mod tests { let palette = workspace.update(cx, |workspace, cx| { workspace - .current_modal::(cx) + .active_modal::(cx) .unwrap() .read(cx) .picker diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index aae3bca160..57fc60a025 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -34,7 +34,7 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &Toggle, cx| { - let Some(file_finder) = workspace.current_modal::(cx) else { + let Some(file_finder) = workspace.active_modal::(cx) else { Self::open(workspace, cx); return; }; @@ -738,1236 +738,1103 @@ 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::{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; -// cx.executor().run_until_parked(); -// 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) -// } -// } +#[cfg(test)] +mod tests { + use std::{assert_eq, 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; + + cx.simulate_input("bna"); + + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!(active_editor.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)); + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + let editor = cx.update(|cx| workspace.read(cx).active_item_as::(cx).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)); + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + let editor = cx.update(|cx| workspace.read(cx).active_item_as::(cx).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, 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().as_u64() as usize) + }); + + // 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 = ProjectPath { + worktree_id, + path: Arc::from(Path::new("/root/dir2/b.txt")), + }; + workspace + .update(cx, |workspace, cx| { + workspace.open_path(b_path, None, true, cx) + }) + .await + .unwrap(); + let finder = open_file_picker(&workspace, cx); + finder + .update(cx, |f, cx| { + f.delegate.spawn_search(test_path_like("a.txt"), cx) + }) + .await; + + finder.update(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 (picker, _workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + picker + .update(cx, |f, cx| { + f.delegate.spawn_search(test_path_like("dir"), cx) + }) + .await; + cx.read(|cx| { + let finder = picker.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].entity_id().as_u64() as usize) + }); + + // 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. + workspace.update(cx, |_, cx| dbg!(cx.focused())); + + 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].entity_id().as_u64() as usize) + }); + 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().as_u64() as usize != worktree_id.to_usize() + }) + .expect("New worktree should have a different id") + .entity_id() + .as_u64() as usize, + ) + }); + cx.dispatch_action(workspace::CloseActiveItem { save_intent: None }); + + 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; + cx.executor().run_until_parked(); + 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 picker = active_file_picker(&workspace, cx); + let selected_index = picker.update(cx, |picker, _| picker.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 + .active_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().as_u64() as usize) + }); + + // 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; + + let finder = open_file_picker(&workspace, cx); + let first_query = "f"; + finder + .update(cx, |finder, cx| { + finder.delegate.update_matches(first_query.to_string(), cx) + }) + .await; + finder.update(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 = active_file_picker(&workspace, cx); + 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 = active_file_picker(&workspace, cx); + finder + .update(cx, |finder, cx| { + finder + .delegate + .update_matches(first_query_again.to_string(), cx) + }) + .await; + finder.update(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; + + let finder = open_file_picker(&workspace, cx); + let query = "collab_ui"; + cx.simulate_input(query); + finder.update(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; + + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("rs"); + + picker.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 { + let picker = open_file_picker(&workspace, cx); + cx.simulate_input(input); + + 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() + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + let active_editor_title = active_editor.read(cx).title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + cx.dispatch_action(workspace::CloseActiveItem { save_intent: None }); + + history_items + } + + 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 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)); + let picker = open_file_picker(&workspace, &mut cx); + (picker, workspace, cx) + } + + #[track_caller] + fn open_file_picker( + workspace: &View, + cx: &mut VisualTestContext, + ) -> View> { + cx.dispatch_action(Toggle); + active_file_picker(workspace, cx) + } + + #[track_caller] + fn active_file_picker( + workspace: &View, + cx: &mut VisualTestContext, + ) -> View> { + workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }) + } +} diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 5397a2214d..7eb10c675f 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -225,6 +225,9 @@ impl TestAppContext { self.background_executor.run_until_parked() } + /// simulate_keystrokes takes a space-separated list of keys to type. + /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") + /// will run backspace on the current editor through the command palette. pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { for keystroke in keystrokes .split(" ") @@ -237,6 +240,17 @@ impl TestAppContext { self.background_executor.run_until_parked() } + /// simulate_input takes a string of text to type. + /// cx.simulate_input("abc") + /// will type abc into your current editor. + pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { + for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { + self.dispatch_keystroke(window, keystroke.into(), false); + } + + self.background_executor.run_until_parked() + } + pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -455,6 +469,10 @@ impl<'a> VisualTestContext<'a> { pub fn simulate_keystrokes(&mut self, keystrokes: &str) { self.cx.simulate_keystrokes(self.window, keystrokes) } + + pub fn simulate_input(&mut self, input: &str) { + self.cx.simulate_input(self.window, input) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index c91df732c7..cd5995d65e 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -72,7 +72,7 @@ impl ModalLayer { cx.notify(); } - pub fn current_modal(&self) -> Option> + pub fn active_modal(&self) -> Option> where V: 'static, { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 67ecc16165..668ce2f207 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -8,8 +8,8 @@ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, - EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, - WeakView, WindowContext, + EventEmitter, FocusHandle, Focusable, Model, PromptLevel, Render, Task, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -1017,7 +1017,11 @@ impl Pane { .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); let should_activate = activate_pane || self.has_focus(cx); - self.activate_item(index_to_activate, should_activate, should_activate, cx); + if self.items.len() == 1 && should_activate { + self.focus_handle.focus(cx); + } else { + self.activate_item(index_to_activate, should_activate, should_activate, cx); + } } let item = self.items.remove(item_index); @@ -1913,11 +1917,12 @@ impl Pane { // } impl Render for Pane { - type Element = Div; + type Element = Focusable>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() .key_context("Pane") + .track_focus(&self.focus_handle) .size_full() .on_action(|pane: &mut Self, action, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 13997e7588..6c2d0c0ede 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,9 +38,9 @@ use futures::{ use gpui::{ actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, - EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, - Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render, + Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -433,7 +433,6 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, - focus_handle: FocusHandle, workspace_actions: Vec) -> Div>>, zoomed: Option, zoomed_position: Option, @@ -651,7 +650,6 @@ impl Workspace { cx.defer(|this, cx| this.update_window_title(cx)); Workspace { weak_self: weak_handle.clone(), - focus_handle: cx.focus_handle(), zoomed: None, zoomed_position: None, center: PaneGroup::new(center_pane.clone()), @@ -1450,6 +1448,11 @@ impl Workspace { self.active_pane().read(cx).active_item() } + pub fn active_item_as(&self, cx: &AppContext) -> Option> { + let item = self.active_item(cx)?; + item.to_any().downcast::().ok() + } + fn active_project_path(&self, cx: &ViewContext) -> Option { self.active_item(cx).and_then(|item| item.project_path(cx)) } @@ -1570,7 +1573,7 @@ impl Workspace { } if focus_center { - cx.focus(&self.focus_handle); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } cx.notify(); @@ -1704,7 +1707,7 @@ impl Workspace { } if focus_center { - cx.focus(&self.focus_handle); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } if self.zoomed_position != dock_to_reveal { @@ -3475,8 +3478,8 @@ impl Workspace { div } - pub fn current_modal(&mut self, cx: &ViewContext) -> Option> { - self.modal_layer.read(cx).current_modal() + pub fn active_modal(&mut self, cx: &ViewContext) -> Option> { + self.modal_layer.read(cx).active_modal() } pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B)