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
819 lines
34 KiB
Markdown
819 lines
34 KiB
Markdown
<!-- 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 往返次数、节省无效上下文、更快得到答案。
|
||
|
||

|
||
|
||
### 一行安装
|
||
|
||
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)。它们会打印针对你客户端的精确接入说明。
|
||
|
||
### Homebrew(macOS / 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.
|