Detailed changes
@@ -6183,6 +6183,7 @@ dependencies = [
"file_icons",
"futures 0.3.32",
"fuzzy",
+ "fuzzy_nucleo",
"gpui",
"menu",
"open_path_prompt",
@@ -6740,6 +6741,15 @@ dependencies = [
"thread_local",
]
+[[package]]
+name = "fuzzy_nucleo"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "nucleo",
+ "util",
+]
+
[[package]]
name = "gaoya"
version = "0.2.0"
@@ -11063,6 +11073,27 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "nucleo"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
+dependencies = [
+ "nucleo-matcher",
+ "parking_lot",
+ "rayon",
+]
+
+[[package]]
+name = "nucleo-matcher"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
+dependencies = [
+ "memchr",
+ "unicode-segmentation",
+]
+
[[package]]
name = "num"
version = "0.4.3"
@@ -13203,6 +13234,7 @@ dependencies = [
"fs",
"futures 0.3.32",
"fuzzy",
+ "fuzzy_nucleo",
"git",
"git2",
"git_hosting_providers",
@@ -78,6 +78,7 @@ members = [
"crates/fs",
"crates/fs_benchmarks",
"crates/fuzzy",
+ "crates/fuzzy_nucleo",
"crates/git",
"crates/git_graph",
"crates/git_hosting_providers",
@@ -325,6 +326,7 @@ file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
fs = { path = "crates/fs" }
fuzzy = { path = "crates/fuzzy" }
+fuzzy_nucleo = { path = "crates/fuzzy_nucleo" }
git = { path = "crates/git" }
git_graph = { path = "crates/git_graph" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
@@ -609,6 +611,7 @@ naga = { version = "29.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = "1.2.0"
nix = "0.29"
+nucleo = "0.5"
num-format = "0.4.4"
objc = "0.2"
objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] }
@@ -21,6 +21,7 @@ editor.workspace = true
file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
+fuzzy_nucleo.workspace = true
gpui.workspace = true
menu.workspace = true
open_path_prompt.workspace = true
@@ -9,7 +9,8 @@ use client::ChannelId;
use collections::HashMap;
use editor::Editor;
use file_icons::FileIcons;
-use fuzzy::{CharBag, PathMatch, PathMatchCandidate, StringMatch, StringMatchCandidate};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
@@ -663,15 +664,6 @@ impl Matches {
// For file-vs-file matches, use the existing detailed comparison.
if let (Some(a_panel), Some(b_panel)) = (a.panel_match(), b.panel_match()) {
- let a_in_filename = Self::is_filename_match(a_panel);
- let b_in_filename = Self::is_filename_match(b_panel);
-
- match (a_in_filename, b_in_filename) {
- (true, false) => return cmp::Ordering::Greater,
- (false, true) => return cmp::Ordering::Less,
- _ => {}
- }
-
return a_panel.cmp(b_panel);
}
@@ -691,32 +683,6 @@ impl Matches {
Match::CreateNew(_) => 0.0,
}
}
-
- /// Determines if the match occurred within the filename rather than in the path
- fn is_filename_match(panel_match: &ProjectPanelOrdMatch) -> bool {
- if panel_match.0.positions.is_empty() {
- return false;
- }
-
- if let Some(filename) = panel_match.0.path.file_name() {
- let path_str = panel_match.0.path.as_unix_str();
-
- if let Some(filename_pos) = path_str.rfind(filename)
- && panel_match.0.positions[0] >= filename_pos
- {
- let mut prev_position = panel_match.0.positions[0];
- for p in &panel_match.0.positions[1..] {
- if *p != prev_position + 1 {
- return false;
- }
- prev_position = *p;
- }
- return true;
- }
- }
-
- false
- }
}
fn matching_history_items<'a>(
@@ -731,25 +697,16 @@ fn matching_history_items<'a>(
let history_items_by_worktrees = history_items
.into_iter()
.chain(currently_opened)
- .filter_map(|found_path| {
+ .map(|found_path| {
let candidate = PathMatchCandidate {
is_dir: false, // You can't open directories as project items
path: &found_path.project.path,
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
// E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
// it would be shown first always, despite the latter being a better match.
- char_bag: CharBag::from_iter(
- found_path
- .project
- .path
- .file_name()?
- .to_string()
- .to_lowercase()
- .chars(),
- ),
};
candidates_paths.insert(&found_path.project, found_path);
- Some((found_path.project.worktree_id, candidate))
+ (found_path.project.worktree_id, candidate)
})
.fold(
HashMap::default(),
@@ -767,8 +724,9 @@ fn matching_history_items<'a>(
let worktree_root_name = worktree_name_by_id
.as_ref()
.and_then(|w| w.get(&worktree).cloned());
+
matching_history_paths.extend(
- fuzzy::match_fixed_path_set(
+ fuzzy_nucleo::match_fixed_path_set(
candidates,
worktree.to_usize(),
worktree_root_name,
@@ -778,6 +736,18 @@ fn matching_history_items<'a>(
path_style,
)
.into_iter()
+ // filter matches where at least one matched position is in filename portion, to prevent directory matches, nucleo scores them higher as history items are matched against their full path
+ .filter(|path_match| {
+ if let Some(filename) = path_match.path.file_name() {
+ let filename_start = path_match.path.as_unix_str().len() - filename.len();
+ path_match
+ .positions
+ .iter()
+ .any(|&pos| pos >= filename_start)
+ } else {
+ true
+ }
+ })
.filter_map(|path_match| {
candidates_paths
.remove_entry(&ProjectPath {
@@ -940,7 +910,7 @@ impl FileFinderDelegate {
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn_in(window, async move |picker, cx| {
- let matches = fuzzy::match_path_sets(
+ let matches = fuzzy_nucleo::match_path_sets(
candidate_sets.as_slice(),
query.path_query(),
&relative_to,
@@ -1452,7 +1422,6 @@ impl PickerDelegate for FileFinderDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
- let raw_query = raw_query.replace(' ', "");
let raw_query = raw_query.trim();
let raw_query = match &raw_query.get(0..2) {
@@ -4161,3 +4161,233 @@ async fn test_clear_navigation_history(cx: &mut TestAppContext) {
"Should have no history items after clearing"
);
}
+
+#[gpui::test]
+async fn test_order_independent_search(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "internal": {
+ "auth": {
+ "login.rs": "",
+ }
+ }
+ }),
+ )
+ .await;
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ // forward order
+ picker
+ .update_in(cx, |picker, window, cx| {
+ picker
+ .delegate
+ .spawn_search(test_path_position("auth internal"), window, cx)
+ })
+ .await;
+ picker.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_matches_only();
+ assert_eq!(matches.len(), 1);
+ assert_eq!(matches[0].path.as_unix_str(), "internal/auth/login.rs");
+ });
+
+ // reverse order should give same result
+ picker
+ .update_in(cx, |picker, window, cx| {
+ picker
+ .delegate
+ .spawn_search(test_path_position("internal auth"), window, cx)
+ })
+ .await;
+ picker.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_matches_only();
+ assert_eq!(matches.len(), 1);
+ assert_eq!(matches[0].path.as_unix_str(), "internal/auth/login.rs");
+ });
+}
+
+#[gpui::test]
+async fn test_filename_preferred_over_directory_match(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "crates": {
+ "settings_ui": {
+ "src": {
+ "pages": {
+ "audio_test_window.rs": "",
+ "audio_input_output_setup.rs": "",
+ }
+ }
+ },
+ "audio": {
+ "src": {
+ "audio_settings.rs": "",
+ }
+ }
+ }
+ }),
+ )
+ .await;
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ picker
+ .update_in(cx, |picker, window, cx| {
+ picker
+ .delegate
+ .spawn_search(test_path_position("settings audio"), window, cx)
+ })
+ .await;
+ picker.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_matches_only();
+ assert!(!matches.is_empty(),);
+ assert_eq!(
+ matches[0].path.as_unix_str(),
+ "crates/audio/src/audio_settings.rs"
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_start_of_word_preferred_over_scattered_match(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "crates": {
+ "livekit_client": {
+ "src": {
+ "livekit_client": {
+ "playback.rs": "",
+ }
+ }
+ },
+ "vim": {
+ "test_data": {
+ "test_record_replay_interleaved.json": "",
+ }
+ }
+ }
+ }),
+ )
+ .await;
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ picker
+ .update_in(cx, |picker, window, cx| {
+ picker
+ .delegate
+ .spawn_search(test_path_position("live pla"), window, cx)
+ })
+ .await;
+ picker.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_matches_only();
+ assert!(!matches.is_empty(),);
+ assert_eq!(
+ matches[0].path.as_unix_str(),
+ "crates/livekit_client/src/livekit_client/playback.rs",
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_exact_filename_stem_preferred(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "assets": {
+ "icons": {
+ "file_icons": {
+ "nix.svg": "",
+ }
+ }
+ },
+ "crates": {
+ "zed": {
+ "resources": {
+ "app-icon-nightly@2x.png": "",
+ "app-icon-preview@2x.png": "",
+ }
+ }
+ }
+ }),
+ )
+ .await;
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ picker
+ .update_in(cx, |picker, window, cx| {
+ picker
+ .delegate
+ .spawn_search(test_path_position("nix icon"), window, cx)
+ })
+ .await;
+ picker.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_matches_only();
+ assert!(!matches.is_empty(),);
+ assert_eq!(
+ matches[0].path.as_unix_str(),
+ "assets/icons/file_icons/nix.svg",
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_exact_filename_with_directory_token(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/src",
+ json!({
+ "crates": {
+ "agent_servers": {
+ "src": {
+ "acp.rs": "",
+ "agent_server.rs": "",
+ "custom.rs": "",
+ }
+ }
+ }
+ }),
+ )
+ .await;
+ let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ picker
+ .update_in(cx, |picker, window, cx| {
+ picker
+ .delegate
+ .spawn_search(test_path_position("acp server"), window, cx)
+ })
+ .await;
+ picker.update(cx, |picker, _| {
+ let matches = collect_search_matches(picker).search_matches_only();
+ assert!(!matches.is_empty(),);
+ assert_eq!(
+ matches[0].path.as_unix_str(),
+ "crates/agent_servers/src/acp.rs",
+ );
+ });
+}
@@ -0,0 +1,21 @@
+[package]
+name = "fuzzy_nucleo"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/fuzzy_nucleo.rs"
+doctest = false
+
+[dependencies]
+nucleo.workspace = true
+gpui.workspace = true
+util.workspace = true
+
+[dev-dependencies]
+util = {workspace = true, features = ["test-support"]}
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,5 @@
+mod matcher;
+mod paths;
+pub use paths::{
+ PathMatch, PathMatchCandidate, PathMatchCandidateSet, match_fixed_path_set, match_path_sets,
+};
@@ -0,0 +1,39 @@
+use std::sync::Mutex;
+
+static MATCHERS: Mutex<Vec<nucleo::Matcher>> = Mutex::new(Vec::new());
+
+pub const LENGTH_PENALTY: f64 = 0.01;
+
+pub fn get_matcher(config: nucleo::Config) -> nucleo::Matcher {
+ let mut matchers = MATCHERS.lock().unwrap();
+ match matchers.pop() {
+ Some(mut matcher) => {
+ matcher.config = config;
+ matcher
+ }
+ None => nucleo::Matcher::new(config),
+ }
+}
+
+pub fn return_matcher(matcher: nucleo::Matcher) {
+ MATCHERS.lock().unwrap().push(matcher);
+}
+
+pub fn get_matchers(n: usize, config: nucleo::Config) -> Vec<nucleo::Matcher> {
+ let mut matchers: Vec<_> = {
+ let mut pool = MATCHERS.lock().unwrap();
+ let available = pool.len().min(n);
+ pool.drain(..available)
+ .map(|mut matcher| {
+ matcher.config = config.clone();
+ matcher
+ })
+ .collect()
+ };
+ matchers.resize_with(n, || nucleo::Matcher::new(config.clone()));
+ matchers
+}
+
+pub fn return_matchers(mut matchers: Vec<nucleo::Matcher>) {
+ MATCHERS.lock().unwrap().append(&mut matchers);
+}
@@ -0,0 +1,352 @@
+use gpui::BackgroundExecutor;
+use std::{
+ cmp::Ordering,
+ sync::{
+ Arc,
+ atomic::{self, AtomicBool},
+ },
+};
+use util::{paths::PathStyle, rel_path::RelPath};
+
+use nucleo::Utf32Str;
+use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
+
+use crate::matcher::{self, LENGTH_PENALTY};
+
+#[derive(Clone, Debug)]
+pub struct PathMatchCandidate<'a> {
+ pub is_dir: bool,
+ pub path: &'a RelPath,
+}
+
+#[derive(Clone, Debug)]
+pub struct PathMatch {
+ pub score: f64,
+ pub positions: Vec<usize>,
+ pub worktree_id: usize,
+ pub path: Arc<RelPath>,
+ pub path_prefix: Arc<RelPath>,
+ pub is_dir: bool,
+ /// Number of steps removed from a shared parent with the relative path
+ /// Used to order closer paths first in the search list
+ pub distance_to_relative_ancestor: usize,
+}
+
+pub trait PathMatchCandidateSet<'a>: Send + Sync {
+ type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
+ fn id(&self) -> usize;
+ fn len(&self) -> usize;
+ fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+ fn root_is_file(&self) -> bool;
+ fn prefix(&self) -> Arc<RelPath>;
+ fn candidates(&'a self, start: usize) -> Self::Candidates;
+ fn path_style(&self) -> PathStyle;
+}
+
+impl PartialEq for PathMatch {
+ fn eq(&self, other: &Self) -> bool {
+ self.cmp(other).is_eq()
+ }
+}
+
+impl Eq for PathMatch {}
+
+impl PartialOrd for PathMatch {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for PathMatch {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.score
+ .partial_cmp(&other.score)
+ .unwrap_or(Ordering::Equal)
+ .then_with(|| self.worktree_id.cmp(&other.worktree_id))
+ .then_with(|| {
+ other
+ .distance_to_relative_ancestor
+ .cmp(&self.distance_to_relative_ancestor)
+ })
+ .then_with(|| self.path.cmp(&other.path))
+ }
+}
+
+fn make_atoms(query: &str, smart_case: bool) -> Vec<Atom> {
+ let case = if smart_case {
+ CaseMatching::Smart
+ } else {
+ CaseMatching::Ignore
+ };
+ query
+ .split_whitespace()
+ .map(|word| Atom::new(word, case, Normalization::Smart, AtomKind::Fuzzy, false))
+ .collect()
+}
+
+pub(crate) fn distance_between_paths(path: &RelPath, relative_to: &RelPath) -> usize {
+ let mut path_components = path.components();
+ let mut relative_components = relative_to.components();
+
+ while path_components
+ .next()
+ .zip(relative_components.next())
+ .map(|(path_component, relative_component)| path_component == relative_component)
+ .unwrap_or_default()
+ {}
+ path_components.count() + relative_components.count() + 1
+}
+
+fn get_filename_match_bonus(
+ candidate_buf: &str,
+ query_atoms: &[Atom],
+ matcher: &mut nucleo::Matcher,
+) -> f64 {
+ let filename = match std::path::Path::new(candidate_buf).file_name() {
+ Some(f) => f.to_str().unwrap_or(""),
+ None => return 0.0,
+ };
+ if filename.is_empty() || query_atoms.is_empty() {
+ return 0.0;
+ }
+ let mut buf = Vec::new();
+ let haystack = Utf32Str::new(filename, &mut buf);
+ let mut total_score = 0u32;
+ for atom in query_atoms {
+ if let Some(score) = atom.score(haystack, matcher) {
+ total_score = total_score.saturating_add(score as u32);
+ }
+ }
+ total_score as f64 / filename.len().max(1) as f64
+}
+struct Cancelled;
+
+fn path_match_helper<'a>(
+ matcher: &mut nucleo::Matcher,
+ atoms: &[Atom],
+ candidates: impl Iterator<Item = PathMatchCandidate<'a>>,
+ results: &mut Vec<PathMatch>,
+ worktree_id: usize,
+ path_prefix: &Arc<RelPath>,
+ root_is_file: bool,
+ relative_to: &Option<Arc<RelPath>>,
+ path_style: PathStyle,
+ cancel_flag: &AtomicBool,
+) -> Result<(), Cancelled> {
+ let mut candidate_buf = if !path_prefix.is_empty() && !root_is_file {
+ let mut s = path_prefix.display(path_style).to_string();
+ s.push_str(path_style.primary_separator());
+ s
+ } else {
+ String::new()
+ };
+ let path_prefix_len = candidate_buf.len();
+ let mut buf = Vec::new();
+ let mut matched_chars: Vec<u32> = Vec::new();
+ let mut atom_matched_chars = Vec::new();
+ for candidate in candidates {
+ buf.clear();
+ matched_chars.clear();
+ if cancel_flag.load(atomic::Ordering::Relaxed) {
+ return Err(Cancelled);
+ }
+
+ candidate_buf.truncate(path_prefix_len);
+ if root_is_file {
+ candidate_buf.push_str(path_prefix.as_unix_str());
+ } else {
+ candidate_buf.push_str(candidate.path.as_unix_str());
+ }
+
+ let haystack = Utf32Str::new(&candidate_buf, &mut buf);
+
+ let mut total_score: u32 = 0;
+ let mut all_matched = true;
+
+ for atom in atoms {
+ atom_matched_chars.clear();
+ if let Some(score) = atom.indices(haystack, matcher, &mut atom_matched_chars) {
+ total_score = total_score.saturating_add(score as u32);
+ matched_chars.extend_from_slice(&atom_matched_chars);
+ } else {
+ all_matched = false;
+ break;
+ }
+ }
+
+ if all_matched && !atoms.is_empty() {
+ matched_chars.sort_unstable();
+ matched_chars.dedup();
+
+ let length_penalty = candidate_buf.len() as f64 * LENGTH_PENALTY;
+ let filename_bonus = get_filename_match_bonus(&candidate_buf, atoms, matcher);
+ let adjusted_score = total_score as f64 + filename_bonus - length_penalty;
+ let mut positions: Vec<usize> = candidate_buf
+ .char_indices()
+ .enumerate()
+ .filter_map(|(char_offset, (byte_offset, _))| {
+ matched_chars
+ .contains(&(char_offset as u32))
+ .then_some(byte_offset)
+ })
+ .collect();
+ positions.sort_unstable();
+
+ results.push(PathMatch {
+ score: adjusted_score,
+ positions,
+ worktree_id,
+ path: if root_is_file {
+ Arc::clone(path_prefix)
+ } else {
+ candidate.path.into()
+ },
+ path_prefix: if root_is_file {
+ RelPath::empty().into()
+ } else {
+ Arc::clone(path_prefix)
+ },
+ is_dir: candidate.is_dir,
+ distance_to_relative_ancestor: relative_to
+ .as_ref()
+ .map_or(usize::MAX, |relative_to| {
+ distance_between_paths(candidate.path, relative_to.as_ref())
+ }),
+ });
+ }
+ }
+ Ok(())
+}
+
+pub fn match_fixed_path_set(
+ candidates: Vec<PathMatchCandidate>,
+ worktree_id: usize,
+ worktree_root_name: Option<Arc<RelPath>>,
+ query: &str,
+ smart_case: bool,
+ max_results: usize,
+ path_style: PathStyle,
+) -> Vec<PathMatch> {
+ let mut config = nucleo::Config::DEFAULT;
+ config.set_match_paths();
+ let mut matcher = matcher::get_matcher(config);
+
+ let atoms = make_atoms(query, smart_case);
+
+ let root_is_file = worktree_root_name.is_some() && candidates.iter().all(|c| c.path.is_empty());
+
+ let path_prefix = worktree_root_name.unwrap_or_else(|| RelPath::empty().into());
+
+ let mut results = Vec::new();
+
+ path_match_helper(
+ &mut matcher,
+ &atoms,
+ candidates.into_iter(),
+ &mut results,
+ worktree_id,
+ &path_prefix,
+ root_is_file,
+ &None,
+ path_style,
+ &AtomicBool::new(false),
+ )
+ .ok();
+ util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
+ matcher::return_matcher(matcher);
+ results
+}
+
+pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
+ candidate_sets: &'a [Set],
+ query: &str,
+ relative_to: &Option<Arc<RelPath>>,
+ smart_case: bool,
+ max_results: usize,
+ cancel_flag: &AtomicBool,
+ executor: BackgroundExecutor,
+) -> Vec<PathMatch> {
+ let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
+ if path_count == 0 {
+ return Vec::new();
+ }
+
+ let path_style = candidate_sets[0].path_style();
+
+ let query = if path_style.is_windows() {
+ query.replace('\\', "/")
+ } else {
+ query.to_owned()
+ };
+
+ let atoms = make_atoms(&query, smart_case);
+
+ let num_cpus = executor.num_cpus().min(path_count);
+ let segment_size = path_count.div_ceil(num_cpus);
+ let mut segment_results = (0..num_cpus)
+ .map(|_| Vec::with_capacity(max_results))
+ .collect::<Vec<_>>();
+ let mut config = nucleo::Config::DEFAULT;
+ config.set_match_paths();
+ let mut matchers = matcher::get_matchers(num_cpus, config);
+ executor
+ .scoped(|scope| {
+ for (segment_idx, (results, matcher)) in segment_results
+ .iter_mut()
+ .zip(matchers.iter_mut())
+ .enumerate()
+ {
+ let atoms = atoms.clone();
+ let relative_to = relative_to.clone();
+ scope.spawn(async move {
+ let segment_start = segment_idx * segment_size;
+ let segment_end = segment_start + segment_size;
+
+ let mut tree_start = 0;
+ for candidate_set in candidate_sets {
+ let tree_end = tree_start + candidate_set.len();
+
+ if tree_start < segment_end && segment_start < tree_end {
+ let start = tree_start.max(segment_start) - tree_start;
+ let end = tree_end.min(segment_end) - tree_start;
+ let candidates = candidate_set.candidates(start).take(end - start);
+
+ if path_match_helper(
+ matcher,
+ &atoms,
+ candidates,
+ results,
+ candidate_set.id(),
+ &candidate_set.prefix(),
+ candidate_set.root_is_file(),
+ &relative_to,
+ path_style,
+ cancel_flag,
+ )
+ .is_err()
+ {
+ break;
+ }
+ }
+
+ if tree_end >= segment_end {
+ break;
+ }
+ tree_start = tree_end;
+ }
+ });
+ }
+ })
+ .await;
+
+ matcher::return_matchers(matchers);
+ if cancel_flag.load(atomic::Ordering::Acquire) {
+ return Vec::new();
+ }
+
+ let mut results = segment_results.concat();
+ util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
+ results
+}
@@ -52,6 +52,7 @@ fancy-regex.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
+fuzzy_nucleo.workspace = true
git.workspace = true
git_hosting_providers.workspace = true
globset.workspace = true
@@ -6186,6 +6186,76 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
}
}
+impl<'a> fuzzy_nucleo::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
+ type Candidates = PathMatchCandidateSetNucleoIter<'a>;
+ fn id(&self) -> usize {
+ self.snapshot.id().to_usize()
+ }
+ fn len(&self) -> usize {
+ match self.candidates {
+ Candidates::Files => {
+ if self.include_ignored {
+ self.snapshot.file_count()
+ } else {
+ self.snapshot.visible_file_count()
+ }
+ }
+ Candidates::Directories => {
+ if self.include_ignored {
+ self.snapshot.dir_count()
+ } else {
+ self.snapshot.visible_dir_count()
+ }
+ }
+ Candidates::Entries => {
+ if self.include_ignored {
+ self.snapshot.entry_count()
+ } else {
+ self.snapshot.visible_entry_count()
+ }
+ }
+ }
+ }
+ fn prefix(&self) -> Arc<RelPath> {
+ if self.snapshot.root_entry().is_some_and(|e| e.is_file()) || self.include_root_name {
+ self.snapshot.root_name().into()
+ } else {
+ RelPath::empty().into()
+ }
+ }
+ fn root_is_file(&self) -> bool {
+ self.snapshot.root_entry().is_some_and(|f| f.is_file())
+ }
+ fn path_style(&self) -> PathStyle {
+ self.snapshot.path_style()
+ }
+ fn candidates(&'a self, start: usize) -> Self::Candidates {
+ PathMatchCandidateSetNucleoIter {
+ traversal: match self.candidates {
+ Candidates::Directories => self.snapshot.directories(self.include_ignored, start),
+ Candidates::Files => self.snapshot.files(self.include_ignored, start),
+ Candidates::Entries => self.snapshot.entries(self.include_ignored, start),
+ },
+ }
+ }
+}
+
+pub struct PathMatchCandidateSetNucleoIter<'a> {
+ traversal: Traversal<'a>,
+}
+
+impl<'a> Iterator for PathMatchCandidateSetNucleoIter<'a> {
+ type Item = fuzzy_nucleo::PathMatchCandidate<'a>;
+ fn next(&mut self) -> Option<Self::Item> {
+ self.traversal
+ .next()
+ .map(|entry| fuzzy_nucleo::PathMatchCandidate {
+ is_dir: entry.kind.is_dir(),
+ path: &entry.path,
+ })
+ }
+}
+
impl EventEmitter<Event> for Project {}
impl<'a> From<&'a ProjectPath> for SettingsLocation<'a> {