diff --git a/Makefile b/Makefile index c87792a1..281c964d 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,50 @@ PLENARY_DIR ?= ../plenary.nvim -.PHONY: build test test-rust test-lua test-version test-bun test-node prepare-bun prepare-node set-npm-version header +PREFIX ?= /usr/local +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include + +.PHONY: build build-c-lib install uninstall test test-rust test-lua test-version test-bun test-node prepare-bun prepare-node set-npm-version header all: format test lint build: cargo build --release --features zlob +build-c-lib: + cargo build --release -p fff-c --features zlob + header: cbindgen --config crates/fff-c/cbindgen.toml --crate fff-c --output crates/fff-c/include/fff.h +# Install the C library and header under $(PREFIX) (default /usr/local). +# Override PREFIX for user-local installs, e.g. `make install PREFIX=$$HOME/.local`. +# DESTDIR is honoured for packagers. +install: build-c-lib + install -d $(DESTDIR)$(LIBDIR) + install -d $(DESTDIR)$(INCLUDEDIR) + install -m 0644 crates/fff-c/include/fff.h $(DESTDIR)$(INCLUDEDIR)/fff.h + @if [ -f target/release/libfff_c.dylib ]; then \ + install -m 0755 target/release/libfff_c.dylib $(DESTDIR)$(LIBDIR)/libfff_c.dylib; \ + echo "Installed $(DESTDIR)$(LIBDIR)/libfff_c.dylib"; \ + fi + @if [ -f target/release/libfff_c.so ]; then \ + install -m 0755 target/release/libfff_c.so $(DESTDIR)$(LIBDIR)/libfff_c.so; \ + echo "Installed $(DESTDIR)$(LIBDIR)/libfff_c.so"; \ + fi + @if [ -f target/release/fff_c.dll ]; then \ + install -m 0755 target/release/fff_c.dll $(DESTDIR)$(LIBDIR)/fff_c.dll; \ + echo "Installed $(DESTDIR)$(LIBDIR)/fff_c.dll"; \ + fi + @echo "Installed header $(DESTDIR)$(INCLUDEDIR)/fff.h" + +uninstall: + rm -f $(DESTDIR)$(LIBDIR)/libfff_c.dylib + rm -f $(DESTDIR)$(LIBDIR)/libfff_c.so + rm -f $(DESTDIR)$(LIBDIR)/fff_c.dll + rm -f $(DESTDIR)$(INCLUDEDIR)/fff.h + @echo "Removed fff-c from $(DESTDIR)$(PREFIX)" + test-setup: @if [ ! -d "$(PLENARY_DIR)" ]; then \ echo "Cloning plenary.nvim..."; \ diff --git a/README.md b/README.md index 9d223b02..18c4e871 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,144 @@ -

-

FFF

+

+ + + + FFF +

-

- AI agents (MCP)   |   Neovim users +

+ A file search toolkit for humans and AI agents. Really fast.

-

- A fast file search for your AI and neovim, with memory built-in -

+Typo-resistant path and content search, frecency-ranked file access, a background watcher, and a lightweight in-memory content index. Way faster than CLIs like ripgrep and fzf in any long-running process that searches more than once. -

- - Stars - - Issues - Contributors -

