Understanding fff.nvim Architecture: A Deep Dive into the Rust-Lua File Picker

fff.nvim combines a lightweight Lua frontend for UI and configuration with a high-performance Rust backend for indexing and fuzzy searching, communicating through a minimal FFI layer to deliver sub-second file picking in Neovim.

Understanding the architecture of fff.nvim reveals how this Neovim plugin achieves high-performance file picking by separating concerns between a responsive Lua interface and a compiled Rust engine. The repository dmtrKovalenko/fff.nvim structures its codebase into distinct layers that handle everything from lazy initialization to floating window management, all while maintaining a clean separation between presentation and search logic.

Plugin Entry Point and Lazy Initialization

The architecture begins at plugin/fff.lua, which serves as the entry point for Vim script loading. This file prevents double-loading by checking vim.g.fff_loaded and defers heavy initialization until after the UIEnter event.

The initialization logic detects whether Neovim has already started:

if vim.v.vim_did_enter == 1 then
  init()
else
  vim.api.nvim_create_autocmd('UIEnter', {
    callback = init,
  })
end

This design ensures the Rust backend only starts when the user interface is ready. The init() function eventually calls require('fff.core').ensure_initialized(), which triggers the full startup sequence and registers user commands like :FFFFind and :FFFScan.

Core State Management

The lua/fff/core.lua module maintains global state and orchestrates the relationship between Lua and Rust. It stores critical flags in a state table, including initialized, file_picker_initialized, and the Rust fuzzy object reference (fuzzy).

The ensure_initialized() function performs three critical operations:

  1. Loads user configuration and merges it with defaults
  2. Starts the Rust database via fuzzy.init_db
  3. Initializes the file-picker backend through fuzzy.init_file_picker

Additionally, setup_global_autocmds() registers event listeners for directory changes and buffer access patterns, enabling real-time frecency tracking and automatic index updates when DirChanged events fire.

Configuration System

Configuration handling lives in lua/fff/conf.lua, which implements a singleton pattern for settings management. The module defines a complete FffConfig schema covering base paths, UI layout dimensions, preview settings, keymaps, and Git integration options.

The system supports backward compatibility through handle_deprecated_config(), which automatically migrates legacy top-level options—such as moving width to layout.width—while emitting deprecation warnings. User settings are accessed throughout the codebase via require('fff.conf').get(), ensuring consistent configuration resolution without global variable pollution.

The Rust Bridge and FFI Layer

The lua/fff/fuzzy.lua file acts as the sole bridge to the compiled Rust library. It loads the dynamic library through require('fff.rust') and re-exports every native function for use by higher-level Lua modules.

Key Rust functions exposed to Lua include:

  • fuzzy.fuzzy_search_files – Performs fuzzy matching with frecency scoring
  • fuzzy.live_grep – Executes content search with regex and fuzzy modes
  • fuzzy.scan_files – Asynchronous filesystem indexing
  • fuzzy.init_db – Database initialization and persistence

This FFI layer keeps the Lua codebase lightweight and testable while delegating all CPU-intensive operations—such as fuzzy scoring algorithms and Git status tracking—to the optimized Rust backend.

File Search Implementation

The lua/fff/file_picker/init.lua module provides the high-level API for file searching. It wraps the Rust fuzzy_search_files function while handling pagination, combo-boost overrides, and thread count configuration.

When search_files_paginated() receives a query, it forwards the request to Rust with parameters for maximum results and scoring preferences. The Rust engine returns results already sorted by relevance (combining fuzzy match scores and frecency data), eliminating the need for Lua-side sorting. Additional functions like get_search_metadata() and track_access() expose indexing statistics and update access patterns for future scoring calculations.

Live Grep Implementation

Content searching follows a similar pattern in lua/fff/grep/init.lua. The search() function accepts a query string, file offset for pagination, page size, and grep mode (plain, regex, or fuzzy), then delegates to fuzzy.live_grep.

