Fff.nvim – 抗错误代码搜索
Fff.nvim – Typo-resistant code search

原始链接: https://github.com/dmtrKovalenko/fff.nvim

## FFF.nvim:Neovim 的快速模糊文件查找器 FFF.nvim(令人惊叹的快速模糊文件查找器)是一个基于 Rust 的、具有明确观点的文件选择器,专为 Neovim 中的速度和效率而设计。 借助一个跟踪文件访问、修改和 Git 状态的专用文件索引器,即使在拥有 50k+ 文件的情况下,它也能实现小于 10 毫秒的搜索时间。 **主要特点:** * **容错模糊搜索:** 即使查询不完整,也能快速找到文件。 * **Git 集成:** 直接在选择器中显示文件状态。 * **图像预览:** 支持显示图像(需要 snacks.nvim)。 * **可定制:** 提供广泛的配置选项,用于外观、行为和性能。 * **延迟加载:** 自动初始化,不会影响启动时间。 * **实时 grep:** 包含强大的实时 grep 功能,具有普通、正则表达式和模糊模式。 **要求:** Neovim 0.10.0+ 和 Rustup(nightly 工具链)。 安装通常涉及一个构建过程,由插件自动处理或通过 `cargo build` 手动处理。 FFF.nvim 提供用于查找文件 (`ff`)、实时 grep (`fg`, `fz`) 以及 `:FFFFind` 和 `:FFFScan` 等命令的键绑定。 可用调试工具和日志用于故障排除。 它正在积极开发中,并欢迎贡献。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Fff.nvim – 抗错别字代码搜索 (github.com/dmtrkovalenko) 8 分,neogoose 发表于 1 小时前 | 隐藏 | 过去 | 收藏 | 1 条评论 Fff.nvim 的新版本展示了对人类和代理代码搜索的新兼容性 - 抗错别字且可用的代码搜索,适用于真实代码库。 它可以将查询 "shcema" 仅匹配到 "schema",将 "SortedMap" 仅匹配到 "SortedArrayMap" 和 "SortedHashMap",而不会使结果膨胀。 retrofuturism 发表于 17 分钟前 | 下一个 [–] 啊,这正是我需要的插件。我真的想像 issue #77 所述那样使用 j/k 键导航。回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Finally a smart fuzzy file picker for neovim.

Stars Issues Contributors

FFF stands for freakin fast fuzzy file finder (pick 3) and it is an opinionated fuzzy file picker for neovim. Just for files, but we'll try to solve file picking completely.

It comes with a dedicated rust backend runtime that keep tracks of the file index, your file access and modifications, git status, and provides a comprehensive typo-resistant fuzzy search experience.

  • Works out of the box with no additional configuration
  • Typo resistant fuzzy search
  • Git status integration allowing to take advantage of last modified times within a worktree
  • Separate file index maintained by a dedicated backend allows <10 milliseconds search time for 50k files codebase
  • Display images in previews (for now requires snacks.nvim)
  • Smart in a plenty of different ways hopefully helpful for your workflow
  • This plugin initializes itself lazily by default

Note

Although we'll try to make sure to keep 100% backward compatibility, by using you should understand that silly bugs and breaking changes may happen. And also we hope for your contributions and feedback to make this plugin ideal for everyone.

FFF.nvim requires:

  • Neovim 0.10.0+
  • Rustup (we require nightly for building the native backend rustup will handle toolchain automatically)
{
  'dmtrKovalenko/fff.nvim',
  build = function()
    -- this will download prebuild binary or try to use existing rustup toolchain to build from source
    -- (if you are using lazy you can use gb for rebuilding a plugin if needed)
    require("fff.download").download_or_build_binary()
  end,
  -- if you are using nixos
  -- build = "nix run .#release",
  opts = { -- (optional)
    debug = {
      enabled = true,     -- we expect your collaboration at least during the beta
      show_scores = true, -- to help us optimize the scoring system, feel free to share your scores!
    },
  },
  -- No need to lazy-load with lazy.nvim.
  -- This plugin initializes itself lazily.
  lazy = false,
  keys = {
    {
      "ff", -- try it if you didn't it is a banger keybinding for a picker
      function() require('fff').find_files() end,
      desc = 'FFFind files',
    },
    {
      "fg",
      function() require('fff').live_grep() end,
      desc = 'LiFFFe grep',
    },
    {
      "fz",
      function() require('fff').live_grep({
	grep = {
	  modes = { 'fuzzy', 'plain' }
	}
      }) end,
      desc = 'Live fffuzy grep',
    }
  }
}
vim.pack.add({ 'https://github.com/dmtrKovalenko/fff.nvim' })

