Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/fff-c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,7 @@ pub unsafe extern "C" fn fff_live_grep(
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: None,
file_offset: file_offset as usize,
page_limit: default_u32(page_limit, 50) as usize,
mode: grep_mode_from_u8(mode),
Expand Down Expand Up @@ -840,6 +841,7 @@ pub unsafe extern "C" fn fff_multi_grep(
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: None,
file_offset: file_offset as usize,
page_limit: default_u32(page_limit, 50) as usize,
mode: fff::GrepMode::PlainText, // ignored by multi_grep_search
Expand Down
56 changes: 41 additions & 15 deletions crates/fff-core/src/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -335,6 +343,10 @@ pub struct GrepSearchOptions {
pub max_file_size: u64,
pub max_matches_per_file: usize,
pub smart_case: bool,
/// Explicit case mode. When `Some`, overrides `smart_case`. When `None`,
/// `smart_case` is used (true => Smart, false => Sensitive). Non-breaking
/// addition; existing callers can ignore this field.
pub case_mode: Option<CaseMode>,
/// 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.
Expand Down Expand Up @@ -364,12 +376,25 @@ pub struct GrepSearchOptions {
pub abort_signal: Option<Arc<AtomicBool>>,
}

impl GrepSearchOptions {
/// Resolves the effective case mode, preferring explicit `case_mode` over
/// the legacy `smart_case` boolean.
pub fn effective_case_mode(&self) -> CaseMode {
match self.case_mode {
Some(m) => m,
None if self.smart_case => CaseMode::Smart,
None => CaseMode::Sensitive,
}
}
}

impl Default for GrepSearchOptions {
fn default() -> Self {
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(),
Expand Down Expand Up @@ -1045,11 +1070,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()
Expand Down Expand Up @@ -1117,7 +1141,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<regex::bytes::Regex, String> {
fn build_regex(pattern: &str, case_mode: CaseMode) -> Result<regex::bytes::Regex, String> {
if pattern.is_empty() {
return Err("empty pattern".to_string());
}
Expand All @@ -1128,10 +1152,10 @@ fn build_regex(pattern: &str, smart_case: bool) -> Result<regex::bytes::Regex, S
pattern.to_string()
};

let case_insensitive = if smart_case {
!pattern.chars().any(|c| c.is_uppercase())
} else {
false
let case_insensitive = match case_mode {
CaseMode::Smart => !pattern.chars().any(|c| c.is_uppercase()),
CaseMode::Insensitive => true,
CaseMode::Sensitive => false,
};

regex::bytes::RegexBuilder::new(&regex_pattern)
Expand Down Expand Up @@ -1995,10 +2019,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<String> = None;
Expand Down Expand Up @@ -2089,7 +2113,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);

Expand Down Expand Up @@ -2445,6 +2469,7 @@ mod tests {
max_file_size: MAX_FFFILE_SIZE,
max_matches_per_file: 0,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 100,
mode: super::GrepMode::PlainText,
Expand Down Expand Up @@ -2629,6 +2654,7 @@ mod tests {
max_file_size: MAX_FFFILE_SIZE,
max_matches_per_file: 0,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 100,
mode: super::GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/bigram_overlay_coherence_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,7 @@ fn grep_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/bigram_overlay_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ fn grep_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 200,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/fuzz_file_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ fn grep_plain_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode: GrepMode::PlainText,
Expand Down
3 changes: 3 additions & 0 deletions crates/fff-core/tests/fuzz_git_watcher_stress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ fn grep_plain_matches(shared: &SharedFilePicker, query: &str) -> Vec<String> {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode: GrepMode::PlainText,
Expand Down Expand Up @@ -929,6 +930,7 @@ fn grep_fuzzy_matches(shared: &SharedFilePicker, query: &str) -> Vec<String> {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode: GrepMode::Fuzzy,
Expand Down Expand Up @@ -961,6 +963,7 @@ fn grep_regex_matches(shared: &SharedFilePicker, query: &str) -> Vec<String> {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode: GrepMode::Regex,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/fuzz_real_repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ fn grep_opts(mode: GrepMode) -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode,
Expand Down
3 changes: 3 additions & 0 deletions crates/fff-core/tests/grep_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fn plain_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 200,
mode: GrepMode::PlainText,
Expand All @@ -50,6 +51,7 @@ fn regex_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 200,
mode: GrepMode::Regex,
Expand All @@ -68,6 +70,7 @@ fn fuzzy_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 200,
mode: GrepMode::Fuzzy,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/new_directory_watcher_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ fn grep_plain_count(picker: &FilePicker, query: &str) -> usize {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 500,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/path_separator_constraint_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn plain_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 200,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-core/tests/real_binary_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fn plain_opts() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 200,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-mcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fn make_grep_options(
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: matches_per_file,
smart_case: true,
case_mode: None,
file_offset,
page_limit: 50,
mode,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-nvim/benches/fuzzy_search_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ fn bench_grep_search(c: &mut Criterion) {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 0,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 100,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-nvim/benches/grep_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ fn plain_options() -> GrepSearchOptions {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 50,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-nvim/src/bin/bench_grep_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fn run_grep(picker: &FilePicker, query: &str, iters: usize) {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: usize::MAX,
mode: GrepMode::PlainText,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-nvim/src/bin/fuzzy_grep_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fn run_fuzzy_query(picker: &FilePicker, query: &str, label: &str) {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 100,
mode: GrepMode::Fuzzy,
Expand Down
2 changes: 2 additions & 0 deletions crates/fff-nvim/src/bin/grep_profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ impl<'a> GrepBench<'a> {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 50,
mode,
Expand Down Expand Up @@ -410,6 +411,7 @@ fn main() {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset,
page_limit: 50,
mode: Default::default(),
Expand Down
2 changes: 2 additions & 0 deletions crates/fff-nvim/src/bin/grep_vs_rg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ fn run_fff_full(picker: &FilePicker, query: &str) -> (usize, Duration) {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: usize::MAX,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: usize::MAX,
mode: Default::default(),
Expand All @@ -186,6 +187,7 @@ fn run_fff_page(picker: &FilePicker, query: &str) -> (usize, Duration) {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
case_mode: None,
file_offset: 0,
page_limit: 50,
mode: Default::default(),
Expand Down
10 changes: 10 additions & 0 deletions crates/fff-nvim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ pub fn live_grep(
grep_mode,
time_budget_ms,
trim_whitespace,
case_mode,
): (
String,
Option<usize>,
Expand All @@ -446,6 +447,7 @@ pub fn live_grep(
Option<String>,
Option<u64>,
Option<bool>,
Option<String>,
),
) -> LuaResult<LuaValue> {
let file_picker_guard = FILE_PICKER.read().into_lua_result()?;
Expand All @@ -460,10 +462,18 @@ 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),
_ => None,
};

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,
Expand Down
1 change: 1 addition & 0 deletions crates/fff-python/src/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ fn grep_options(
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: None,
file_offset: cursor_offset,
page_limit: defaulted_usize(page_limit, defaults.page_limit),
mode,
Expand Down
4 changes: 3 additions & 1 deletion lua/fff/conf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading