From 57a95d17995a4dbf9f76d1d0906d220a0fa97ee2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Sep 2023 00:23:17 +0200 Subject: [PATCH] Preserve matching history items and their order --- Cargo.lock | 1 + crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 123 ++++++++++++++++++++++---- crates/fuzzy/src/fuzzy.rs | 4 +- crates/fuzzy/src/paths.rs | 38 ++++++++ 5 files changed, 151 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2576220b81..983f6946ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2610,6 +2610,7 @@ dependencies = [ name = "file_finder" version = "0.1.0" dependencies = [ + "collections", "ctor", "editor", "env_logger 0.9.3", diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 6f6be7427b..55e4304643 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] editor = { path = "../editor" } +collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 77493187d4..62abab0b9d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,5 +1,6 @@ +use collections::{HashMap, HashSet}; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; -use fuzzy::PathMatch; +use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, }; @@ -36,6 +37,10 @@ pub struct FileFinderDelegate { enum Matches { History(Vec), Search(Vec), + Mixed { + history: Vec, + search: Vec, + }, } #[derive(Debug)] @@ -49,6 +54,7 @@ impl Matches { match self { Self::History(items) => items.len(), Self::Search(items) => items.len(), + Self::Mixed { history, search } => history.len() + search.len(), } } @@ -56,6 +62,103 @@ impl Matches { match self { Self::History(items) => items.get(index).map(Match::History), Self::Search(items) => items.get(index).map(Match::Search), + Self::Mixed { history, search } => { + if index < history.len() { + history.get(index).map(Match::History) + } else { + search.get(index - history.len()).map(Match::Search) + } + } + } + } + + fn push_new_matches( + &mut self, + query: &PathLikeWithPosition, + mut new_search_matches: Vec, + extend_old_matches: bool, + ) { + match self { + Matches::Search(search_matches) => { + if extend_old_matches { + util::extend_sorted( + search_matches, + new_search_matches.into_iter(), + 100, + |a, b| b.cmp(a), + ) + } else { + *search_matches = new_search_matches; + } + return; + } + Matches::History(history_matches) => { + *self = Matches::Mixed { + history: std::mem::take(history_matches), + search: Vec::new(), + } + } + Matches::Mixed { .. } => {} + } + + if let Matches::Mixed { history, search } = self { + let history_paths = history + .iter() + .map(|h| &h.project.path) + .collect::>(); + new_search_matches.retain(|path_match| !history_paths.contains(&path_match.path)); + + if extend_old_matches { + util::extend_sorted(search, new_search_matches.into_iter(), 100, |a, b| b.cmp(a)) + } else { + let candidates_by_worktrees = history + .iter() + .map(|found_path| { + let path = &found_path.project.path; + let candidate = PathMatchCandidate { + path, + char_bag: CharBag::from_iter( + path.to_string_lossy().to_lowercase().chars(), + ), + }; + (found_path.project.worktree_id, candidate) + }) + .fold( + HashMap::default(), + |mut candidates, (worktree_id, new_candidate)| { + candidates + .entry(worktree_id) + .or_insert_with(Vec::new) + .push(new_candidate); + candidates + }, + ); + + let mut matching_history_paths = HashSet::default(); + for (worktree, candidates) in candidates_by_worktrees { + let max_results = candidates.len() + 1; + matching_history_paths.extend( + fuzzy::match_fixed_path_set( + candidates, + worktree.to_usize(), + query.path_like.path_query(), + false, + max_results, + ) + .into_iter() + .map(|path_match| path_match.path), + ); + } + + history.retain(|history_path| { + matching_history_paths.contains(&history_path.project.path) + }); + if history.is_empty() { + *self = Matches::Search(new_search_matches); + } else { + *search = new_search_matches; + } + } } } } @@ -271,24 +374,14 @@ impl FileFinderDelegate { ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; - if self.latest_search_did_cancel + let extend_old_matches = self.latest_search_did_cancel && Some(query.path_like.path_query()) == self .latest_search_query .as_ref() - .map(|query| query.path_like.path_query()) - { - match &mut self.matches { - Matches::History(_) => self.matches = Matches::Search(matches), - Matches::Search(search_matches) => { - util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| { - b.cmp(a) - }) - } - } - } else { - self.matches = Matches::Search(matches); - } + .map(|query| query.path_like.path_query()); + self.matches + .push_new_matches(&query, matches, extend_old_matches); self.latest_search_query = Some(query); self.latest_search_did_cancel = did_cancel; cx.notify(); diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 4968023644..b9595df61f 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -4,5 +4,7 @@ mod paths; mod strings; pub use char_bag::CharBag; -pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet}; +pub use paths::{ + match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet, +}; pub use strings::{match_strings, StringMatch, StringMatchCandidate}; diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index 1cb7174fcc..4eb31936a8 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -90,6 +90,44 @@ impl Ord for PathMatch { } } +pub fn match_fixed_path_set( + candidates: Vec, + worktree_id: usize, + query: &str, + smart_case: bool, + max_results: usize, +) -> Vec { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let mut matcher = Matcher::new( + &query, + &lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + + let mut results = Vec::new(); + matcher.match_candidates( + &[], + &[], + candidates.into_iter(), + &mut results, + &AtomicBool::new(false), + |candidate, score| PathMatch { + score, + worktree_id, + positions: Vec::new(), + path: candidate.path.clone(), + path_prefix: Arc::from(""), + distance_to_relative_ancestor: usize::MAX, + }, + ); + results +} + pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( candidate_sets: &'a [Set], query: &str,