vim.api.nvim_create_autocmd('PackChanged', {
  callback = function(event)
    if event.data.updated then
      require('fff.download').download_or_build_binary()
    end
  end,
})

-- the plugin will automatically lazy load
vim.g.fff = {
  lazy_sync = true, -- start syncing only when the picker is open
  debug = {
    enabled = true,
    show_scores = true,
  },
}

vim.keymap.set(
  'n',
  'ff',
  function() require('fff').find_files() end,
  { desc = 'FFFind files' }
)

FFF.nvim comes with sensible defaults. Here's the complete configuration with all available options:

require('fff').setup({
    base_path = vim.fn.getcwd(),
    prompt = '🪿 ',
    title = 'FFFiles',
    max_results = 100,
    max_threads = 4,
    lazy_sync = true, -- set to false if you want file indexing to start on open
    layout = {
      height = 0.8,
      width = 0.8,
      prompt_position = 'bottom', -- or 'top'
      preview_position = 'right', -- or 'left', 'right', 'top', 'bottom'
      preview_size = 0.5,
      show_scrollbar = true, -- Show scrollbar for pagination
      -- How to shorten long directory paths in the file list:
      -- 'middle_number' (default): uses dots for 1-3 hidden (a/./b, a/../b, a/.../b)
      --                            and numbers for 4+ (a/.4./b, a/.5./b)
      -- 'middle': always uses dots (a/./b, a/../b, a/.../b)
      -- 'end': truncates from the end (home/user/projects)
      path_shorten_strategy = 'middle_number',
    },
    preview = {
      enabled = true,
      max_size = 10 * 1024 * 1024, -- Do not try to read files larger than 10MB
      chunk_size = 8192, -- Bytes per chunk for dynamic loading (8kb - fits ~100-200 lines)
      binary_file_threshold = 1024, -- amount of bytes to scan for binary content (set 0 to disable)
      imagemagick_info_format_str = '%m: %wx%h, %[colorspace], %q-bit',
      line_numbers = false,
      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>',
      -- you can assign multiple keys to any action
      move_up = { '<Up>', '<C-p>' },
      move_down = { '<Down>', '<C-n>' },
      preview_scroll_up = '<C-u>',
      preview_scroll_down = '<C-d>',
      toggle_debug = '<F2>',
      -- goes to the previous query in history
      cycle_previous_query = '<C-Up>',
      -- multi-select keymaps for quickfix
      toggle_select = '<Tab>',
      send_to_quickfix = '<C-q>',
      -- grep mode: cycle between plain text, regex, and fuzzy search
      toggle_grep_regex = '<S-Tab>',
    },
    hl = {
      border = 'FloatBorder',
      normal = 'Normal',
      cursor = 'CursorLine',
      matched = 'IncSearch',
      title = 'Title',
      prompt = 'Question',
      active_file = 'Visual',
      frecency = 'Number',
      debug = 'Comment',
      combo_header = 'Number',
      scrollbar = 'Comment', -- Highlight for scrollbar thumb (track uses border)
      directory_path = 'Comment', -- Highlight for directory path in file list
      -- Multi-select highlights
      selected = 'FFFSelected',
      selected_active = 'FFFSelectedActive',
      -- Git text highlights for file names
      git_staged = 'FFFGitStaged',
      git_modified = 'FFFGitModified',
      git_deleted = 'FFFGitDeleted',
      git_renamed = 'FFFGitRenamed',
      git_untracked = 'FFFGitUntracked',
      git_ignored = 'FFFGitIgnored',
      -- Git sign/border highlights
      git_sign_staged = 'FFFGitSignStaged',
      git_sign_modified = 'FFFGitSignModified',
      git_sign_deleted = 'FFFGitSignDeleted',
      git_sign_renamed = 'FFFGitSignRenamed',
      git_sign_untracked = 'FFFGitSignUntracked',
      git_sign_ignored = 'FFFGitSignIgnored',
      -- Git sign selected highlights
      git_sign_staged_selected = 'FFFGitSignStagedSelected',
      git_sign_modified_selected = 'FFFGitSignModifiedSelected',
      git_sign_deleted_selected = 'FFFGitSignDeletedSelected',
      git_sign_renamed_selected = 'FFFGitSignRenamedSelected',
      git_sign_untracked_selected = 'FFFGitSignUntrackedSelected',
      git_sign_ignored_selected = 'FFFGitSignIgnoredSelected',
      -- Grep highlights
      grep_match = 'IncSearch',               -- Highlight for matched text in grep results
      grep_line_number = 'LineNr',            -- Highlight for :line:col location
      grep_regex_active = 'DiagnosticInfo',   -- Highlight for keybind + label when regex is on
      grep_regex_inactive = 'Comment',        -- Highlight for keybind + label when regex is off
      -- Cross-mode suggestion highlights
      suggestion_header = 'WarningMsg',       -- Highlight for the "No results found. Suggested..." banner
    },
    -- Store file open frecency
    frecency = {
      enabled = true,
      db_path = vim.fn.stdpath('cache') .. '/fff_nvim',
    },
    -- Store successfully opened queries with respective matches
    history = {
      enabled = true,
      db_path = vim.fn.stdpath('data') .. '/fff_queries',
      min_combo_count = 3, -- file will get a boost if it was selected 3 in a row times per specific query
      combo_boost_score_multiplier = 100, -- Score multiplier for combo matches
    },
    -- Git integration
    git = {
      status_text_color = false, -- Apply git status colors to filename text (default: false, only sign column)
    },
    debug = {
      enabled = false, -- Set to true to show scores in the UI
      show_scores = false,
      show_file_info = false, -- Show file info panel in preview
    },
    logging = {
      enabled = true,
      log_file = vim.fn.stdpath('log') .. '/fff.log',
      log_level = 'info',
    },
    -- Live grep search configuration
    grep = {
      max_file_size = 10 * 1024 * 1024, -- Skip files larger than 10MB
      max_matches_per_file = 200, -- Maximum matches per file
      smart_case = true, -- Case-insensitive unless query has uppercase
      time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
      modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
    }
})
require('fff').find_files()                         -- Find files in current directory
require('fff').find_in_git_root()                   -- Find files in the current git repository
require('fff').scan_files()                         -- Trigger rescan of files in the current directory
require('fff').refresh_git_status()                 -- Refresh git status for the active file lock
require('fff').find_files_in_dir(path)              -- Find files in a specific directory
require('fff').change_indexing_directory(new_path)  -- Change the base directory for the file picker

