Skip to content
Merged
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 lua/fff/fuzzy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ M.get_scan_progress = rust_module.get_scan_progress
M.is_scanning = rust_module.is_scanning
M.refresh_git_status = rust_module.refresh_git_status
M.stop_background_monitor = rust_module.stop_background_monitor
M.cleanup_file_picker = rust_module.cleanup_file_picker
M.init_tracing = rust_module.init_tracing
M.wait_for_initial_scan = rust_module.wait_for_initial_scan

Expand Down
49 changes: 28 additions & 21 deletions lua/fff/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ function M.setup(config)
M.config = merged_config

M.setup_commands()

if merged_config.frecency.enabled then M.setup_global_file_tracking() end
M.setup_global_autocmds()

local git_utils = require('fff.git_utils')
git_utils.setup_highlights()
Expand All @@ -126,27 +125,29 @@ function M.setup(config)
return true
end

function M.setup_global_file_tracking()
function M.setup_global_autocmds()
local group = vim.api.nvim_create_augroup('fff_file_tracking', { clear = true })

vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, {
group = group,
callback = function(args)
local file_path = args.file

if file_path and file_path ~= '' and not vim.startswith(file_path, 'term://') then
-- never block the UI
vim.schedule(function()
local stat = vim.uv.fs_stat(file_path)
if stat and stat.type == 'file' then
local relative_path = vim.fn.fnamemodify(file_path, ':.')
pcall(fuzzy.access_file, relative_path)
end
end)
end
end,
desc = 'Track file access for FFF frecency',
})
if M.config.frecency.enabled then
vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, {
group = group,
callback = function(args)
local file_path = args.file

if file_path and file_path ~= '' and not vim.startswith(file_path, 'term://') then
-- never block the UI
vim.schedule(function()
local stat = vim.uv.fs_stat(file_path)
if stat and stat.type == 'file' then
local relative_path = vim.fn.fnamemodify(file_path, ':.')
pcall(fuzzy.access_file, relative_path)
end
end)
end
end,
desc = 'Track file access for FFF frecency',
})
end

-- make sure that this won't work correctly if autochdir plugins are enabled
-- using a pure :cd command but will work using lua api or :e command
Expand All @@ -167,6 +168,12 @@ function M.setup_global_file_tracking()
end,
desc = 'Automatically sync FFF directory changes',
})

vim.api.nvim_create_autocmd('VimLeavePre', {
group = group,
callback = function() pcall(fuzzy.cleanup_file_picker) end,
desc = 'Cleanup FFF background threads on Neovim exit',
})
end

function M.setup_commands()
Expand Down
31 changes: 13 additions & 18 deletions lua/fff/rust/error.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Thread panicked")]
ThreadPanic,
#[error("Invalid path {0}")]
InvalidPath(std::path::PathBuf),
#[error("File picker not initialized")]
FilePickerMissing,
#[error("Failed to acquire lock for frecency")]
AcquireFrecencyLock,

#[error("Failed to acquire lock for items by provider")]
AcquireItemLock,

#[error("Attempted to use frecency before initialization")]
UseFrecencyBeforeInit,

#[error(
"Attempted to fuzzy match for provider {provider_id} before setting the provider's items"
)]
FuzzyBeforeSetItems { provider_id: String },

