diff --git a/crates/fff-c/src/lib.rs b/crates/fff-c/src/lib.rs index 0e0d0c91..d6c5cc3e 100644 --- a/crates/fff-c/src/lib.rs +++ b/crates/fff-c/src/lib.rs @@ -736,7 +736,11 @@ pub unsafe extern "C" fn fff_live_grep( let options = fff::GrepSearchOptions { max_file_size: default_u64(max_file_size, 10 * 1024 * 1024), max_matches_per_file: max_matches_per_file as usize, - smart_case, + case_mode: Some(if smart_case { + fff::CaseMode::Smart + } else { + fff::CaseMode::Sensitive + }), file_offset: file_offset as usize, page_limit: default_u32(page_limit, 50) as usize, mode: grep_mode_from_u8(mode), @@ -746,6 +750,7 @@ pub unsafe extern "C" fn fff_live_grep( classify_definitions, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let result = picker.grep(&parsed, &options); @@ -839,7 +844,11 @@ pub unsafe extern "C" fn fff_multi_grep( let options = fff::GrepSearchOptions { max_file_size: default_u64(max_file_size, 10 * 1024 * 1024), max_matches_per_file: max_matches_per_file as usize, - smart_case, + case_mode: Some(if smart_case { + fff::CaseMode::Smart + } else { + fff::CaseMode::Sensitive + }), file_offset: file_offset as usize, page_limit: default_u32(page_limit, 50) as usize, mode: fff::GrepMode::PlainText, // ignored by multi_grep_search @@ -849,6 +858,7 @@ pub unsafe extern "C" fn fff_multi_grep( classify_definitions, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let result = picker.multi_grep(&patterns, constraint_refs, &options); diff --git a/crates/fff-core/src/grep.rs b/crates/fff-core/src/grep.rs index 5c73fa5b..2c857fe7 100644 --- a/crates/fff-core/src/grep.rs +++ b/crates/fff-core/src/grep.rs @@ -228,6 +228,14 @@ fn replace_unescaped_newline_escapes(text: &str) -> String { String::from_utf8(result).unwrap_or_else(|_| text.to_string()) } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum CaseMode { + #[default] + Smart, + Sensitive, + Insensitive, +} + /// Controls how the grep pattern is interpreted. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum GrepMode { @@ -334,7 +342,10 @@ pub use crate::constants::MAX_FFFILE_SIZE; pub struct GrepSearchOptions { pub max_file_size: u64, pub max_matches_per_file: usize, + #[deprecated(note = "use `case_mode` instead")] pub smart_case: bool, + /// When `Some`, overrides `smart_case`. + pub case_mode: Option, /// File-based pagination offset: index into the sorted/filtered file list /// to start searching from. Pass 0 for the first page, then use /// `GrepResult::next_file_offset` for subsequent pages. @@ -364,12 +375,25 @@ pub struct GrepSearchOptions { pub abort_signal: Option>, } +impl GrepSearchOptions { + fn effective_case_mode(&self) -> CaseMode { + #[allow(deprecated)] + match self.case_mode { + Some(m) => m, + None if self.smart_case => CaseMode::Smart, + None => CaseMode::Sensitive, + } + } +} + impl Default for GrepSearchOptions { fn default() -> Self { + #[allow(deprecated)] Self { max_file_size: MAX_FFFILE_SIZE, max_matches_per_file: 200, smart_case: true, + case_mode: None, file_offset: 0, page_limit: 50, mode: GrepMode::default(), @@ -1045,11 +1069,10 @@ pub(crate) fn multi_grep_search<'a>( }; } - // Smart case: case-insensitive when all patterns are lowercase - let case_insensitive = if options.smart_case { - !patterns.iter().any(|p| p.chars().any(|c| c.is_uppercase())) - } else { - false + let case_insensitive = match options.effective_case_mode() { + CaseMode::Smart => !patterns.iter().any(|p| p.chars().any(|c| c.is_uppercase())), + CaseMode::Insensitive => true, + CaseMode::Sensitive => false, }; let ac = aho_corasick::AhoCorasickBuilder::new() @@ -1117,7 +1140,7 @@ const fn is_utf8_char_boundary(b: u8) -> bool { /// - The input is passed directly to the regex engine without escaping /// - Smart case still applies /// - Returns `None` for invalid regex patterns — the caller falls back to literal mode -fn build_regex(pattern: &str, smart_case: bool) -> Result { +fn build_regex(pattern: &str, case_mode: CaseMode) -> Result { if pattern.is_empty() { return Err("empty pattern".to_string()); } @@ -1128,10 +1151,10 @@ fn build_regex(pattern: &str, smart_case: bool) -> Result !pattern.chars().any(|c| c.is_uppercase()), + CaseMode::Insensitive => true, + CaseMode::Sensitive => false, }; regex::bytes::RegexBuilder::new(®ex_pattern) @@ -1995,10 +2018,10 @@ pub(crate) fn grep_search<'a>( }; } - let case_insensitive = if options.smart_case { - !grep_text.chars().any(|c| c.is_uppercase()) - } else { - false + let case_insensitive = match options.effective_case_mode() { + CaseMode::Smart => !grep_text.chars().any(|c| c.is_uppercase()), + CaseMode::Insensitive => true, + CaseMode::Sensitive => false, }; let mut regex_fallback_error: Option = None; @@ -2089,7 +2112,7 @@ pub(crate) fn grep_search<'a>( overflow_arena, ); } - GrepMode::Regex => build_regex(&grep_text, options.smart_case) + GrepMode::Regex => build_regex(&grep_text, options.effective_case_mode()) .inspect_err(|err| { tracing::warn!("Regex compilation failed for {}. Error {}", grep_text, err); @@ -2444,7 +2467,7 @@ mod tests { let options = super::GrepSearchOptions { max_file_size: MAX_FFFILE_SIZE, max_matches_per_file: 0, - smart_case: true, + case_mode: Some(super::CaseMode::Smart), file_offset: 0, page_limit: 100, mode: super::GrepMode::PlainText, @@ -2454,6 +2477,7 @@ mod tests { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let no_cancel = AtomicBool::new(false); @@ -2628,7 +2652,7 @@ mod tests { let options = super::GrepSearchOptions { max_file_size: MAX_FFFILE_SIZE, max_matches_per_file: 0, - smart_case: true, + case_mode: Some(super::CaseMode::Smart), file_offset: 0, page_limit: 100, mode: super::GrepMode::PlainText, @@ -2638,6 +2662,7 @@ mod tests { classify_definitions: false, trim_whitespace: false, abort_signal: Some(std::sync::Arc::new(AtomicBool::new(false))), + ..Default::default() }; let result = picker.grep(&query, &options); diff --git a/crates/fff-core/tests/bigram_overlay_coherence_test.rs b/crates/fff-core/tests/bigram_overlay_coherence_test.rs index 6cee64e4..7369168d 100644 --- a/crates/fff-core/tests/bigram_overlay_coherence_test.rs +++ b/crates/fff-core/tests/bigram_overlay_coherence_test.rs @@ -1309,7 +1309,7 @@ fn grep_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode: GrepMode::PlainText, @@ -1319,6 +1319,7 @@ fn grep_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -1653,4 +1654,4 @@ fn bigram_overlay_coherence_fuzzy_grep_finds_overflow_files() { } stop_picker(&shared_picker); -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/bigram_overlay_integration.rs b/crates/fff-core/tests/bigram_overlay_integration.rs index c663acf8..8d5cbedb 100644 --- a/crates/fff-core/tests/bigram_overlay_integration.rs +++ b/crates/fff-core/tests/bigram_overlay_integration.rs @@ -368,7 +368,7 @@ fn grep_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 200, mode: GrepMode::PlainText, @@ -378,6 +378,7 @@ fn grep_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -407,4 +408,4 @@ fn wait_for_bigram(shared_picker: &SharedFilePicker) { "Timed out waiting for bigram build" ); } -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/fuzz_file_operations.rs b/crates/fff-core/tests/fuzz_file_operations.rs index e39a6906..24374d8e 100644 --- a/crates/fff-core/tests/fuzz_file_operations.rs +++ b/crates/fff-core/tests/fuzz_file_operations.rs @@ -626,7 +626,7 @@ fn grep_plain_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode: GrepMode::PlainText, @@ -636,6 +636,7 @@ fn grep_plain_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -857,4 +858,4 @@ fn drop_during_post_scan_does_not_crash() { The test is not exercising the race. ({caught_active}/10)" ); eprintln!("Caught post-scan active in {caught_active}/10 rounds"); -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/fuzz_git_watcher_stress.rs b/crates/fff-core/tests/fuzz_git_watcher_stress.rs index f7ca1e80..74ed7306 100644 --- a/crates/fff-core/tests/fuzz_git_watcher_stress.rs +++ b/crates/fff-core/tests/fuzz_git_watcher_stress.rs @@ -891,7 +891,7 @@ fn grep_plain_matches(shared: &SharedFilePicker, query: &str) -> Vec { let opts = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode: GrepMode::PlainText, @@ -901,6 +901,7 @@ fn grep_plain_matches(shared: &SharedFilePicker, query: &str) -> Vec { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let result = picker.grep(&parsed, &opts); // `GrepResult::files` is the already-deduplicated list of files that @@ -928,7 +929,7 @@ fn grep_fuzzy_matches(shared: &SharedFilePicker, query: &str) -> Vec { let opts = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode: GrepMode::Fuzzy, @@ -938,6 +939,7 @@ fn grep_fuzzy_matches(shared: &SharedFilePicker, query: &str) -> Vec { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let result = picker.grep(&parsed, &opts); result @@ -960,7 +962,7 @@ fn grep_regex_matches(shared: &SharedFilePicker, query: &str) -> Vec { let opts = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode: GrepMode::Regex, @@ -970,6 +972,7 @@ fn grep_regex_matches(shared: &SharedFilePicker, query: &str) -> Vec { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let result = picker.grep(&parsed, &opts); result @@ -1607,4 +1610,4 @@ fn expect_file_status( } std::thread::sleep(CONVERGE_POLL); } -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/fuzz_real_repos.rs b/crates/fff-core/tests/fuzz_real_repos.rs index f83a0f52..81db683b 100644 --- a/crates/fff-core/tests/fuzz_real_repos.rs +++ b/crates/fff-core/tests/fuzz_real_repos.rs @@ -226,7 +226,7 @@ fn grep_opts(mode: GrepMode) -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode, @@ -236,6 +236,7 @@ fn grep_opts(mode: GrepMode) -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -737,4 +738,4 @@ proptest! { fn fuzz_real_repos_proptest(ops in ops_strategy()) { run_scenario(&ops); } -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/grep_integration.rs b/crates/fff-core/tests/grep_integration.rs index 2765de3c..71e0a5f8 100644 --- a/crates/fff-core/tests/grep_integration.rs +++ b/crates/fff-core/tests/grep_integration.rs @@ -31,7 +31,7 @@ fn plain_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 200, mode: GrepMode::PlainText, @@ -41,6 +41,7 @@ fn plain_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -49,7 +50,7 @@ fn regex_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 200, mode: GrepMode::Regex, @@ -59,6 +60,7 @@ fn regex_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -67,7 +69,7 @@ fn fuzzy_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 200, mode: GrepMode::Fuzzy, @@ -77,6 +79,7 @@ fn fuzzy_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -1834,4 +1837,4 @@ fn plain_text_smart_case_finds_uppercase_content_with_lowercase_query() { 1, "lowercase query should case-insensitively match 'VFIO-KVM'" ); -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/new_directory_watcher_test.rs b/crates/fff-core/tests/new_directory_watcher_test.rs index 9cb9fc11..095a510f 100644 --- a/crates/fff-core/tests/new_directory_watcher_test.rs +++ b/crates/fff-core/tests/new_directory_watcher_test.rs @@ -132,7 +132,7 @@ fn grep_plain_count(picker: &FilePicker, query: &str) -> usize { let opts = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 500, mode: GrepMode::PlainText, @@ -142,6 +142,7 @@ fn grep_plain_count(picker: &FilePicker, query: &str) -> usize { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; picker.grep(&parsed, &opts).matches.len() } @@ -536,4 +537,4 @@ fn gitignored_new_directory_excluded() { let grep_count = grep_plain_count(picker, "IGNORED_BUILD_TOKEN"); assert_eq!(grep_count, 0, "Gitignored content should NOT be grepable"); } -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/path_separator_constraint_test.rs b/crates/fff-core/tests/path_separator_constraint_test.rs index 9519ef83..7b7110c9 100644 --- a/crates/fff-core/tests/path_separator_constraint_test.rs +++ b/crates/fff-core/tests/path_separator_constraint_test.rs @@ -36,7 +36,7 @@ fn plain_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 200, mode: GrepMode::PlainText, @@ -46,6 +46,7 @@ fn plain_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -245,4 +246,4 @@ fn fuzzy_search_with_glob_constraint_matches_on_windows_paths() { results.iter().any(|p| p.contains("handler.lua")), "glob `**/src/**/*.lua` must match services/handler.lua, got {results:?}" ); -} +} \ No newline at end of file diff --git a/crates/fff-core/tests/real_binary_fixtures.rs b/crates/fff-core/tests/real_binary_fixtures.rs index d7f383b6..4b6c8261 100644 --- a/crates/fff-core/tests/real_binary_fixtures.rs +++ b/crates/fff-core/tests/real_binary_fixtures.rs @@ -30,7 +30,7 @@ fn plain_opts() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff_search::grep::CaseMode::Smart), file_offset: 0, page_limit: 200, mode: GrepMode::PlainText, @@ -40,6 +40,7 @@ fn plain_opts() -> GrepSearchOptions { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() } } @@ -150,4 +151,4 @@ fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool { haystack .windows(needle.len()) .any(|window| window == needle) -} +} \ No newline at end of file diff --git a/crates/fff-mcp/src/server.rs b/crates/fff-mcp/src/server.rs index 0fc6102a..4e9ad7ec 100644 --- a/crates/fff-mcp/src/server.rs +++ b/crates/fff-mcp/src/server.rs @@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex}; use crate::cursor::CursorStore; use crate::output::{GrepFormatter, OutputMode, file_suffix}; -use fff::grep::{GrepMode, GrepSearchOptions, has_regex_metacharacters}; +use fff::grep::{CaseMode, GrepMode, GrepSearchOptions, has_regex_metacharacters}; use fff::types::{FileItem, PaginationArgs}; use fff::{FuzzySearchOptions, QueryParser, SharedFilePicker, SharedFrecency}; use fff_query_parser::AiGrepConfig; @@ -66,7 +66,7 @@ fn make_grep_options( GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: matches_per_file, - smart_case: true, + case_mode: Some(CaseMode::Smart), file_offset, page_limit: 50, mode, @@ -76,6 +76,7 @@ fn make_grep_options( classify_definitions: true, trim_whitespace: true, abort_signal: None, + ..Default::default() }, auto_expand, ) diff --git a/crates/fff-nvim/benches/fuzzy_search_bench.rs b/crates/fff-nvim/benches/fuzzy_search_bench.rs index 8a1fcd57..fe12bdc0 100644 --- a/crates/fff-nvim/benches/fuzzy_search_bench.rs +++ b/crates/fff-nvim/benches/fuzzy_search_bench.rs @@ -627,7 +627,7 @@ fn bench_grep_search(c: &mut Criterion) { let options = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 0, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: 100, mode: GrepMode::PlainText, @@ -637,6 +637,7 @@ fn bench_grep_search(c: &mut Criterion) { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let test_queries = vec![ @@ -672,4 +673,4 @@ criterion_group!( bench_grep_search, ); -criterion_main!(benches); +criterion_main!(benches); \ No newline at end of file diff --git a/crates/fff-nvim/benches/grep_bench.rs b/crates/fff-nvim/benches/grep_bench.rs index 64939946..225e8a75 100644 --- a/crates/fff-nvim/benches/grep_bench.rs +++ b/crates/fff-nvim/benches/grep_bench.rs @@ -110,7 +110,7 @@ fn plain_options() -> GrepSearchOptions { GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: 50, mode: GrepMode::PlainText, @@ -233,4 +233,4 @@ criterion_group!( bench_plain_no_index, ); -criterion_main!(benches); +criterion_main!(benches); \ No newline at end of file diff --git a/crates/fff-nvim/src/bin/bench_grep_query.rs b/crates/fff-nvim/src/bin/bench_grep_query.rs index 063d0816..19cf13ff 100644 --- a/crates/fff-nvim/src/bin/bench_grep_query.rs +++ b/crates/fff-nvim/src/bin/bench_grep_query.rs @@ -22,7 +22,7 @@ fn run_grep(picker: &FilePicker, query: &str, iters: usize) { let options = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: usize::MAX, mode: GrepMode::PlainText, @@ -32,6 +32,7 @@ fn run_grep(picker: &FilePicker, query: &str, iters: usize) { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let parsed = parse_grep_query(query); @@ -135,4 +136,4 @@ fn main() { query, iters ); run_grep(&picker, query, iters); -} +} \ No newline at end of file diff --git a/crates/fff-nvim/src/bin/fuzzy_grep_test.rs b/crates/fff-nvim/src/bin/fuzzy_grep_test.rs index e3bceac8..7fd2cf31 100644 --- a/crates/fff-nvim/src/bin/fuzzy_grep_test.rs +++ b/crates/fff-nvim/src/bin/fuzzy_grep_test.rs @@ -27,7 +27,7 @@ fn run_fuzzy_query(picker: &FilePicker, query: &str, label: &str) { let options = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: 100, mode: GrepMode::Fuzzy, @@ -37,6 +37,7 @@ fn run_fuzzy_query(picker: &FilePicker, query: &str, label: &str) { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let parsed = parse_grep_query(query); @@ -180,4 +181,4 @@ fn main() { } eprintln!("=== Done ==="); -} +} \ No newline at end of file diff --git a/crates/fff-nvim/src/bin/grep_profiler.rs b/crates/fff-nvim/src/bin/grep_profiler.rs index 4c0386b2..487e4a9e 100644 --- a/crates/fff-nvim/src/bin/grep_profiler.rs +++ b/crates/fff-nvim/src/bin/grep_profiler.rs @@ -88,7 +88,7 @@ impl<'a> GrepBench<'a> { options: GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: 50, mode, @@ -98,6 +98,7 @@ impl<'a> GrepBench<'a> { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }, } } @@ -409,7 +410,7 @@ fn main() { let opts = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset, page_limit: 50, mode: Default::default(), @@ -419,6 +420,7 @@ fn main() { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let start = Instant::now(); let result = picker.grep(&parsed, &opts); @@ -450,4 +452,4 @@ fn main() { eprintln!("\nDone. For perf profiling:"); eprintln!(" perf record -g --call-graph dwarf -F 999 ./target/release/grep_profiler"); eprintln!(" perf report --no-children"); -} +} \ No newline at end of file diff --git a/crates/fff-nvim/src/bin/grep_vs_rg.rs b/crates/fff-nvim/src/bin/grep_vs_rg.rs index f202fc1f..9e05a5fe 100644 --- a/crates/fff-nvim/src/bin/grep_vs_rg.rs +++ b/crates/fff-nvim/src/bin/grep_vs_rg.rs @@ -162,7 +162,7 @@ fn run_fff_full(picker: &FilePicker, query: &str) -> (usize, Duration) { let options = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: usize::MAX, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: usize::MAX, mode: Default::default(), @@ -172,6 +172,7 @@ fn run_fff_full(picker: &FilePicker, query: &str) -> (usize, Duration) { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let start = Instant::now(); let result = picker.grep(&parsed, &options); @@ -185,7 +186,7 @@ fn run_fff_page(picker: &FilePicker, query: &str) -> (usize, Duration) { let options = GrepSearchOptions { max_file_size: 10 * 1024 * 1024, max_matches_per_file: 200, - smart_case: true, + case_mode: Some(fff::grep::CaseMode::Smart), file_offset: 0, page_limit: 50, mode: Default::default(), @@ -195,6 +196,7 @@ fn run_fff_page(picker: &FilePicker, query: &str) -> (usize, Duration) { classify_definitions: false, trim_whitespace: false, abort_signal: None, + ..Default::default() }; let start = Instant::now(); let result = picker.grep(&parsed, &options); @@ -440,4 +442,4 @@ fn main() { eprintln!(" on EVERY invocation (= every keystroke in telescope/fzf-lua)."); eprintln!(" fff pays this cost once at startup, then searches from warm cached mmaps."); eprintln!(); -} +} \ No newline at end of file diff --git a/crates/fff-nvim/src/lib.rs b/crates/fff-nvim/src/lib.rs index a9f3cb2d..52a71251 100644 --- a/crates/fff-nvim/src/lib.rs +++ b/crates/fff-nvim/src/lib.rs @@ -436,6 +436,7 @@ pub fn live_grep( grep_mode, time_budget_ms, trim_whitespace, + case_mode, ): ( String, Option, @@ -446,6 +447,7 @@ pub fn live_grep( Option, Option, Option, + Option, ), ) -> LuaResult { let file_picker_guard = FILE_PICKER.read().into_lua_result()?; @@ -460,10 +462,21 @@ pub fn live_grep( _ => fff::GrepMode::PlainText, // "plain" or nil or unknown }; + let case_mode = match case_mode.as_deref() { + Some("smart") => Some(fff::CaseMode::Smart), + Some("sensitive") => Some(fff::CaseMode::Sensitive), + Some("insensitive") => Some(fff::CaseMode::Insensitive), + _ => Some(if smart_case.unwrap_or(true) { + fff::CaseMode::Smart + } else { + fff::CaseMode::Sensitive + }), + }; + let options = fff::GrepSearchOptions { max_file_size: max_file_size.unwrap_or(10 * 1024 * 1024), max_matches_per_file: max_matches_per_file.unwrap_or(200), - smart_case: smart_case.unwrap_or(true), + case_mode, file_offset: file_offset.unwrap_or(0), page_limit: page_size.unwrap_or(50), mode, @@ -473,6 +486,7 @@ pub fn live_grep( classify_definitions: false, trim_whitespace: trim_whitespace.unwrap_or(false), abort_signal: None, + ..Default::default() }; let result = picker.grep(&parsed_query, &options); diff --git a/crates/fff-python/src/finder.rs b/crates/fff-python/src/finder.rs index e61232fc..78106e65 100644 --- a/crates/fff-python/src/finder.rs +++ b/crates/fff-python/src/finder.rs @@ -5,8 +5,8 @@ use fff::file_picker::FilePicker; use fff::frecency::FrecencyTracker; use fff::query_tracker::QueryTracker; use fff::{ - FFFMode, FilePickerOptions, FuzzySearchOptions, GrepSearchOptions, PaginationArgs, QueryParser, - SharedFilePicker, SharedFrecency, SharedQueryTracker, + CaseMode, FFFMode, FilePickerOptions, FuzzySearchOptions, GrepSearchOptions, PaginationArgs, + QueryParser, SharedFilePicker, SharedFrecency, SharedQueryTracker, }; use pyo3::prelude::*; use pyo3::types::PyDict; @@ -90,7 +90,11 @@ fn grep_options( GrepSearchOptions { max_file_size: defaulted_u64(max_file_size, defaults.max_file_size), max_matches_per_file: max_matches_per_file as usize, - smart_case, + case_mode: Some(if smart_case { + CaseMode::Smart + } else { + CaseMode::Sensitive + }), file_offset: cursor_offset, page_limit: defaulted_usize(page_limit, defaults.page_limit), mode, @@ -100,6 +104,7 @@ fn grep_options( classify_definitions, trim_whitespace: false, abort_signal: None, + ..Default::default() } } diff --git a/lua/fff/conf.lua b/lua/fff/conf.lua index dcb53e01..d5c3d968 100644 --- a/lua/fff/conf.lua +++ b/lua/fff/conf.lua @@ -56,6 +56,7 @@ local M = {} --- @field max_file_size number --- @field max_matches_per_file number --- @field smart_case boolean +--- @field case_mode? "smart"|"sensitive"|"insensitive" --- @field time_budget_ms number --- @field modes string[] --- @field trim_whitespace boolean @@ -414,7 +415,8 @@ local function init() grep = { max_file_size = 10 * 1024 * 1024, -- Skip files larger than 10MB max_matches_per_file = 100, -- Maximum matches per file (set 0 to unlimited) - smart_case = true, -- Case-insensitive unless query has uppercase + smart_case = true, -- Case-insensitive unless query has uppercase (legacy; prefer case_mode) + case_mode = nil, -- Optional: "smart" | "sensitive" | "insensitive". Overrides smart_case when set. time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit) modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order trim_whitespace = false, -- Strip leading whitespace from matched lines (useful for cleaner display) diff --git a/lua/fff/main.lua b/lua/fff/main.lua index 1418a2d7..59eee551 100644 --- a/lua/fff/main.lua +++ b/lua/fff/main.lua @@ -302,6 +302,7 @@ end --- @field max_file_size? number Skip files larger than N bytes (default: config.grep.max_file_size). --- @field max_matches_per_file? number Cap matches per file, 0 = unlimited (default: config.grep.max_matches_per_file). --- @field smart_case? boolean Case-insensitive when query is all lowercase (default: config.grep.smart_case). +--- @field case_mode? "smart"|"sensitive"|"insensitive" Explicit case mode (default: config.grep.case_mode). Overrides smart_case. --- @field page_size? number Max matches returned (default: 50). --- @field file_offset? number File-based pagination offset (default: 0). --- @field time_budget_ms? number Max wall-clock time, 0 = unlimited (default: config.grep.time_budget_ms). @@ -348,6 +349,7 @@ function M.content_search(query, opts) max_file_size = opts.max_file_size or grep_cfg.max_file_size, max_matches_per_file = opts.max_matches_per_file or grep_cfg.max_matches_per_file, smart_case = opts.smart_case == nil and grep_cfg.smart_case or opts.smart_case, + case_mode = opts.case_mode == nil and grep_cfg.case_mode or opts.case_mode, time_budget_ms = opts.time_budget_ms or grep_cfg.time_budget_ms, trim_whitespace = opts.trim_whitespace == nil and grep_cfg.trim_whitespace or opts.trim_whitespace, } diff --git a/lua/fff/picker_ui/grep_renderer.lua b/lua/fff/picker_ui/grep_renderer.lua index ef022afc..db20acc7 100644 --- a/lua/fff/picker_ui/grep_renderer.lua +++ b/lua/fff/picker_ui/grep_renderer.lua @@ -40,7 +40,8 @@ function M.search(query, file_offset, page_size, config, grep_mode) conf.smart_case, grep_mode or 'plain', conf.time_budget_ms, - conf.trim_whitespace + conf.trim_whitespace, + conf.case_mode ) return last_result end