From ddae8295701250876c2307e781750017d845f4c4 Mon Sep 17 00:00:00 2001 From: gustav-fff <286169375+gustav-fff@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:48:11 -0700 Subject: [PATCH 1/2] fix(score): rank exact filename stem match above generic fuzzy filename (#722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `is_exact_filename` required the needle to equal the full filename (including extension), so `lsp` vs `lsp.lua` fell through to the `fuzzy_filename` branch capped at ~16% and only outranked `lsp/typos_lsp.lua` by a couple of points — any frecency or git bonus on the longer file overturned the order. Add an `is_exact_stem` check: needle equals the filename portion before the LAST dot (`lsp` ↔ `lsp.lua`, but NOT `foo.bar` ↔ `foo.bar.lua`). Awards a 30% base_score bonus — clearly above fuzzy_filename (~16%), below exact_filename (40%) — so the intended file wins by a comfortable margin without stealing rank from truly exact filename hits. Closes #722 --- crates/fff-core/src/score.rs | 40 +++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/fff-core/src/score.rs b/crates/fff-core/src/score.rs index 8fe21ceb..0f9cb964 100644 --- a/crates/fff-core/src/score.rs +++ b/crates/fff-core/src/score.rs @@ -760,9 +760,27 @@ fn match_and_score_in_arena<'a>( main_needle.eq_ignore_ascii_case(fname_buf.as_bytes()) }); + // Exact stem match: needle equals the filename's stem (bytes before + // the last dot). Ranks between fuzzy_filename and exact_filename so + // e.g. `lsp` prefers `lsp.lua` over `lsp/typos_lsp.lua`. + let is_exact_stem = !is_exact_filename + && is_filename_match + && (main_needle_len as usize + 2) <= fname_len + && { + file.write_file_name_from_arena(arena, &mut fname_buf); + let bytes = fname_buf.as_bytes(); + let n = main_needle_len as usize; + bytes.len() > n + 1 + && bytes[n] == b'.' + && !bytes[n + 1..].contains(&b'.') + && main_needle.eq_ignore_ascii_case(&bytes[..n]) + }; + let mut has_special_filename_bonus = false; let filename_bonus = if is_exact_filename { base_score / 5 * 2 // 40% bonus for exact filename match + } else if is_exact_stem { + base_score * 30 / 100 // 30% bonus for exact filename stem match } else if is_filename_match { // 16% bonus for fuzzy filename match that landed in the filename region. // For fallback matches (where the path match landed in a directory segment), @@ -868,9 +886,11 @@ fn match_and_score_in_arena<'a>( distance_penalty, combo_match_boost, path_alignment_bonus, - exact_match: is_exact_filename || path_match.exact, + exact_match: is_exact_filename || is_exact_stem || path_match.exact, match_type: if is_exact_filename { "exact_filename" + } else if is_exact_stem { + "exact_stem" } else if is_filename_match { "fuzzy_filename" } else if path_match.exact { @@ -1393,6 +1413,24 @@ mod filename_bonus_tests { ); } + /// Regression: query that exactly matches the filename stem (name minus + /// extension) should rank clearly above files that just contain the query + /// as a fuzzy filename substring. https://github.com/dmtrKovalenko/fff/issues/722 + #[test] + fn test_exact_stem_beats_fuzzy_filename() { + let (files, arena) = make_files(&["lsp/typos_lsp.lua", "lsp.lua"]); + + let results = search(&files, "lsp", arena); + + assert!(results.len() >= 2); + assert_eq!( + results[0].0, "lsp.lua", + "lsp.lua (exact filename stem) should rank above typos_lsp.lua" + ); + assert_eq!(results[0].1.match_type, "exact_stem"); + assert!(results[0].1.filename_bonus > results[1].1.filename_bonus); + } + #[test] fn test_path_separator_disables_filename_bonus() { let (files, arena) = make_files(&["src/controllers/user.rs"]); From 9d8f63f3a7cb4ee274ebf5a541a9d667f01122b7 Mon Sep 17 00:00:00 2001 From: gustav-fff <286169375+gustav-fff@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:20:25 -0700 Subject: [PATCH 2/2] perf(score): skip filename read when neither exact nor stem is plausible Length-gate the exact-filename and exact-stem checks by fname_len before touching the arena. Both variants are mutually exclusive by length, so the filename is now read at most once per candidate (previously the stem branch redundantly rewrote fname_buf even after exact_filename already did) and only when the length pre-check makes either plausible. --- crates/fff-core/src/score.rs | 49 +++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/fff-core/src/score.rs b/crates/fff-core/src/score.rs index 0f9cb964..6793bf57 100644 --- a/crates/fff-core/src/score.rs +++ b/crates/fff-core/src/score.rs @@ -753,28 +753,37 @@ fn match_and_score_in_arena<'a>( let is_filename_match = end_col_filename_match || simd_filename_match.is_some(); let fname_len = file.path.byte_len as usize - file.path.filename_offset as usize; - - let is_exact_filename = simd_filename_match.is_some_and(|m| m.exact) - || (end_col_filename_match && main_needle_len as usize == fname_len && { - file.write_file_name_from_arena(arena, &mut fname_buf); - main_needle.eq_ignore_ascii_case(fname_buf.as_bytes()) - }); - - // Exact stem match: needle equals the filename's stem (bytes before - // the last dot). Ranks between fuzzy_filename and exact_filename so - // e.g. `lsp` prefers `lsp.lua` over `lsp/typos_lsp.lua`. - let is_exact_stem = !is_exact_filename - && is_filename_match - && (main_needle_len as usize + 2) <= fname_len - && { + let needle_len = main_needle_len as usize; + + // Length-gated exact-filename / exact-stem detection: both variants + // are mutually exclusive by length (exact needs n == fname_len, stem + // needs n + 2 <= fname_len), so the arena read happens at most once + // and is skipped when neither is plausible. Reuses `fname_buf` to + // avoid a per-file allocation. + let (is_exact_filename, is_exact_stem) = if !is_filename_match { + (false, false) + } else if simd_filename_match.is_some_and(|m| m.exact) { + (true, false) + } else { + let could_be_exact = end_col_filename_match && needle_len == fname_len; + let could_be_stem = needle_len + 2 <= fname_len; + if !could_be_exact && !could_be_stem { + (false, false) + } else { file.write_file_name_from_arena(arena, &mut fname_buf); let bytes = fname_buf.as_bytes(); - let n = main_needle_len as usize; - bytes.len() > n + 1 - && bytes[n] == b'.' - && !bytes[n + 1..].contains(&b'.') - && main_needle.eq_ignore_ascii_case(&bytes[..n]) - }; + if could_be_exact { + (main_needle.eq_ignore_ascii_case(bytes), false) + } else if bytes[needle_len] == b'.' + && !bytes[needle_len + 1..].contains(&b'.') + && main_needle.eq_ignore_ascii_case(&bytes[..needle_len]) + { + (false, true) + } else { + (false, false) + } + } + }; let mut has_special_filename_bonus = false; let filename_bonus = if is_exact_filename {