FFF.nvim — Search Data Flow

Tap any step to expand. Lua → FFI → Rust → Scoring → Render.

Lua (UI)
Rust (Core)
DB / Storage
❯
󰈔
󰈔
󰈔
waiting for input…
① Input — Lua UI Layer
1 LUA
Keypress → Buffer Attach
picker_ui.lua · setup_keymaps()
▶
User types in the floating input buffer. Neovim's nvim_buf_attach(on_lines) fires on every buffer change and schedules the search on the main thread via vim.schedule().
data shape
-- on_lines callback args
buf_id   = 42
changedtick = 7
first_line  = 0
last_line   = 1
why vim.schedule?
Buffer-attach callbacks fire in a restricted context. Scheduling defers the search until Neovim's event loop is idle, preventing re-entrancy.
▼
2 LUA
Extract Query String
picker_ui.lua · on_input_change()
▶
Reads the input buffer, strips the prompt prefix (e.g. ❯ ), trims whitespace, and stores the clean query. Then calls update_results_sync().
transform
"❯  scr/main"
      ↓ strip prompt + trim
"scr/main"  → M.state.query
▼
3 LUA
Dispatch Mode & Build Context
picker_ui.lua · update_results_sync()
▶
Determines which search path to take based on M.state.mode. Calculates page size from window height and builds call arguments.
branch logic
if M.state.mode == 'grep' then
  grep.search(query, offset, page_size, ...)
else
  file_picker.search_files_paginated(query, ...)
end
inputs computed here
query: string page_size: number current_file: path min_combo_override
② FFI Bridge
▼ file picker path ↓
4 LUA
FFI Call via fuzzy.so
rust/init.lua · fuzzy.fuzzy_search_files()
▶
The compiled Rust shared library (libfff_nvim.so) is loaded via LuaJIT's FFI. The Lua side calls the exported C-ABI function directly — no subprocess or RPC overhead.
C ABI signature
fuzzy_search_files(
  query              -- string
  max_threads        -- usize
  current_file       -- string | nil
  combo_boost_mult   -- i32
  min_combo_count    -- u32 | nil
  page_index         -- usize | nil
  page_size          -- usize | nil
)
why FFI not RPC?
Direct FFI eliminates serialization and socket latency. Results come back as a Lua table in microseconds, keeping search imperceptibly fast.
③ Rust Core — Parse & Route
▼
5 RUST
FFI Entry — Acquire Locks & Parse Query
lib.rs · fuzzy_search_files()
▶
Global state is protected by RwLock. This function acquires read locks on both FILE_PICKER and QUERY_TRACKER, then parses the raw query string into a structured FFFQuery.
locking
let picker  = FILE_PICKER.read();   // shared read
let tracker = QUERY_TRACKER.read(); // shared read
query parsing output
struct FFFQuery {
  fuzzy_parts:  Vec<String>,  // "scr/main" → ["scr", "main"]
  constraints:  Vec<Constraint>, // *.rs, path:src/
  location:     Option<(u32, u32)>, // :42:7
}
▼
④ Rust Core — Fuzzy Match
6 RUST
Query Tracker Lookup (combo boost)
file_picker.rs · fuzzy_search()
▶
Before scoring, checks if the user has previously typed this exact query and selected a file. If so, that file gets a massive score multiplier (combo boost).
combo boost lookup
tracker.get_last_query_entry(
  query, project_path
) → Option<QueryEntry>

// if count ≥ min_combo_count:
score *= combo_boost_multiplier  // default 100×
example
Type main → always picks src/main.rs. After a few picks, the tracker locks that file to the top for that query string.
▼
7 RUST
Frizbee Fuzzy Match (parallel)
score.rs · fuzzy_match_and_score_files()
▶
Uses the neo_frizbee library to match the query against every indexed file path in parallel via Rayon threadpool. Each fuzzy part is matched independently and averaged.
parallel match call
neo_frizbee::match_list_parallel_resolved(
  paths,       // *const u8 pointers into arena
  query_part,  // one fuzzy part
  max_typos,   // query.len() / 4, clamped 2..6
) → Vec<Match>   // (index, score 0–65535)

