diff --git a/Cargo.lock b/Cargo.lock index acc7a56875..464824b939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3973,6 +3973,7 @@ dependencies = [ "menu", "picker", "project", + "serde", "serde_json", "settings", "text", diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 2fe030bf7b..9bcfb553e5 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -24,6 +24,7 @@ menu.workspace = true picker.workspace = true project.workspace = true settings.workspace = true +serde.workspace = true text.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 096e8c0eaa..8e07446802 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -3,13 +3,13 @@ mod file_finder_tests; mod new_path_prompt; -use collections::{HashMap, HashSet}; +use collections::{BTreeSet, HashMap}; use editor::{scroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, - Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View, - ViewContext, VisualContext, WeakView, + actions, impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, + FocusHandle, FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, + Styled, Task, View, ViewContext, VisualContext, WeakView, }; use itertools::Itertools; use new_path_prompt::NewPathPrompt; @@ -29,7 +29,14 @@ use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::{item::PreviewTabsSettings, ModalView, Workspace}; -actions!(file_finder, [Toggle, SelectPrev]); +actions!(file_finder, [SelectPrev]); +impl_actions!(file_finder, [Toggle]); + +#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)] +pub struct Toggle { + #[serde(default)] + pub separate_history: bool, +} impl ModalView for FileFinder {} @@ -45,9 +52,9 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &Toggle, cx| { + workspace.register_action(|workspace, action: &Toggle, cx| { let Some(file_finder) = workspace.active_modal::(cx) else { - Self::open(workspace, cx); + Self::open(workspace, action.separate_history, cx); return; }; @@ -60,7 +67,7 @@ impl FileFinder { }); } - fn open(workspace: &mut Workspace, cx: &mut ViewContext) { + fn open(workspace: &mut Workspace, separate_history: bool, cx: &mut ViewContext) { let project = workspace.project().read(cx); let currently_opened_path = workspace @@ -92,6 +99,7 @@ impl FileFinder { project, currently_opened_path, history_items, + separate_history, cx, ); @@ -161,6 +169,7 @@ pub struct FileFinderDelegate { has_changed_selected_index: bool, cancel_flag: Arc, history_items: Vec, + separate_history: bool, } /// Use a custom ordering for file finder: the regular one @@ -198,104 +207,117 @@ impl PartialOrd for ProjectPanelOrdMatch { #[derive(Debug, Default)] struct Matches { - history: Vec<(FoundPath, Option)>, - search: Vec, + separate_history: bool, + matches: Vec, } -#[derive(Debug)] -enum Match<'a> { - History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>), - Search(&'a ProjectPanelOrdMatch), +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +enum Match { + History(FoundPath, Option), + Search(ProjectPanelOrdMatch), } impl Matches { fn len(&self) -> usize { - self.history.len() + self.search.len() + self.matches.len() } - fn get(&self, index: usize) -> Option> { - if index < self.history.len() { - self.history - .get(index) - .map(|(path, path_match)| Match::History(path, path_match.as_ref())) - } else { - self.search - .get(index - self.history.len()) - .map(Match::Search) - } + fn get(&self, index: usize) -> Option<&Match> { + self.matches.get(index) } - fn push_new_matches( - &mut self, - history_items: &Vec, - currently_opened: Option<&FoundPath>, - query: &PathLikeWithPosition, + fn push_new_matches<'a>( + &'a mut self, + history_items: impl IntoIterator + Clone, + currently_opened: Option<&'a FoundPath>, + query: Option<&PathLikeWithPosition>, new_search_matches: impl Iterator, extend_old_matches: bool, ) { + let no_history_score = 0; let matching_history_paths = - matching_history_item_paths(history_items, currently_opened, query); + matching_history_item_paths(history_items.clone(), currently_opened, query); let new_search_matches = new_search_matches - .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path)); - - self.set_new_history( - currently_opened, - Some(&matching_history_paths), - history_items, - ); - if extend_old_matches { - self.search - .retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path)); - } else { - self.search.clear(); - } - util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a)); - } - - fn set_new_history<'a>( - &mut self, - currently_opened: Option<&'a FoundPath>, - query_matches: Option<&'a HashMap, ProjectPanelOrdMatch>>, - history_items: impl IntoIterator + 'a, - ) { - let mut processed_paths = HashSet::default(); - self.history = history_items + .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path)) + .map(Match::Search) + .map(|m| (no_history_score, m)); + let old_search_matches = self + .matches + .drain(..) + .filter(|_| extend_old_matches) + .filter(|m| matches!(m, Match::Search(_))) + .map(|m| (no_history_score, m)); + let history_matches = history_items .into_iter() .chain(currently_opened) - .filter(|&path| processed_paths.insert(path)) - .filter_map(|history_item| match &query_matches { - Some(query_matches) => Some(( - history_item.clone(), - Some(query_matches.get(&history_item.project.path)?.clone()), - )), - None => Some((history_item.clone(), None)), - }) .enumerate() - .sorted_by( - |(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match ( - Some(path_a) == currently_opened, - Some(path_b) == currently_opened, - ) { + .filter_map(|(i, history_item)| { + let query_match = matching_history_paths + .get(&history_item.project.path) + .cloned(); + let query_match = if query.is_some() { + query_match? + } else { + query_match.flatten() + }; + Some((i + 1, Match::History(history_item.clone(), query_match))) + }); + + let mut unique_matches = BTreeSet::new(); + self.matches = old_search_matches + .chain(history_matches) + .chain(new_search_matches) + .filter(|(_, m)| unique_matches.insert(m.clone())) + .sorted_by(|(history_score_a, a), (history_score_b, b)| { + match (a, b) { // bubble currently opened files to the top - (true, false) => cmp::Ordering::Less, - (false, true) => cmp::Ordering::Greater, - // arrange the files by their score (best score on top) and by their occurrence in the history - // (history items visited later are on the top) - _ => match_b.cmp(match_a).then(index_a.cmp(index_b)), - }, - ) - .map(|(_, paths)| paths) + (Match::History(path, _), _) if Some(path) == currently_opened => { + cmp::Ordering::Less + } + (_, Match::History(path, _)) if Some(path) == currently_opened => { + cmp::Ordering::Greater + } + + (Match::History(_, _), Match::Search(_)) if self.separate_history => { + cmp::Ordering::Less + } + (Match::Search(_), Match::History(_, _)) if self.separate_history => { + cmp::Ordering::Greater + } + + (Match::History(_, match_a), Match::History(_, match_b)) => match_b + .cmp(match_a) + .then(history_score_a.cmp(history_score_b)), + (Match::History(_, match_a), Match::Search(match_b)) => { + Some(match_b).cmp(&match_a.as_ref()) + } + (Match::Search(match_a), Match::History(_, match_b)) => { + match_b.as_ref().cmp(&Some(match_a)) + } + (Match::Search(match_a), Match::Search(match_b)) => match_b.cmp(match_a), + } + }) + .take(100) + .map(|(_, m)| m) .collect(); } } -fn matching_history_item_paths( - history_items: &Vec, - currently_opened: Option<&FoundPath>, - query: &PathLikeWithPosition, -) -> HashMap, ProjectPanelOrdMatch> { +fn matching_history_item_paths<'a>( + history_items: impl IntoIterator, + currently_opened: Option<&'a FoundPath>, + query: Option<&PathLikeWithPosition>, +) -> HashMap, Option> { + let Some(query) = query else { + return history_items + .into_iter() + .chain(currently_opened) + .map(|found_path| (Arc::clone(&found_path.project.path), None)) + .collect(); + }; + let history_items_by_worktrees = history_items - .iter() + .into_iter() .chain(currently_opened) .filter_map(|found_path| { let candidate = PathMatchCandidate { @@ -340,7 +362,7 @@ fn matching_history_item_paths( .map(|path_match| { ( Arc::clone(&path_match.path), - ProjectPanelOrdMatch(path_match), + Some(ProjectPanelOrdMatch(path_match)), ) }), ); @@ -399,6 +421,7 @@ impl FileFinderDelegate { project: Model, currently_opened_path: Option, history_items: Vec, + separate_history: bool, cx: &mut ViewContext, ) -> Self { Self::subscribe_to_updates(&project, cx); @@ -416,6 +439,7 @@ impl FileFinderDelegate { selected_index: 0, cancel_flag: Arc::new(AtomicBool::new(false)), history_items, + separate_history, } } @@ -510,7 +534,7 @@ impl FileFinderDelegate { self.matches.push_new_matches( &self.history_items, self.currently_opened_path.as_ref(), - &query, + Some(&query), matches.into_iter(), extend_old_matches, ); @@ -523,7 +547,7 @@ impl FileFinderDelegate { fn labels_for_match( &self, - path_match: Match, + path_match: &Match, cx: &AppContext, ix: usize, ) -> (String, Vec, String, Vec) { @@ -727,12 +751,21 @@ impl PickerDelegate for FileFinderDelegate { } fn separators_after_indices(&self) -> Vec { - let history_items = self.matches.history.len(); - if history_items == 0 || self.matches.search.is_empty() { - Vec::new() - } else { - vec![history_items - 1] + if self.separate_history { + let first_non_history_index = self + .matches + .matches + .iter() + .enumerate() + .find(|(_, m)| !matches!(m, Match::History(_, _))) + .map(|(i, _)| i); + if let Some(first_non_history_index) = first_non_history_index { + if first_non_history_index > 0 { + return vec![first_non_history_index - 1]; + } + } } + Vec::new() } fn update_matches( @@ -746,18 +779,20 @@ impl PickerDelegate for FileFinderDelegate { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); self.matches = Matches { - history: Vec::new(), - search: Vec::new(), + separate_history: self.separate_history, + ..Matches::default() }; - self.matches.set_new_history( - self.currently_opened_path.as_ref(), - None, + self.matches.push_new_matches( self.history_items.iter().filter(|history_item| { project .worktree_for_id(history_item.project.worktree_id, cx) .is_some() || (project.is_local() && history_item.absolute.is_some()) }), + self.currently_opened_path.as_ref(), + None, + None.into_iter(), + false, ); self.selected_index = 0; @@ -919,12 +954,23 @@ impl PickerDelegate for FileFinderDelegate { .get(ix) .expect("Invalid matches state: no element for index {ix}"); + let icon = match &path_match { + Match::History(_, _) => Icon::new(IconName::HistoryRerun) + .color(Color::Muted) + .size(IconSize::Small) + .into_any_element(), + Match::Search(_) => v_flex() + .flex_none() + .size(IconSize::Small.rems()) + .into_any_element(), + }; let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); Some( ListItem::new(ix) .spacing(ListItemSpacing::Sparse) + .end_slot::(Some(icon)) .inset(true) .selected(selected) .child( diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 5d03db8820..eaa591c84f 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -116,7 +116,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) { .await; picker.update(cx, |picker, _| { assert_eq!( - collect_search_matches(picker).search_only(), + collect_search_matches(picker).search_paths_only(), vec![PathBuf::from("a/b/file2.txt")], "Matching abs path should be the only match" ) @@ -138,7 +138,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) { .await; picker.update(cx, |picker, _| { assert_eq!( - collect_search_matches(picker).search_only(), + collect_search_matches(picker).search_paths_only(), Vec::::new(), "Mismatching abs path should produce no matches" ) @@ -171,7 +171,7 @@ async fn test_complex_path(cx: &mut TestAppContext) { picker.update(cx, |picker, _| { assert_eq!(picker.delegate.matches.len(), 1); assert_eq!( - collect_search_matches(picker).search_only(), + collect_search_matches(picker).search_paths_only(), vec![PathBuf::from("其他/S数据表格/task.xlsx")], ) }); @@ -369,12 +369,8 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) { }); picker.update(cx, |picker, cx| { + let matches = collect_search_matches(picker).search_matches_only(); 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. @@ -383,7 +379,10 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) { delegate.latest_search_id, true, // did-cancel query.clone(), - vec![matches[1].clone(), matches[3].clone()], + vec![ + ProjectPanelOrdMatch(matches[1].clone()), + ProjectPanelOrdMatch(matches[3].clone()), + ], cx, ); @@ -393,15 +392,20 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) { delegate.latest_search_id, true, // did-cancel query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + vec![ + ProjectPanelOrdMatch(matches[0].clone()), + ProjectPanelOrdMatch(matches[2].clone()), + ProjectPanelOrdMatch(matches[3].clone()), + ], cx, ); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" + assert_eq!( + collect_search_matches(picker) + .search_matches_only() + .as_slice(), + &matches[0..4] ); - assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); }); } @@ -480,15 +484,11 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) { 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(); + let matches = collect_search_matches(picker).search_matches_only(); assert_eq!(matches.len(), 1); let (file_name, file_name_positions, full_path, full_path_positions) = - delegate.labels_for_path_match(&matches[0].0); + 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, ""); @@ -552,15 +552,10 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) { }) .await; - finder.update(cx, |f, _| { - let delegate = &f.delegate; - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = &delegate.matches.search; - assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt")); - assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt")); + finder.update(cx, |picker, _| { + let matches = collect_search_matches(picker).search_paths_only(); + assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt")); + assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt")); }); } @@ -877,7 +872,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { 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); + cx.dispatch_action(Toggle::default()); let picker = active_file_picker(&workspace, cx); let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index()); assert_eq!( @@ -886,7 +881,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { ); } - cx.dispatch_action(Toggle); + cx.dispatch_action(Toggle::default()); let selected_index = workspace.update(cx, |workspace, cx| { workspace .active_modal::(cx) @@ -945,20 +940,19 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { 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( + finder.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); + let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying"); + assert_eq!(history_match, &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().0.path.as_ref(), Path::new("test/fourth.rs")); + assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); + assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs")); }); let second_query = "fsdasdsa"; @@ -968,14 +962,11 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { finder.delegate.update_matches(second_query.to_string(), cx) }) .await; - finder.update(cx, |finder, _| { - let delegate = &finder.delegate; + finder.update(cx, |picker, _| { assert!( - delegate.matches.history.is_empty(), - "No history entries should match {second_query}" - ); - assert!( - delegate.matches.search.is_empty(), + collect_search_matches(picker) + .search_paths_only() + .is_empty(), "No search entries should match {second_query}" ); }); @@ -990,20 +981,19 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { .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( + finder.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(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 = matches.history_found_paths.first().expect("Should have path matches for history items after querying"); + assert_eq!(history_match, &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().0.path.as_ref(), Path::new("test/fourth.rs")); + assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); + assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs")); }); } @@ -1139,6 +1129,9 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( assert_eq!(finder.delegate.matches.len(), 5); assert_match_at_position(finder, 0, "main.rs"); assert_match_selection(finder, 1, "bar.rs"); + assert_match_at_position(finder, 2, "lib.rs"); + assert_match_at_position(finder, 3, "moo.rs"); + assert_match_at_position(finder, 4, "maaa.rs"); }); // main.rs is not among matches, select top item @@ -1150,6 +1143,7 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( picker.update(cx, |finder, _| { assert_eq!(finder.delegate.matches.len(), 2); assert_match_at_position(finder, 0, "bar.rs"); + assert_match_at_position(finder, 1, "lib.rs"); }); // main.rs is back, put it on top and select next item @@ -1162,6 +1156,7 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( assert_eq!(finder.delegate.matches.len(), 3); assert_match_at_position(finder, 0, "main.rs"); assert_match_selection(finder, 1, "moo.rs"); + assert_match_at_position(finder, 2, "maaa.rs"); }); // get back to the initial state @@ -1174,6 +1169,99 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( assert_eq!(finder.delegate.matches.len(), 3); assert_match_selection(finder, 0, "main.rs"); assert_match_at_position(finder, 1, "lib.rs"); + assert_match_at_position(finder, 2, "bar.rs"); + }); +} + +#[gpui::test] +async fn test_non_separate_history_items(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "bar.rs": "// Bar file", + "lib.rs": "// Lib file", + "maaa.rs": "// Maaaaaaa", + "main.rs": "// Main file", + "moo.rs": "// Moooooo", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; + open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; + open_queried_buffer("main", 1, "main.rs", &workspace, cx).await; + + cx.dispatch_action(Toggle::default()); + let picker = active_file_picker(&workspace, cx); + // main.rs is on top, previously used is selected + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 3); + assert_match_selection(finder, 0, "main.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + assert_match_at_position(finder, 2, "bar.rs"); + }); + + // all files match, main.rs is still on top, but the second item is selected + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches(".rs".to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 5); + assert_match_at_position(finder, 0, "main.rs"); + assert_match_selection(finder, 1, "moo.rs"); + assert_match_at_position(finder, 2, "bar.rs"); + assert_match_at_position(finder, 3, "lib.rs"); + assert_match_at_position(finder, 4, "maaa.rs"); + }); + + // main.rs is not among matches, select top item + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches("b".to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "bar.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + }); + + // main.rs is back, put it on top and select next item + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches("m".to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 3); + assert_match_at_position(finder, 0, "main.rs"); + assert_match_selection(finder, 1, "moo.rs"); + assert_match_at_position(finder, 2, "maaa.rs"); + }); + + // get back to the initial state + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches("".to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 3); + assert_match_selection(finder, 0, "main.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + assert_match_at_position(finder, 2, "bar.rs"); }); } @@ -1266,19 +1354,8 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo 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.0.path.to_path_buf()) - .collect::>(); + finder.update(cx, |picker, _| { + let search_entries = collect_search_matches(picker).search_paths_only(); assert_eq!( search_entries, vec![ @@ -1321,15 +1398,9 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) 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").0.path.to_path_buf()) - .collect::>(); + picker.update(cx, |picker, _| { assert_eq!( - history_entries, + collect_search_matches(picker).history, vec![ PathBuf::from("test/first.rs"), PathBuf::from("test/third.rs"), @@ -1582,7 +1653,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav( // Back to navigation with initial shortcut // Open file on modifiers release cx.simulate_modifiers_change(Modifiers::secondary_key()); - cx.dispatch_action(Toggle); + cx.dispatch_action(Toggle::default()); cx.simulate_modifiers_change(Modifiers::none()); cx.read(|cx| { let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); @@ -1776,7 +1847,9 @@ fn open_file_picker( workspace: &View, cx: &mut VisualTestContext, ) -> View> { - cx.dispatch_action(Toggle); + cx.dispatch_action(Toggle { + separate_history: true, + }); active_file_picker(workspace, cx) } @@ -1795,15 +1868,17 @@ fn active_file_picker( }) } -#[derive(Debug)] +#[derive(Debug, Default)] struct SearchEntries { history: Vec, + history_found_paths: Vec, search: Vec, + search_matches: Vec, } impl SearchEntries { #[track_caller] - fn search_only(self) -> Vec { + fn search_paths_only(self) -> Vec { assert!( self.history.is_empty(), "Should have no history matches, but got: {:?}", @@ -1811,35 +1886,50 @@ impl SearchEntries { ); self.search } + + #[track_caller] + fn search_matches_only(self) -> Vec { + assert!( + self.history.is_empty(), + "Should have no history matches, but got: {:?}", + self.history + ); + self.search_matches + } } fn collect_search_matches(picker: &Picker) -> SearchEntries { - let matches = &picker.delegate.matches; - SearchEntries { - history: matches - .history - .iter() - .map(|(history_path, path_match)| { - path_match - .as_ref() - .map(|path_match| { - Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path) - }) - .unwrap_or_else(|| { - history_path - .absolute - .as_deref() - .unwrap_or_else(|| &history_path.project.path) - .to_path_buf() - }) - }) - .collect(), - search: matches - .search - .iter() - .map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)) - .collect(), + let mut search_entries = SearchEntries::default(); + for m in &picker.delegate.matches.matches { + match m { + Match::History(history_path, path_match) => { + search_entries.history.push( + path_match + .as_ref() + .map(|path_match| { + Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path) + }) + .unwrap_or_else(|| { + history_path + .absolute + .as_deref() + .unwrap_or_else(|| &history_path.project.path) + .to_path_buf() + }), + ); + search_entries + .history_found_paths + .push(history_path.clone()); + } + Match::Search(path_match) => { + search_entries + .search + .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)); + search_entries.search_matches.push(path_match.0.clone()); + } + } } + search_entries } #[track_caller] diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 66f473d829..f264d796a9 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -42,13 +42,9 @@ const MIN_INPUT_WIDTH_REMS: f32 = 10.; const MAX_INPUT_WIDTH_REMS: f32 = 30.; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; -const fn true_value() -> bool { - true -} - #[derive(PartialEq, Clone, Deserialize)] pub struct Deploy { - #[serde(default = "true_value")] + #[serde(default = "util::serde::default_true")] pub focus: bool, #[serde(default)] pub replace_enabled: bool, diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 3e715a6163..963948d207 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -138,7 +138,7 @@ pub fn app_menus() -> Vec> { MenuItem::separator(), MenuItem::action("Command Palette...", command_palette::Toggle), MenuItem::separator(), - MenuItem::action("Go to File...", file_finder::Toggle), + MenuItem::action("Go to File...", file_finder::Toggle::default()), // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), MenuItem::action("Go to Symbol in Editor...", outline::Toggle), MenuItem::action("Go to Line/Column...", go_to_line::Toggle),