#[error("Failed to create frecency database directory: {0}")]
CreateDir(#[source] std::io::Error),
CreateDir(#[from] std::io::Error),
#[error("Failed to open frecency database env: {0}")]
EnvOpen(#[source] heed::Error),
#[error("Failed to create frecency database: {0}")]
Expand All @@ -35,16 +31,15 @@ pub enum Error {
DbWrite(#[source] heed::Error),
#[error("Failed to commit write transaction to frecency database: {0}")]
DbCommit(#[source] heed::Error),

#[error("Invalid file path: {0}")]
InvalidPath(String),

#[error("Failed to scan directory: {0}")]
DirectoryScan(String),
#[error("Failed to start file system watcher: {0}")]
FileSystemWatch(#[from] notify::Error),
}

impl From<Error> for mlua::Error {
fn from(value: Error) -> Self {
mlua::Error::RuntimeError(value.to_string())
let string_value = value.to_string();

::tracing::error!(string_value);
mlua::Error::RuntimeError(string_value)
}
}
147 changes: 88 additions & 59 deletions lua/fff/rust/file_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
Arc, Mutex, RwLock,
};
use std::thread;
use std::time::{Duration, SystemTime};
Expand All @@ -27,6 +27,11 @@ struct FileSync {
scan_generation: u64,
}

type Debouncer = notify_debouncer_full::Debouncer<
notify::RecommendedWatcher,
notify_debouncer_full::RecommendedCache,
>;

impl FileSync {
fn new() -> Self {
Self {
Expand Down Expand Up @@ -170,9 +175,8 @@ pub struct FilePicker {
base_path: PathBuf,
git_workdir: Option<PathBuf>,
sync_data: Arc<RwLock<FileSync>>,
shutdown_signal: Arc<AtomicBool>,
is_scanning: Arc<AtomicBool>,
_background_handle: Option<thread::JoinHandle<()>>,
_debouncer: Arc<Mutex<Option<Debouncer>>>,
}

impl std::fmt::Debug for FilePicker {
Expand All @@ -190,7 +194,7 @@ impl FilePicker {
let path = PathBuf::from(&base_path);
if !path.exists() {
error!("Base path does not exist: {}", base_path);
return Err(Error::InvalidPath(path.to_string_lossy().into_owned()));
return Err(Error::InvalidPath(path));
}

let git_workdir = Repository::discover(&path)
Expand All @@ -204,25 +208,20 @@ impl FilePicker {
}

let sync_data = Arc::new(RwLock::new(FileSync::new()));
let shutdown = Arc::new(AtomicBool::new(false));
let scan_signal = Arc::new(AtomicBool::new(false));
let debouncer_holder = Arc::new(Mutex::new(None));

let picker = Self {
base_path: path.clone(),
git_workdir: git_workdir.clone(),
sync_data: Arc::clone(&sync_data),
is_scanning: Arc::clone(&scan_signal),
_debouncer: Arc::clone(&debouncer_holder),
};

let background_handle = spawn_background_watcher(
path.clone(),
git_workdir.clone(),
Arc::clone(&sync_data),
Arc::clone(&shutdown),
Arc::clone(&scan_signal),
);
spawn_async_initialization(path, git_workdir, sync_data, scan_signal, debouncer_holder);

Ok(Self {
base_path: path,
git_workdir,
sync_data,
shutdown_signal: shutdown,
is_scanning: scan_signal,
_background_handle: Some(background_handle),
})
Ok(picker)
}

pub fn fuzzy_search(
Expand Down Expand Up @@ -322,6 +321,16 @@ impl FilePicker {
self.get_cached_files()
}

pub fn stop_background_monitor(&self) -> Result<(), Error> {
if let Ok(mut debouncer_guard) = self._debouncer.lock() {
if let Some(debouncer) = debouncer_guard.take() {
debouncer.stop_nonblocking();
info!("File watcher stopped successfully");
}
}
Ok(())
}

pub fn trigger_rescan(&self) -> Result<(), Error> {
if self.is_scanning.load(Ordering::Relaxed) {
debug!("Scan already in progress, skipping trigger_rescan");
Expand Down Expand Up @@ -358,10 +367,6 @@ impl FilePicker {
pub fn is_scan_active(&self) -> bool {
self.is_scanning.load(Ordering::Relaxed)
}

pub fn stop_background_monitor(&self) {
self.shutdown_signal.store(true, Ordering::Relaxed);
}
}

#[allow(unused)]
Expand All @@ -372,21 +377,21 @@ pub struct ScanProgress {
pub is_scanning: bool,
}

fn spawn_background_watcher(
fn spawn_async_initialization(
base_path: PathBuf,
git_workdir: Option<PathBuf>,
sync_data: Arc<RwLock<FileSync>>,
shutdown: Arc<AtomicBool>,
scan_signal: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
debouncer_holder: Arc<Mutex<Option<Debouncer>>>,
) {
thread::spawn(move || {
scan_signal.store(true, Ordering::Relaxed);
info!("starting background watcher thread");
info!("Starting async initialization for file picker");

match scan_filesystem(&base_path, git_workdir.as_ref()) {
Ok((files, git_cache)) => {
info!(
"Initial parallel filesystem scan completed: found {} files",
"Initial filesystem scan completed: found {} files",
files.len()
);
if let Ok(mut data) = sync_data.write() {
Expand All @@ -395,43 +400,57 @@ fn spawn_background_watcher(
}
}
Err(e) => {
error!("Failed to scan filesystem: {:?}", e);
error!("Initial scan failed: {:?}", e);
}
}

scan_signal.store(false, Ordering::Relaxed);
Comment thread
dmtrKovalenko marked this conversation as resolved.
error!("is_scanning = FALSE (initial scan completed)");

let mut debouncer = match new_debouncer(Duration::from_millis(500), None, {
let sync_data = Arc::clone(&sync_data);
let base_path = base_path.clone();
let git_workdir = git_workdir.clone();

move |result: DebounceEventResult| match result {
Ok(events) => {
handle_debounced_events(events, &sync_data, &base_path, &git_workdir);
}
Err(errors) => {
error!("File watcher errors: {:?}", errors);
match create_file_watcher_sync(base_path, git_workdir, Arc::clone(&sync_data)) {
Ok(debouncer) => {
if let Ok(mut holder) = debouncer_holder.lock() {
*holder = Some(debouncer);
info!("File watcher setup completed successfully");
} else {
error!("Failed to store debouncer - mutex poisoned");
}
}
}) {
Ok(debouncer) => debouncer,
Err(e) => {
error!("Failed to create debouncer: {:?}", e);
return;
error!("Failed to create file watcher: {:?}", e);
}
};

if let Err(e) = debouncer.watch(&base_path, RecursiveMode::Recursive) {
error!("Failed to start watching: {:?}", e);
return;
}
});
}

while !shutdown.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(100));
fn create_file_watcher_sync(
base_path: PathBuf,
git_workdir: Option<PathBuf>,
sync_data: Arc<RwLock<FileSync>>,
) -> Result<Debouncer, Error> {
let mut debouncer = new_debouncer(Duration::from_millis(500), None, {
let sync_data = Arc::clone(&sync_data);
let base_path = base_path.clone();
let git_workdir = git_workdir.clone();

move |result: DebounceEventResult| match result {
Ok(events) => {
handle_debounced_events(events, &sync_data, &base_path, &git_workdir);
}
Err(errors) => {
error!("File watcher errors: {:?}", errors);
}
}
})
})?;

if let Err(e) = debouncer.watch(&base_path, RecursiveMode::Recursive) {
error!(
"Failed to start watching path: {}, error {e:?}",
base_path.display(),
);
return Err(e.into());
}

info!("File watcher started for path: {}", base_path.display());
Ok(debouncer)
}

fn handle_debounced_events(
Expand Down Expand Up @@ -616,9 +635,10 @@ fn scan_filesystem(
let walker_time = walker_start.elapsed();
info!("SCAN: File walking completed in {:?}", walker_time);

let git_cache = git_handle
.join()
.map_err(|_| Error::InvalidPath("Git status thread panicked".to_string()))?;
let git_cache = git_handle.join().map_err(|_| {
error!("Failed to join git status thread");
Error::ThreadPanic
})?;

if let Some(git_cache) = &git_cache {
files.par_iter_mut().for_each(|file| {
Expand Down Expand Up @@ -698,6 +718,15 @@ fn is_git_file(path: &Path) -> bool {

impl Drop for FilePicker {
fn drop(&mut self) {
self.shutdown_signal.store(true, Ordering::Relaxed);
info!("FilePicker is being dropped, stopping file watcher");

if let Ok(mut debouncer_guard) = self._debouncer.lock() {
if let Some(debouncer) = debouncer_guard.take() {
debouncer.stop();
info!("File watcher stopped successfully");
}
} else {
error!("Failed to acquire debouncer lock during drop");
}
}
}
Loading
Loading