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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ thiserror = "2.0.10"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
strsim = "0.11.0"
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<img alt="Contributors" src="https://img.shields.io/github/contributors/dmtrKovalenko/fff.nvim?color=%23DDB6F2&label=CONTRIBUTORS&logo=git&style=for-the-badge&logoColor=D9E0EE&labelColor=302D41"/></a>
</p>

**FFF** stands for ~freakin fast fuzzy file finder~ (pick 3) and it is an opinionated fuzzy file picker for neovim. Just for files, but we'll try to solve file picking completely.
**FFF** stands for ~freakin fast fuzzy file finder~ (pick 3) and it is an opinionated fuzzy file picker for neovim. Just for files, but we'll try to solve file picking completely.

It comes with a dedicated rust backend runtime that keep tracks of the file index, your file access and modifications, git status, and provides a comprehensive typo-resistant fuzzy search experience.

Expand Down Expand Up @@ -113,6 +113,11 @@ require("fff").setup({
debug = 'Comment',
},

-- Scoring configuration
scoring = {
same_dir_preference = 0.7, -- How much to prefer files near current file (0.0-1.0)
},

-- Debug options
debug = {
show_scores = false, -- Toggle with F2 or :FFFDebug
Expand Down Expand Up @@ -196,6 +201,11 @@ require("fff").setup({
debug = 'Comment',
},

-- Scoring configuration
scoring = {
same_dir_preference = 0.7, -- How much to prefer files near current file (0.0-1.0)
},

debug = {
show_scores = true, -- We hope for your collaboratio
},
Expand Down
67 changes: 11 additions & 56 deletions doc/fff.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ LAZY.NVIM
<


DEFAULT CONFIGURATION ~

FFF.nvim comes with sensible defaults. Here’s the complete default
configuration:
DEFAULT CONFIGURATION *fff.nvim-default-configuration*

>lua
require("fff").setup({
Expand All @@ -93,7 +90,7 @@ configuration:
title = 'FFF Files', -- Window title
max_results = 60, -- Maximum search results to display
max_threads = 4, -- Maximum threads for fuzzy search

-- Key mappings (supports both single keys and arrays for multiple bindings)
keymaps = {
close = '<Esc>',
Expand All @@ -106,7 +103,7 @@ configuration:
preview_scroll_up = '<C-u>',
preview_scroll_down = '<C-d>',
},

-- Highlight groups
hl = {
border = 'FloatBorder',
Expand All @@ -119,7 +116,12 @@ configuration:
frecency = 'Number',
debug = 'Comment',
},


-- Scoring configuration
scoring = {
same_dir_preference = 0.7, -- How much to prefer files near current file (0.0 - 1.0)
},

-- Debug options
debug = {
show_scores = false, -- Toggle with F2 or :FFFDebug
Expand Down Expand Up @@ -160,9 +162,9 @@ Toggle scoring information display:
- Enable by default with `debug.show_scores = true`

>

#### vim-plug

```vim
Plug 'MunifTanjim/nui.nvim'
Plug 'dmtrKovalenko/fff.nvim', { 'do': 'cargo build --release' }
Expand All @@ -171,53 +173,6 @@ Toggle scoring information display:

CONFIGURATION *fff.nvim-configuration*


DEFAULT CONFIGURATION ~

FFF.nvim comes with sensible defaults. Here’s the complete default
configuration:

>lua
require("fff").setup({
-- UI dimensions and appearance
width = 0.8, -- Window width as fraction of screen
height = 0.8, -- Window height as fraction of screen
preview_width = 0.5, -- Preview pane width as fraction of picker
prompt = '🪿 ', -- Input prompt symbol
title = 'FFF Files', -- Window title
max_results = 60, -- Maximum search results to display
max_threads = 4, -- Maximum threads for fuzzy search

keymaps = {
close = '<Esc>',
select = '<CR>',
select_split = '<C-s>',
select_vsplit = '<C-v>',
select_tab = '<C-t>',
move_up = { '<Up>', '<C-p>' }, -- Multiple bindings supported
move_down = { '<Down>', '<C-n>' }, -- Multiple bindings supported
preview_scroll_up = '<C-u>',
preview_scroll_down = '<C-d>',
},

hl = {
border = 'FloatBorder',
normal = 'Normal',
cursor = 'CursorLine',
matched = 'IncSearch',
title = 'Title',
prompt = 'Question',
active_file = 'Visual',
frecency = 'Number',
debug = 'Comment',
},

debug = {
show_scores = true, -- We hope for your collaboratio
},
})
<

Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>

vim:tw=78:ts=8:noet:ft=help:norl:
44 changes: 44 additions & 0 deletions lua/fff/config_utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
local M = {}

M.DEFAULT_SAME_DIR_PREFERENCE = 0.7
M.DEFAULT_SCORING_CONFIG = {
same_dir_preference = M.DEFAULT_SAME_DIR_PREFERENCE,
}

--- @param config table Configuration table to validate
--- @param default_value number Default value to use if invalid
--- @return boolean True if value was valid, false if it was corrected
function M.validate_same_dir_preference(config, default_value)
default_value = default_value or M.DEFAULT_SAME_DIR_PREFERENCE

if not config.scoring or not config.scoring.same_dir_preference then return true end

local preference = config.scoring.same_dir_preference
if preference < 0.0 or preference > 1.0 then
vim.notify(
string.format(
"Invalid 'scoring.same_dir_preference' (%g). Must be between 0.0 and 1.0. Using default (%.1f).",
preference,
default_value
),
vim.log.levels.WARN
)
config.scoring.same_dir_preference = default_value
return false
end

return true
end

--- @param preference number User preference value between 0.0 and 1.0
--- @return table Internal scoring parameters
function M.map_preference_to_scoring(preference)
return {
directory_distance_penalty = -8, -- Balanced penalty for different directories
filename_similarity_bonus_max = math.floor(50 * preference), -- Moderate sibling bonus (35 with default 0.7)
filename_similarity_threshold = 0.5, -- Good relevance/performance balance
max_search_directory_levels = math.floor(1 + 3 * preference),
}
end

return M
23 changes: 22 additions & 1 deletion lua/fff/file_picker/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,17 @@ function M.setup(config)
height = 0.8,
width = 0.8,
},
scoring = {
same_dir_preference = require('fff.config_utils').DEFAULT_SAME_DIR_PREFERENCE,
},
}

M.config = vim.tbl_deep_extend('force', defaults, config)

local config_utils = require('fff.config_utils')
local internal_scoring = config_utils.map_preference_to_scoring(M.config.scoring.same_dir_preference)
M.config.scoring = vim.tbl_extend('force', M.config.scoring, internal_scoring)

M.state.config = M.config

local db_path = vim.fn.stdpath('cache') .. '/fff_nvim'
Expand Down Expand Up @@ -105,7 +113,19 @@ function M.search_files(query, max_results, max_threads, current_file)
max_results = max_results or M.config.max_results
max_threads = max_threads or M.config.max_threads

local ok, search_result = pcall(fuzzy.fuzzy_search_files, query, max_results, max_threads, current_file)
local distance_penalty = M.config.scoring.directory_distance_penalty
local relation_bonus_max = M.config.scoring.filename_similarity_bonus_max
local relation_similarity_threshold = M.config.scoring.filename_similarity_threshold
local ok, search_result = pcall(
fuzzy.fuzzy_search_files,
query,
max_results,
max_threads,
current_file,
distance_penalty,
relation_bonus_max,
relation_similarity_threshold
)
if not ok then
vim.notify('Failed to search files: ' .. tostring(search_result), vim.log.levels.ERROR)
return {}
Expand Down Expand Up @@ -144,6 +164,7 @@ function M.get_file_score(index)
special_filename_bonus = score.special_filename_bonus or 0,
frecency_boost = score.frecency_boost or 0,
distance_penalty = score.distance_penalty or 0,
relation_bonus = score.relation_bonus or 0,
match_type = score.match_type or 'unknown',
}
end
Expand Down
7 changes: 6 additions & 1 deletion lua/fff/file_picker/preview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,12 @@ function M.create_file_info_content(file, info, file_index)
)
table.insert(
lines,
string.format('Score Modifiers: frec_boost=%d, dist_penalty=%d', score.frecency_boost, score.distance_penalty)
string.format(
'Score Modifiers: frec_boost=%d, dist_penalty=%d, rel_bonus=%d',
score.frecency_boost,
score.distance_penalty,
score.relation_bonus
)
)
else
table.insert(lines, 'Score Breakdown: N/A (no score data available)')
Expand Down
13 changes: 11 additions & 2 deletions lua/fff/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,20 @@ function M.setup(config)
icons = {
enabled = true,
},
scoring = {
same_dir_preference = require('fff.config_utils').DEFAULT_SAME_DIR_PREFERENCE,
},
ui_enabled = true,
}

local merged_config = vim.tbl_deep_extend('force', default_config, config or {})
M.config = merged_config

local config_utils = require('fff.config_utils')
config_utils.validate_same_dir_preference(merged_config, config_utils.DEFAULT_SAME_DIR_PREFERENCE)
local internal_scoring = config_utils.map_preference_to_scoring(merged_config.scoring.same_dir_preference)
merged_config.scoring = vim.tbl_extend('force', merged_config.scoring, internal_scoring)

local db_path = merged_config.frecency.db_path or (vim.fn.stdpath('cache') .. '/fff_nvim')
local ok, result = pcall(fuzzy.init_db, db_path, true)
if not ok then vim.notify('Failed to initialize frecency database: ' .. result, vim.log.levels.WARN) end
Expand Down Expand Up @@ -411,13 +419,14 @@ function M.debug_file_ordering()
if score then
print(
string.format(
' Total Score: %d (base=%d, name_bonus=%d, special_bonus=%d, frec=%d, dist=%d)',
' Total Score: %d (base=%d, name_bonus=%d, special_bonus=%d, frec=%d, dist=%d, rel=%d)',
score.total,
score.base_score,
score.filename_bonus,
score.special_filename_bonus,
score.frecency_boost,
score.distance_penalty
score.distance_penalty,
score.relation_bonus
)
)
else
Expand Down
36 changes: 20 additions & 16 deletions lua/fff/picker_ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ function M.create_ui()

preview.set_preview_window(M.state.preview_win)

-- Brief wait to allow immediate sibling files to appear, then render.
file_picker.wait_for_initial_scan(50)
M.update_results_sync()
M.clear_preview()
M.update_status()
Expand Down Expand Up @@ -463,15 +465,6 @@ function M.update_results() M.update_results_sync() end
function M.update_results_sync()
if not M.state.active then return end

if not M.state.current_file_cache then
local current_buf = vim.api.nvim_get_current_buf()
if current_buf and vim.api.nvim_buf_is_valid(current_buf) then
local current_file = vim.api.nvim_buf_get_name(current_buf)
M.state.current_file_cache = (current_file ~= '' and vim.fn.filereadable(current_file) == 1) and current_file
or nil
end
end

local results = file_picker.search_files(
M.state.query,
M.state.config.max_results,
Expand Down Expand Up @@ -1025,15 +1018,31 @@ end
function M.open(opts)
if M.state.active then return end

-- Detect current file BEFORE opening picker UI
local current_buf = vim.api.nvim_get_current_buf()
if current_buf and vim.api.nvim_buf_is_valid(current_buf) then
local current_file = vim.api.nvim_buf_get_name(current_buf)
if current_file ~= '' and vim.fn.filereadable(current_file) == 1 then
-- Convert to relative path to match file picker storage format
M.state.current_file_cache = vim.fn.fnamemodify(current_file, ':.')
else
M.state.current_file_cache = nil
end
end

if not file_picker.is_initialized() then
local config = {
-- Get the main config that includes scoring settings
local main = require('fff.main')
local main_config = main.config or {}

local config = vim.tbl_deep_extend('force', {
base_path = opts and opts.cwd or vim.fn.getcwd(),
max_results = 100,
frecency = {
enabled = true,
db_path = vim.fn.stdpath('cache') .. '/fff_nvim',
},
}
}, main_config)

if not file_picker.setup(config) then
vim.notify('Failed to initialize file picker', vim.log.levels.ERROR)
Expand Down Expand Up @@ -1070,11 +1079,6 @@ function M.monitor_scan_progress()
vim.defer_fn(function() M.monitor_scan_progress() end, 500)
else
M.update_results()

vim.defer_fn(function()
local refreshed = file_picker.refresh_git_status()
if refreshed and #refreshed > 0 then M.update_results() end
end, 500) -- Wait 500ms for git status to complete
end
end

Expand Down
Loading