fff

El kit de búsqueda de archivos más rápido y preciso para agentes de IA

Documentación

FFF

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.

dmtrKovalenko%2Ffff | Trendshift


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.

Benchmark chart comparing FFF against the built-in AI file-search tools

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. IsOffTheRecord encuentra 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:

ModoQué hace
tools-and-ui (predeterminado)Agrega las herramientas ffgrep y fffind, reemplaza el autocompletado de menciones @ con FFF.
tools-onlySolo inyección de herramientas. Mantiene el autocompletado nativo del editor de pi.
overrideReemplaza 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. Acepta path, exclude (coma, espacio o array; el ! inicial es opcional), caseSensitive, context y 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 cwd coincide con la raíz actualmente indexada, la llamada regresa inmediatamente contra el índice existente.
  • Si cwd difiere, 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 pasar wait_for_index_ms = N para bloquear hasta N ms independientemente de si cwd provocó el intercambio. Pasa 0 para omitir la espera por completo (útil para llamadas de disparar y olvidar donde los resultados parciales son aceptables).
  • Las rutas cwd inválidas o inexistentes devuelven un resultado vacío y emiten un error vía vim.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 de modified, staged, deleted, renamed, untracked, ignored.
  • test/. Cualquier hijo profundamente anidado de test/.
  • !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:

clavepredeterminadousado para
file_info_sectionTitleetiqueta del encabezado de sección
file_info_separatorFloatBorderguiones que actúan como bordes de sección
file_info_labelCommentetiquetas de fila (Tamaño, Tipo, Git, ...)
file_info_valueNormal fgvalores simples
file_info_value_dimNonTextvalores atenuados, separadores dentro de filas
file_info_sizeNumbervalor del tamaño de archivo
file_info_typeTypevalor del tipo de archivo
file_info_pathDirectoryruta completa
file_info_total_scorenegrita + Numberpuntuación total (negrita)
file_info_match_typenegrita + Specialtipo de coincidencia (negrita)
file_info_score_posDiagnosticOkcomponentes de puntuación positivos
file_info_score_negDiagnosticErrorcomponentes 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

  • :FFFHealth verifica la inicialización del picker, las dependencias opcionales y la conectividad con la base de datos.
  • :FFFOpenLog abre 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 -- nvim o gdb -- nvim y 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 walker ignore (ripgrep) de Rust puro y globset.

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 con Box de Rust. Libera con fff_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, de fff_get_base_path) se liberan con fff_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 frecencia
  • grep / multi_grep — búsqueda de contenido simple, regex o difusa con líneas de contexto y paginación por cursor
  • track_query / get_historical_query — bases de datos opcionales de frecencia e historial de consultas
  • reindex, 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 regex de 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, impl y 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.