// multi-part: average across all parts
base_score = parts.iter().sum() / parts.len()
memory layout
File paths live in a ChunkedPathStore arena. Frizbee receives raw pointers — zero copy. Overflow files (added after initial scan) have their own arena.
⑤ Rust Core — Score Composition
▼
8 RUST
Score Composition (7 factors)
score.rs
▶
Each candidate file's final score is the sum of multiple weighted components. The design lets frecency nudge ranking without overwhelming match quality.
score breakdown
base_score
frizbee match (0–65535)
filename_bonus
match on name vs. full path
frecency_boost
recent/frequent access weight
git_status_boost
modified files ranked up
distance_penalty
deeper paths rank lower
current_file_penalty
deprioritize open file
combo_match_boost
100× if query-history hit
exact_match flag
perfect match sentinel
then sort + paginate
items.sort_by(|a, b| b.score.cmp(&a.score))
result = items[offset .. offset + limit]
▼
consulted during scoring ↓
DB
Frecency DB
frecency.rs · LMDB
▶
decay formula
// 10-day half-life
λ = 0.0693
score = Σ exp(−λ × days_ago)

// recency multipliers
<2 min  → 16×
<15 min → 8×
<1 hr   → 4×
LMDB gives lock-free concurrent reads — no contention with background writes.
DB
Query Tracker
query_tracker.rs · LMDB
▶
stored per query
struct QueryEntry {
  file_path:      String,
  count:          u32,
  last_timestamp: u64,
}
Key = hash(query + project_path). Enables per-project query muscle memory.
⑥ Rust → Lua — Serialize Results
▼
9 RUST
Convert SearchResult → Lua Table
lua_types.rs · SearchResultLua
▶
The top-N results are serialized into a Lua table. ChunkedString paths are resolved to full strings. Score breakdowns are included for debug/display.
Lua table shape
{
  items = {
    {
      relative_path = "src/main.rs",
      name          = "main.rs",
      git_status    = "M",
      is_binary     = false,
      total_frecency_score = 650,
    }, ...
  },
  scores = {
    { total=9500, base_score=5000, frecency_boost=1500, ... }
  },
  total_matched = 47,
  total_files   = 1234,
}
⑦ Lua — Update State & Render
▼
10 LUA
Update State & Reset Cursor
picker_ui.lua · update_results_sync()
▶
Stores results in M.state, resets cursor to item 1, checks for zero-result cross-mode suggestions, then schedules a debounced render.
state mutations
M.state.items            = results.items
M.state.filtered_items   = results.items
M.state.pagination.total_matched = n
M.state.cursor           = 1  -- best result
M.state.location         = loc -- :line:col
M.render_debounced()
zero-result fallback
If file mode returns 0 results, silently runs a grep suggestion and shows it as a hint below the empty list.
▼
11 LUA
Render List to Buffer
picker_ui.lua · render_list()
▶
Iterates results in display order (reversed if prompt_position="bottom"), writes lines into list_buf, and applies extmarks for icons, git indicators, and highlight groups.
per-item render
for i, item in ipairs(display_items) do
  -- icon + name + path + git badge
  renderer.render_file_item(buf, row, item)
  -- highlight matched chars
  set_extmark(buf, ns, row, col, hl)
end
render_scrollbar(win, total, offset)
combo separator
If the top result is a combo-boosted item, a visual separator is drawn between it and the regular results.
▼
12 LUA
Update Preview Pane
file_picker/preview.lua · update_preview()
▶
Debounced 100 ms. Reads the highlighted file, renders it with syntax highlighting into the preview buffer, and jumps to line:col if the query contained a location suffix.
debounce
-- avoid expensive reads on every keypress
vim.defer_fn(function()
  read_and_highlight_file(item.path)
end, 100)
⑧ Threading & Storage Overview
Threads
Storage
Perf tricks
Component Model Notes
Lua search dispatch Neovim main thread Non-blocking, scheduled
Fuzzy match Rayon threadpool max_threads from config
Grep search Rayon threadpool early-exit after page_limit
Frecency/QT reads Shared read locks Concurrent, no writer wait
File system watcher Dedicated bg thread Async inotify/FSEvents
Initial scanner bg threadpool Brief write lock at merge
Store Engine Key → Value
Frecency DB LMDB path_hash → Vec<timestamp>
Query Tracker LMDB hash(query+project) → QueryEntry
File index In-memory Vec ChunkedPathStore arena
Bigram index In-memory bigram → file_id bitset
Git cache In-memory HashMap path → git::Status
Technique Benefit
ChunkedPathStore arena Zero-copy path pointers to frizbee
Bigram inverted index Skip non-matching files before grep
LMDB memory-mapped Lock-free concurrent frecency reads
mmap file I/O (grep) Zero-copy file reading
Frecency-ordered grep Best files searched first, early exit
StableVec snapshots Post-scan index access without copy
100 ms preview debounce Avoid file reads on every keypress