Compare commits

...

1 Commits

Author SHA1 Message Date
Dmitriy Kovalenko fe4bcaee44 feat: Correctly handle change of cwd automatically and via API
closes https://github.com/dmtrKovalenko/fff.nvim/issues/20

This allows to use

```
require('fff.main').find_files_in_dir('/Users/neogoose/dev/lightsource')
```

to change cwd and start the picker immediately to choose the file.
2025-08-03 00:18:38 +02:00
6 changed files with 105 additions and 82 deletions
+1 -1
View File
@@ -127,7 +127,7 @@ end
function M.display_image_info(file_path, bufnr, reason)
local info = {}
local stat = vim.loop.fs_stat(file_path)
local stat = vim.uv.fs_stat(file_path)
if stat then
table.insert(info, string.format('📁 File: %s', vim.fn.fnamemodify(file_path, ':t')))
table.insert(info, string.format('📏 Size: %d bytes', stat.size))
+2 -2
View File
@@ -157,7 +157,7 @@ end
--- @param file_path string Path to the file
--- @return table File information
function M.get_file_info(file_path)
local stat = vim.loop.fs_stat(file_path)
local stat = vim.uv.fs_stat(file_path)
if not stat then return nil end
local info = {
@@ -554,7 +554,7 @@ function M.preview(file_path, bufnr, file)
M.state.current_file = file_path
M.state.bufnr = bufnr
local stat = vim.loop.fs_stat(file_path)
local stat = vim.uv.fs_stat(file_path)
if not stat then
M.clear_buffer_completely(bufnr)
safe_set_buffer_lines(bufnr, 0, -1, false, {
+1
View File
@@ -15,6 +15,7 @@ M.get_keyword_range = rust_module.get_keyword_range
M.guess_edit_range = rust_module.guess_edit_range
M.get_words = rust_module.get_words
M.init_file_picker = rust_module.init_file_picker
M.restart_index_in_path = rust_module.restart_index_in_path
M.scan_files = rust_module.scan_files
M.get_cached_files = rust_module.get_cached_files
M.fuzzy_search_files = rust_module.fuzzy_search_files
+66 -77
View File
@@ -126,7 +126,7 @@ function M.setup_global_file_tracking()
if file_path and file_path ~= '' and not vim.startswith(file_path, 'term://') then
-- never block the UI
vim.schedule(function()
local stat = vim.loop.fs_stat(file_path)
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)
@@ -136,6 +136,26 @@ function M.setup_global_file_tracking()
end,
desc = 'Track file access for FFF frecency',
})
-- 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
vim.api.nvim_create_autocmd('DirChanged', {
group = group,
callback = function()
local new_cwd = vim.v.event.cwd
if M.is_initialized() and new_cwd and new_cwd ~= M.config.base_path then
vim.schedule(function()
local ok, err = pcall(M.change_indexing_directory, new_cwd)
if not ok then
vim.notify('FFF: Failed to change indexing directory: ' .. tostring(err), vim.log.levels.ERROR)
else
M.config.base_path = new_cwd
end
end)
end
end,
desc = 'Automatically sync FFF directory changes',
})
end
function M.setup_commands()
@@ -234,12 +254,7 @@ function M.find_in_git_root()
return
end
local picker_ok, picker_ui = pcall(require, 'fff.picker_ui')
if picker_ok then
picker_ui.open({ title = 'Git Files', cwd = git_root })
else
vim.notify('Failed to load picker UI', vim.log.levels.ERROR)
end
M.find_files_in_dir(git_root)
end
--- Trigger rescan of files in the current directory
@@ -331,76 +346,6 @@ function M.get_preview(file_path)
return table.concat(lines, '\n')
end
function M.debug_file_ordering()
print('FFF Debug File Ordering')
print('=======================')
if not M.is_initialized() then
print('File picker not initialized. Run :FFFScan first.')
return
end
local picker = M.picker or M.core
print('Getting top 10 files with debug info...')
-- Enable debug mode temporarily
local old_debug = M.config.debug.show_scores
M.config.debug.show_scores = true
-- Search with empty query to get default ordering
local files = picker.search_files('', 10)
print('🏆 TOP FILES (in order they appear):')
print('=' .. string.rep('=', 70))
for i, file in ipairs(files) do
local frecency_stars = ''
if file.frecency_score > 0 then frecency_stars = '' .. file.frecency_score end
-- Extract directory information
local dir = vim.fn.fnamemodify(file.relative_path, ':h')
local filename = vim.fn.fnamemodify(file.relative_path, ':t')
local dir_display = (dir == '.' or dir == '') and 'root' or dir
-- Get score information for this file
local score = picker.get_file_score(i)
print(string.format('%2d. %s%s', i, filename, frecency_stars))
print(string.format(' Path: %s/', dir_display))
print(string.format(' Debug: %s', score and score.match_type or 'no debug info'))
if score then
print(
string.format(
' Total Score: %d (base=%d, name_bonus=%d, special_bonus=%d, frec=%d, dist=%d)',
score.total,
score.base_score,
score.filename_bonus,
score.special_filename_bonus,
score.frecency_boost,
score.distance_penalty
)
)
else
print(' Total Score: N/A (no score data)')
end
local now = os.time()
local age_hours = math.floor((now - file.modified) / 3600)
local age_days = math.floor(age_hours / 24)
print(string.format(' Age: %d hours (%d days) since last modified', age_hours, age_days))
print('')
end
print('💡 EXPLANATION:')
print('• Files are sorted by FRECENCY first (⭐ score), then by modification time')
print('• Frecency combines how often AND how recently you accessed files')
print('• The file at #1 has either:')
print(' - Highest frecency score, OR')
print(' - Same frecency as others but most recent modification')
M.config.debug.show_scores = old_debug
end
function M.health_check()
local health = {
ok = true,
@@ -460,4 +405,48 @@ end
function M.is_initialized() return M.state and M.state.initialized or false end
--- Find files in a specific directory
--- @param directory string Directory path to search in
function M.find_files_in_dir(directory)
if not directory then
vim.notify('Directory path required for find_files_in_dir', vim.log.levels.ERROR)
return
end
M.change_indexing_directory(directory)
local picker_ok, picker_ui = pcall(require, 'fff.picker_ui')
if picker_ok then
picker_ui.open({ title = 'Files in ' .. vim.fn.fnamemodify(directory, ':t') })
else
vim.notify('Failed to load picker UI', vim.log.levels.ERROR)
end
end
--- Change the base directory for the file picker
--- @param new_path string New directory path to use as base
--- @return boolean `true` if successful, `false` otherwise
function M.change_indexing_directory(new_path)
if not new_path or new_path == '' then
vim.notify('Directory path is required', vim.log.levels.ERROR)
return false
end
local expanded_path = vim.fn.expand(new_path)
if vim.fn.isdirectory(expanded_path) ~= 1 then
vim.notify('Directory does not exist: ' .. expanded_path, vim.log.levels.ERROR)
return false
end
local ok, result = pcall(fuzzy.restart_index_in_path, expanded_path)
if not ok then
vim.notify('Failed to change directory: ' .. result, vim.log.levels.ERROR)
return false
end
M.config.base_path = expanded_path
return true
end
return M
-2
View File
@@ -930,7 +930,6 @@ function M.scroll_preview_down()
preview.scroll(scroll_lines)
end
--- Select current item
function M.select(action)
if not M.state.active then return end
@@ -960,7 +959,6 @@ function M.select(action)
end
end
--- Close picker
function M.close()
if not M.state.active then return end
+35
View File
@@ -46,6 +46,37 @@ pub fn init_file_picker(_: &Lua, base_path: String) -> LuaResult<bool> {
Ok(true)
}
fn reinit_file_picker_internal(path: std::path::PathBuf) -> Result<(), Error> {
let mut file_picker = FILE_PICKER.write().map_err(|_| Error::AcquireItemLock)?;
// drop should clean it anyway but just to be extra sure
if let Some(picker) = file_picker.take() {
picker.stop_background_monitor();
}
let new_picker = FilePicker::new(path.to_string_lossy().to_string())?;
*file_picker = Some(new_picker);
Ok(())
}
pub fn restart_index_in_path(_: &Lua, new_path: String) -> LuaResult<bool> {
let path = std::path::PathBuf::from(&new_path);
if !path.exists() {
return Err(LuaError::RuntimeError(format!(
"Path does not exist: {}",
new_path
)));
}
let canonical_path = path.canonicalize().map_err(|e| {
LuaError::RuntimeError(format!("Failed to canonicalize path '{}': {}", new_path, e))
})?;
reinit_file_picker_internal(canonical_path)?;
Ok(true)
}
pub fn scan_files(_: &Lua, _: ()) -> LuaResult<()> {
let file_picker = FILE_PICKER.read().map_err(|_| Error::AcquireItemLock)?;
let picker = file_picker
@@ -163,6 +194,10 @@ fn create_exports(lua: &Lua) -> LuaResult<LuaTable> {
exports.set("init_db", lua.create_function(init_db)?)?;
exports.set("destroy_db", lua.create_function(destroy_db)?)?;
exports.set("init_file_picker", lua.create_function(init_file_picker)?)?;
exports.set(
"restart_index_in_path",
lua.create_function(restart_index_in_path)?,
)?;
exports.set("scan_files", lua.create_function(scan_files)?)?;
exports.set("get_cached_files", lua.create_function(get_cached_files)?)?;
exports.set(