FFF.nvim provides several commands for interacting with the file picker:

  • :FFFFind [path|query] - Open file picker. Optional: provide directory path or search query
  • :FFFScan - Manually trigger a rescan of files in the current directory
  • :FFFRefreshGit - Manually refresh git status for all files
  • :FFFClearCache [all|frecency|files] - Clear various caches
  • :FFFHealth - Check FFF health status and dependencies
  • :FFFDebug [on|off|toggle] - Toggle debug scores display
  • :FFFOpenLog - Open the FFF log file in a new tab

The input field automatically handles multiline clipboard content by joining all lines into a single search query. This is particularly useful when copying file paths from terminal output.

Toggle scoring information display:

  • Press F2 while in the picker
  • Use :FFFDebug command
  • Enable by default with debug.show_scores = true

Multi-Select and Quickfix Integration

Select multiple files and send them to Neovim's quickfix list (keymaps are configurable):

  • <Tab> - Toggle selection for the current file (shows thick border in signcolumn)
  • <C-q> - Send selected files to quickfix list and close picker

Live grep supports three search modes, cycled with <S-Tab>:

  • Plain text (default) - The query is matched literally. Special regex characters like ., *, (, ), $ have no special meaning. This is the safest mode for searching code containing regex metacharacters.
  • Regex - The query is interpreted as a regular expression. Supports character classes ([a-z]), quantifiers (+, *, {n}), alternation (foo|bar), anchors (^, $), word boundaries (\b), and more.
  • Fuzzy - The query is fuzzy matched using Smith-Waterman scoring. Accommodates typos and scattered characters (e.g., "mtxlk" matches "mutex_lock"). Results are filtered by a quality threshold to avoid overly fuzzy matches.

The current mode is shown on the right side of the input field (e.g., plain, regex, fuzzy) with color-coded highlighting.

You can customize which modes are available and their cycling order globally in your configuration, or per-call when invoking live_grep().