The Rust backend returns a structured SearchResult containing:

  • items – Array of matched lines with file paths and line numbers
  • total_matched – Total hit count for pagination calculations
  • next_file_offset – Cursor for fetching subsequent pages
  • regex_fallback_error – Optional error when regex compilation fails

The UI layer can cycle between search modes using :FFFDebug cycle_grep_modes, with the Lua module handling mode state while Rust executes the actual text matching across indexed files.

UI Rendering and Layout Management

All visual components are managed by lua/fff/picker_ui.lua, which remains entirely decoupled from search algorithms. This module calculates window geometry through compute_layout(config), converting relative dimensions (like width = 0.9) into absolute terminal coordinates.

The build_window_configs() function constructs option tables for nvim_open_win, applying border styles (single, double, rounded) and determining preview pane positioning. When M.open() executes, it creates floating windows for the input field, result list, and optional file preview, then attaches buffer-local autocmds for input change detection.

Rendering utilities in lua/fff/list_renderer.lua and lua/fff/file_renderer.lua transform raw Rust result items into formatted buffer lines, handling icon injection, Git status highlighting, and score display when debug mode is enabled.

Public API Surface

The lua/fff/main.lua module exposes the user-facing API consumed by plugin commands and external scripts. It maintains the active picker state while providing functions for common operations.

Key exported functions include:

  • setup(config) – Stores configuration in vim.g.fff
  • find_files(opts) – Opens the file picker UI
  • live_grep(opts) – Opens the content search UI
  • search(query, max_results) – Programmatic search returning a Lua table
  • change_indexing_directory(new_path) – Switches the root directory for subsequent searches

These functions act as thin wrappers that delegate to core, file_picker, grep, or picker_ui submodules, maintaining clean separation of concerns while providing a unified interface for both interactive and scripted usage.

Summary

  • fff.nvim uses a dual-language architecture with Lua handling UI and configuration while Rust manages indexing and search algorithms
  • The entry point at plugin/fff.lua implements lazy loading to prevent startup time impact
  • lua/fff/core.lua maintains global state and manages the Rust object lifecycle
  • Configuration is centralized in lua/fff/conf.lua with automatic migration for deprecated options
  • The FFI bridge in lua/fff/fuzzy.lua provides zero-overhead access to Rust functions
  • Search operations in lua/fff/file_picker/init.lua and lua/fff/grep/init.lua receive pre-sorted results from Rust, eliminating Lua-side processing
  • UI rendering in lua/fff/picker_ui.lua handles floating window layout independently of search logic

Frequently Asked Questions

How does fff.nvim achieve fast search speeds?

The plugin achieves sub-second search performance by implementing the fuzzy matching algorithm, frecency scoring, and filesystem indexing in Rust rather than Lua. The Rust backend compiles to native machine code and can leverage multi-threading for directory scanning, while the Lua frontend focuses solely on rendering results that are pre-sorted by the fuzzy.fuzzy_search_files function.

Can I use fff.nvim programmatically without opening the UI?

Yes, the lua/fff/main.lua module exposes search(query, max_results), which returns a Lua table of matches without opening floating windows. This allows other plugins or scripts to leverage the Rust fuzzy engine directly: local results = require('fff').search('config', 15) returns the top 15 matches as a structured table.

How is the configuration validated and migrated?

The configuration system in lua/fff/conf.lua maintains a complete schema with default values and includes a handle_deprecated_config() function that automatically migrates legacy options. For example, top-level width settings are automatically moved to layout.width with a deprecation warning, ensuring backward compatibility while guiding users toward the current API.

What happens when I change directories while fff.nvim is running?

When the DirChanged autocmd fires, lua/fff/core.lua detects the event through setup_global_autocmds() and automatically calls picker.change_indexing_directory. This updates the Rust backend's indexing root without requiring a manual refresh, allowing seamless project switching while maintaining frecency scores and Git status awareness across directories.

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 →