fff
El kit de búsqueda de archivos más rápido y preciso para agentes de IA
Documentación
Un kit de búsqueda de archivos para humanos y agentes de IA. Realmente rápido.
Búsqueda de rutas y contenido resistente a errores tipográficos, acceso a archivos clasificados por frecuencia, un vigilante en segundo plano y un índice de contenido ligero en memoria. Mucho más rápido que CLIs como ripgrep y fzf en cualquier proceso de larga duración que busque más de una vez.
Impulsa la búsqueda de archivos en opencode, nushell y muchos otros proyectos increíbles.
Originalmente comenzó como un plugin de Neovim que la gente amaba, pero resultó que muchos harnesses de IA y editores de código necesitan lo mismo: búsqueda de archivos precisa y rápida como biblioteca. Eso es lo que es fff.
Elige lo que te interesa:
Servidor MCP
Funciona con Claude Code, Codex, OpenCode, Cursor, Cline y cualquier cliente compatible con MCP. Menos roundtrips de grep, menos contexto desperdiciado, respuestas más rápidas.

Instalación en una línea
Linux / macOS:
curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash
Windows (PowerShell):
irm https://raw.githubusercontent.com/dmtrKovalenko/fff/main/install-mcp.ps1 | iex
Los scripts viven en install-mcp.sh y install-mcp.ps1 si quieres leerlos primero. Imprimen las instrucciones exactas de conexión para tu cliente.
Homebrew (macOS / Linux)
brew install dmtrKovalenko/fff/fff-mcp
brew upgrade fff-mcp # after new stable releases
La fórmula vive en Formula/fff-mcp.rb en este repositorio y se actualiza automáticamente en cada release estable (ver bump-homebrew-formula en .github/workflows/release.yaml). Instala el binario precompilado fff-mcp desde GitHub releases.
Configuración de Codex
Registra el binario instalado usando su ruta absoluta, ya que las sesiones de escritorio de Codex pueden no heredar el PATH de tu shell interactivo.
Homebrew:
codex mcp add fff -- "$(brew --prefix)/bin/fff-mcp"
Instalador de una línea:
codex mcp add fff -- "$HOME/.local/bin/fff-mcp"
Esto crea una entrada en ~/.codex/config.toml similar a:
[mcp_servers.fff]
command = "/opt/homebrew/bin/fff-mcp"
Usa la ruta instalada real para tu sistema, luego reinicia Codex o inicia una nueva tarea para que cargue el servidor.
Una vez que el servidor esté conectado, pide al agente que "use fff" y este tomará las herramientas ffgrep, fffind y fff-multi-grep.
Prompt recomendado para el agente
Pon esto en el CLAUDE.md de tu proyecto o equivalente:
For any file search or grep in the current git-indexed directory, use fff tools.
Qué cambia
- Memoria de frecencia. Los archivos que realmente abres se clasifican más alto la próxima vez. El calentamiento desde el historial de touch de git se ejecuta automáticamente.
- Sugerencias priorizando definiciones. Las líneas que parecen definiciones de código se clasifican en el lado de Rust, sin sobrecarga de regex en tu prompt.
- Sensibilidad a mayúsculas inteligente con respaldo difuso automático.
IsOffTheRecordencuentra variantes snake_case; las consultas sin coincidencias se reintentan como difusas y muestran los mejores resultados aproximados. - Anotaciones conscientes de git. Los archivos modificados, sin seguimiento y en stage se etiquetan para que el agente alcance lo que estás cambiando activamente.
Fuente: crates/fff-mcp/.
El servidor MCP le da a cualquier agente una herramienta de búsqueda de archivos más rápida y eficiente en tokens que la integrada.
Extensión del agente Pi
Instalación
pi install npm:@ff-labs/pi-fff
Modos
Tres modos de operación, conmutables en tiempo de ejecución con /fff-mode:
| Modo | Qué hace |
|---|---|
tools-and-ui (predeterminado) | Agrega las herramientas ffgrep y fffind, reemplaza el autocompletado de menciones @ con FFF. |
tools-only | Solo inyección de herramientas. Mantiene el autocompletado nativo del editor de pi. |
override | Reemplaza los grep, find y multi_grep integrados de pi con implementaciones de FFF. |
Variables de entorno: PI_FFF_MODE, FFF_FRECENCY_DB, FFF_HISTORY_DB. Banderas: --fff-mode, --fff-frecency-db, --fff-history-db. Las bases de datos usan por defecto las existentes de fff.nvim cuando están presentes; de lo contrario, ~/.pi/agent/fff/.
Herramientas orientadas al agente
ffgrep. Búsqueda de contenido. Aceptapath,exclude(coma, espacio o array; el!inicial es opcional),caseSensitive,contexty paginación con cursor. Detecta automáticamente regex, cae en difuso ante cero coincidencias exactas y rechaza patrones de solo comodines estilo.*de antemano.fffind. Búsqueda de rutas y nombres de archivo. Coincide con toda la ruta relativa al repositorio, no solo con el nombre del archivo. Consciente de frecencia. El detector de coincidencias débiles marca el ruido difuso disperso antes de que inunde el contexto del agente.
Comandos
/fff-mode [tools-and-ui | tools-only | override]. Mostrar o cambiar el modo./fff-health. Estado del picker, frecencia e integración con git./fff-rescan. Forzar un nuevo escaneo.
Fuente: packages/pi-fff/.
La extensión de Pi intercambia las herramientas nativas de pi por implementaciones de FFF y alimenta el autocompletado de menciones @ del editor interactivo desde el índice clasificado por frecencia.
fff.nvim
Demo en el repositorio del kernel de Linux (100k archivos, 8GB):
https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb
Instalación
lazy.nvim
-- Package name changed from `fff.nvim` to `fff`. If you installed fff.nvim before, clean with `:Lazy clean`
{
'dmtrKovalenko/fff',
build = function()
-- downloads a prebuilt binary or falls back to cargo build
require("fff.download").download_or_build_binary()
end,
-- for nixos:
-- build = "nix run .#release",
opts = {
debug = {
enabled = true,
show_scores = true,
},
},
lazy = false, -- the plugin lazy-initialises itself
keys = {
{ "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',
},
{ "fw",
function() require('fff').live_grep_under_cursor() end,
mode = { 'n', 'x' },
desc = 'Search current word / selection',
},
},
}
vim.pack
-- Package name changed from `fff.nvim` to `fff`. If you installed fff.nvim before, clean with `:packdel fff.nvim`
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff' })
vim.api.nvim_create_autocmd('PackChanged', {
callback = function(ev)
local name, kind = ev.data.spec.name, ev.data.kind
if name == 'fff' and (kind == 'install' or kind == 'update') then
if not ev.data.active then vim.cmd.packadd('fff') end
require('fff.download').download_or_build_binary()
end
end,
})
vim.g.fff = {
lazy_sync = true,
debug = { enabled = true, show_scores = true },
}
vim.keymap.set('n', 'ff', function() require('fff').find_files() end, { desc = 'FFFind files' })
API pública
require('fff').find_files() -- find files in current repo
require('fff').live_grep() -- live content grep
require('fff').live_grep_under_cursor() -- grep <cword> in normal, selection in visual
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
-- Programmatic search (no UI). Useful for plugin integrations.
require('fff').file_search(query, opts) -- fuzzy search files / dirs / mixed
require('fff').content_search(query, opts) -- programmatic grep
file_search(query, opts)
Devuelve un resultado estructurado { items, scores, total_matched, total_files?, total_dirs?, location? }. Cada elemento tiene un campo type ("file" o "directory") y name / relative_path. Los elementos de archivo también exponen size, modified, git_status, is_binary y puntuaciones de frecencia.
local r = require('fff').file_search('button', {
mode = 'mixed', -- 'files' (default) | 'directories' | 'mixed'
max_results = 50,
page = 0, -- 0-based pagination
current_file = nil, -- path to deprioritize for distance scoring
max_threads = 4,
cwd = nil, -- switch indexed root if different (see below)
wait_for_index_ms = nil, -- override the default scan wait timeout
})
for _, item in ipairs(r.items) do
print(item.type, item.relative_path)
end
content_search(query, opts)
Devuelve un GrepResult { items, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error? }. Cada elemento de coincidencia tiene relative_path, name, line_number, col, line_content, match_ranges, más los mismos metadatos de archivo que file_search.
local r = require('fff').content_search('TODO', {
mode = 'plain', -- 'plain' (default) | 'regex' | 'fuzzy'
max_file_size = 10 * 1024 * 1024,
max_matches_per_file = 100,
smart_case = true,
page_size = 50,
file_offset = 0,
time_budget_ms = 0,
trim_whitespace = false,
cwd = nil, -- switch indexed root if different
wait_for_index_ms = nil, -- override the default scan wait timeout
})
for _, m in ipairs(r.items) do
print(string.format('%s:%d %s', m.relative_path, m.line_number, m.line_content))
end
Ambas funciones aceptan la misma sintaxis de restricciones que los pickers de la interfaz (por ejemplo, git:modified, *.rs, !test/, patrones glob).
cwd e indexación
Tanto file_search como content_search respetan un campo opcional cwd. La primera llamada a cualquiera de las funciones inicializa perezosamente el picker en config.base_path (tu cwd de Neovim por defecto).
- Si
cwdcoincide con la raíz actualmente indexada, la llamada regresa inmediatamente contra el índice existente. - Si
cwddifiere, el picker se reindexa en la nueva raíz y la llamada bloquea (por defecto hasta 10 s) hasta que el nuevo picker esté instalado y su escaneo inicial se complete — así los llamadores siempre obtienen resultados del árbol correcto. - Si el índice aún se está calentando después de un
change_indexing_directory, puedes pasarwait_for_index_ms = Npara bloquear hastaNms independientemente de sicwdprovocó el intercambio. Pasa0para omitir la espera por completo (útil para llamadas de disparar y olvidar donde los resultados parciales son aceptables). - Las rutas
cwdinválidas o inexistentes devuelven un resultado vacío y emiten un error víavim.notify.
Comandos
:FFFScan. Reescanear archivos.:FFFRefreshGit. Refrescar el estado de git.:FFFClearCache [all|frecency|files]. Limpiar cachés.:FFFHealth. Verificación de salud.:FFFDebug [on|off|toggle]. Alternar la visualización de puntuaciones.:FFFOpenLog. Abrir~/.local/state/nvim/log/fff.log.
Configuración
Los valores predeterminados son sensatos. Sobrescribe solo lo que te importa.
require('fff').setup({
base_path = vim.fn.getcwd(),
prompt = '> ',
title = 'FFFiles',
max_results = 100,
max_threads = 4,
lazy_sync = true,
prompt_vim_mode = false,
follow_symlinks = false,
-- Allow indexing the user's $HOME directory. Enabled by default.
-- Disable if you strictly sure you don't want this, as it makes whole fff error hard
enable_home_dir_scanning = true,
-- Allow indexing a filesystem root (e.g. `/`, `C:\`). Disabled by default
enable_fs_root_scanning = false,
layout = {
height = 0.8,
width = 0.8,
prompt_position = 'bottom', -- or 'top'
preview_position = 'right', -- 'left' | 'right' | 'top' | 'bottom'
preview_size = 0.5,
-- Border style for the picker windows. Leave unset (nil) to follow the
-- global `vim.o.winborder`; set it to override fff's borders independently.
border = nil, -- 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'none'
-- border = {
-- { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' },
-- { ' ', ' ', ' ', ' ', ' ' },
-- },
flex = { size = 130, wrap = 'top' },
min_list_height = 10, -- do not display anything except the list below this threshold
show_scrollbar = true,
path_shorten_strategy = 'middle_number', -- 'middle_number' | 'middle' | 'end' | 'start'
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 = '<Esc>',
select = '<CR>',
select_split = '<C-s>',
select_vsplit = '<C-v>',
select_tab = '<C-t>',
move_up = { '<Up>', '<C-p>' },
move_down = { '<Down>', '<C-n>' },
preview_scroll_up = '<C-u>',
preview_scroll_down = '<C-d>',
toggle_debug = '<F2>',
cycle_grep_modes = '<S-Tab>',
insert_newline_escape = '<C-CR>',
-- grep mode only: jump cursor to first match of next/prev file group
grep_jump_to_next_file = { '<C-A-n>', '<A-Down>' },
grep_jump_to_prev_file = { '<C-A-p>', '<A-Up>' },
cycle_previous_query = '<C-Up>',
toggle_select = '<Tab>',
send_to_quickfix = '<C-q>',
focus_list = '<leader>l',
focus_preview = '<leader>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
},
file_picker = {
fuzzy_query_highlighting = false, -- true to highlight fuzzy query matches in file picker results
},
select = {
-- Return winid to open the chosen file in, or nil to open in the original window
select_window = function(current_buf, action) --[[ default impl ]] end,
},
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,
enable_filename_constraint = false, -- treat filename-like tokens (e.g. `score.rs`) in a grep query as a file-path filter scoping the search; off = searched as literal text
location_format = ':%d:%d', -- printf format for line:col prefix in grep results, e.g. ':%d' for line-only
},
debug = {
enabled = false, -- show the file info panel next to the preview
show_scores = false, -- inline scores in the file list
-- Per-section toggles for the file info panel. Accepts a boolean shorthand
-- (`show_file_info = true|false`) to flip everything at once. The panel
-- adapts to width: narrow renders sections vertically, wide renders them
-- as a two-column grid. Disable a section to also shrink the panel.
show_file_info = {
file_info = true, -- size, type, git status, frecency
score_breakdown = true, -- total + match type, bonuses, modifiers, penalty
-- modified + accessed timestamps; pass a table to hide individual rows:
-- timings = { modified = false, accessed = true }
timings = true,
full_path = true, -- relative path at the bottom (wraps if too long)
},
},
logging = {
-- logs will be written in a parent directory of this file path in files like
-- `<stem>+<UTC-timestamp>+<pid>.<ext>`. Run :FFFOpenLog to open current one
log_file = vim.fn.stdpath('log') .. '/fff.log',
log_level = 'info',
retain_runs = 20,
},
})
Modos de grep en vivo
<S-Tab> cicla entre plain, regex y fuzzy. La lista es configurable vía grep.modes, y las configuraciones de modo único ocultan el indicador por completo.
Sobrescritura por llamada:
require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } })
require('fff').live_grep({ query = 'search term' }) -- pre-fill
Restricciones
Tanto find como grep aceptan estos tokens para refinar una consulta:
git:modified. Uno demodified,staged,deleted,renamed,untracked,ignored.test/. Cualquier hijo profundamente anidado detest/.!something,!test/,!git:modified. Exclusión. Las exclusiones de texto necesitan al menos 3 caracteres alfanuméricos, para que operadores como!=o!==funcionen../**/*.{rs,lua}. Cualquier glob válido, impulsado por zlob.
Solo grep:
*.md,*.{c,h}. Filtro de extensión.src/main.rs. Grep dentro de un solo archivo.
Mezcla libremente: git:modified src/**/*.rs !src/**/mod.rs user controller.
Abrir en la ventana invocadora
Por defecto, fff.nvim intentará abrir un archivo en la ventana más adecuada, para que los buffers que no son de archivo no se vean afectados. Puedes personalizar o deshabilitar esto proporcionando:
require('fff').setup({
select = {
select_window = function(_current_buf, _action) return nil end,
},
})
Advertencia: el archivo elegido reemplaza el buffer en la ventana invocadora incluso si es un buftype no modificable / especial. Las ventanas winfixbuf aún recurren a :split para evitar E1513.
Multi-selección y quickfix
<Tab>. Alternar selección (muestra un▊grueso en la columna de signos).<C-q>. Enviar los archivos seleccionados a la lista quickfix y cerrar el picker.
Resaltado de estado de git
Los indicadores de la columna de signos están activados por defecto. Para colorear el texto del nombre de archivo según el estado de git, establece git.status_text_color = true y ajusta los grupos hl.git_*. Consulta :help fff.nvim para la lista completa.
Colores del float
El picker mapea su contenido flotante a NormalFloat (vía hl.normal) y el borde a FloatBorder. El FloatBorder predeterminado enlaza a NormalFloat, por lo que el borde y el contenido comparten un fondo de fábrica y el picker se lee como un solo popup. Sobrescribe hl.normal = 'Normal' para hacer que el picker se mezcle con el editor en su lugar.
Para un control más fino, establece hl.winhl para sobrescribir el winhighlight por ventana. Acepta una sola cadena aplicada a cada ventana del picker, o una tabla con claves opcionales prompt, list, preview y file_info. Las claves faltantes recurren al valor predeterminado construido desde hl.normal, hl.border y hl.title.
-- Apply the same winhighlight to all picker windows
hl = { winhl = 'Normal:NormalFloat,FloatBorder:FloatBorder,FloatTitle:Title' }
-- Or override specific windows only
hl = {
winhl = {
prompt = 'Normal:Pmenu,FloatBorder:FloatBorder',
list = 'Normal:NormalFloat,FloatBorder:FloatBorder',
preview = 'Normal:NormalFloat,FloatBorder:FloatBorder',
},
}
Panel de información del archivo
Habilita con debug.enabled = true. El panel se sitúa sobre la vista previa y muestra
metadatos del archivo, desglose de puntuación, marcas de tiempo y la ruta absoluta completa. Se
adapta al ancho del panel: en anchos estrechos las secciones se apilan verticalmente (B2),
en anchos amplios las secciones se renderizan como una cuadrícula de dos columnas (H2). Cada sección puede
deshabilitarse individualmente vía debug.show_file_info.
Personaliza el panel vía hl:
| clave | predeterminado | usado para |
|---|---|---|
file_info_section | Title | etiqueta del encabezado de sección |
file_info_separator | FloatBorder | guiones que actúan como bordes de sección |
file_info_label | Comment | etiquetas de fila (Tamaño, Tipo, Git, ...) |
file_info_value | Normal fg | valores simples |
file_info_value_dim | NonText | valores atenuados, separadores dentro de filas |
file_info_size | Number | valor del tamaño de archivo |
file_info_type | Type | valor del tipo de archivo |
file_info_path | Directory | ruta completa |
file_info_total_score | negrita + Number | puntuación total (negrita) |
file_info_match_type | negrita + Special | tipo de coincidencia (negrita) |
file_info_score_pos | DiagnosticOk | componentes de puntuación positivos |
file_info_score_neg | DiagnosticError | componentes de puntuación negativos |
Filtrado de archivos
FFF respeta .gitignore. Para ignorados solo del picker que no tocan git, agrega un archivo hermano .ignore:
*.md
docs/archive/**/*.md
Ejecuta :FFFScan para forzar un nuevo escaneo.
Solución de problemas
:FFFHealthverifica la inicialización del picker, las dependencias opcionales y la conectividad con la base de datos.:FFFOpenLogabre el archivo de registro de la sesión actual.- Los archivos de registro históricos se almacenan cerca del archivo de registro principal
<state>/log/fff+<UTC-timestamp>+<pid>.log(hasta 20 archivos). - Para obtener un backtrace de un fallo, ejecuta
lldb -- nvimogdb -- nvimy reproduce el problema.
El mejor picker de búsqueda de archivos para neovim. Punto. Consultas más rápidas e intuitivas, clasificación por frecencia, clasificación de definiciones y mucho más.
SDK de Node y Bun
npm install @ff-labs/fff-node
# or
bun add @ff-labs/fff-node
import { FileFinder } from "@ff-labs/fff-node";
const finder = FileFinder.create({ basePath: process.cwd(), aiMode: true });
if (!finder.ok) throw new Error(finder.error);
await finder.value.waitForScan(10_000);
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,
});
// Run extremely fast glob matching which is significantly (10-100 times) faster than Bun's and Node implementation
const rustFiles = finder.value.glob("**/*.rs", { pageSize: 100 });
finder.value.destroy();
Cada método devuelve un Result<T> ({ ok: true, value } | { ok: false, error }). Referencia de tipos completa: packages/fff-node/src/types.ts.
Envoltorio de TypeScript sobre la biblioteca C para nodejs y bun. Construye herramientas de agente personalizadas, CLIs o integraciones de IDE sobre FFF.
Crate de Rust
Añadir la dependencia
FFF está escrito en Rust, por lo que esta es la forma de menor sobrecarga para usarlo.
[dependencies]
fff-search = "0.6"
Documentación completa de la API: docs.rs/fff-search.
Crate nativo de Rust que realiza toda la búsqueda. Estable y bien documentado.
Biblioteca C
Compilar
# Builds only the C cdylib (fastest):
make build-c-lib
# or directly with cargo:
cargo build --release -p fff-c --features zlob
La característica
zlob(requiere el toolchain de Zig) cambia tanto el glob matching como el recorrido del sistema de archivos al walker paralelo nativo de zlob. Sin ella, la compilación predeterminada usa el walkerignore(ripgrep) de Rust puro yglobset.
La salida es un cdylib (libfff_c.so / libfff_c.dylib / fff_c.dll). El encabezado se encuentra en crates/fff-c/include/fff.h.
Los binarios precompilados para cada versión, incluidos cada commit en main, están en la página de lanzamientos. Los mismos binarios también se incluyen dentro de los paquetes npm @ff-labs/fff-bin-*.
Instalar
# 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
Coloca libfff_c.{so,dylib,dll} en $(PREFIX)/lib y el encabezado en $(PREFIX)/include/fff.h. Elimínalo con make uninstall, que respeta las mismas PREFIX y DESTDIR.
Enlázalo después de la instalación:
cc my_app.c -lfff_c -o my_app
Asegúrate de que $(PREFIX)/lib esté en tu ruta de búsqueda de bibliotecas en tiempo de ejecución (LD_LIBRARY_PATH en Linux, DYLD_LIBRARY_PATH en macOS, o una entrada en /etc/ld.so.conf.d/).
Ejemplo mínimo
#include <fff.h>
#include <stdio.h>
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;
}
Struct de opciones versionada (preferida)
Para la creación de instancias usa FffCreateOptions — una
struct versionada que evoluciona sin romper la ABI. Los inicializadores designados de C99
mantienen los puntos de llamada legibles y ponen a cero los campos no especificados:
FffResult *res = fff_create_instance_with(&(FffCreateOptions){
.version = FFF_CREATE_OPTIONS_VERSION,
.base_path = "/path/to/repo",
.ai_mode = true,
.watch = true,
.enable_fs_root_scanning = false, // off by default
.enable_home_dir_scanning = false, // off by default
});
Búsqueda solo con glob
fff_glob filtra los archivos indexados por un único patrón glob, clasifica por frecencia,
pagina — omite el analizador de consultas regular por completo. Úsalo cuando ya
tengas un glob literal (*.rs, **/*.test.ts, src/**) y no quieras
coincidencia difusa superpuesta.
FffResult *res = fff_glob(handle, "**/*.rs", "", 0, 0, 100);
// FffSearchResult in res->handle, free with fff_free_search_result.
Notas
- Cada función que devuelve
FffResult*asigna memoria conBoxde Rust. Libera confff_free_result, no uses el free de malloc. - Los payloads (resultados de búsqueda, resultados de grep, progreso de escaneo) tienen sus propias funciones de liberación dedicadas listadas en el encabezado.
- Las cadenas C devueltas en el campo
handle(por ejemplo, defff_get_base_path) se liberan confff_free_string.
Fuente: crates/fff-c/.
ABI C estable. Enlaza desde C/C++, Zig, Go mediante cgo, Python mediante ctypes, o cualquier cosa con FFI de C.
Enlaces de Python
Instalar
pip install fff-search
O compila e instala desde el código fuente:
cd packages/fff-python
uv sync --all-extras
uv run maturin develop --release
Uso básico
from fff import FileFinder
with FileFinder("/path/to/project", watch=False) as finder:
finder.wait_for_scan_blocking(timeout_ms=5000)
result = finder.search("main")
for item, score in zip(result.items, result.scores):
print(f"{item.relative_path}: {score.total}")
hits = finder.grep("class Profile", mode="plain", before_context=1, after_context=1)
Uso asíncrono
wait_for_scan es una corrutina que consulta el estado del escaneo y cede al bucle de
eventos, por lo que nunca bloquea otras tareas. Usa wait_for_scan_blocking desde
código síncrono.
import asyncio
from fff import FileFinder
async def main():
with FileFinder("/path/to/project", watch=False) as finder:
await finder.wait_for_scan(timeout_ms=5000)
result = finder.search("main")
print(result)
asyncio.run(main())
Lo que obtienes
search,glob,directory_search,mixed_search— búsqueda difusa de archivos/directorios clasificada por frecenciagrep/multi_grep— búsqueda de contenido simple, regex o difusa con líneas de contexto y paginación por cursortrack_query/get_historical_query— bases de datos opcionales de frecencia e historial de consultasreindex,refresh_git_status,scan_progress,health_check— ciclo de vida y diagnóstico
Objetos de resultado tipados (FileItem, Score, GrepMatch, …) con stubs de py.typed
incluidos. Se distribuye como un wheel abi3 compatible con Python 3.10+.
Fuente: packages/fff-python/.
Enlaces nativos de Python construidos con PyO3. Úsalos para notebooks, scripts de agente o cualquier herramienta de Python que necesite búsqueda rápida de archivos.
¿Qué es FFF y por qué usarlo en lugar de ripgrep o fzf?
FFF es una biblioteca de búsqueda de archivos, no un CLI. Ripgrep y fzf son excelentes herramientas, pero son programas de línea de comandos: cada llamada crea un nuevo proceso, vuelve a leer .gitignore, vuelve a hacer stat de los directorios y reconstruye cualquier estado que necesite en memoria antes de poder responder. Eso está bien cuando haces grep una vez desde un shell. Es malo cuando un editor o un agente de IA quiere ejecutar cientos de búsquedas por sesión.
FFF mantiene el índice y la caché de archivos residentes en un único proceso de larga duración y expone el mismo núcleo de Rust a través de cuatro capas delgadas: un crate nativo (fff-search), una biblioteca C (libfff_c), un SDK de Node/Bun (@ff-labs/fff-node) y un servidor MCP. Llamas a FileFinder.create() una vez, y luego cada búsqueda posterior golpea memoria caliente. En un checkout de Chromium de 500k archivos, esa es la diferencia entre 3-9 SEGUNDOS por spawn de ripgrep y menos de 10 ms por consulta de FFF.
El algoritmo de coincidencia difusa es mucho más completo que el algoritmo de fzf. Es resistente a errores tipográficos y proporcionamos un lenguaje de consulta con análisis de restricciones adicionales para el pre-filtrado, por ejemplo, "*.rs !test/ shcema" es una consulta perfectamente válida para fff, pero fzf no encontraría nada incluso para un solo error tipográfico en "shcema".
Por qué importa una API programática
- Sin spawn de procesos. Cada llamada permanece en el proceso y evita el fork, exec, análisis de argv y configuración de la tubería stdout que domina las invocaciones cortas de
rg. - Un solo recorrido del FS, recopilación de metadatos y análisis de
.gitignore. El walker de ignorados se ejecuta una vez en el momento del escaneo y el resultado se reutiliza para cada búsqueda. - Los resultados vuelven como objetos tipados, no como texto que tienes que volver a analizar. El SDK te da
{ relativePath, lineNumber, lineContent, gitStatus, totalFrecencyScore, isDefinition, ... }directamente. - Paginación por cursor que sobrevive entre llamadas. Ripgrep no tiene concepto de "página 2 de estas coincidencias"; FFF sí.
- Un proceso de larga duración abre optimizaciones que un CLI de un solo disparo no puede aplicar: cachés calientes, re-indexación incremental, frecencia entre consultas y estado SIMD compartido.
Qué hace realmente el núcleo
- Coincidencia difusa clasificada por frecencia. Cada archivo indexado lleva una puntuación de acceso y una puntuación de modificación. Las búsquedas clasifican los archivos que has abierto recientemente y con frecuencia por encima de los resultados fríos. Esta es la misma idea que la lista de recientes de VS Code, pero aplicada a cada resultado de búsqueda, no solo a una barra lateral.
- Coincidencia resistente a errores tipográficos tanto para rutas como para contenido. La puntuación difusa de Smith-Waterman está disponible en la ruta de grep; la búsqueda de rutas usa coincidencia difusa acelerada por SIMD (a través del núcleo derivado de
frizbee) que sobrevive a caracteres omitidos y reordenamientos. - Grep de contenido con tres modos. Literal simple (SIMD memmem), regex (el crate
regexde Rust) y difuso (Smith-Waterman por línea). Detecta automáticamente qué modo usar a partir del patrón, y cae al modo difuso cuando una búsqueda simple devuelve cero resultados. - Búsqueda OR de múltiples patrones. SIMD Aho-Corasick para "encontrar cualquiera de estos 20 identificadores a la vez", que es más rápido que la alternancia regex y mucho más rápido que 20 ejecuciones separadas de ripgrep.
- Observador de archivos en segundo plano. El índice se actualiza a medida que los archivos cambian. Nunca pagas por un nuevo escaneo en la ruta crítica.
- Conciencia del estado de git. Los estados modificado, preparado, no rastreado e ignorado se almacenan en caché y se devuelven con cada resultado, para que los llamadores puedan ordenarlos o filtrarlos sin invocar a git. El observador habla con libgit2 directamente en lugar de lanzar el CLI de
git. - Clasificador de definiciones. Un escáner a nivel de byte en el lado de Rust etiqueta líneas que comienzan con
struct,fn,class,def,imply similares.
Elecciones de rendimiento que importan
- Asignador de memoria eficiente y estrategia de asignación de memoria (ver siguiente párrafo). Por defecto usamos
mimaloc. - Pipeline de búsqueda paralelo de múltiples hilos que no está contaminado por la lógica de orquestación.
- Algoritmos SIMD primero para todo. Ordenación eficiente y sin asignación.
- Optimizaciones específicas de plataforma para FS (getdents64, API NTFS en Windows y otras).
- Índice de contenido ligero en vuelo para grep en tiempo real incluso resistente a errores tipográficos.
- Caché de contenido mapeado en memoria. Almacenamos algunos de los archivos en memoria virtual (la cantidad está limitada).
- Almacenamiento de fragmentos de cadena en un único arena contiguo. Reduce significativamente la cantidad de memoria con la que trabajar y aumenta drásticamente los aciertos de caché de CPU.
Asignación de memoria
Sí, fff fundamentalmente requiere más memoria que llamar a un solo proceso hijo. Esa es la fuente principal de la aceleración. En la práctica, junto a uno de los pickers de búsqueda de archivos más populares para Neovim, fff termina usando menos RAM que una ráfaga de invocaciones de ripgrep.
FFF también mantiene un índice de contenido, alrededor de 360 bytes por archivo indexado, es decir, aproximadamente 36 MB para un repositorio de 100k archivos. No todos los archivos se indexan: los binarios, los archivos de gran tamaño y cualquier cosa no elegible para grep se omiten. Si incluso esa huella es demasiado, el índice puede respaldarse con un archivo mapeado en memoria en lugar de RAM anónima.
Qué significa esto en la práctica
Si estás construyendo un agente, una extensión de IDE, una comprobación de pre-commit o cualquier herramienta de larga duración que busque en el mismo repositorio muchas veces, llamar a FFF como biblioteca es dramáticamente más barato que invocar a ripgrep desde un shell. La compensación es memoria real: FFF mantiene el índice en RAM y calienta la caché de contenido. En un repositorio de 14k archivos, eso cuesta alrededor de 26 MB residentes. En un repositorio de 500k archivos como Chromium, espera unos pocos cientos de MB. A cambio, cada búsqueda individual se enriquece con estado de git, clasificación por frecencia, metadatos de archivo, marcas de tiempo del último acceso y edición, y así sucesivamente.
Si estás ejecutando un grep desde una terminal, rg sigue siendo la herramienta adecuada. Si ejecutas docenas dentro del mismo proceso, FFF se pagará por sí mismo a partir de la segunda llamada. Si trabajas en un agente de IA, fff terminará el trabajo de preparación antes de que tu IA tenga la oportunidad de llamarlo.
Cómo se compara
- ripgrep: FFF usa el mismo motor regex subyacente y algoritmos de coincidencia de texto plano más avanzados. Almacena el índice de contenido y el árbol de archivos. Gana principalmente en cargas de trabajo de búsqueda repetida. Pierde en "grep una vez desde bash y salir".
- fzf: La búsqueda de rutas de FFF es difusa como fzf, pero también es consciente de la frecencia y de git, y trae un algoritmo más tolerante a errores tipográficos. fzf es una herramienta pura de coincidencia y filtrado; FFF clasifica los resultados por la frecuencia con la que realmente los abres.
- Telescope / fzf-lua / snacks.picker: FFF trae su propio picker de Neovim con la misma clasificación que usan el servidor MCP y el SDK. El picker es opcional; el núcleo es el mismo.
- Tantivy u otros motores de búsqueda de texto completo: una clase de herramienta diferente. Tantivy indexa documentos para puntuación en tiempo de consulta a escala. FFF está limitado a un repositorio y optimizado para respuestas de menos de 10 ms. No persiste un índice invertido en disco.
Estructura del repositorio
crates/fff-search,crates/fff-grep,crates/fff-query-parser- Núcleo en Rust.crates/fff-c- FFI en C utilizado por cada enlace de lenguaje.crates/fff-nvim- Enlaces Lua/mlua para el plugin de Neovim.crates/fff-mcp- Binario del servidor MCP.packages/fff-node- SDK de Node.js (@ff-labs/fff-node).packages/fff-bun- SDK de Bun (@ff-labs/fff-bun).packages/pi-fff- Extensión pi (@ff-labs/pi-fff).lua/- Código del plugin del lado de Neovim.
Contribuciones
Se aceptan informes de errores y solicitudes de extracción. Se permite el uso de herramientas de codificación agénticas, pero la revisión humana es obligatoria.
Licencia
MIT y código abierto para siempre.
Preguntas frecuentes
¿Qué significa FFF?
A propósito no existe una definición canónica única. Elige tu favorita:
- Fast File Finder (Buscador rápido de archivos)
- Fuzzy File Finder (Buscador difuso de archivos)
- buscará Files For Food (archivos por comida)
El hex de la marca es #F87216, no #FFF. Variantes del logotipo: naranja · oscuro · claro.
