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