How to Integrate fff.nvim with Neovim: Complete Setup Guide

To integrate fff.nvim with Neovim, install the plugin via lazy.nvim or vim.pack, execute the build step to compile the Rust binary, optionally configure settings via require('fff').setup(), and map keys to find_files() or live_grep().

fff.nvim is a high-performance fuzzy finder and live grep plugin for Neovim that leverages a compiled Rust core for sub-millisecond search speeds. According to the dmtrKovalenko/fff.nvim source code, the plugin follows a three-layer architecture: configuration management in lua/fff/conf.lua, a Rust binary bridge in lua/fff/core.lua, and a floating window UI in lua/fff/picker_ui.lua. This guide covers the complete integration process from installation to advanced configuration.

Installation Methods

Add the plugin to your lazy.nvim specification with a build function to handle the Rust binary automatically:

{
  'dmtrKovalenko/fff.nvim',
  build = function()
    -- Downloads a pre-built binary or compiles from source
    require('fff.download').download_or_build_binary()
  end,
  opts = {
    debug = { enabled = true, show_scores = true },
  },
  lazy = false,
  keys = {
    { "ff", function() require('fff').find_files() end, desc = "FFFind files" },
    { "fg", function() require('fff').live_grep() end, desc = "Live grep" },
    { "fz", function()
        require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } })
      end,
      desc = "Live fuzzy grep" },
  },
}

The build key ensures the native fff-core binary is available by downloading a pre-built release or falling back to compilation. Setting lazy = false allows the plugin to self-initialize, though the picker itself opens only when invoked.

Option 2: Using vim.pack (Built-in Package Manager)

For Neovim versions with native package management (vim.pack), use the following configuration:

vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })

vim.api.nvim_create_autocmd('PackChanged', {
  callback = function(event)
    if event.data.updated then
      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' })
vim.keymap.set('n', 'fg', function() require('fff').live_grep() end,
               { desc = 'Live grep' })

This approach uses vim.pack.add to clone the repository into ~/.local/share/nvim/site/pack/, and the PackChanged autocmd automatically rebuilds the binary after updates.

Configuration and Setup

While fff.nvim works with sensible defaults, you can customize behavior through require('fff').setup(). In lua/fff/conf.lua, the M.setup function deep-merges user options with defaults and stores them in vim.g.fff:

require('fff').setup({
  base_path = vim.fn.getcwd(),
  prompt = '🪿 ',
  title = 'FFFiles',
  max_results = 200,
  layout = {
    height = 0.85,
    width = 0.85,
    prompt_position = 'bottom',
    preview_position = 'right',
    preview_size = 0.5,
    path_shorten_strategy = 'middle_number',
  },
  preview = {
    enabled = true,
    line_numbers = true,
    wrap_lines = false,
    filetypes = { markdown = { wrap_lines = true } },
  },
  keymaps = {
    close = '<Esc>',
    select = '<CR>',
    move_up = { '<Up>', '<C-p>' },
    move_down = { '<Down>', '<C-n>' },
    cycle_grep_modes = '<S-Tab>',
    toggle_debug = '<F2>',
  },
  git = { status_text_color = true },
})

The UI module (lua/fff/picker_ui.lua) reads conf.get() during rendering, ensuring runtime configuration changes (such as toggling debug mode) reflect immediately without restarting Neovim.

Usage Examples and Key Bindings

Basic File Finding and Live Grep

After setup, invoke the picker through the public API exposed in lua/fff/main.lua:

-- Open file picker with default settings
vim.keymap.set('n', '<leader>p', function() require('fff').find_files() end,
               { desc = 'Pick file' })

-- Live grep with word under cursor pre-filled
vim.keymap.set('n', '<leader>g',
  function()
    require('fff').live_grep({ query = vim.fn.expand('<cword>') })
  end,
  { desc = 'Grep word under cursor' })

Advanced Configuration Examples

Changing the indexing directory at runtime:

vim.keymap.set('n', '<leader>cd',
  function()
    local new_dir = vim.fn.input('Base directory: ', vim.fn.getcwd(), 'dir')
    if new_dir ~= '' then
      require('fff').change_indexing_directory(new_dir)
      vim.notify('FFF index base switched to ' .. new_dir)
    end
  end,
  { desc = 'Change FFF base directory' })

Programmatic search without opening the UI:

local results = require('fff').search('init', 30)   -- returns up to 30 items
for _, file in ipairs(results) do
  print(string.format('[%s] %s', file.relative_path, file.frecency_score or 0))
end

Toggling debug scoring:

Press <F2> (default) inside the picker, or execute:

vim.api.nvim_command('FFFDebug toggle')

Architecture Overview

Understanding the plugin structure helps debug integration issues:

  • lua/fff/conf.lua: Handles configuration merging, deprecation migrations (e.g., width → layout.width), and stores runtime state.
  • lua/fff/core.lua: Manages the Rust binary lifecycle via ensure_initialized() and provides the bridge to fff-core for search operations.
  • lua/fff/picker_ui.lua: Creates floating windows, manages the results list, preview pane, and processes keymap interactions.
  • lua/fff/main.lua: Exports the public API (setup, find_files, live_grep, search).

When you call require('fff').find_files(), the execution flow moves from main.luapicker_ui.lua (window creation) → core.lua (Rust binary initialization) → rendering with conf.get() for layout calculations.

Summary

  • Install via lazy.nvim with a build step or vim.pack with a PackChanged autocmd to handle the Rust binary.
  • Configure using require('fff').setup() in lua/fff/conf.lua to customize layouts, keymaps, and git integration.
  • Invoke via find_files() or live_grep() from lua/fff/main.lua for fuzzy finding and grep operations.
  • Extend using change_indexing_directory() for runtime path switches or search() for programmatic result retrieval.
  • Troubleshoot with :FFFHealth to validate binary installation and :FFFDebug toggle to inspect scoring algorithms.

Frequently Asked Questions

Do I need to install Rust manually to use fff.nvim?

No. The plugin includes a download mechanism (require('fff.download').download_or_build_binary()) that pulls pre-built binaries for supported platforms. Rust is only required if you are on an unsupported platform or want to compile from source manually.

How do I change the base directory for searches at runtime?

Use the change_indexing_directory() method exposed in lua/fff/main.lua. Pass an absolute path string to this function, and the next invocation of find_files() or live_grep() will search the new directory instead of the original base_path.

What makes fff.nvim faster than Telescope or fzf.vim?

The heavy lifting (file scanning, fuzzy scoring, and grep operations) executes in the compiled Rust binary (fff-core) rather than Lua or Vimscript. According to the source in lua/fff/core.lua, this architecture guarantees sub-millisecond response times even on massive codebases, with paginated results handled natively.

Why does the picker fail to open with a "binary not found" error?

This occurs when the build step hasn't executed. Run :FFFHealth to verify the binary status. If missing, manually trigger require('fff.download').download_or_build_binary() and restart Neovim. Ensure your package manager's build or run hook is properly configured as shown in the lazy.nvim and vim.pack examples above.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →