diff --git a/Cargo.lock b/Cargo.lock index 007effa60..095747a32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1508,9 +1508,12 @@ dependencies = [ [[package]] name = "neo_frizbee" -version = "0.11.0" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2f6120a8da26bea3587731072111062c5d8c51ca3a3a75a716bd8b735d5882" +checksum = "fb0dd0e69b342eaddb657120a21a56f3e887ddc505fac780cf430dbbbb3aed26" +dependencies = [ + "serde", +] [[package]] name = "nix" diff --git a/Cargo.toml b/Cargo.toml index 0e4873bf3..0fa2e2b74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ signal-hook-registry = "1.4" zlob = { version = "=1.6.5" } mlua = { version = "0.11.1", features = ["module", "luajit"] } -neo_frizbee = { version = "0.11.0", features = ["match_end_col"] } +neo_frizbee = { version = "0.13.3", features = ["match_end_col"] } notify = { version = "9.0.0-rc.3" } notify-debouncer-full = { package = "fff-notify-debouncer-full", version = "0.9.4" } once_cell = "1.20.2" @@ -56,7 +56,7 @@ tracing = "0.1" opt-level = 3 lto = "fat" codegen-units = 1 -strip = "debuginfo" +strip = "symbols" [profile.ci] inherits = "release" diff --git a/crates/fff-core/src/grep/fuzzy_grep.rs b/crates/fff-core/src/grep/fuzzy_grep.rs index fb4143b43..c8f0e2d29 100644 --- a/crates/fff-core/src/grep/fuzzy_grep.rs +++ b/crates/fff-core/src/grep/fuzzy_grep.rs @@ -53,7 +53,7 @@ pub(super) fn fuzzy_grep_search<'a>( &neo_frizbee::Config { // Use the real max_typos so frizbee's SIMD prefilter actually rejects non-matching lines (~2 SIMD instructions per line vs full SW scoring). max_typos: Some(max_typos as u16), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, scoring, ..Default::default() }, @@ -271,7 +271,7 @@ pub(super) fn fuzzy_grep_search<'a>( if let (Some(&first), Some(&last)) = (indices.first(), indices.last()) { // reject widely scattered matches - let span = last - first + 1; + let span = (last - first + 1) as usize; if span > max_match_span { continue; } diff --git a/crates/fff-core/src/grep/grep_tests.rs b/crates/fff-core/src/grep/grep_tests.rs index a7c42da34..c7c0adccf 100644 --- a/crates/fff-core/src/grep/grep_tests.rs +++ b/crates/fff-core/src/grep/grep_tests.rs @@ -46,7 +46,7 @@ fn test_fuzzy_typo_scoring() { let max_typos = (needle.len() / 3).min(2); // 2 let config = neo_frizbee::Config { max_typos: Some(max_typos as u16), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, scoring: neo_frizbee::Scoring { exact_match_bonus: 100, ..neo_frizbee::Scoring::default() @@ -58,7 +58,8 @@ fn test_fuzzy_typo_scoring() { // Helper: check if a match would pass our post-filters let passes = |n: &str, h: &str| -> bool { - let Some(mut mi) = neo_frizbee::match_list_indices(n, &[h], &config) + let Some(mut mi) = neo_frizbee::Matcher::new(n, &config) + .match_list_indices(&[h]) .into_iter() .next() else { @@ -70,7 +71,7 @@ fn test_fuzzy_typo_scoring() { return false; } if let (Some(&first), Some(&last)) = (mi.indices.first(), mi.indices.last()) { - let span = last - first + 1; + let span = (last - first + 1) as usize; if span > max_match_span { return false; } diff --git a/crates/fff-core/src/grep/sink.rs b/crates/fff-core/src/grep/sink.rs index 4111dc986..3fbd28cc0 100644 --- a/crates/fff-core/src/grep/sink.rs +++ b/crates/fff-core/src/grep/sink.rs @@ -201,7 +201,7 @@ pub(super) fn split_multiline_blob(display_bytes: &[u8]) -> (&[u8], Vec) /// Adjacent characters are merged into a single contiguous range. pub(super) fn char_indices_to_byte_offsets( line: &str, - char_indices: &[usize], + char_indices: &[u32], ) -> SmallVec<[(u32, u32); 4]> { if char_indices.is_empty() { return SmallVec::new(); @@ -218,6 +218,7 @@ pub(super) fn char_indices_to_byte_offsets( let mut result: SmallVec<[(u32, u32); 4]> = SmallVec::with_capacity(char_indices.len()); for &ci in char_indices { + let ci = ci as usize; if ci >= char_byte_ranges.len() { continue; // out of bounds (shouldn't happen with valid data) } diff --git a/crates/fff-core/src/score.rs b/crates/fff-core/src/score.rs index 3dce9dcfa..e387a85e0 100644 --- a/crates/fff-core/src/score.rs +++ b/crates/fff-core/src/score.rs @@ -18,6 +18,14 @@ enum FileItems<'a> { } impl<'a> FileItems<'a> { + #[inline] + fn len(&self) -> usize { + match self { + FileItems::All(s) => s.len(), + FileItems::Filtered(v) => v.len(), + } + } + #[inline] fn index(&self, index: usize) -> &'a FileItem { match self { @@ -61,32 +69,17 @@ fn match_fuzzy_parts( return vec![]; } - let resolve = |file: &FileItem, - buf: &mut [*const u8; MAX_PATH_CHUNKS]| - -> Option<(usize, u16)> { resolve_file_chunks(file, arena, buf) }; - - // because we reassemble the vec of reference we have to use a different type - // to narrow down the [&FileItem] which would be resolved by frizbee as && - let resolve_ref = |file: &&FileItem, - buf: &mut [*const u8; MAX_PATH_CHUNKS]| - -> Option<(usize, u16)> { resolve_file_chunks(file, arena, buf) }; - - let first_part_matches = match working_files { - FileItems::All(files) => neo_frizbee::match_list_parallel_resolved( - valid_parts[0], - files, - &resolve, - options, - max_threads, - ), - FileItems::Filtered(files) => neo_frizbee::match_list_parallel_resolved( - valid_parts[0], - files.as_slice(), - &resolve_ref, - options, - max_threads, - ), - }; + // Index-based resolver: no `Vec<&FileItem>` materialization for either the + // full list or the per-part subsets, and a single monomorphized hot loop. + let first_part_matches = neo_frizbee::match_range_parallel_resolved( + valid_parts[0], + working_files.len(), + &|index, buf: &mut [*const u8; MAX_PATH_CHUNKS]| { + resolve_file_chunks(working_files.index(index as usize), arena, buf) + }, + options, + max_threads, + ); if valid_parts.len() == 1 { return first_part_matches; @@ -98,16 +91,16 @@ fn match_fuzzy_parts( let mut part_options = *options; part_options.max_typos = options.max_typos.map(|t| t.min(part.len() as u16)); - // Collect the subset of files that survived the previous round. - let subset: Vec<&FileItem> = matches - .iter() - .map(|m| working_files.index(m.index as usize)) - .collect(); - - let sub_matches = neo_frizbee::match_list_parallel_resolved( + // Match only the files that survived the previous round, addressed + // through the previous matches without collecting a subset. + let survivors = &matches; + let sub_matches = neo_frizbee::match_range_parallel_resolved( part, - subset.as_slice(), - &resolve_ref, + survivors.len(), + &|index, buf: &mut [*const u8; MAX_PATH_CHUNKS]| { + let file = working_files.index(survivors[index as usize].index as usize); + resolve_file_chunks(file, arena, buf) + }, &part_options, max_threads, ); @@ -206,7 +199,7 @@ pub(crate) fn fuzzy_match_byte_offsets_for_page<'q>( .any(|part| part.chars().any(|ch| ch.is_uppercase())); let config = neo_frizbee::Config { max_typos: Some(max_typos), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, scoring: Scoring { capitalization_bonus: if has_uppercase { 8 } else { 0 }, matching_case_bonus: if has_uppercase { 4 } else { 0 }, @@ -215,6 +208,8 @@ pub(crate) fn fuzzy_match_byte_offsets_for_page<'q>( ..Default::default() }; + // Match on `&str` so this shares frizbee's instantiation with fuzzy grep. + let path_strs: Vec<&str> = paths.iter().map(String::as_str).collect(); for (idx, part) in parts.iter().copied().enumerate() { let mut part_config = config; if idx > 0 { @@ -222,7 +217,7 @@ pub(crate) fn fuzzy_match_byte_offsets_for_page<'q>( } let mut matcher = neo_frizbee::Matcher::new(part, &part_config); - for mut matched in matcher.match_list_indices(&paths) { + for mut matched in matcher.match_list_indices(&path_strs) { let item_idx = matched.index as usize; let Some(path) = paths.get(item_idx) else { continue; @@ -240,7 +235,7 @@ pub(crate) fn fuzzy_match_byte_offsets_for_page<'q>( ranges_by_item } -fn char_indices_to_byte_offsets(line: &str, char_indices: &[usize]) -> SmallVec<[(u32, u32); 4]> { +fn char_indices_to_byte_offsets(line: &str, char_indices: &[u32]) -> SmallVec<[(u32, u32); 4]> { let char_byte_ranges: Vec<(usize, usize)> = line .char_indices() .map(|(byte_pos, ch)| (byte_pos, byte_pos + ch.len_utf8())) @@ -248,7 +243,7 @@ fn char_indices_to_byte_offsets(line: &str, char_indices: &[usize]) -> SmallVec< let mut result: SmallVec<[(u32, u32); 4]> = SmallVec::with_capacity(char_indices.len()); for &char_idx in char_indices { - let Some(&(start, end)) = char_byte_ranges.get(char_idx) else { + let Some(&(start, end)) = char_byte_ranges.get(char_idx as usize) else { continue; }; @@ -328,15 +323,12 @@ fn match_fuzzy_parts_dirs( return vec![]; } - let resolve_chunks_for_frizbee = - |dir: &&DirItem, buf: &mut [*const u8; MAX_PATH_CHUNKS]| -> Option<(usize, u16)> { - resolve_dir_chunks(dir, arena, overflow_arena, buf) - }; - - let first_part_matches = neo_frizbee::match_list_parallel_resolved( + let first_part_matches = neo_frizbee::match_range_parallel_resolved( valid_parts[0], - working_dirs, - &resolve_chunks_for_frizbee, + working_dirs.len(), + &|index, buf: &mut [*const u8; MAX_PATH_CHUNKS]| { + resolve_dir_chunks(working_dirs[index as usize], arena, overflow_arena, buf) + }, options, max_threads, ); @@ -351,16 +343,16 @@ fn match_fuzzy_parts_dirs( let mut part_options = *options; part_options.max_typos = options.max_typos.map(|t| t.min(part.len() as u16)); - // Collect the subset of dirs that survived the previous round. - let subset: Vec<&DirItem> = matches - .iter() - .map(|m| working_dirs[m.index as usize]) - .collect(); - - let sub_matches = neo_frizbee::match_list_parallel_resolved( + // Match only the dirs that survived the previous round, addressed + // through the previous matches without collecting a subset. + let survivors = &matches; + let sub_matches = neo_frizbee::match_range_parallel_resolved( part, - subset.as_slice(), - &resolve_chunks_for_frizbee, + survivors.len(), + &|index, buf: &mut [*const u8; MAX_PATH_CHUNKS]| { + let dir = working_dirs[survivors[index as usize].index as usize]; + resolve_dir_chunks(dir, arena, overflow_arena, buf) + }, &part_options, max_threads, ); @@ -441,7 +433,7 @@ pub(crate) fn fuzzy_match_and_score_dirs<'a>( let options = neo_frizbee::Config { max_typos: Some(context.max_typos), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, scoring: Scoring { capitalization_bonus: if has_uppercase { 8 } else { 0 }, matching_case_bonus: if has_uppercase { 4 } else { 0 }, @@ -645,7 +637,7 @@ fn match_and_score_in_arena<'a>( let options = neo_frizbee::Config { max_typos: Some(context.max_typos), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, scoring: Scoring { capitalization_bonus: if has_uppercase { 8 } else { 0 }, matching_case_bonus: if has_uppercase { 4 } else { 0 }, @@ -686,16 +678,18 @@ fn match_and_score_in_arena<'a>( if fallback_filenames.is_empty() { vec![] } else { - let mut matches = neo_frizbee::match_list_parallel( - fuzzy_parts[0], - &fallback_filenames, - &options, - if path_matches.len() > 4096 { - context.max_threads.div_ceil(2048) - } else { - 1 - }, - ); + // Match on `&str` so frizbee reuses the instantiation its index + // resolver already emits instead of a separate `Cow` copy. + let filename_strs: Vec<&str> = fallback_filenames.iter().map(Cow::as_ref).collect(); + let mut matches = neo_frizbee::Matcher::new(fuzzy_parts[0], &options) + .match_list_parallel( + &filename_strs, + if path_matches.len() > 4096 { + context.max_threads.div_ceil(2048) + } else { + 1 + }, + ); sort_by_key_with_buffer(&mut matches, |m| fallback_indices[m.index as usize]); matches @@ -1496,21 +1490,21 @@ mod filename_bonus_tests { let options = neo_frizbee::Config { max_typos: Some(2), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, ..Default::default() }; - let matches = neo_frizbee::match_list("aipart", &[path], &options); + let matches = neo_frizbee::Matcher::new("aipart", &options).match_list(&[path]); assert!(!matches.is_empty(), "'aipart' should match the path"); - let matches = neo_frizbee::match_list("core", &[path], &options); + let matches = neo_frizbee::Matcher::new("core", &options).match_list(&[path]); assert!(!matches.is_empty(), "'core' should match the path"); let co_options = neo_frizbee::Config { max_typos: Some(2), ..options }; - let matches = neo_frizbee::match_list("co", &[path], &co_options); + let matches = neo_frizbee::Matcher::new("co", &co_options).match_list(&[path]); assert!(!matches.is_empty(), "'co' should match the path"); } @@ -1520,14 +1514,14 @@ mod filename_bonus_tests { let options = neo_frizbee::Config { max_typos: Some(2), - sort: false, + sort: neo_frizbee::SortStrategy::Unsorted, ..Default::default() }; - let matches = neo_frizbee::match_list("co", &[path.as_str()], &options); + let matches = neo_frizbee::Matcher::new("co", &options).match_list(&[path.as_str()]); assert!(!matches.is_empty(), "'co' should match the lowercase path"); - let matches = neo_frizbee::match_list("core", &[path.as_str()], &options); + let matches = neo_frizbee::Matcher::new("core", &options).match_list(&[path.as_str()]); assert!( !matches.is_empty(), "'core' should match the lowercase path"