+It started life as a [Neovim plugin](#neovim-plugin) people loved, but it turned out that plenty of AI harnesses and code editors need the same thing: accurate, fast file search as a library. That is what fff is. --- -**FFF** stands for ~~freakin fast fuzzy file finder~~ (pick 3) and it is an opinionated fuzzy file picker for your AI agent and Neovim. Just for file search, but we do the file search really fff well. - -FFF is a tool for grepping, fuzzy file matching, globbing, and multigrepping with a strong focus on performance and useful search results. For humans - provides an unbelievable typo-resistant experience, for AI agents - implements the fastest file search with additional free memory suggesting the best search results based on various factors like frecency, git status, file size, definition matches, and more. +Pick what you are interested in: -## MCP +
+ +

MCP server

+
-FFF is an amazing way to reduce the time and tokens by giving your AI agent a bit of memory built-in to their file search tools. It makes your AI harness to find the code faster and spend less tokens by doing less roundtrips and reading less useless files. +Works with Claude Code, Codex, OpenCode, Cursor, Cline, and any MCP-capable client. Fewer grep roundtrips, less wasted context, faster answers. -![Chart showing the superiority of fff.nvim over builtin claude code tools](./chart.png) +![Benchmark chart comparing FFF against the built-in AI file-search tools](./chart.png) -You can install FFF as a dependency for your AI agent using a simple bash script: +### One-line install ```bash curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash ``` -> The installation script is here [./install-mcp.sh](./install-mcp.sh) if you want to review it before running. +The script lives at [`install-mcp.sh`](./install-mcp.sh) if you want to read it first. + +It prints the exact wiring instructions for your client. Once the server is connected, ask the agent to "use fff" and it picks up the `ffgrep`, `fffind`, and `fff-multi-grep` tools. + +### Recommended agent prompt -It will print out the instructions on how to connect it to your `Claude Code`, `Codex`, `OpenCode`, etc. Once you have it connected just ask your agent to "use fff". -Here is an example addition to `CLAUDE.md` that works perfectly: +Drop this into your project's `CLAUDE.md` or equivalent: -```sh -# CLAUDE.md -For any file search or grep in the current git indexed directory use fff tools +```markdown +For any file search or grep in the current git-indexed directory, use fff tools. ``` -## Neovim guide +### What changes + +- Frecency memory. Files you actually open rank higher next time. Warm-up from git touch history runs automatically. +- Definition-first hinting. Lines that look like code definitions are classified on the Rust side, no regex overhead in your prompt. +- Smart-case with auto-fuzzy fallback. `IsOffTheRecord` finds snake_case variants; zero-match queries retry as fuzzy and surface the best approximate hits. +- Git-aware annotations. Modified, untracked, and staged files are tagged so the agent reaches for what you are actively changing. + +Source: [`crates/fff-mcp/`](./crates/fff-mcp/). + +
+ +The MCP server gives any agent a file search tool that is faster and more token-efficient than the built-in one. + +
+ +

Pi agent extension

+
+ +### Install + +```bash +pi extension add @ff-labs/pi-fff +``` + +### Modes + +Three operating modes, switchable at runtime with `/fff-mode`: + +| Mode | What it does | +| ------------------------ | --------------------------------------------------------------------------------- | +| `tools-and-ui` (default) | Adds `ffgrep` and `fffind` tools, replaces `@`-mention autocomplete with FFF. | +| `tools-only` | Only tool injection. Keeps pi's native editor autocomplete. | +| `override` | Replaces pi's built-in `grep`, `find`, and `multi_grep` with FFF implementations. | -Here is some demo on the linux repository (100k files, 8GB) but you better fill it yourself and see the magic +Env vars: `PI_FFF_MODE`, `FFF_FRECENCY_DB`, `FFF_HISTORY_DB`. Flags: `--fff-mode`, `--fff-frecency-db`, `--fff-history-db`. + +### Agent-facing tools + +- `ffgrep`. Content search. Accepts `path`, `exclude` (comma, space, or array; leading `!` optional), `caseSensitive`, `context`, and cursor pagination. Auto-detects regex, falls back to fuzzy on zero exact matches, rejects `.*`-style wildcard-only patterns up front. +- `fffind`. Path and filename search. Matches the whole repo-relative path, not just the filename. Frecency-aware. The weak-match detector flags scattered fuzzy noise before it floods the agent's context. + +### Commands + +- `/fff-mode [tools-and-ui | tools-only | override]`. Show or switch the mode. +- `/fff-health`. Picker, frecency, and git integration status. +- `/fff-rescan`. Force a rescan. + +Source: [`packages/pi-fff/`](./packages/pi-fff/). + +
+ +The Pi extension swaps pi's native tools for FFF implementations and feeds the interactive editor's `@`-mention autocomplete from the frecency-ranked index. + +
+ +

Neovim plugin

+
+ +Demo on the Linux kernel repo (100k files, 8GB): https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb ### Installation -FFF.nvim requires neovim 0.10.0 or higher - #### lazy.nvim ```lua { 'dmtrKovalenko/fff.nvim', build = function() - -- this will download prebuild binary or try to use existing rustup toolchain to build from source - -- (if you are using lazy you can use gb for rebuilding a plugin if needed) + -- downloads a prebuilt binary or falls back to cargo build require("fff.download").download_or_build_binary() end, - -- if you are using nixos + -- for nixos: -- build = "nix run .#release", - opts = { -- (optional) + opts = { debug = { - enabled = true, -- we expect your collaboration at least during the beta - show_scores = true, -- to help us optimize the scoring system, feel free to share your scores! + enabled = true, + show_scores = true, }, }, - -- No need to lazy-load with lazy.nvim. - -- This plugin initializes itself lazily. - lazy = false, + lazy = false, -- the plugin lazy-initialises itself keys = { - { - "ff", -- try it if you didn't it is a banger keybinding for a picker - 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, + { "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', }, - { - "fc", + { "fc", function() require('fff').live_grep({ query = vim.fn.expand("") }) end, desc = 'Search current word', }, - } + }, } ``` @@ -115,393 +151,402 @@ 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 + if not ev.data.active then vim.cmd.packadd('fff.nvim') end require('fff.download').download_or_build_binary() end end, }) --- the plugin will automatically lazy load vim.g.fff = { - lazy_sync = true, -- start syncing only when the picker is open - debug = { - enabled = true, - show_scores = true, - }, + 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', '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').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 ``` +### 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 -FFF.nvim comes with sensible defaults. Here's the complete configuration with all available options: +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, -- set to false if you want file indexing to start on open - prompt_vim_mode = false, -- set to true to enable vim-mode in the prompt: leaves insert for normal mode bindings (also allows p or l to jump around) the second closes the picker - layout = { - height = 0.8, - width = 0.8, - prompt_position = 'bottom', -- or 'top' - preview_position = 'right', -- or 'left', 'right', 'top', 'bottom' - preview_size = 0.5, - flex = { -- set to false to disable flex layout - size = 130, -- column threshold: if screen width >= size, use preview_position; otherwise use wrap - wrap = 'top', -- position to use when screen is narrower than size - }, - show_scrollbar = true, -- Show scrollbar for pagination - -- How to shorten long directory paths in the file list: - -- 'middle_number' (default): uses dots for 1-3 hidden (a/./b, a/../b, a/.../b) - -- and numbers for 4+ (a/.4./b, a/.5./b) - -- 'middle': always uses dots (a/./b, a/../b, a/.../b) - -- 'end': truncates from the end (home/user/projects) - path_shorten_strategy = 'middle_number', - anchor = 'center', -- picker placement: 'center', 'top_left', 'top', 'top_right', 'left', 'right', 'bottom_left', 'bottom', 'bottom_right' - }, - preview = { - enabled = true, - max_size = 10 * 1024 * 1024, -- Do not try to read files larger than 10MB - chunk_size = 8192, -- Bytes per chunk for dynamic loading (8kb - fits ~100-200 lines) - binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable) - imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', - line_numbers = false, - cursorlineopt = 'both', -- the cursorlineopt used for lines in grep file previews, see :h cursorlineopt - 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 = '', - -- you can assign multiple keys to any action - move_up = { '', '' }, - move_down = { '', '' }, - preview_scroll_up = '', - preview_scroll_down = '', - toggle_debug = '', - -- grep mode: cycle between plain text, regex, and fuzzy search - cycle_grep_modes = '', - -- goes to the previous query in history - cycle_previous_query = '', - -- multi-select keymaps for quickfix - toggle_select = '', - send_to_quickfix = '', - -- this are specific for the normal mode (you can exit it using any other keybind like jj) - focus_list = 'l', - focus_preview = 'p', - }, - hl = { - border = 'FloatBorder', - normal = 'Normal', - cursor = 'CursorLine', -- Falls back to 'Visual' if CursorLine is not defined - matched = 'IncSearch', - title = 'Title', - prompt = 'Question', - frecency = 'Number', - debug = 'Comment', - combo_header = 'Number', - scrollbar = 'Comment', - directory_path = 'Comment', - -- Multi-select highlights - selected = 'FFFSelected', - selected_active = 'FFFSelectedActive', - -- Git text highlights for file names - git_staged = 'FFFGitStaged', - git_modified = 'FFFGitModified', - git_deleted = 'FFFGitDeleted', - git_renamed = 'FFFGitRenamed', - git_untracked = 'FFFGitUntracked', - git_ignored = 'FFFGitIgnored', - -- Git sign/border highlights - git_sign_staged = 'FFFGitSignStaged', - git_sign_modified = 'FFFGitSignModified', - git_sign_deleted = 'FFFGitSignDeleted', - git_sign_renamed = 'FFFGitSignRenamed', - git_sign_untracked = 'FFFGitSignUntracked', - git_sign_ignored = 'FFFGitSignIgnored', - -- Git sign selected highlights - git_sign_staged_selected = 'FFFGitSignStagedSelected', - git_sign_modified_selected = 'FFFGitSignModifiedSelected', - git_sign_deleted_selected = 'FFFGitSignDeletedSelected', - git_sign_renamed_selected = 'FFFGitSignRenamedSelected', - git_sign_untracked_selected = 'FFFGitSignUntrackedSelected', - git_sign_ignored_selected = 'FFFGitSignIgnoredSelected', - -- Grep highlights - grep_match = 'IncSearch', -- Highlight for matched text in grep results - grep_line_number = 'LineNr', -- Highlight for :line:col location - grep_regex_active = 'DiagnosticInfo', -- Highlight for keybind + label when regex is on - grep_plain_active = 'Comment', -- Highlight for keybind + label when regex is off - grep_fuzzy_active = 'DiagnosticHint', -- Highlight for keybind + label when fuzzy is on - -- Cross-mode suggestion highlights - suggestion_header = 'WarningMsg', -- Highlight for the "No results found. Suggested..." banner - }, - -- Store file open frecency - frecency = { - enabled = true, - db_path = vim.fn.stdpath('cache') .. '/fff_nvim', - }, - -- Store successfully opened queries with respective matches - history = { - enabled = true, - db_path = vim.fn.stdpath('data') .. '/fff_queries', - min_combo_count = 3, -- Minimum selections before combo boost applies (3 = boost starts on 3rd selection) - combo_boost_score_multiplier = 100, -- Score multiplier for combo matches (files repeatedly opened with same query) - }, - -- Git integration - git = { - status_text_color = false, -- Apply git status colors to filename text (default: false, only sign column) - }, - debug = { - enabled = false, -- Show file info panel in preview - show_scores = false, -- Show scores inline in the UI - }, - logging = { - enabled = true, - log_file = vim.fn.stdpath('log') .. '/fff.log', - log_level = 'info', - }, - -- find_files settings - file_picker = { - current_file_label = '(current)', - }, - -- grep settings - grep = { - max_file_size = 10 * 1024 * 1024, -- Skip files larger than 10MB - max_matches_per_file = 100, -- Maximum matches per file (set 0 to unlimited) - smart_case = true, -- Case-insensitive unless query has uppercase - time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit) - modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order - trim_whitespace = false, -- Strip leading whitespace from matched lines + base_path = vim.fn.getcwd(), + prompt = '> ', + title = 'FFFiles', + max_results = 100, + max_threads = 4, + lazy_sync = true, + prompt_vim_mode = false, + layout = { + height = 0.8, + width = 0.8, + prompt_position = 'bottom', -- or 'top' + preview_position = 'right', -- 'left' | 'right' | 'top' | 'bottom' + preview_size = 0.5, + flex = { size = 130, wrap = 'top' }, + show_scrollbar = true, + path_shorten_strategy = 'middle_number', -- 'middle_number' | 'middle' | 'end' + 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 = '', + 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 + }, + 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, + }, + debug = { enabled = false, show_scores = false }, + logging = { + enabled = true, + log_file = vim.fn.stdpath('log') .. '/fff.log', + log_level = 'info', + }, +}) ``` -### Key Features +### Live grep modes + +`` cycles between `plain`, `regex`, and `fuzzy`. The list is configurable via `grep.modes`, and single-mode setups hide the indicator entirely. -#### Available Methods +Per-call override: ```lua -require('fff').find_files() -- Find files in current repository -require('fff').scan_files() -- Trigger rescan of files in the current directory -require('fff').refresh_git_status() -- Refresh git status for the active file list -require('fff').find_files_in_dir(path) -- Find files in a specific directory -require('fff').change_indexing_directory(new_path) -- Change the base directory for the file picker +require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) +require('fff').live_grep({ query = 'search term' }) -- pre-fill ``` -just jump to the definition and see what other APIs are exposed we have a plenty +### Constraints -#### Commands +Both find and grep accept these tokens to refine a query: -FFF.nvim provides several commands for interacting with the file picker: +- `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](https://github.com/dmtrKovalenko/zlob). -- `:FFFScan` - Manually trigger a rescan of files in the current directory -- `:FFFRefreshGit` - Manually refresh git status for all files -- `:FFFClearCache [all|frecency|files]` - Clear various caches -- `:FFFHealth` - Check FFF health status and dependencies -- `:FFFDebug [on|off|toggle]` - Toggle debug scores display -- `:FFFOpenLog` - Open the FFF log file in a new tab +Grep-only: -#### Debug Mode +- `*.md`, `*.{c,h}`. Extension filter. +- `src/main.rs`. Grep inside a single file. -Toggle scoring information display: +Mix freely: `git:modified src/**/*.rs !src/**/mod.rs user controller`. -- Press `F2` while in the picker -- Use `:FFFDebug` command -- Enable by default with `debug.show_scores = true` +### Multi-select and quickfix -#### Multi-Select and Quickfix Integration +- ``. Toggle selection (shows a thick `▊` in the signcolumn). +- ``. Send selected files to the quickfix list and close the picker. -Select multiple files and send them to Neovim's quickfix list (keymaps are configurable): +### Git status highlighting -- `` - Toggle selection for the current file (shows thick border `▊` in signcolumn) -- `` - Send selected files to quickfix list and close picker +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. -#### Live Grep Search Modes +### File filtering -Live grep supports three search modes, cycled with ``: +FFF honours `.gitignore`. For picker-only ignores that do not touch git, add a sibling `.ignore` file: -- **Plain text** (default) - The query is matched literally. Special regex characters like `.`, `*`, `(`, `)`, `$` have no special meaning. This is the safest mode for searching code containing regex metacharacters. -- **Regex** - The query is interpreted as a regular expression. Supports character classes (`[a-z]`), quantifiers (`+`, `*`, `{n}`), alternation (`foo|bar`), anchors (`^`, `$`), word boundaries (`\b`), and more. -- **Fuzzy** - The query is fuzzy matched using Smith-Waterman scoring. Accommodates typos and scattered characters (e.g., "mtxlk" matches "mutex_lock"). Results are filtered by a quality threshold to avoid overly fuzzy matches. +```gitignore +*.md +docs/archive/**/*.md +``` -The current mode is shown on the right side of the input field (e.g., `plain`, `regex`, `fuzzy`) with color-coded highlighting. +Run `:FFFScan` to force a rescan. -You can customize which modes are available and their cycling order globally in your configuration, or per-call when invoking `live_grep()`. +### Troubleshooting -**Global configuration:** +- `:FFFHealth` verifies picker init, optional dependencies, and DB connectivity. +- `:FFFOpenLog` opens the log file. -```lua -require('fff').setup({ - grep = { - modes = { 'plain', 'regex' }, -- Only plain and regex, no fuzzy - } -}) +
+ +The best file search picker for neovim. Period. Faster and more intuitive queries, frecency ranking, definition classification and much more. + +
+ +

Node & Bun SDK

+
+ +```bash +npm install @ff-labs/fff-node +# or +bun add @ff-labs/fff-node ``` -**Per-call configuration:** +```ts +import { FileFinder } from "@ff-labs/fff-node"; -```lua --- Only fuzzy and plain modes for this specific grep -require('fff').live_grep({ - grep = { - modes = { 'fuzzy', 'plain' }, - } -}) +const finder = FileFinder.create({ basePath: process.cwd(), aiMode: true }); +if (!finder.ok) throw new Error(finder.error); +await finder.value.waitForScan(10_000); --- Single mode (hides mode indicator completely) -require('fff').live_grep({ - grep = { - modes = { 'fuzzy' }, - } -}) +const files = finder.value.fileSearch("incognito profile", { pageSize: 20 }); +const hits = finder.value.grep("GetOffTheRecordProfile", { + mode: "plain", + smartCase: true, + beforeContext: 1, + afterContext: 1, + classifyDefinitions: true, +}); --- Pre-fill the search with an initial query -require('fff').live_grep({ query = 'search term' }) +finder.value.destroy(); ``` -When only one mode is configured, the mode indicator is hidden completely and the cycle keybind does nothing. +Every method returns a `Result` (`{ ok: true, value } | { ok: false, error }`). Full type reference: [`packages/fff-node/src/types.ts`](./packages/fff-node/src/types.ts). -#### Constraints +
-There are a number of constraints you can use to refine your search in both grep and file search mode: +TypeScript wrapper over the C library for nodejs and bun. Build custom agent tools, CLIs, or IDE integrations on top of FFF. -- `git:modified` - show only modified files (one of `modified`, `staged`, `deleted`, `renamed`, `untracked`, `ignored`) -- `test/` - any deeply nested children of any test/ dir -- `!something` - exclude results matching something -- `!test/`, `!git:modified` - combining with any other constraint works as negation -- `./**/*.{rs,lua}` - any valid glob expression via [the fastest globbing library](https://github.com/dmtrKovalenko/zlob) +
+ +

Rust crate

+
-For grep only: +### Add the dependency -- `*.md`, `*.{c,h}` - extension filtering -- `src/main.rs` - grep in a single file - -In addition to that, all constraints can be combined together like: +FFF is written in Rust, so this is the lowest-overhead way to use it. +```toml +[dependencies] +fff-search = "0.6" ``` -git:modified src/**/*.rs !src/**/mod.rs user controller + +Full API documentation: [docs.rs/fff-search](https://docs.rs/fff-search/latest/fff_search/). + +
+ +Native rust crate that is performing all the search. Stable and well documented. + +
+ +

C library

+
+ +### Build + +```bash +# Builds only the C cdylib (fastest): +make build-c-lib + +# or directly with cargo: +cargo build --release -p fff-c --features zlob ``` -This will find all the files that qualify the constraints and: +The output is a `cdylib` (`libfff_c.so` / `libfff_c.dylib` / `fff_c.dll`). The header lives at [`crates/fff-c/include/fff.h`](./crates/fff-c/include/fff.h). -- match **both** user and controller (for file mode) -- match "user controller" (for grep mode) +Prebuilt binaries for every version, including every commit on main, are on the [releases page](https://github.com/dmtrKovalenko/fff.nvim/releases). The same binaries also ship inside the `@ff-labs/fff-bin-*` npm packages. -#### Cross-Mode Suggestions +### Install -When a search returns no results, FFF automatically queries the opposite search mode and displays the results as suggestions: +```bash +# System-wide (needs sudo): +sudo make install -- **File search with no matches** → shows suggested **content matches** (grep results) for the same query -- **Grep search with no matches** → shows suggested **file name matches** for the same query +# User-local, no sudo: +make install PREFIX=$HOME/.local -Suggestions are clearly labeled with a "No results found. Suggested ..." banner (highlighted with `hl.suggestion_header`). You can navigate and select suggestion items just like normal results — selecting a grep suggestion will open the file at the matching line. +# Staged install for packagers: +make install DESTDIR=/tmp/pkgroot PREFIX=/usr +``` -#### Git Status Highlighting +Drops `libfff_c.{so,dylib,dll}` into `$(PREFIX)/lib` and the header into `$(PREFIX)/include/fff.h`. Remove with `make uninstall`, which honours the same `PREFIX` and `DESTDIR`. -FFF integrates with git to show file status through sign column indicators (enabled by default) and optional filename text coloring. +Link against it after install: -**Sign Column Indicators** (enabled by default) - Border characters shown in the sign column: +```bash +cc my_app.c -lfff_c -o my_app +``` -```lua -hl = { - git_sign_staged = 'FFFGitSignStaged', - git_sign_modified = 'FFFGitSignModified', - git_sign_deleted = 'FFFGitSignDeleted', - git_sign_renamed = 'FFFGitSignRenamed', - git_sign_untracked = 'FFFGitSignUntracked', - git_sign_ignored = 'FFFGitSignIgnored', +Ensure `$(PREFIX)/lib` is on your runtime library search path (`LD_LIBRARY_PATH` on Linux, `DYLD_LIBRARY_PATH` on macOS, or an entry in `/etc/ld.so.conf.d/`). + +### Minimal example + +```c +#include +#include + +int main(void) { + FffResult *res = fff_create_instance( + ".", // base_path + "", // frecency_db_path (empty = default) + "", // history_db_path + false, // use_unsafe_no_lock + true, // enable_mmap_cache + true, // enable_content_indexing + true, // watch + false // ai_mode + ); + if (!res->success) { + fprintf(stderr, "init failed: %s\n", res->error); + fff_free_result(res); + return 1; + } + void *handle = res->handle; + fff_free_result(res); + + // Search + FffResult *search = fff_search(handle, "main.rs", "", 0, 0, 20, 100, 3); + // ... read FffSearchResult from search->handle, then fff_free_search_result() + + fff_destroy(handle); + return 0; } ``` -**Text Highlights** (opt-in) - Apply colors to filenames based on git status: +### Notes -To enable git status text coloring, set `git.status_text_color = true`: +- Every function returning `FffResult*` allocates with Rust's `Box`. Free with `fff_free_result` — not `libc::free`. +- Payloads (search results, grep results, scan progress) have their own dedicated free functions listed in the header. +- C strings returned in the `handle` field (e.g. from `fff_get_base_path`) are freed with `fff_free_string`. -```lua -require('fff').setup({ - git = { - status_text_color = true, -- Enable git status colors on filename text - }, - hl = { - git_staged = 'FFFGitStaged', -- Files staged for commit - git_modified = 'FFFGitModified', -- Modified unstaged files - git_deleted = 'FFFGitDeleted', -- Deleted files - git_renamed = 'FFFGitRenamed', -- Renamed files - git_untracked = 'FFFGitUntracked', -- New untracked files - git_ignored = 'FFFGitIgnored', -- Git-ignored files - } -}) -``` +Source: [`crates/fff-c/`](./crates/fff-c/). -The plugin provides sensible default highlight groups that link to common git highlight groups (e.g., GitSignsAdd, GitSignsChange). You can override these with your own custom highlight groups to match your colorscheme. +
-**Example - Custom Bright Colors for Text:** +Stable C ABI. Bind from C/C++, Zig, Go via cgo, Python via ctypes, or anything with C FFI. -```lua -vim.api.nvim_set_hl(0, 'CustomGitModified', { fg = '#FFA500' }) -vim.api.nvim_set_hl(0, 'CustomGitUntracked', { fg = '#00FF00' }) +--- -require('fff').setup({ - git = { - status_text_color = true, - }, - hl = { - git_modified = 'CustomGitModified', - git_untracked = 'CustomGitUntracked', - } -}) -``` +## What is FFF and why use it over ripgrep or fzf? -#### File Filtering +FFF is a file search library, not a CLI. Ripgrep and fzf are great tools, but they are command-line programs: every call forks a new process, re-reads `.gitignore`, re-stats directories, and rebuilds whatever state it needs in memory before it can answer. That is fine when you grep once from a shell. It is bad when an editor or an AI agent wants to run hundreds of searches per session. -FFF.nvim respects `.gitignore` patterns automatically. To filter files from the picker without modifying `.gitignore`, create a `.ignore` file in your project root: +FFF keeps the index and the file cache resident in one long-lived process and exposes the same Rust core through four thin layers: a native crate (`fff-search`), a C library (`libfff_c`), a Node/Bun SDK (`@ff-labs/fff-node`), and an MCP server. You call `FileFinder.create()` once, then every subsequent search hits warm memory. On a 500k-file Chromium checkout, that is the difference between 3-9 **SECONDS** per ripgrep spawn and sub-10 ms per FFF query. -```gitignore -# Exclude all markdown files -*.md +Algorithm for fuzzy matching is much more comprehensive than fzf's algorithm it is **typo-resistant** and we provide a query language with additional constraint parsing for prefiltering e.g. "*.rs !test/ shcema" is a perfectly valid query for fff, but fzf wouldn't find anything even for a single typo in "shcema". -# Exclude specific subdirectory -docs/archive/**/*.md -``` +### Why a programmatic API matters -Run `:FFFScan` to force a rescan if needed. +- No process spawn. Every call stays in-process and avoids the fork, exec, argv parsing, and stdout pipe setup that dominates short `rg` invocations. +- One FS walk, metadata collection, and parse of `.gitignore`. The ignore walker runs once at scan time and the result is reused for every search. +- Results come back as typed objects, not text you have to re-parse. The SDK gives you `{ relativePath, lineNumber, lineContent, gitStatus, totalFrecencyScore, isDefinition, ... }` directly. +- Cursor pagination that survives across calls. Ripgrep has no concept of "page 2 of these matches"; FFF does. +- A long-lived process opens up optimisations that a one-shot CLI cannot apply: warm caches, incremental re-indexing, cross-query frecency, and shared SIMD state. -### Troubleshooting +### What the core actually does -#### Health Check +- **Frecency-ranked fuzzy matching.** Every indexed file carries an access score and a modification score. Searches rank files you have opened recently and frequently above cold results. This is the same idea as VS Code's recently-opened list, but applied to every search result, not just a sidebar. +- **Typo-resistant matching for both paths and content.** Smith-Waterman fuzzy scoring is available on the grep path; path search uses SIMD-accelerated fuzzy matching (via the [`frizbee`](https://github.com/saghm/frizbee)-derived core) that survives dropped characters and reorderings. +- **Content grep with three modes.** Plain literal (SIMD memmem), regex (the Rust `regex` crate), and fuzzy (Smith-Waterman per line). Auto-detects which mode to use from the pattern, falls back to fuzzy when a plain search returns zero hits. +- **Multi-pattern OR search.** SIMD Aho-Corasick for "find any of these 20 identifiers at once", which is faster than regex alternation and a lot faster than 20 separate ripgrep runs. +- **Background file watcher.** The index updates as files change. You never pay for a rescan on the hot path. +- **Git status awareness.** Modified, staged, untracked, and ignored states are cached and returned with every result, so callers can sort or filter them without shelling out to git. The watcher talks to libgit2 directly instead of spawning the `git` CLI. +- **Definition classifier.** A byte-level scanner on the Rust side tags lines that start with `struct`, `fn`, `class`, `def`, `impl`, and friends. -Run `:FFFHealth` to check the status of FFF.nvim and its dependencies. This will verify: +### Performance choices that matter -- File picker initialization status -- Optional dependencies (git, image preview tools) -- Database connectivity +- Efficient memory allocator and memory allocation strategy (see next paragraph). By default we use `mimaloc` +- Parallel multi thread search pipeline that is not contaganted by the orchistration logic +- SIMD first algorithms for everything. Efficinet & non-allocating sorting. +- Platform specific optimizations for FS ([getdents64](https://linux.die.net/man/2/getdents64), NTFS api on windows and others) +- Lightweight on the flight content index for realtime even typo resistant grep +- Memory mapped content cache. We store some of the files in virtual memory (the amount is limited) +- Single contiguous arena storage of string chunks. Significantly reduces the amount of memory to work with and dramatically increases CPU cache hits. -#### Viewing Logs +### Memory allocation -If you encounter issues, check the log file: +Yes, fff fundamentally requires more memory than calling a single child process. That is the primary source of the speedup. In practice, alongside one of the most popular file search pickers for Neovim, [fff ends up using less RAM than a burst of ripgrep invocations](https://x.com/neogoose_btw/status/2041606853155811442). -``` -:FFFOpenLog -``` -Or manually open the log file at `~/.local/state/nvim/log/fff.log` (default location). +FFF also keeps a content index, around 360 bytes per indexed file, so roughly 36 MB for a 100k-file repo. Not every file is indexed — binaries, oversized files, and anything not eligible for grep are skipped. If even that footprint is too much, the index can be backed by a memory-mapped file instead of anonymous RAM. + +### What this means in practice + +If you are building an agent, an IDE extension, a pre-commit check, or any long-running tool that searches the same repository many times, calling FFF as a library is dramatically cheaper than shelling out to ripgrep. The tradeoff is real memory: FFF keeps the index in RAM and warms the content cache. On a 14k-file repo that costs about 26 MB resident. On a 500k-file repo like Chromium, expect a few hundred MB. In exchange, every single search is enriched with git status, frecency ranking, file metadata, timestamps of last access and edit and so on. + +If you are running one grep from a terminal, `rg` is still the right tool. If you run dozens of them inside the same process, FFF will pay for itself starting from the second call. If you work on AI agent fff will finish preparation work before your AI will have a chance to call it. + +### How it compares + +- **ripgrep**: FFF uses the same underlying regex engine and more advanced plain text matching algorithms. Stores content index and file tree. Main wins on repeated-search workloads. Loses on "grep once from bash and exit." +- **fzf**: FFF's path search is fuzzy like fzf, but it is also frecency-aware and git-aware, and ships a more typo-tolerant algorithm. fzf is a pure match-and-filter tool; FFF ranks results by how often you actually open them. +- **Telescope / fzf-lua / snacks.picker**: FFF ships its own Neovim picker with the same ranking the MCP server and SDK use. The picker is optional; the core is the same. +- **Tantivy or other full-text search engines**: different class of tool. Tantivy indexes documents for query-time scoring at scale. FFF is scoped to one repository and optimised for sub-10 ms response. It does not persist an inverted index on disk. + +--- + +## Repository layout + +- `crates/fff-search`, `crates/fff-grep`, `crates/fff-query-parser` - Rust core. +- `crates/fff-c` - C FFI used by every language binding. +- `crates/fff-nvim` - Lua/mlua bindings for the Neovim plugin. +- `crates/fff-mcp` - MCP server binary. +- `packages/fff-node` - Node.js SDK (`@ff-labs/fff-node`). +- `packages/fff-bun` - Bun SDK (`@ff-labs/fff-node`). +- `packages/pi-fff` - pi extension (`@ff-labs/pi-fff`). +- `lua/` - Neovim-side plugin code. + +## Contributing + +Bug reports and pull requests welcome. Agentic coding tools are welcome to be used, but human review is mandatory. + +## License + +[MIT](./LICENSE) & open source forever. diff --git a/_typos.toml b/_typos.toml index bdb0eb57..91a86b32 100644 --- a/_typos.toml +++ b/_typos.toml @@ -9,6 +9,7 @@ ue = "ue" # some typos we use for tests comparsion = "comparsion" modfiers = "modfiers" +shcema = "shcema" [default] extend-ignore-re = [ diff --git a/assets/logo-dark.png b/assets/logo-dark.png new file mode 100644 index 00000000..57e2b763 Binary files /dev/null and b/assets/logo-dark.png differ diff --git a/assets/logo-light.png b/assets/logo-light.png new file mode 100644 index 00000000..e3d27acd Binary files /dev/null and b/assets/logo-light.png differ diff --git a/assets/logo-orange.png b/assets/logo-orange.png new file mode 100644 index 00000000..1b3c6c82 Binary files /dev/null and b/assets/logo-orange.png differ diff --git a/doc/fff.nvim.txt b/doc/fff.nvim.txt index e59207f6..751ee1a2 100644 --- a/doc/fff.nvim.txt +++ b/doc/fff.nvim.txt @@ -1,60 +1,122 @@ *fff.nvim.txt* - For Neovim >= 0.10.0 Last change: 2026 April 22 + For Neovim >= 0.10.0 Last change: 2026 April 23 ============================================================================== Table of Contents *fff.nvim-table-of-contents* - - MCP |fff.nvim-mcp| - - Neovim guide |fff.nvim-neovim-guide| + - What is FFF and why use it over ripgrep or fzf?|fff.nvim-what-is-fff-and-why-use-it-over-ripgrep-or-fzf?| + - Repository layout |fff.nvim-repository-layout| + - Contributing |fff.nvim-contributing| + - License |fff.nvim-license| 1. Links |fff.nvim-links| -FFFAI agents (MCP) | Neovim usersA fast file search for your AI and neovim, with memory built-in - ------------------------------------------------------------------------------ -**FFF** stands for ~freakin fast fuzzy file finder~ (pick 3) and it is an -opinionated fuzzy file picker for your AI agent and Neovim. Just for file -search, but we do the file search really fff well. -FFF is a tool for grepping, fuzzy file matching, globbing, and multigrepping -with a strong focus on performance and useful search results. For humans - -provides an unbelievable typo-resistant experience, for AI agents - implements -the fastest file search with additional free memory suggesting the best search -results based on various factors like frecency, git status, file size, -definition matches, and more. +A file search toolkit for humans and AI agents. Really fast.Typo-resistant path and content search, frecency-ranked file access, a +background watcher, and a lightweight in-memory content index. Way faster than +CLIs like ripgrep and fzf in any long-running process that searches more than +once. +It started life as a |fff.nvim-neovim-plugin| people loved, but it turned out +that plenty of AI harnesses and code editors need the same thing: accurate, +fast file search as a library. That is what fff is. -MCP *fff.nvim-mcp* +------------------------------------------------------------------------------ +Pick what you are interested in: -FFF is an amazing way to reduce the time and tokens by giving your AI agent a -bit of memory built-in to their file search tools. It makes your AI harness to -find the code faster and spend less tokens by doing less roundtrips and reading -less useless files. +MCP server ~ -You can install FFF as a dependency for your AI agent using a simple bash -script: +Works with Claude Code, Codex, OpenCode, Cursor, Cline, and any MCP-capable +client. Fewer grep roundtrips, less wasted context, faster answers. + + +ONE-LINE INSTALL ~ >bash curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash < +The script lives at `install-mcp.sh` <./install-mcp.sh> if you want to read it +first. + +It prints the exact wiring instructions for your client. Once the server is +connected, ask the agent to "use fff" and it picks up the `ffgrep`, `fffind`, +and `fff-multi-grep` tools. + + +RECOMMENDED AGENT PROMPT ~ - The installation script is here ./install-mcp.sh <./install-mcp.sh> if you want - to review it before running. -It will print out the instructions on how to connect it to your `Claude Code`, -`Codex`, `OpenCode`, etc. Once you have it connected just ask your agent to -"use fff". Here is an example addition to `CLAUDE.md` that works perfectly: +Drop this into your project’s `CLAUDE.md` or equivalent: ->sh - # CLAUDE.md - For any file search or grep in the current git indexed directory use fff tools +>markdown + For any file search or grep in the current git-indexed directory, use fff tools. < -NEOVIM GUIDE *fff.nvim-neovim-guide* +WHAT CHANGES ~ + +- Frecency memory. Files you actually open rank higher next time. Warm-up from git touch history runs automatically. +- Definition-first hinting. Lines that look like code definitions are classified on the Rust side, no regex overhead in your prompt. +- Smart-case with auto-fuzzy fallback. `IsOffTheRecord` finds snake_case variants; zero-match queries retry as fuzzy and surface the best approximate hits. +- Git-aware annotations. Modified, untracked, and staged files are tagged so the agent reaches for what you are actively changing. + +Source: `crates/fff-mcp/` <./crates/fff-mcp/>. + +The MCP server gives any agent a file search tool that is faster and more +token-efficient than the built-in one. + +Pi agent extension ~ + + +INSTALL ~ + +>bash + pi extension add @ff-labs/pi-fff +< + + +MODES ~ + +Three operating modes, switchable at runtime with `/fff-mode`: + + ----------------------------------------------------------------------- + Mode What it does + ---------------- ------------------------------------------------------ + tools-and-ui Adds ffgrep and fffind tools, replaces @-mention + (default) autocomplete with FFF. + + tools-only Only tool injection. Keeps pi’s native editor + autocomplete. -Here is some demo on the linux repository (100k files, 8GB) but you better fill -it yourself and see the magic + override Replaces pi’s built-in grep, find, and multi_grep with + FFF implementations. + ----------------------------------------------------------------------- +Env vars: `PI_FFF_MODE`, `FFF_FRECENCY_DB`, `FFF_HISTORY_DB`. Flags: +`--fff-mode`, `--fff-frecency-db`, `--fff-history-db`. + + +AGENT-FACING TOOLS ~ + +- `ffgrep`. Content search. Accepts `path`, `exclude` (comma, space, or array; leading `!` optional), `caseSensitive`, `context`, and cursor pagination. Auto-detects regex, falls back to fuzzy on zero exact matches, rejects `.*`-style wildcard-only patterns up front. +- `fffind`. Path and filename search. Matches the whole repo-relative path, not just the filename. Frecency-aware. The weak-match detector flags scattered fuzzy noise before it floods the agent’s context. + + +COMMANDS ~ + +- `/fff-mode [tools-and-ui | tools-only | override]`. Show or switch the mode. +- `/fff-health`. Picker, frecency, and git integration status. +- `/fff-rescan`. Force a rescan. + +Source: `packages/pi-fff/` <./packages/pi-fff/>. + +The Pi extension swaps pi’s native tools for FFF implementations and feeds +the interactive editor’s `@`-mention autocomplete from the frecency-ranked +index. + +Neovim plugin ~ + +Demo on the Linux kernel repo (100k files, 8GB): https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb @@ -62,8 +124,6 @@ https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb INSTALLATION ~ -FFF.nvim requires neovim 0.10.0 or higher - LAZY.NVIM @@ -71,47 +131,30 @@ LAZY.NVIM { 'dmtrKovalenko/fff.nvim', build = function() - -- this will download prebuild binary or try to use existing rustup toolchain to build from source - -- (if you are using lazy you can use gb for rebuilding a plugin if needed) + -- downloads a prebuilt binary or falls back to cargo build require("fff.download").download_or_build_binary() end, - -- if you are using nixos + -- for nixos: -- build = "nix run .#release", - opts = { -- (optional) + opts = { debug = { - enabled = true, -- we expect your collaboration at least during the beta - show_scores = true, -- to help us optimize the scoring system, feel free to share your scores! + enabled = true, + show_scores = true, }, }, - -- No need to lazy-load with lazy.nvim. - -- This plugin initializes itself lazily. - lazy = false, + lazy = false, -- the plugin lazy-initialises itself keys = { - { - "ff", -- try it if you didn't it is a banger keybinding for a picker - 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, + { "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', }, - { - "fc", + { "fc", function() require('fff').live_grep({ query = vim.fn.expand("") }) end, desc = 'Search current word', }, - } + }, } < @@ -125,434 +168,467 @@ VIM.PACK 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 + if not ev.data.active then vim.cmd.packadd('fff.nvim') end require('fff.download').download_or_build_binary() end end, }) - -- the plugin will automatically lazy load vim.g.fff = { - lazy_sync = true, -- start syncing only when the picker is open - debug = { - enabled = true, - show_scores = true, - }, + 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', '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').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 < +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 ~ -FFF.nvim comes with sensible defaults. Here’s the complete configuration with -all available options: +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, -- set to false if you want file indexing to start on open - prompt_vim_mode = false, -- set to true to enable vim-mode in the prompt: leaves insert for normal mode bindings (also allows p or l to jump around) the second closes the picker - layout = { - height = 0.8, - width = 0.8, - prompt_position = 'bottom', -- or 'top' - preview_position = 'right', -- or 'left', 'right', 'top', 'bottom' - preview_size = 0.5, - flex = { -- set to false to disable flex layout - size = 130, -- column threshold: if screen width >= size, use preview_position; otherwise use wrap - wrap = 'top', -- position to use when screen is narrower than size - }, - show_scrollbar = true, -- Show scrollbar for pagination - -- How to shorten long directory paths in the file list: - -- 'middle_number' (default): uses dots for 1-3 hidden (a/./b, a/../b, a/.../b) - -- and numbers for 4+ (a/.4./b, a/.5./b) - -- 'middle': always uses dots (a/./b, a/../b, a/.../b) - -- 'end': truncates from the end (home/user/projects) - path_shorten_strategy = 'middle_number', - anchor = 'center', -- picker placement: 'center', 'top_left', 'top', 'top_right', 'left', 'right', 'bottom_left', 'bottom', 'bottom_right' - }, - preview = { - enabled = true, - max_size = 10 * 1024 * 1024, -- Do not try to read files larger than 10MB - chunk_size = 8192, -- Bytes per chunk for dynamic loading (8kb - fits ~100-200 lines) - binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable) - imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit', - line_numbers = false, - cursorlineopt = 'both', -- the cursorlineopt used for lines in grep file previews, see :h cursorlineopt - 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 = '', - -- you can assign multiple keys to any action - move_up = { '', '' }, - move_down = { '', '' }, - preview_scroll_up = '', - preview_scroll_down = '', - toggle_debug = '', - -- grep mode: cycle between plain text, regex, and fuzzy search - cycle_grep_modes = '', - -- goes to the previous query in history - cycle_previous_query = '', - -- multi-select keymaps for quickfix - toggle_select = '', - send_to_quickfix = '', - -- this are specific for the normal mode (you can exit it using any other keybind like jj) - focus_list = 'l', - focus_preview = 'p', - }, - hl = { - border = 'FloatBorder', - normal = 'Normal', - cursor = 'CursorLine', -- Falls back to 'Visual' if CursorLine is not defined - matched = 'IncSearch', - title = 'Title', - prompt = 'Question', - frecency = 'Number', - debug = 'Comment', - combo_header = 'Number', - scrollbar = 'Comment', - directory_path = 'Comment', - -- Multi-select highlights - selected = 'FFFSelected', - selected_active = 'FFFSelectedActive', - -- Git text highlights for file names - git_staged = 'FFFGitStaged', - git_modified = 'FFFGitModified', - git_deleted = 'FFFGitDeleted', - git_renamed = 'FFFGitRenamed', - git_untracked = 'FFFGitUntracked', - git_ignored = 'FFFGitIgnored', - -- Git sign/border highlights - git_sign_staged = 'FFFGitSignStaged', - git_sign_modified = 'FFFGitSignModified', - git_sign_deleted = 'FFFGitSignDeleted', - git_sign_renamed = 'FFFGitSignRenamed', - git_sign_untracked = 'FFFGitSignUntracked', - git_sign_ignored = 'FFFGitSignIgnored', - -- Git sign selected highlights - git_sign_staged_selected = 'FFFGitSignStagedSelected', - git_sign_modified_selected = 'FFFGitSignModifiedSelected', - git_sign_deleted_selected = 'FFFGitSignDeletedSelected', - git_sign_renamed_selected = 'FFFGitSignRenamedSelected', - git_sign_untracked_selected = 'FFFGitSignUntrackedSelected', - git_sign_ignored_selected = 'FFFGitSignIgnoredSelected', - -- Grep highlights - grep_match = 'IncSearch', -- Highlight for matched text in grep results - grep_line_number = 'LineNr', -- Highlight for :line:col location - grep_regex_active = 'DiagnosticInfo', -- Highlight for keybind + label when regex is on - grep_plain_active = 'Comment', -- Highlight for keybind + label when regex is off - grep_fuzzy_active = 'DiagnosticHint', -- Highlight for keybind + label when fuzzy is on - -- Cross-mode suggestion highlights - suggestion_header = 'WarningMsg', -- Highlight for the "No results found. Suggested..." banner - }, - -- Store file open frecency - frecency = { - enabled = true, - db_path = vim.fn.stdpath('cache') .. '/fff_nvim', - }, - -- Store successfully opened queries with respective matches - history = { - enabled = true, - db_path = vim.fn.stdpath('data') .. '/fff_queries', - min_combo_count = 3, -- Minimum selections before combo boost applies (3 = boost starts on 3rd selection) - combo_boost_score_multiplier = 100, -- Score multiplier for combo matches (files repeatedly opened with same query) - }, - -- Git integration - git = { - status_text_color = false, -- Apply git status colors to filename text (default: false, only sign column) - }, - debug = { - enabled = false, -- Show file info panel in preview - show_scores = false, -- Show scores inline in the UI - }, - logging = { - enabled = true, - log_file = vim.fn.stdpath('log') .. '/fff.log', - log_level = 'info', - }, - -- find_files settings - file_picker = { - current_file_label = '(current)', - }, - -- grep settings - grep = { - max_file_size = 10 * 1024 * 1024, -- Skip files larger than 10MB - max_matches_per_file = 100, -- Maximum matches per file (set 0 to unlimited) - smart_case = true, -- Case-insensitive unless query has uppercase - time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit) - modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order - trim_whitespace = false, -- Strip leading whitespace from matched lines + base_path = vim.fn.getcwd(), + prompt = '> ', + title = 'FFFiles', + max_results = 100, + max_threads = 4, + lazy_sync = true, + prompt_vim_mode = false, + layout = { + height = 0.8, + width = 0.8, + prompt_position = 'bottom', -- or 'top' + preview_position = 'right', -- 'left' | 'right' | 'top' | 'bottom' + preview_size = 0.5, + flex = { size = 130, wrap = 'top' }, + show_scrollbar = true, + path_shorten_strategy = 'middle_number', -- 'middle_number' | 'middle' | 'end' + 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 = '', + 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 + }, + 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, + }, + debug = { enabled = false, show_scores = false }, + logging = { + enabled = true, + log_file = vim.fn.stdpath('log') .. '/fff.log', + log_level = 'info', + }, + }) < -KEY FEATURES ~ +LIVE GREP MODES ~ +`` cycles between `plain`, `regex`, and `fuzzy`. The list is +configurable via `grep.modes`, and single-mode setups hide the indicator +entirely. -AVAILABLE METHODS +Per-call override: >lua - require('fff').find_files() -- Find files in current repository - require('fff').scan_files() -- Trigger rescan of files in the current directory - require('fff').refresh_git_status() -- Refresh git status for the active file list - require('fff').find_files_in_dir(path) -- Find files in a specific directory - require('fff').change_indexing_directory(new_path) -- Change the base directory for the file picker + require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } }) + require('fff').live_grep({ query = 'search term' }) -- pre-fill < -just jump to the definition and see what other APIs are exposed we have a -plenty +CONSTRAINTS ~ -COMMANDS +Both find and grep accept these tokens to refine a query: -FFF.nvim provides several commands for interacting with the file picker: +- `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 . -- `:FFFScan` - Manually trigger a rescan of files in the current directory -- `:FFFRefreshGit` - Manually refresh git status for all files -- `:FFFClearCache [all|frecency|files]` - Clear various caches -- `:FFFHealth` - Check FFF health status and dependencies -- `:FFFDebug [on|off|toggle]` - Toggle debug scores display -- `:FFFOpenLog` - Open the FFF log file in a new tab +Grep-only: +- `*.md`, `*.{c,h}`. Extension filter. +- `src/main.rs`. Grep inside a single file. -DEBUG MODE +Mix freely: `git:modified src/**/*.rs !src/**/mod.rs user controller`. -Toggle scoring information display: -- Press `F2` while in the picker -- Use `:FFFDebug` command -- Enable by default with `debug.show_scores = true` +MULTI-SELECT AND QUICKFIX ~ +- ``. Toggle selection (shows a thick `▊` in the signcolumn). +- ``. Send selected files to the quickfix list and close the picker. -MULTI-SELECT AND QUICKFIX INTEGRATION -Select multiple files and send them to Neovim’s quickfix list (keymaps are -configurable): +GIT STATUS HIGHLIGHTING ~ -- `` - Toggle selection for the current file (shows thick border `▊` in signcolumn) -- `` - Send selected files to quickfix list and close picker +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. -LIVE GREP SEARCH MODES +FILE FILTERING ~ -Live grep supports three search modes, cycled with ``: +FFF honours `.gitignore`. For picker-only ignores that do not touch git, add a +sibling `.ignore` file: -- **Plain text** (default) - The query is matched literally. Special regex characters like `.`, `*`, `(`, `)`, `$` have no special meaning. This is the safest mode for searching code containing regex metacharacters. -- **Regex** - The query is interpreted as a regular expression. Supports character classes (`[a-z]`), quantifiers (`+`, `*`, `{n}`), alternation (`foo|bar`), anchors (`^`, `$`), word boundaries (`\b`), and more. -- **Fuzzy** - The query is fuzzy matched using Smith-Waterman scoring. Accommodates typos and scattered characters (e.g., "mtxlk" matches "mutex_lock"). Results are filtered by a quality threshold to avoid overly fuzzy matches. +>gitignore + *.md + docs/archive/**/*.md +< -The current mode is shown on the right side of the input field (e.g., `plain`, -`regex`, `fuzzy`) with color-coded highlighting. +Run `:FFFScan` to force a rescan. -You can customize which modes are available and their cycling order globally in -your configuration, or per-call when invoking `live_grep()`. -**Global configuration:** +TROUBLESHOOTING ~ ->lua - require('fff').setup({ - grep = { - modes = { 'plain', 'regex' }, -- Only plain and regex, no fuzzy - } - }) -< +- `:FFFHealth` verifies picker init, optional dependencies, and DB connectivity. +- `:FFFOpenLog` opens the log file. -**Per-call configuration:** +The best file search picker for neovim. Period. Faster and more intuitive +queries, frecency ranking, definition classification and much more. ->lua - -- Only fuzzy and plain modes for this specific grep - require('fff').live_grep({ - grep = { - modes = { 'fuzzy', 'plain' }, - } - }) +Node & Bun SDK ~ + +>bash + npm install @ff-labs/fff-node + # or + bun add @ff-labs/fff-node +< + +>ts + import { FileFinder } from "@ff-labs/fff-node"; - -- Single mode (hides mode indicator completely) - require('fff').live_grep({ - grep = { - modes = { 'fuzzy' }, - } - }) + const finder = FileFinder.create({ basePath: process.cwd(), aiMode: true }); + if (!finder.ok) throw new Error(finder.error); + await finder.value.waitForScan(10_000); - -- Pre-fill the search with an initial query - require('fff').live_grep({ query = 'search term' }) + const files = finder.value.fileSearch("incognito profile", { pageSize: 20 }); + const hits = finder.value.grep("GetOffTheRecordProfile", { + mode: "plain", + smartCase: true, + beforeContext: 1, + afterContext: 1, + classifyDefinitions: true, + }); + + finder.value.destroy(); < -When only one mode is configured, the mode indicator is hidden completely and -the cycle keybind does nothing. +Every method returns a `Result` (`{ ok: true, value } | { ok: false, error +}`). Full type reference: `packages/fff-node/src/types.ts` +<./packages/fff-node/src/types.ts>. + +TypeScript wrapper over the C library for nodejs and bun. Build custom agent +tools, CLIs, or IDE integrations on top of FFF. + +Rust crate ~ -CONSTRAINTS +ADD THE DEPENDENCY ~ -There are a number of constraints you can use to refine your search in both -grep and file search mode: +FFF is written in Rust, so this is the lowest-overhead way to use it. + +>toml + [dependencies] + fff-search = "0.6" +< -- `git:modified` - show only modified files (one of `modified`, `staged`, `deleted`, `renamed`, `untracked`, `ignored`) -- `test/` - any deeply nested children of any test/ dir -- `!something` - exclude results matching something -- `!test/`, `!git:modified` - combining with any other constraint works as negation -- `./**/*.{rs,lua}` - any valid glob expression via the fastest globbing library +Full API documentation: docs.rs/fff-search +. -For grep only: +Native rust crate that is performing all the search. Stable and well +documented. -- `*.md`, `*.{c,h}` - extension filtering -- `src/main.rs` - grep in a single file +C library ~ -In addition to that, all constraints can be combined together like: -> - git:modified src/**/*.rs !src/**/mod.rs user controller +BUILD ~ + +>bash + # Builds only the C cdylib (fastest): + make build-c-lib + + # or directly with cargo: + cargo build --release -p fff-c --features zlob < -This will find all the files that qualify the constraints and: +The output is a `cdylib` (`libfff_c.so` / `libfff_c.dylib` / `fff_c.dll`). The +header lives at `crates/fff-c/include/fff.h` <./crates/fff-c/include/fff.h>. -- match **both** user and controller (for file mode) -- match "user controller" (for grep mode) +Prebuilt binaries for every version, including every commit on main, are on the +releases page . The same +binaries also ship inside the `@ff-labs/fff-bin-*` npm packages. -CROSS-MODE SUGGESTIONS +INSTALL ~ -When a search returns no results, FFF automatically queries the opposite search -mode and displays the results as suggestions: +>bash + # System-wide (needs sudo): + sudo make install + + # User-local, no sudo: + make install PREFIX=$HOME/.local + + # Staged install for packagers: + make install DESTDIR=/tmp/pkgroot PREFIX=/usr +< -- **File search with no matches** → shows suggested **content matches** (grep results) for the same query -- **Grep search with no matches** → shows suggested **file name matches** for the same query +Drops `libfff_c.{so,dylib,dll}` into `$(PREFIX)/lib` and the header into +`$(PREFIX)/include/fff.h`. Remove with `make uninstall`, which honours the same +`PREFIX` and `DESTDIR`. -Suggestions are clearly labeled with a "No results found. Suggested …" banner -(highlighted with `hl.suggestion_header`). You can navigate and select -suggestion items just like normal results — selecting a grep suggestion will -open the file at the matching line. +Link against it after install: +>bash + cc my_app.c -lfff_c -o my_app +< -GIT STATUS HIGHLIGHTING +Ensure `$(PREFIX)/lib` is on your runtime library search path +(`LD_LIBRARY_PATH` on Linux, `DYLD_LIBRARY_PATH` on macOS, or an entry in +`/etc/ld.so.conf.d/`). -FFF integrates with git to show file status through sign column indicators -(enabled by default) and optional filename text coloring. -**Sign Column Indicators** (enabled by default) - Border characters shown in -the sign column: +MINIMAL EXAMPLE ~ ->lua - hl = { - git_sign_staged = 'FFFGitSignStaged', - git_sign_modified = 'FFFGitSignModified', - git_sign_deleted = 'FFFGitSignDeleted', - git_sign_renamed = 'FFFGitSignRenamed', - git_sign_untracked = 'FFFGitSignUntracked', - git_sign_ignored = 'FFFGitSignIgnored', +>c + #include + #include + + int main(void) { + FffResult *res = fff_create_instance( + ".", // base_path + "", // frecency_db_path (empty = default) + "", // history_db_path + false, // use_unsafe_no_lock + true, // enable_mmap_cache + true, // enable_content_indexing + true, // watch + false // ai_mode + ); + if (!res->success) { + fprintf(stderr, "init failed: %s\n", res->error); + fff_free_result(res); + return 1; + } + void *handle = res->handle; + fff_free_result(res); + + // Search + FffResult *search = fff_search(handle, "main.rs", "", 0, 0, 20, 100, 3); + // ... read FffSearchResult from search->handle, then fff_free_search_result() + + fff_destroy(handle); + return 0; } < -**Text Highlights** (opt-in) - Apply colors to filenames based on git status: -To enable git status text coloring, set `git.status_text_color = true`: +NOTES ~ ->lua - require('fff').setup({ - git = { - status_text_color = true, -- Enable git status colors on filename text - }, - hl = { - git_staged = 'FFFGitStaged', -- Files staged for commit - git_modified = 'FFFGitModified', -- Modified unstaged files - git_deleted = 'FFFGitDeleted', -- Deleted files - git_renamed = 'FFFGitRenamed', -- Renamed files - git_untracked = 'FFFGitUntracked', -- New untracked files - git_ignored = 'FFFGitIgnored', -- Git-ignored files - } - }) -< +- Every function returning `FffResult*` allocates with Rust’s `Box`. Free with `fff_free_result` — not `libc::free`. +- Payloads (search results, grep results, scan progress) have their own dedicated free functions listed in the header. +- C strings returned in the `handle` field (e.g. from `fff_get_base_path`) are freed with `fff_free_string`. -The plugin provides sensible default highlight groups that link to common git -highlight groups (e.g., GitSignsAdd, GitSignsChange). You can override these -with your own custom highlight groups to match your colorscheme. +Source: `crates/fff-c/` <./crates/fff-c/>. -**Example - Custom Bright Colors for Text:** +Stable C ABI. Bind from C/C++, Zig, Go via cgo, Python via ctypes, or anything +with C FFI. ->lua - vim.api.nvim_set_hl(0, 'CustomGitModified', { fg = '#FFA500' }) - vim.api.nvim_set_hl(0, 'CustomGitUntracked', { fg = '#00FF00' }) - - require('fff').setup({ - git = { - status_text_color = true, - }, - hl = { - git_modified = 'CustomGitModified', - git_untracked = 'CustomGitUntracked', - } - }) -< +------------------------------------------------------------------------------ +WHAT IS FFF AND WHY USE IT OVER RIPGREP OR FZF?*fff.nvim-what-is-fff-and-why-use-it-over-ripgrep-or-fzf?* -FILE FILTERING +FFF is a file search library, not a CLI. Ripgrep and fzf are great tools, but +they are command-line programs: every call forks a new process, re-reads +`.gitignore`, re-stats directories, and rebuilds whatever state it needs in +memory before it can answer. That is fine when you grep once from a shell. It +is bad when an editor or an AI agent wants to run hundreds of searches per +session. -FFF.nvim respects `.gitignore` patterns automatically. To filter files from the -picker without modifying `.gitignore`, create a `.ignore` file in your project -root: +FFF keeps the index and the file cache resident in one long-lived process and +exposes the same Rust core through four thin layers: a native crate +(`fff-search`), a C library (`libfff_c`), a Node/Bun SDK (`@ff-labs/fff-node`), +and an MCP server. You call `FileFinder.create()` once, then every subsequent +search hits warm memory. On a 500k-file Chromium checkout, that is the +difference between 3-9 **SECONDS** per ripgrep spawn and sub-10 ms per FFF +query. ->gitignore - # Exclude all markdown files - *.md - - # Exclude specific subdirectory - docs/archive/**/*.md -< +Algorithm for fuzzy matching is much more comprehensive than fzf’s algorithm +it is **typo-resistant** and we provide a query language with additional +constraint parsing for prefiltering e.g. “.rs !test/ shcema” is a perfectly +valid query for fff, but fzf wouldn’t find anything even for a single typo in +"shcema". -Run `:FFFScan` to force a rescan if needed. +WHY A PROGRAMMATIC API MATTERS ~ -TROUBLESHOOTING ~ +- No process spawn. Every call stays in-process and avoids the fork, exec, argv parsing, and stdout pipe setup that dominates short `rg` invocations. +- One FS walk, metadata collection, and parse of `.gitignore`. The ignore walker runs once at scan time and the result is reused for every search. +- Results come back as typed objects, not text you have to re-parse. The SDK gives you `{ relativePath, lineNumber, lineContent, gitStatus, totalFrecencyScore, isDefinition, ... }` directly. +- Cursor pagination that survives across calls. Ripgrep has no concept of "page 2 of these matches"; FFF does. +- A long-lived process opens up optimisations that a one-shot CLI cannot apply: warm caches, incremental re-indexing, cross-query frecency, and shared SIMD state. -HEALTH CHECK +WHAT THE CORE ACTUALLY DOES ~ -Run `:FFFHealth` to check the status of FFF.nvim and its dependencies. This -will verify: +- **Frecency-ranked fuzzy matching.** Every indexed file carries an access score and a modification score. Searches rank files you have opened recently and frequently above cold results. This is the same idea as VS Code’s recently-opened list, but applied to every search result, not just a sidebar. +- **Typo-resistant matching for both paths and content.** Smith-Waterman fuzzy scoring is available on the grep path; path search uses SIMD-accelerated fuzzy matching (via the `frizbee` -derived core) that survives dropped characters and reorderings. +- **Content grep with three modes.** Plain literal (SIMD memmem), regex (the Rust `regex` crate), and fuzzy (Smith-Waterman per line). Auto-detects which mode to use from the pattern, falls back to fuzzy when a plain search returns zero hits. +- **Multi-pattern OR search.** SIMD Aho-Corasick for "find any of these 20 identifiers at once", which is faster than regex alternation and a lot faster than 20 separate ripgrep runs. +- **Background file watcher.** The index updates as files change. You never pay for a rescan on the hot path. +- **Git status awareness.** Modified, staged, untracked, and ignored states are cached and returned with every result, so callers can sort or filter them without shelling out to git. The watcher talks to libgit2 directly instead of spawning the `git` CLI. +- **Definition classifier.** A byte-level scanner on the Rust side tags lines that start with `struct`, `fn`, `class`, `def`, `impl`, and friends. -- File picker initialization status -- Optional dependencies (git, image preview tools) -- Database connectivity +PERFORMANCE CHOICES THAT MATTER ~ -VIEWING LOGS +- Efficient memory allocator and memory allocation strategy (see next paragraph). By default we use `mimaloc` +- Parallel multi thread search pipeline that is not contaganted by the orchistration logic +- SIMD first algorithms for everything. Efficinet & non-allocating sorting. +- Platform specific optimizations for FS (getdents64 , NTFS api on windows and others) +- Lightweight on the flight content index for realtime even typo resistant grep +- Memory mapped content cache. We store some of the files in virtual memory (the amount is limited) +- Single contiguous arena storage of string chunks. Significantly reduces the amount of memory to work with and dramatically increases CPU cache hits. -If you encounter issues, check the log file: -> - :FFFOpenLog -< +MEMORY ALLOCATION ~ + +Yes, fff fundamentally requires more memory than calling a single child +process. That is the primary source of the speedup. In practice, alongside one +of the most popular file search pickers for Neovim, fff ends up using less RAM +than a burst of ripgrep invocations +. + +FFF also keeps a content index, around 360 bytes per indexed file, so roughly +36 MB for a 100k-file repo. Not every file is indexed — binaries, oversized +files, and anything not eligible for grep are skipped. If even that footprint +is too much, the index can be backed by a memory-mapped file instead of +anonymous RAM. + + +WHAT THIS MEANS IN PRACTICE ~ + +If you are building an agent, an IDE extension, a pre-commit check, or any +long-running tool that searches the same repository many times, calling FFF as +a library is dramatically cheaper than shelling out to ripgrep. The tradeoff is +real memory: FFF keeps the index in RAM and warms the content cache. On a +14k-file repo that costs about 26 MB resident. On a 500k-file repo like +Chromium, expect a few hundred MB. In exchange, every single search is enriched +with git status, frecency ranking, file metadata, timestamps of last access and +edit and so on. + +If you are running one grep from a terminal, `rg` is still the right tool. If +you run dozens of them inside the same process, FFF will pay for itself +starting from the second call. If you work on AI agent fff will finish +preparation work before your AI will have a chance to call it. + + +HOW IT COMPARES ~ + +- **ripgrep**: FFF uses the same underlying regex engine and more advanced plain text matching algorithms. Stores content index and file tree. Main wins on repeated-search workloads. Loses on "grep once from bash and exit." +- **fzf**: FFF’s path search is fuzzy like fzf, but it is also frecency-aware and git-aware, and ships a more typo-tolerant algorithm. fzf is a pure match-and-filter tool; FFF ranks results by how often you actually open them. +- **Telescope / fzf-lua / snacks.picker**: FFF ships its own Neovim picker with the same ranking the MCP server and SDK use. The picker is optional; the core is the same. +- **Tantivy or other full-text search engines**: different class of tool. Tantivy indexes documents for query-time scoring at scale. FFF is scoped to one repository and optimised for sub-10 ms response. It does not persist an inverted index on disk. + +------------------------------------------------------------------------------ + +REPOSITORY LAYOUT *fff.nvim-repository-layout* + +- `crates/fff-search`, `crates/fff-grep`, `crates/fff-query-parser` - Rust core. +- `crates/fff-c` - C FFI used by every language binding. +- `crates/fff-nvim` - Lua/mlua bindings for the Neovim plugin. +- `crates/fff-mcp` - MCP server binary. +- `packages/fff-node` - Node.js SDK (`@ff-labs/fff-node`). +- `packages/fff-bun` - Bun SDK (`@ff-labs/fff-node`). +- `packages/pi-fff` - pi extension (`@ff-labs/pi-fff`). +- `lua/` - Neovim-side plugin code. + + +CONTRIBUTING *fff.nvim-contributing* + +Bug reports and pull requests welcome. Agentic coding tools are welcome to be +used, but human review is mandatory. + + +LICENSE *fff.nvim-license* -Or manually open the log file at `~/.local/state/nvim/log/fff.log` (default -location). +MIT <./LICENSE> & open source forever. ============================================================================== 1. Links *fff.nvim-links* -1. *Chart showing the superiority of fff.nvim over builtin claude code tools*: ./chart.png +1. *Benchmark chart comparing FFF against the built-in AI file-search tools*: ./chart.png Generated by panvimdoc