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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ require('fff').setup({
},
git = {
status_text_color = false, -- true to color filenames by git status
support_submodules = true, -- walk into submodules and report their git status; set false to skip them
},
grep = {
max_file_size = 10 * 1024 * 1024,
Expand Down
30 changes: 30 additions & 0 deletions crates/fff-c/include/fff.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ struct FffResult *fff_create_instance(const char *base_path,
* * `frecency_db_path` – frecency LMDB database path (NULL/empty to skip)
* * `history_db_path` – query history LMDB database path (NULL/empty to skip)
* * `use_unsafe_no_lock` – **deprecated, ignored.** Previously enabled
* `MDB_NOLOCK|MDB_NOSYNC|MDB_NOMETASYNC` for LMDB; benchmarks showed no
* measurable win under realistic contention, so the flag is now a no-op.
* The parameter remains in the signature for ABI compatibility and will be
* removed in a future release.
* * `enable_mmap_cache` – pre-populate mmap caches after the initial scan
* * `enable_content_indexing` – build content index after the initial scan
* * `watch` – start a background file-system watcher for live updates
Expand Down Expand Up @@ -413,6 +417,32 @@ struct FffResult *fff_create_instance2(const char *base_path,
uint64_t cache_budget_max_bytes,
uint64_t cache_budget_max_file_size);

/**
* Create a new file finder instance (v3, with submodule support toggle).
*
* Identical to [`fff_create_instance2`] except for the trailing
* `support_submodules` flag. When `true` (recommended default), submodule
* directories are walked and reported in git status. When `false`, submodule
* paths are skipped during traversal and excluded from git status.
*
* ## Safety
* String parameters must be valid null-terminated UTF-8 or NULL.
*/
struct FffResult *fff_create_instance3(const char *base_path,
const char *frecency_db_path,
const char *history_db_path,
bool _use_unsafe_no_lock,
bool enable_mmap_cache,
bool enable_content_indexing,
bool watch,
bool ai_mode,
const char *log_file_path,
const char *log_level,
uint64_t cache_budget_max_files,
uint64_t cache_budget_max_bytes,
uint64_t cache_budget_max_file_size,
bool support_submodules);

/**
* Destroy a file finder instance and free all its resources.
*
Expand Down
150 changes: 137 additions & 13 deletions crates/fff-c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,121 @@ pub unsafe extern "C" fn fff_create_instance2(
watch,
mode,
cache_budget,
support_submodules: true,
},
) {
return FffResult::err(&format!("Failed to init file picker: {}", e));
}

let instance = Box::new(FffInstance {
picker: shared_picker,
frecency: shared_frecency,
query_tracker,
});

let fff_handle = Box::into_raw(instance) as *mut c_void;
FffResult::ok_handle(fff_handle)
}

/// Create a new file finder instance (v3, with submodule support toggle).
///
/// Identical to [`fff_create_instance2`] except for the trailing
/// `support_submodules` flag. When `true` (recommended default), submodule
/// directories are walked and reported in git status. When `false`, submodule
/// paths are skipped during traversal and excluded from git status.
///
/// ## Safety
/// String parameters must be valid null-terminated UTF-8 or NULL.
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn fff_create_instance3(
base_path: *const c_char,
frecency_db_path: *const c_char,
history_db_path: *const c_char,
_use_unsafe_no_lock: bool,
enable_mmap_cache: bool,
enable_content_indexing: bool,
watch: bool,
ai_mode: bool,
log_file_path: *const c_char,
log_level: *const c_char,
cache_budget_max_files: u64,
cache_budget_max_bytes: u64,
cache_budget_max_file_size: u64,
support_submodules: bool,
) -> *mut FffResult {
let base_path_str = match unsafe { cstr_to_str(base_path) } {
Some(s) if !s.is_empty() => s.to_string(),
_ => return FffResult::err("base_path is null or empty"),
};

if let Some(log_path) = unsafe { optional_cstr(log_file_path) } {
let level = unsafe { optional_cstr(log_level) };
if let Err(e) = fff::log::init_tracing(log_path, level) {
return FffResult::err(&format!("Failed to init tracing: {}", e));
}
}

let frecency_path = unsafe { optional_cstr(frecency_db_path) }.map(|s| s.to_string());
let history_path = unsafe { optional_cstr(history_db_path) }.map(|s| s.to_string());

let shared_picker = SharedFilePicker::default();
let shared_frecency = SharedFrecency::default();
let query_tracker = SharedQueryTracker::default();

if let Some(ref frecency_path) = frecency_path {
if let Some(parent) = PathBuf::from(frecency_path).parent() {
let _ = std::fs::create_dir_all(parent);
}

match FrecencyTracker::open(frecency_path) {
Ok(tracker) => {
if let Err(e) = shared_frecency.init(tracker) {
return FffResult::err(&format!("Failed to acquire frecency lock: {}", e));
}
}
Err(e) => return FffResult::err(&format!("Failed to init frecency db: {}", e)),
}
}

if let Some(ref history_path) = history_path {
if let Some(parent) = PathBuf::from(history_path).parent() {
let _ = std::fs::create_dir_all(parent);
}

match QueryTracker::open(history_path) {
Ok(tracker) => {
if let Err(e) = query_tracker.init(tracker) {
return FffResult::err(&format!("Failed to acquire query tracker lock: {}", e));
}
}
Err(e) => return FffResult::err(&format!("Failed to init query tracker db: {}", e)),
}
}

let mode = if ai_mode {
FFFMode::Ai
} else {
FFFMode::Neovim
};

let cache_budget = fff::ContentCacheBudget::from_overrides(
cache_budget_max_files as usize,
cache_budget_max_bytes,
cache_budget_max_file_size,
);

if let Err(e) = FilePicker::new_with_shared_state(
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path: base_path_str,
enable_mmap_cache,
enable_content_indexing,
watch,
mode,
cache_budget,
support_submodules,
},
) {
return FffResult::err(&format!("Failed to init file picker: {}", e));
Expand Down Expand Up @@ -901,19 +1016,27 @@ pub unsafe extern "C" fn fff_restart_index(
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
};

let (warmup_caches, content_indexing, watch, mode) = if let Some(mut picker) = guard.take() {
let warmup = picker.has_mmap_cache();
let enable_content_indexing = picker.has_content_indexing();
let watch = picker.has_watcher();
let mode = picker.mode();

picker.stop_background_monitor();

(warmup, enable_content_indexing, watch, mode)
} else {
// this is error state anyway
(false, true, true, FFFMode::default())
};
let (warmup_caches, content_indexing, watch, mode, support_submodules) =
if let Some(mut picker) = guard.take() {
let warmup = picker.has_mmap_cache();
let enable_content_indexing = picker.has_content_indexing();
let watch = picker.has_watcher();
let mode = picker.mode();
let support_submodules = picker.has_submodule_support();

picker.stop_background_monitor();

(
warmup,
enable_content_indexing,
watch,
mode,
support_submodules,
)
} else {
// this is error state anyway
(false, true, true, FFFMode::default(), true)
};

drop(guard);

Expand All @@ -927,6 +1050,7 @@ pub unsafe extern "C" fn fff_restart_index(
watch,
mode,
cache_budget: None,
support_submodules,
},
) {
Ok(()) => FffResult::ok_empty(),
Expand Down
29 changes: 21 additions & 8 deletions crates/fff-core/src/background_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ impl BackgroundWatcher {
shared_picker: SharedFilePicker,
shared_frecency: SharedFrecency,
mode: FFFMode,
support_submodules: bool,
) -> Result<Self, Error> {
info!(
"Initializing background watcher for path: {}, mode: {:?}",
Expand Down Expand Up @@ -93,6 +94,7 @@ impl BackgroundWatcher {
mode,
use_recursive,
watch_tx_for_debouncer,
support_submodules,
)?;

info!("Background file watcher initialized successfully");
Expand Down Expand Up @@ -145,6 +147,7 @@ impl BackgroundWatcher {
&strong_picker,
&owner_frecency,
&owner_git_workdir,
support_submodules,
);

// Transient strong ref drops here, back
Expand All @@ -162,6 +165,7 @@ impl BackgroundWatcher {
})
}

#[allow(clippy::too_many_arguments)]
fn create_debouncer(
base_path: PathBuf,
git_workdir: Option<PathBuf>,
Expand All @@ -170,6 +174,7 @@ impl BackgroundWatcher {
mode: FFFMode,
use_recursive: bool,
watch_tx: mpsc::Sender<PathBuf>,
support_submodules: bool,
) -> Result<Debouncer, Error> {
let config = Config::default()
// do not follow symlinks as then notifiers spawns a bunch of events for symlinked
Expand Down Expand Up @@ -212,6 +217,7 @@ impl BackgroundWatcher {
&strong_picker,
&shared_frecency,
mode,
support_submodules,
);

// every new directory creates had to be reflected in the picker state
Expand Down Expand Up @@ -399,6 +405,7 @@ fn handle_debounced_events(
shared_picker: &SharedFilePicker,
shared_frecency: &SharedFrecency,
mode: FFFMode,
support_submodules: bool,
) -> Vec<PathBuf> {
// this will be called very often, we have to minimiy the lock time for file picker
let repo = git_workdir.as_ref().and_then(|p| Repository::open(p).ok());
Expand Down Expand Up @@ -666,7 +673,11 @@ fn handle_debounced_events(
files_to_update_git_status.len()
);

let status = match GitStatusCache::git_status_for_paths(repo, &files_to_update_git_status) {
let status = match GitStatusCache::git_status_for_paths(
repo,
&files_to_update_git_status,
support_submodules,
) {
Ok(status) => status,
Err(e) => {
tracing::error!(?e, "Failed to query git status");
Expand Down Expand Up @@ -697,6 +708,7 @@ fn track_files_from_new_directories(
shared_picker: &SharedFilePicker,
shared_frecency: &SharedFrecency,
git_workdir: &Option<PathBuf>,
support_submodules: bool,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
Expand Down Expand Up @@ -733,13 +745,14 @@ fn track_files_from_new_directories(
}

if let Some(repo) = repo.as_ref() {
let status = match GitStatusCache::git_status_for_paths(repo, &files_to_add) {
Ok(status) => status,
Err(e) => {
tracing::error!(?e, "inject_existing_files: git status query failed");
return;
}
};
let status =
match GitStatusCache::git_status_for_paths(repo, &files_to_add, support_submodules) {
Ok(status) => status,
Err(e) => {
tracing::error!(?e, "inject_existing_files: git status query failed");
return;
}
};

if let Ok(mut guard) = shared_picker.write()
&& let Some(ref mut picker) = *guard
Expand Down
Loading
Loading