Global configuration:

require('fff').setup({
  grep = {
    modes = { 'plain', 'regex' }, -- Only plain and regex, no fuzzy
  }
})

Per-call configuration:

-- Only fuzzy and plain modes for this specific grep
require('fff').live_grep({
  grep = {
    modes = { 'fuzzy', 'plain' },
  }
})

-- Single mode (hides mode indicator completely)
require('fff').live_grep({
  grep = {
    modes = { 'fuzzy' },
  }
})

When only one mode is configured, the mode indicator is hidden completely and the cycle keybind does nothing.

When a search returns no results, FFF automatically queries the opposite search mode and displays the results as suggestions:

  • File search with no matches → shows suggested content matches (grep results) for the same query
  • Grep search with no matches → shows suggested file name matches for the same query

Suggestions are clearly labeled with a "No results found. Suggested ..." banner (highlighted with hl.suggestion_header). You can navigate and select suggestion items just like normal results — selecting a grep suggestion will open the file at the matching line.

FFF integrates with git to show file status through sign column indicators (enabled by default) and optional filename text coloring.

Sign Column Indicators (enabled by default) - Border characters shown in the sign column:

hl = {
  git_sign_staged = 'FFFGitSignStaged',
  git_sign_modified = 'FFFGitSignModified',
  git_sign_deleted = 'FFFGitSignDeleted',
  git_sign_renamed = 'FFFGitSignRenamed',
  git_sign_untracked = 'FFFGitSignUntracked',
  git_sign_ignored = 'FFFGitSignIgnored',
}

Text Highlights (opt-in) - Apply colors to filenames based on git status:

To enable git status text coloring, set git.status_text_color = true:

require('fff').setup({
  git = {
    status_text_color = true, -- Enable git status colors on filename text
  },
  hl = {
    git_staged = 'FFFGitStaged',       -- Files staged for commit
    git_modified = 'FFFGitModified',   -- Modified unstaged files
    git_deleted = 'FFFGitDeleted',     -- Deleted files
    git_renamed = 'FFFGitRenamed',     -- Renamed files
    git_untracked = 'FFFGitUntracked', -- New untracked files
    git_ignored = 'FFFGitIgnored',     -- Git-ignored files
  }
})

The plugin provides sensible default highlight groups that link to common git highlight groups (e.g., GitSignsAdd, GitSignsChange). You can override these with your own custom highlight groups to match your colorscheme.

Example - Custom Bright Colors for Text:

vim.api.nvim_set_hl(0, 'CustomGitModified', { fg = '#FFA500' })
vim.api.nvim_set_hl(0, 'CustomGitUntracked', { fg = '#00FF00' })

require('fff').setup({
  git = {
    status_text_color = true,
  },
  hl = {
    git_modified = 'CustomGitModified',
    git_untracked = 'CustomGitUntracked',
  }
})

FFF.nvim respects .gitignore patterns automatically. To filter files from the picker without modifying .gitignore, create a .ignore file in your project root:

# Exclude all markdown files
*.md

# Exclude specific subdirectory
docs/archive/**/*.md

Run :FFFScan to force a rescan if needed.

Run :FFFHealth to check the status of FFF.nvim and its dependencies. This will verify:

  • File picker initialization status
  • Optional dependencies (git, image preview tools)
  • Database connectivity

If you encounter issues, check the log file:

Or manually open the log file at ~/.local/state/nvim/log/fff.log (default location).

File picker not initializing:

  • Ensure the Rust backend is compiled: cargo build --release in the plugin directory
  • Check that your Neovim version is 0.10.0 or higher

Image previews not working:

  • Verify your terminal supports images (kitty, iTerm2, WezTerm, etc.)
  • For terminals without native image support, install one of: chafa, viu, or img2txt
  • If using snacks.nvim, ensure it's properly configured

Performance issues:

  • Adjust max_threads in configuration based on your system
  • Reduce preview.max_lines and preview.max_size for large files
  • Clear cache if it becomes too large: :FFFClearCache all

Files not being indexed:

  • Run :FFFScan to manually trigger a file scan
  • Check that the base_path is correctly set
  • Verify you have read permissions for the directory

Enable debug mode to see scoring information and troubleshoot search results:

  • Press F2 while in the picker
  • Run :FFFDebug on to enable permanently
  • Set debug.show_scores = true in configuration
联系我们 contact @ memedata.com