*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 June 30 ============================================================================== Table of Contents *fff.nvim-table-of-contents* 1. fff.nvim |fff.nvim-fff.nvim| ============================================================================== 1. fff.nvim *fff.nvim-fff.nvim* The best file search picker for Neovim. Frecency-ranked, typo-resistant, git-award, very fast. Demo on the Linux kernel repo (100k files, 8GB): https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb INSTALLATION ~ LAZY.NVIM >lua { 'dmtrKovalenko/fff.nvim', build = function() -- downloads a prebuilt binary or falls back to cargo build require("fff.download").download_or_build_binary() end, -- for nixos: -- build = "nix run .#release", opts = { debug = { enabled = true, show_scores = true, }, }, lazy = false, -- the plugin lazy-initialises itself keys = { { "ff", function() require('fff').find_files() end, desc = 'FFFind files' }, { "fg", function() require('fff').live_grep() end, desc = 'LiFFFe grep' }, { "fz", function() require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) end, desc = 'Live fffuzy grep', }, { "fw", function() require('fff').live_grep_under_cursor() end, mode = { 'n', 'x' }, desc = 'Search current word / selection', }, }, } < VIM.PACK >lua vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' }) vim.api.nvim_create_autocmd('PackChanged', { callback = function(ev) local name, kind = ev.data.spec.name, ev.data.kind if name == 'fff.nvim' and (kind == 'install' or kind == 'update') then if not ev.data.active then vim.cmd.packadd('fff.nvim') end require('fff.download').download_or_build_binary() end end, }) vim.g.fff = { lazy_sync = true, debug = { enabled = true, show_scores = true }, } vim.keymap.set('n', 'ff', function() require('fff').find_files() end, { desc = 'FFFind files' }) < PUBLIC API ~ >lua require('fff').find_files() -- find files in current repo require('fff').live_grep() -- live content grep require('fff').live_grep_under_cursor() -- grep in normal, selection in visual require('fff').scan_files() -- force rescan require('fff').refresh_git_status() -- refresh git status require('fff').find_files_in_dir(path) -- find in a specific dir require('fff').change_indexing_directory(new_path) -- change root -- Programmatic search (no UI). Useful for plugin integrations. require('fff').file_search(query, opts) -- fuzzy search files / dirs / mixed require('fff').content_search(query, opts) -- programmatic grep < FILE_SEARCH(QUERY, OPTS) Returns a structured result `{ items, scores, total_matched, total_files?, total_dirs?, location? }`. Each item has a `type` field (`"file"` or `"directory"`) and `name` / `relative_path`. File items also expose `size`, `modified`, `git_status`, `is_binary`, and frecency scores. >lua local r = require('fff').file_search('button', { mode = 'mixed', -- 'files' (default) | 'directories' | 'mixed' max_results = 50, page = 0, -- 0-based pagination current_file = nil, -- path to deprioritize for distance scoring max_threads = 4, cwd = nil, -- switch indexed root if different (see below) wait_for_index_ms = nil, -- override the default scan wait timeout }) for _, item in ipairs(r.items) do print(item.type, item.relative_path) end < CONTENT_SEARCH(QUERY, OPTS) Returns a `GrepResult` `{ items, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error? }`. Each match item has `relative_path`, `name`, `line_number`, `col`, `line_content`, `match_ranges`, plus the same file metadata as `file_search`. >lua local r = require('fff').content_search('TODO', { mode = 'plain', -- 'plain' (default) | 'regex' | 'fuzzy' max_file_size = 10 * 1024 * 1024, max_matches_per_file = 100, smart_case = true, page_size = 50, file_offset = 0, time_budget_ms = 0, trim_whitespace = false, cwd = nil, -- switch indexed root if different wait_for_index_ms = nil, -- override the default scan wait timeout }) for _, m in ipairs(r.items) do print(string.format('%s:%d %s', m.relative_path, m.line_number, m.line_content)) end < Both functions accept the same constraint syntax as the UI pickers (e.g. `git:modified`, `*.rs`, `!test/`, glob patterns). CWD AND INDEXING Both `file_search` and `content_search` honour an optional `cwd` field. The first call to either function lazily initialises the picker at `config.base_path` (your Neovim cwd by default). - If `cwd` matches the currently indexed root, the call returns immediately against the existing index. - If `cwd` differs, the picker is re-indexed at the new root and the call **blocks** (default up to 10 s) until the new picker is installed and its initial scan completes — so callers always get results from the right tree. - If the index is still warming up after a `change_indexing_directory`, you can pass `wait_for_index_ms = N` to block for up to `N` ms regardless of whether `cwd` triggered the swap. Pass `0` to skip waiting entirely (useful for fire-and-forget calls where partial results are acceptable). - Invalid or non-existent `cwd` paths return an empty result and emit an error via `vim.notify`. COMMANDS ~ - `:FFFScan`. Rescan files. - `:FFFRefreshGit`. Refresh git status. - `:FFFClearCache [all|frecency|files]`. Clear caches. - `:FFFHealth`. Health check. - `:FFFDebug [on|off|toggle]`. Toggle the scoring display. - `:FFFOpenLog`. Open `~/.local/state/nvim/log/fff.log`. CONFIGURATION ~ Defaults are sensible. Override only what you care about. >lua require('fff').setup({ base_path = vim.fn.getcwd(), prompt = '> ', title = 'FFFiles', max_results = 100, max_threads = 4, lazy_sync = true, prompt_vim_mode = false, follow_symlinks = false, -- Allow indexing the user's $HOME directory. Enabled by default. -- Disable if you strictly sure you don't want this, as it makes whole fff error hard enable_home_dir_scanning = true, -- Allow indexing a filesystem root (e.g. `/`, `C:\`). Disabled by default enable_fs_root_scanning = false, layout = { height = 0.8, width = 0.8, prompt_position = 'bottom', -- or 'top' preview_position = 'right', -- 'left' | 'right' | 'top' | 'bottom' preview_size = 0.5, -- Border style for the picker windows. Leave unset (nil) to follow the -- global `vim.o.winborder`; set it to override fff's borders independently. border = nil, -- 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'none' flex = { size = 130, wrap = 'top' }, min_list_height = 10, -- do not display anything except the list below this threshold show_scrollbar = true, path_shorten_strategy = 'middle_number', -- 'middle_number' | 'middle' | 'end' | 'start' anchor = 'center', }, preview = { enabled = true, max_size = 10 * 1024 * 1024, chunk_size = 8192, binary_file_threshold = 1024, imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', line_numbers = false, cursorlineopt = 'both', wrap_lines = false, filetypes = { svg = { wrap_lines = true }, markdown = { wrap_lines = true }, text = { wrap_lines = true }, }, }, keymaps = { close = '', select = '', select_split = '', select_vsplit = '', select_tab = '', move_up = { '', '' }, move_down = { '', '' }, preview_scroll_up = '', preview_scroll_down = '', toggle_debug = '', cycle_grep_modes = '', -- grep mode only: jump cursor to first match of next/prev file group grep_jump_to_next_file = { '', '' }, grep_jump_to_prev_file = { '', '' }, cycle_previous_query = '', toggle_select = '', send_to_quickfix = '', focus_list = 'l', focus_preview = 'p', }, frecency = { enabled = true, db_path = vim.fn.stdpath('cache') .. '/fff_nvim', }, history = { enabled = true, db_path = vim.fn.stdpath('data') .. '/fff_queries', min_combo_count = 3, combo_boost_score_multiplier = 100, }, git = { status_text_color = false, -- true to color filenames by git status }, select = { -- Return winid to open the chosen file in, or nil to open in the original window select_window = function(current_buf, action) --[[ default impl ]] end, }, grep = { max_file_size = 10 * 1024 * 1024, max_matches_per_file = 100, smart_case = true, time_budget_ms = 150, modes = { 'plain', 'regex', 'fuzzy' }, trim_whitespace = false, enable_filename_constraint = false, -- treat filename-like tokens (e.g. `score.rs`) in a grep query as a file-path filter scoping the search; off = searched as literal text location_format = ':%d:%d', -- printf format for line:col prefix in grep results, e.g. ':%d' for line-only }, debug = { enabled = false, -- show the file info panel next to the preview show_scores = false, -- inline scores in the file list -- Per-section toggles for the file info panel. Accepts a boolean shorthand -- (`show_file_info = true|false`) to flip everything at once. The panel -- adapts to width: narrow renders sections vertically, wide renders them -- as a two-column grid. Disable a section to also shrink the panel. show_file_info = { file_info = true, -- size, type, git status, frecency score_breakdown = true, -- total + match type, bonuses, modifiers, penalty -- modified + accessed timestamps; pass a table to hide individual rows: -- timings = { modified = false, accessed = true } timings = true, full_path = true, -- relative path at the bottom (wraps if too long) }, }, logging = { -- logs will be written in a parent directory of this file path in files like -- `++.`. Run :FFFOpenLog to open current one log_file = vim.fn.stdpath('log') .. '/fff.log', log_level = 'info', retain_runs = 20, }, }) < LIVE GREP MODES ~ `` cycles between `plain`, `regex`, and `fuzzy`. The list is configurable via `grep.modes`, and single-mode setups hide the indicator entirely. Per-call override: >lua require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) require('fff').live_grep({ query = 'search term' }) -- pre-fill < CONSTRAINTS ~ Both find and grep accept these tokens to refine a query: - `git:modified`. One of `modified`, `staged`, `deleted`, `renamed`, `untracked`, `ignored`. - `test/`. Any deeply nested children of `test/`. - `!something`, `!test/`, `!git:modified`. Exclusion. - `./**/*.{rs,lua}`. Any valid glob, powered by zlob . Grep-only: - `*.md`, `*.{c,h}`. Extension filter. - `src/main.rs`. Grep inside a single file. Mix freely: `git:modified src/**/*.rs !src/**/mod.rs user controller`. OPEN IN INVOKING WINDOW ~ By default fff.nvim will try to open a file in the most suitable window, so any non-file buffers are not affected. You can customize or disable this by providing: >lua require('fff').setup({ select = { select_window = function(_current_buf, _action) return nil end, }, }) < Caveat: the chosen file replaces the buffer in the invoking window even if it’s a non-modifiable / special buftype. `winfixbuf` windows still fall back to `:split` to avoid `E1513`. MULTI-SELECT AND QUICKFIX ~ - ``. Toggle selection (shows a thick `▊` in the signcolumn). - ``. Send selected files to the quickfix list and close the picker. GIT STATUS HIGHLIGHTING ~ Sign-column indicators are on by default. To color filename text by git status, set `git.status_text_color = true` and adjust the `hl.git_*` groups. See `:help fff.nvim` for the full list. FLOAT COLORS ~ The picker maps its float content to `NormalFloat` (via `hl.normal`) and the border to `FloatBorder`. Default `FloatBorder` links to `NormalFloat`, so border and content share a background out of the box and the picker reads as a single popup. Override `hl.normal = 'Normal'` to make the picker blend with the editor instead. For finer control, set `hl.winhl` to override the per-window `winhighlight`. It accepts either a single string applied to every picker window, or a table with optional `prompt`, `list`, `preview`, and `file_info` keys. Missing keys fall back to the default built from `hl.normal`, `hl.border`, and `hl.title`. >lua -- Apply the same winhighlight to all picker windows hl = { winhl = 'Normal:NormalFloat,FloatBorder:FloatBorder,FloatTitle:Title' } -- Or override specific windows only hl = { winhl = { prompt = 'Normal:Pmenu,FloatBorder:FloatBorder', list = 'Normal:NormalFloat,FloatBorder:FloatBorder', preview = 'Normal:NormalFloat,FloatBorder:FloatBorder', }, } < FILE INFO PANEL ~ Enable with `debug.enabled = true`. The panel sits above the preview and shows file metadata, score breakdown, timestamps and the full absolute path. It adapts to the panel width: at narrow widths sections stack vertically (B2), at wide widths sections render as a two-column grid (H2). Each section can be disabled individually via `debug.show_file_info`. Customise the panel via `hl`: -------------------------------------------------------------------------- key default used for ----------------------- ----------------- -------------------------------- file_info_section Title section header label file_info_separator FloatBorder dashes that act as section borders file_info_label Comment row labels (Size, Type, Git, …) file_info_value Normal fg plain values file_info_value_dim NonText dim values, separators inside rows file_info_size Number file size value file_info_type Type filetype value file_info_path Directory full path file_info_total_score bold + Number total score (bold) file_info_match_type bold + Special match type (bold) file_info_score_pos DiagnosticOk positive score components file_info_score_neg DiagnosticError negative score components -------------------------------------------------------------------------- FILE FILTERING ~ FFF honours `.gitignore`. For picker-only ignores that do not touch git, add a sibling `.ignore` file: >gitignore *.md docs/archive/**/*.md < Run `:FFFScan` to force a rescan. TROUBLESHOOTING ~ - `:FFFHealth` verifies picker init, optional dependencies, and DB connectivity. - `:FFFOpenLog` opens the current session’s log file. - Historical log files are stored near the main log file `/log/fff++.log` (up to 20 files) - For a crash backtrace, run `lldb -- nvim` or `gdb -- nvim` and reproduce Generated by panvimdoc vim:tw=78:ts=8:noet:ft=help:norl: