Files
wehub-resource-sync fa51ddb29d
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-linux-android (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-linux-android (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Waiting to run
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Waiting to run
Build & Publish / Build Python sdist (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Spelling / Spell Check with Typos (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
docs: make Chinese README the default
2026-07-13 10:15:26 +00:00

819 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- WEHUB_ZH_README -->
> [!NOTE]
> 本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
> [English](./README.en.md) · [原始项目](https://github.com/dmtrKovalenko/fff.nvim) · [上游 README](https://github.com/dmtrKovalenko/fff.nvim/blob/HEAD/README.md)
> 原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
<img alt="FFF" src="./assets/logo-orange.png" width="300">
<p>
<i>面向人类与 AI agent 的文件搜索工具包。速度极快。</i>
</p>
抗拼写错误的路径与内容搜索、按 frecency(访问频次)排序的文件访问、后台监视器,以及轻量级内存内容索引。在任何需要多次搜索的长期运行进程中,比 ripgrep、fzf 等 CLI 快得多。
为 [opencode](http://github.com/anomalyco/opencode/), [nushell](https://github.com/nushell/nushell), 以及许多其他出色项目的文件搜索提供动力!
最初作为广受喜爱的 [Neovim 插件](#neovim-plugin) 起步,但事实证明大量 AI harness 与代码编辑器都需要同样的能力:作为库提供准确、快速的文件搜索。这就是 fff。
---
选择你感兴趣的内容:
<details id="mcp-server">
<summary>
<h2>MCP server</h2>
</summary>
可与 Claude Code、Codex、OpenCode、Cursor、Cline 及任何支持 MCP 的客户端配合使用。减少 grep 往返次数、节省无效上下文、更快得到答案。
![基准测试图表:FFF 与内置 AI 文件搜索工具对比](./chart.png)
### 一行安装
Linux / macOS
```bash
curl -L https://dmtrkovalenko.dev/install-fff-mcp.sh | bash
```
Windows (PowerShell)
```powershell
irm https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.ps1 | iex
```
若想先阅读脚本,可查看 [`install-mcp.sh`](./install-mcp.sh) 与 [`install-mcp.ps1`](./install-mcp.ps1)。它们会打印针对你客户端的精确接入说明。
### HomebrewmacOS / Linux
```bash
brew install dmtrKovalenko/fff/fff-mcp
brew upgrade fff-mcp # after new stable releases
```
Formula 位于本仓库的 [`Formula/fff-mcp.rb`](./Formula/fff-mcp.rb),**每次稳定版发布都会自动 bump**(见 [`.github/workflows/release.yaml`](./.github/workflows/release.yaml) 中的 `bump-homebrew-formula`)。从 [GitHub releases](https://github.com/dmtrKovalenko/fff.nvim/releases). 安装预构建的 `fff-mcp` 二进制。
服务器连接后,让 agent「使用 fff」,即可启用 `ffgrep``fffind``fff-multi-grep` 工具。
### 推荐的 agent 提示词
将其放入项目的 `CLAUDE.md` 或等价文件中:
```markdown
For any file search or grep in the current git-indexed directory, use fff tools.
```
### 带来的变化
- Frecency 记忆。你实际打开过的文件下次排名更高。会基于 git touch 历史自动预热。
- 定义优先提示。看起来像代码定义的行在 Rust 侧完成分类,提示中无需正则开销。
- 智能大小写并自动模糊回退。`IsOffTheRecord` 可匹配 snake_case 变体;零匹配查询会重试为模糊搜索并呈现最佳近似命中。
- Git 感知标注。已修改、未跟踪与已暂存文件会被标记,便于 agent 优先关注你正在修改的内容。
源码:[`crates/fff-mcp/`](./crates/fff-mcp/)。
</details>
MCP server 为任意 agent 提供比内置方案更快、更省 token 的文件搜索工具。
<details id="pi-extension">
<summary>
<h2>Pi agent extension</h2>
</summary>
### 安装
```bash
pi install npm:@ff-labs/pi-fff
```
### 模式
三种运行模式,可在运行时用 `/fff-mode` 切换:
| 模式 | 作用 |
| ------------------------ | --------------------------------------------------------------------------------- |
| `tools-and-ui`(默认) | 添加 `ffgrep``fffind` 工具,用 FFF 替换 `@`-mention 自动补全。 |
| `tools-only` | 仅注入工具。保留 pi 原生编辑器自动补全。 |
| `override` | 用 FFF 实现替换 pi 内置的 `grep``find``multi_grep`。 |
环境变量:`PI_FFF_MODE``FFF_FRECENCY_DB``FFF_HISTORY_DB`。标志:`--fff-mode``--fff-frecency-db``--fff-history-db`
### 面向 agent 的工具
- `ffgrep`。内容搜索。接受 `path``exclude`(逗号、空格或数组;前导 `!` 可选)、`caseSensitive``context`,以及游标分页。自动检测正则,零精确匹配时回退为模糊搜索, upfront 拒绝仅含 `.*` 风格通配符的模式。
- `fffind`。路径与文件名搜索。匹配整个仓库相对路径,而非仅文件名。支持 frecency 感知。弱匹配检测器会在散乱的模糊噪声淹没 agent 上下文之前将其标记出来。
### 命令
- `/fff-mode [tools-and-ui | tools-only | override]`。显示或切换模式。
- `/fff-health`。选择器、frecency 与 git 集成状态。
- `/fff-rescan`。强制重新扫描。
源码:[`packages/pi-fff/`](./packages/pi-fff/)。
</details>
Pi 扩展将 pi 的原生工具替换为 FFF 实现,并从按 frecency 排序的索引为交互式编辑器的 `@`-mention 自动补全提供数据。
<details id="neovim-plugin">
<summary>
<h2>fff.nvim</h2>
</summary>
在 Linux 内核仓库上的演示(10 万文件,8GB):
https://github.com/user-attachments/assets/5d0e1ce9-642c-4c44-aa88-01b05bb86abb
### 安装
#### lazy.nvim
```lua
{
'dmtrKovalenko/fff.nvim',
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
```lua
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })
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
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
```lua
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)`
返回结构化结果 `{ items, scores, total_matched, total_files?, total_dirs?, location? }`。每项包含 `type` 字段(`"file"``"directory"`)以及 `name` / `relative_path`。文件项还暴露 `size``modified``git_status``is_binary` 与 frecency 分数。
```lua
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)`
返回一个 `GrepResult` `{ items, total_matched, total_files_searched, total_files, filtered_file_count, next_file_offset, regex_fallback_error? }`。每个匹配项包含 `relative_path``name``line_number``col``line_content``match_ranges`,以及与 `file_search` 相同的文件元数据。
```lua
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
```
两个函数接受与 UI 选择器相同的约束语法(例如 `git:modified``*.rs``!test/`、glob 模式)。
#### `cwd` 与索引
`file_search``content_search` 均支持可选的 `cwd` 字段。首次调用任一函数时,会在 `config.base_path`(默认为你的 Neovim cwd)处惰性初始化选择器。
- 如果 `cwd` 与当前已索引的根目录一致,则调用会立即针对现有索引返回结果。
- 如果 `cwd` 不同,选择器会在新根目录处重新建立索引,调用会 **阻塞**(默认最长 10 秒),直到新选择器安装完成且初始扫描结束——因此调用方总能从正确的目录树获得结果。
- 若在 `change_indexing_directory` 之后索引仍在预热,可传入 `wait_for_index_ms = N`,无论是否由 `cwd` 触发切换,均可阻塞等待最长 `N` ms。传入 `0` 可完全跳过等待(适用于可接受部分结果的 fire-and-forget 调用)。
- 无效或不存在的 `cwd` 路径会返回空结果,并通过 `vim.notify` 输出错误。
### 命令
- `:FFFScan`。重新扫描文件。
- `:FFFRefreshGit`。刷新 git 状态。
- `:FFFClearCache [all|frecency|files]`。清除缓存。
- `:FFFHealth`。健康检查。
- `:FFFDebug [on|off|toggle]`。切换评分显示。
- `:FFFOpenLog`。打开 `~/.local/state/nvim/log/fff.log`
### 配置
默认值已足够合理。只需覆盖你关心的项。
```lua
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'
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>',
-- 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
},
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,
},
})
```
### 实时 grep 模式
`<S-Tab>``plain``regex``fuzzy` 之间循环切换。列表可通过 `grep.modes` 配置,单模式设置会完全隐藏指示器。
每次调用覆盖:
```lua
require('fff').live_grep({ grep = { modes = { 'fuzzy', 'plain' } } })
require('fff').live_grep({ query = 'search term' }) -- pre-fill
```
### 约束
find 和 grep 均接受以下标记以细化查询:
- `git:modified``modified``staged``deleted``renamed``untracked``ignored` 之一。
- `test/``test/` 的任意深层子项。
- `!something``!test/``!git:modified`。排除。
- `./**/*.{rs,lua}`。任意有效的 glob,由 [zlob](https://github.com/dmtrKovalenko/zlob).
仅 Grep
- `*.md``*.{c,h}`。扩展名过滤。
- `src/main.rs`。在单个文件内 grep。
可自由组合:`git:modified src/**/*.rs !src/**/mod.rs user controller`
### 在调用方窗口中打开
默认情况下,fff.nvim 会尝试在最合适的窗口中打开文件,因此不会影响到非文件缓冲区。你可以通过提供以下配置来自定义或禁用该行为:
```lua
require('fff').setup({
select = {
select_window = function(_current_buf, _action) return nil end,
},
})
```
注意:所选文件会替换调用窗口中的缓冲区,即使该缓冲区不可修改或具有特殊 buftype。`winfixbuf` 窗口仍会回退到 `:split`,以避免 `E1513`
### 多选与 quickfix
- `<Tab>`。切换选中状态(在 signcolumn 中显示粗体 `▊`)。
- `<C-q>`。将所选文件发送到 quickfix 列表并关闭选择器。
### Git 状态高亮
默认开启 sign-column 指示器。若要按 git 状态为文件名文本着色,请设置 `git.status_text_color = true` 并调整 `hl.git_*` 组。完整列表见 `:help fff.nvim`
### 浮动窗口颜色
选择器将其浮动内容映射到 `NormalFloat`(通过 `hl.normal`),边框映射到 `FloatBorder`。默认情况下,`FloatBorder` 链接到 `NormalFloat`,因此边框与内容默认共享同一背景,选择器呈现为单一弹窗。可覆盖 `hl.normal = 'Normal'`,使选择器与编辑器融为一体。
如需更精细的控制,可设置 `hl.winhl` 以覆盖各窗口的 `winhighlight`。它既可接受应用于每个选择器窗口的单个字符串,也可接受包含可选键 `prompt``list``preview``file_info` 的表。缺失的键将回退到由 `hl.normal``hl.border``hl.title` 构建的默认值。
```lua
-- 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',
},
}
```
### 文件信息面板
通过 `debug.enabled = true` 启用。面板位于预览上方,显示文件元数据、分数明细、时间戳和完整绝对路径。它会根据面板宽度自适应:窄宽度时段落垂直堆叠(B2),宽宽度时段落以双列网格渲染(H2)。可通过 `debug.show_file_info` 单独禁用各段落。
通过 `hl` 自定义面板:
| key | default | used for |
| ----------------------- | ----------------- | ---------------------------------- |
| `file_info_section` | `Title` | 段落标题标签 |
| `file_info_separator` | `FloatBorder` | 作为段落边界的短划线 |
| `file_info_label` | `Comment` | 行标签(Size、Type、Git 等) |
| `file_info_value` | `Normal` fg | 普通值 |
| `file_info_value_dim` | `NonText` | 暗淡值、行内分隔符 |
| `file_info_size` | `Number` | 文件大小值 |
| `file_info_type` | `Type` | 文件类型值 |
| `file_info_path` | `Directory` | 完整路径 |
| `file_info_total_score` | bold + `Number` | 总分(粗体) |
| `file_info_match_type` | bold + `Special` | 匹配类型(粗体) |
| `file_info_score_pos` | `DiagnosticOk` | 正分数分量 |
| `file_info_score_neg` | `DiagnosticError` | 负分数分量 |
### 文件过滤
FFF 遵循 `.gitignore`。对于不影响 git 的仅选择器忽略规则,请添加同级的 `.ignore` 文件:
```gitignore
*.md
docs/archive/**/*.md
```
运行 `:FFFScan` 以强制重新扫描。
### 故障排除
- `:FFFHealth` 验证选择器初始化、可选依赖项和数据库连接。
- `:FFFOpenLog` 打开当前会话的日志文件。
- 历史日志文件保存在主日志文件 `<state>/log/fff+<UTC-timestamp>+<pid>.log` 附近(最多 20 个文件)
- 如需崩溃回溯,请运行 `lldb -- nvim``gdb -- nvim` 并复现问题
</details>
Neovim 最好的文件搜索选择器,没有之一。查询更快、更直观,支持 frecency 排序、定义分类等功能。
<details id="node-sdk">
<summary>
<h2>Node & Bun SDK</h2>
</summary>
```bash
npm install @ff-labs/fff-node
# or
bun add @ff-labs/fff-node
```
```ts
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();
```
每个方法均返回 `Result<T>``{ ok: true, value } | { ok: false, error }`)。完整类型参考:[`packages/fff-node/src/types.ts`](./packages/fff-node/src/types.ts)。
</details>
面向 nodejs 和 bun 的 C 库 TypeScript 封装。可在 FFF 之上构建自定义 agent 工具、CLI 或 IDE 集成。
<details id="rust-crate">
<summary>
<h2>Rust crate</h2>
</summary>
### 添加依赖
FFF 用 Rust 编写,因此这是开销最低的用法。
```toml
[dependencies]
fff-search = "0.6"
```
完整 API 文档:[docs.rs/fff-search](https://docs.rs/fff-search/latest/fff_search/).
</details>
执行全部搜索的原生 Rust crate。稳定且文档完善。
<details id="c-library">
<summary>
<h2>C library</h2>
</summary>
### 构建
```bash
# Builds only the C cdylib (fastest):
make build-c-lib
# or directly with cargo:
cargo build --release -p fff-c --features zlob
```
> `zlob` 功能(需要 [Zig](https://ziglang.org) 工具链)会将 glob 匹配**和**文件系统遍历都切换到 [zlob](https://github.com/dmtrKovalenko/zlob)'s
> 原生并行遍历器。未启用时,默认构建使用纯 Rust 的
> [`ignore`](https://crates.io/crates/ignore)ripgrep)遍历器和 `globset`。
输出为 `cdylib``libfff_c.so` / `libfff_c.dylib` / `fff_c.dll`)。头文件位于 [`crates/fff-c/include/fff.h`](./crates/fff-c/include/fff.h)。
每个版本(包括 main 上的每次提交)的预构建二进制文件均在 [releases page](https://github.com/dmtrKovalenko/fff.nvim/releases). 同一二进制文件也随 `@ff-labs/fff-bin-*` npm 包一同发布。
### 安装
```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
```
`libfff_c.{so,dylib,dll}` 放入 `$(PREFIX)/lib`,头文件放入 `$(PREFIX)/include/fff.h`。使用 `make uninstall` 移除,它会遵循相同的 `PREFIX``DESTDIR`
安装后链接:
```bash
cc my_app.c -lfff_c -o my_app
```
确保 `$(PREFIX)/lib` 在运行时库搜索路径中(Linux 上为 `LD_LIBRARY_PATH`macOS 上为 `DYLD_LIBRARY_PATH`,或在 `/etc/ld.so.conf.d/` 中有一条条目)。
### 最小示例
```c
#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;
}
```
### 版本化选项结构体(推荐)
创建实例请使用 [`FffCreateOptions`](./crates/fff-c/include/fff.h)——一种可在不破坏 ABI 的情况下演进的版本化结构体。C99 指定初始化器(designated initializers)使调用点保持可读,并将未指定字段零初始化:
```c
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
});
```
### 仅 Glob 搜索
`fff_glob` 按单个 glob 模式筛选已索引文件,按 frecency(访问频率)排序并分页——完全绕过常规查询解析器。当你已有字面 glob(`*.rs``**/*.test.ts``src/**`)且不希望在其上叠加模糊匹配时使用。
```c
FffResult *res = fff_glob(handle, "**/*.rs", "", 0, 0, 100);
// FffSearchResult in res->handle, free with fff_free_search_result.
```
### 说明
- 每个返回 `FffResult*` 的函数都使用 Rust 的 `Box` 分配内存。请用 `fff_free_result` 释放,不要使用 malloc 的 free
- 载荷(搜索结果、grep 结果、扫描进度)各有专用的释放函数,列于头文件中。
- `handle` 字段中返回的 C 字符串(例如来自 `fff_get_base_path`)使用 `fff_free_string` 释放。
源码:[`crates/fff-c/`](./crates/fff-c/)。
</details>
稳定的 C ABI。可从 C/C++、Zig、通过 cgo 的 Go、通过 ctypes 的 Python,或任何支持 C FFI 的环境绑定。
<details id="python-bindings">
<summary>
<h2>Python 绑定</h2>
</summary>
### 安装
```bash
pip install fff-search
```
或从源码构建并安装:
```bash
cd packages/fff-python
uv sync --all-extras
uv run maturin develop --release
```
### 基本用法
```python
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)
```
### 异步用法
`wait_for_scan` 是一个协程,轮询扫描状态并让出给事件循环,因此不会阻塞其他任务。在同步代码中使用 `wait_for_scan_blocking`
```python
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())
```
### 你能获得什么
- `search``glob``directory_search``mixed_search` — 按 frecency 排序的模糊文件/目录搜索
- `grep` / `multi_grep` — 纯文本、正则或模糊内容搜索,带上下文行与游标分页
- `track_query` / `get_historical_query` — 可选的 frecency 与查询历史数据库
- `reindex``refresh_git_status``scan_progress``health_check` — 生命周期与诊断
类型化结果对象(`FileItem``Score``GrepMatch`、…),包含 `py.typed` 桩。以兼容 Python 3.10+ 的 `abi3` wheel 形式发布。
源码:[`packages/fff-python/`](./packages/fff-python/)。
</details>
使用 PyO3 构建的原生 Python 绑定。适用于 notebook、agent 脚本或任何需要快速文件搜索的 Python 工具。
---
## FFF 是什么,为何选用它而非 ripgrep 或 fzf
FFF 是文件搜索库,而非 CLI。Ripgrep 和 fzf 都是出色的工具,但它们是命令行程序:每次调用都会 fork 新进程、重新读取 `.gitignore`、重新 stat 目录,并在能给出答案前于内存中重建所需状态。从 shell 里 grep 一次时这没问题;但当编辑器或 AI agent 希望在单次会话中执行数百次搜索时就很糟糕。
FFF 将索引与文件缓存保留在一个长生命周期进程中,并通过四层薄封装暴露同一 Rust 核心:原生 crate(`fff-search`)、C 库(`libfff_c`)、Node/Bun SDK`@ff-labs/fff-node`)以及 MCP server。你只需调用一次 `FileFinder.create()`,之后每次搜索都命中温热内存。在 50 万文件的 Chromium checkout 上,这就是每次 spawn ripgrep 需 3-9 **秒**与每次 FFF 查询亚 10 ms 的差别。
模糊匹配算法比 fzf 的算法全面得多,具有**抗 typo(拼写错误)**能力;我们还提供带额外约束解析的查询语言用于预过滤,例如 "\*.rs !test/ shcema" 对 fff 是完全有效的查询,但 fzf 即使在 "shcema" 中有一个 typo 也找不到任何结果。
### 为何程序化 API 很重要
- 无进程 spawn。每次调用都在进程内完成,避免在短 `rg` 调用中占主导的 fork、exec、argv 解析与 stdout 管道设置。
- 一次 FS 遍历、元数据收集与 `.gitignore` 解析。ignore walker 仅在扫描时运行一次,结果供每次搜索复用。
- 结果以类型化对象返回,而非需重新解析的文本。SDK 直接提供 `{ relativePath, lineNumber, lineContent, gitStatus, totalFrecencyScore, isDefinition, ... }`
- 跨调用可用的游标分页。Ripgrep 没有「这些匹配的第 2 页」的概念;FFF 有。
- 长生命周期进程带来一次性 CLI 无法应用的优化:温热缓存、增量重索引、跨查询 frecency 与共享 SIMD 状态。
### 核心实际做了什么
- **Frecency 排序的模糊匹配。** 每个已索引文件都带有访问分数与修改分数。搜索会将你最近且频繁打开的文件排在冷门结果之上。这与 VS Code 的最近打开列表是同一思路,但应用于每条搜索结果,而不仅是侧边栏。
- **路径与内容均支持抗 typo 匹配。** grep 路径可使用 Smith-Waterman 模糊评分;路径搜索使用 SIMD 加速的模糊匹配(通过 [`frizbee`](https://github.com/saghen/frizbee)-derived core),可容忍丢字与重排)。
- **三种模式的内容 grep。** 纯文本字面量(SIMD memmem)、正则(Rust `regex` crate)与模糊(每行 Smith-Waterman)。根据模式自动检测使用哪种模式,纯文本搜索零命中时回退到模糊模式。
- **多模式 OR 搜索。** SIMD Aho-Corasick 用于「一次性查找这 20 个标识符中的任意一个」,比正则交替更快,也远快于 20 次独立的 ripgrep 运行。
- **后台文件监视器。** 文件变更时索引会更新。热路径上无需为重新扫描付出代价。
- **Git 状态感知。** 已修改、已暂存、未跟踪与已忽略状态会被缓存并随每条结果返回,调用方可排序或筛选而无需 shell 调用 git。监视器直接与 libgit2 通信,而非 spawn `git` CLI。
- **定义分类器。** Rust 侧的字节级扫描器为以 `struct``fn``class``def``impl` 等开头的行打标签。
### 重要的性能取舍
- 高效的内存分配器与分配策略(见下段)。默认使用 `mimaloc`
- 并行多线程搜索流水线,不受编排逻辑拖累
- 一切以 SIMD 优先的算法。高效且无分配的排序。
- 针对 FS 的平台特定优化([getdents64](https://linux.die.net/man/2/getdents64), Windows 上的 NTFS API 等)
- 轻量级即时内容索引,支持实时且抗 typo 的 grep
- 内存映射内容缓存。我们将部分文件存放在虚拟内存中(数量有限)
- 字符串块的单一连续 arena 存储。显著减少工作集内存并大幅提升 CPU 缓存命中。
### 内存分配
是的,fff 本质上比调用单个子进程需要更多内存。这正是速度提升的主要来源。实践中,与 Neovim 最流行的文件搜索选择器之一搭配使用时,[fff 最终占用的 RAM 少于一连串 ripgrep 调用](https://x.com/neogoose_btw/status/2041606853155811442).
FFF 还维护内容索引,每个已索引文件约 360 字节,因此 10 万文件仓库约 36 MB。并非所有文件都会被索引——二进制文件、超大文件以及不适合 grep 的内容会被跳过。若连该占用都过高,索引可放在内存映射文件中而非匿名 RAM。
### 实际意义
如果你正在构建 agent、IDE 扩展、pre-commit 检查,或任何会多次搜索同一仓库的长期运行工具,将 FFF 作为库调用,比通过 shell 调用 ripgrep 要划算得多。代价是真实的内存占用:FFF 会将索引保留在 RAM 中,并预热内容缓存。在约 1.4 万文件的仓库上,常驻内存大约 26 MB。在像 Chromium 这样约 50 万文件的仓库上,预计需要几百 MB。作为交换,每一次搜索都会附带 git 状态、frecency 排序、文件元数据、最近访问与编辑时间戳等信息。
如果你只是在终端里执行一次 grep`rg` 仍然是合适的选择。如果你在同一个进程里执行几十次,从第二次调用起 FFF 就能收回成本。如果你在做 AI agent,fff 会在你的 AI 有机会调用它之前完成准备工作。
### 对比
- **ripgrep**:FFF 使用相同的底层正则引擎,以及更先进的纯文本匹配算法。会存储内容索引和文件树。在重复搜索场景下优势最明显;在「从 bash grep 一次然后退出」的场景下则不如。
- **fzf**FFF 的路径搜索与 fzf 一样是模糊匹配,但它还支持 frecency 感知和 git 感知,并内置容错性更强的算法。fzf 是纯匹配与过滤工具;FFF 会按你实际打开文件的频率对结果排序。
- **Telescope / fzf-lua / snacks.picker**FFF 自带 Neovim picker,排序逻辑与 MCP server 和 SDK 一致。picker 是可选的;核心相同。
- **Tantivy 或其他全文搜索引擎**:属于不同类别的工具。Tantivy 为大规模查询时评分而索引文档。FFF 限定在单个仓库,并针对亚 10 ms 响应进行优化。它不会在磁盘上持久化倒排索引。
---
## 仓库结构
- `crates/fff-search``crates/fff-grep``crates/fff-query-parser` — Rust 核心。
- `crates/fff-c` — 各语言绑定共用的 C FFI。
- `crates/fff-nvim` — Neovim 插件的 Lua/mlua 绑定。
- `crates/fff-mcp` — MCP server 二进制。
- `packages/fff-node` — Node.js SDK`@ff-labs/fff-node`)。
- `packages/fff-bun` — Bun SDK`@ff-labs/fff-bun`)。
- `packages/pi-fff` — pi 扩展(`@ff-labs/pi-fff`)。
- `lua/` — Neovim 侧插件代码。
## 贡献
欢迎提交 bug 报告和 pull request。可以使用 agentic 编码工具,但必须经过人工审查。
## 许可证
[MIT](./LICENSE) & open source forever.