ITADN
MagicDuck/grug-far.nvim
MagicDuck/grug-far.nvim · 文件 下载 ZIP
文件最后提交记录最后更新时间
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

grug-far.nvim

Find And Replace plugin for neovim

image

Grug 查找!Grug 替换!Grug 开心!

✨ 特性

  • 使用 rgast-grep全部功能进行搜索
  • 使用 rgast-grep 的几乎全部功能进行替换。例如,对于 rg,某些标志如 --binary--json 等被列入黑名单,以防止意外输出。当使用此类标志时,UI 会发出警告并阻止替换。
  • 自动防抖搜索,或在退出插入模式(以及普通模式更改)时手动搜索
  • 在 quickfix 列表中打开搜索结果
  • 在结果输出的行上按下 <Enter> 时,跳转到匹配项的文件/行/列(键位可配置)。
  • 使用可配置的键位内联编辑结果行,并将其同步回其源文件位置。
  • 手动/自动保存搜索历史并重新加载
  • 语法高亮的搜索结果
  • 搜索结果折叠
  • 多行搜索与替换
  • 在由可视选择标记的缓冲区范围内进行搜索/替换
  • 在“滚动”浏览结果时“预览”结果源
  • 使用 lua 解释的替换对每个匹配项进行替换

搜索:

image

替换:

image
更多截图:

Rg 向你展示它的用法

image

帮助:

image

在缓冲区范围内搜索

image

历史:

image

Ast-grep 多行搜索与替换:

image

Ast-grep debug-query

image

使用 lua 解释替换的 Ripgrep

image

使用 vimscript 解释替换的 Ast-grep

image

Ast-grep 规则

image

视频

linkarzu 贴心地制作了一段视频,展示了部分功能的实际运行效果(如果你觉得有帮助,别忘了点赞以感谢他的辛勤付出):

linkarzu youtube video

🤔 哲学

  1. 力求降低心智负担。 你可以执行的所有操作以及尽可能多的帮助都直接呈现在你面前(部分可配置)。Grug 经常忘记如何使用捕获组,或者哪个标志符起什么作用。
  2. 透明性。 不试图隐藏底层工具。例如,来自 rg 的错误消息会原样显示,因为当你写错正则表达式时,它们其实相当友好。你可以逐步学习 rgast-grep 标志符,或者利用在 CLI 中运行它时已有的知识。你甚至可以输入 --help 标志符来查看完整的 rg 帮助,或使用 --debug-query=ast 标志符来调试你的 ast-grep 查询。Grug 喜欢!
  3. 复用肌肉记忆。 不试图阻止任何类型的缓冲区编辑,例如删除行等。这类操作很容易出错,一旦出错,Grug 就无法在编写大型正则表达式的过程中修改文本。Grug 很生气!!仅确保优雅恢复,以维护基本 UI 完整性(这得益于 extmarks 的魔力)。恢复应该只需简单的撤销操作即可。
  4. uniformity. 仅使用一个工具来同时执行搜索和替换操作,以保持行为的一致性。例如,不会将 rg 与其他工具(如 sed)组合使用,尽管 rg 并不直接支持替换功能。在编写正则表达式时,不应需要担心兼容性差异。此外,这也为使用许多高级的 rg 标志(例如不同的正则表达式引擎)打开了大门,而这些在混合环境中是无法实现的。目前存在一个小的例外,这是因为 ast-grep 目前不支持类似 --glob 标志的功能,因此我们必须通过 rg 来过滤文件,但希望未来能够解决这一问题。

⚡️ 需求

如果你遇到意外问题,请运行 :checkhealth grug-far

📦 安装与配置

使用 lazy.nvim

  {
    'MagicDuck/grug-far.nvim',
    -- Note (lazy loading): grug-far.lua defers all it's requires so it's lazy by default
    -- additional lazy config to defer loading is not really needed...
    config = function()
      -- optional setup call to override plugin options
      -- alternatively you can set options with vim.g.grug_far = { ... }
      require('grug-far').setup({
        -- options, see Configuration section below
        -- there are no required options atm
      });
    end
  },

关于配置,更多详情请参见 :h grug-far

重要提示: 请确保你已配置 <localleader>。默认情况下,grug-far 将使用 <localleader> 作为其缓冲区局部键位映射。

🚀 用法

打开与编辑

你可以使用 :GrugFar 命令打开一个新的 grug-far.nvim 垂直分屏缓冲区。 注意该命令支持典型的 command-modifiers,例如 botrightaboveleft 等,以及可视范围。 在可视模式下,该命令会用当前的可视选区预填充搜索字符串。 注意,如果你希望在可视选区内部进行搜索和替换,应改用 :GrugFarWithin

最好为其映射一个键位以便轻松触发。 因为它只是一个缓冲区,你可以随意编辑。UI 会尝试引导 你操作,并在你执行诸如 ggVGd(删除所有行)等操作时优雅地恢复。 归根结底,它将控制权交还给你,无论如何,恢复只需几次 u 按键即可。

你可以创建多个此类缓冲区,它们可能具有不同的搜索内容,这些内容将反映在每个缓冲区的标题中(可配置)。 如果需要切换到这些缓冲区,它们应显示在缓冲区列表中。

搜索和替换

搜索通过填写相应的输入框完成,并会在你输入时以防抖方式执行。如果你提供了替换内容, 将显示差异。要触发实际的替换,你需要调用 Replace 操作(默认为 <localleader>r)。

注意: 当用空字符串替换匹配项时,系统会提示你确认,因为由于 UI 将其视为仅搜索,该更改在结果区域中不可见。如果你 希望在结果区域中看到实际的替换内容,请在标志中添加 --replace=

在选项中,你还可以指定触发搜索前必须输入的最小字符数。默认为 2。

搜索时,你可以指定文件过滤器以缩小搜索范围,并添加更多标志以进一步细化。路径输入可用于 定位特定的目录和文件。

注意: 路径输入支持相对路径和绝对路径,~,环境变量以及“路径提供程序”。后者是特殊的字符串,会展开为路径列表。当前可用的 path providers 有:

  • <buflist>:展开为对应于已打开缓冲区文件的列表
  • <buflist-cwd>:类似于 <buflist>,但仅筛选出当前工作目录中的文件
  • <qflist>:展开为对应于快速修复列表文件的列表
  • ... 完整列表请参见 :h grug-far-opts 并搜索“path providers” ...

当输入无效标志等时,来自 ripgrep/astgrep 的错误消息会显示出来以引导您。

将每个匹配项替换为解释脚本的结果

某些情况需要为每个搜索匹配项执行任意代码的能力,以确定适当的替换内容。 在这些情况下,您可以使用 Swap Replacement Interpreter 操作切换到所需的替换解释器, 例如 luavimscript。 例如,使用 lua 解释器,这将允许您在 Replace: 输入中编写多行 lua 代码,基本上就是 lua 函数的主体。

您可以使用 match 来引用每个匹配项,并且需要 return 您希望作为替换值的值。 对于 astgrep 引擎,您还可以通过访问 vars 表来访问元变量。例如,$Avars.A 引用,$$$ARGSvars.ARGS 引用。

对于 vimscript 解释器,情况类似。

将结果行同步回源文件

可以将结果区域中各行的文本同步回其源文件。这允许在 grug-far 缓冲区中自由编辑结果,甚至包括旧的 %s/foo/bar。 有 3 种类型的操作可以完成此同步:

  1. Sync Line - 同步当前行
  2. Sync All - 同步所有行
  3. Apply Next/Apply Prev - 同步当前行/差异并智能地从结果缓冲区中删除它

只有当某一行与源文件相比发生了某种变化时,才会发生同步,即正在发生替换或您已手动编辑了该行。

删除结果行会导致它们被排除在 Sync All 操作的同步范围之外。 在某些情况下,如果您想排除特定文件 或某些特定匹配项,这是一种很好的细化替换的方法。

注意: 在进行多行替换(--multiline 标志)时,同步功能被禁用

注意: 如果您希望在使用空字符串进行替换时同步功能正常工作,请将 --replace= 添加到标志中。

注意: 同步功能仅由 ripgrep 引擎支持。以下关于同步与替换之间差异的说明 是 ripgrep 引擎特有的:

如果你不编辑结果列表,Sync AllReplace 具有等效的结果,除了一个情况。 当你使用 --multiline--multiline-dot-all 标志进行多行替换时,同步将不起作用,因此你 必须使用 replace。本质上区别在于 Replace 在每个文件上运行 rg --replace=... --passthrough, 并且完全不依赖于结果区域中的内容。Sync All 基于结果区域中的内容进行逐行 同步。

前往 / 打开 / 预览结果位置

当光标放置在结果文件路径上时,你可以在普通模式下按 <enter> 前往该文件(Goto 操作的默认键位)。 当它放置在结果匹配行上时,你将前往匹配的文件/行/列。默认情况下,文件缓冲区 在你打开 grug-far 之前所在的最后一个窗口中打开,这通常是另一个垂直分割窗口。

如果你希望执行相同操作,但让光标保持原位,你可以改用 Open 操作。

注意: 对于 GotoOpen 操作,如果之前输入了 <count>,则使用对应于 <count> 结果行的 位置,而不是当前光标行。如果你希望 对 <count> 有视觉指示,可以设置选项 resultLocation.showNumberLabel = true

为了平滑地按顺序 Open 每个结果位置,你可以使用 Open Next(<down> by default) 和 Open Prev(<up> 默认) 操作。

如果你希望保持缓冲区布局,可以改用 Preview 操作,它将在浮动窗口中打开位置。

在 quickfix 列表中打开结果行

结果行可以在快速修复列表中打开。删除结果行将导致它们不被包含在内。

注意: 执行多行替换时(--multiline 标志),快速修复列表操作将被禁用。

历史记录

grug-far 可以跟踪你的搜索历史。这可以通过手动使用 History Add 操作添加历史记录条目,或在某些成功的操作(如 ReplaceSync All)时自动完成。

当你希望选择其中一条历史记录条目以重复使用时,可以使用 History Open 操作 将搜索历史作为缓冲区打开。在那里,你可以选择一个条目,该条目将被自动填充。

请注意,如果需要做一些清理工作,你可以像编辑其他任何缓冲区一样编辑并保存历史缓冲区。 历史记录条目的格式如下:

<optional comment, e.g. My special search>
Engine: <astgrep|astgrep-rules|ripgrep>(|lua)?
Search: <text>
Replace: <text>
Files Filter: <text>
Flags: <text>

其中 <text> 可以在“续行前缀”(| )的帮助下跨越多行。例如:

Replace: something
| additional replace text
| more replace text

请注意,某些引擎可能使用其他输入。例如,astgrep-rules 使用 Rules 而不是 Search,并且没有 Replace。 历史记录条目由一个或多个空行分隔。

注意: grug-far 将忽略不以上述前缀开头的行

查看完整的搜索命令

有时,主要用于调试目的,查看搜索时执行的完整 CLI 命令会很有用。你 可以使用 Toggle Show Command 操作将其打开,该命令将作为 搜索结果区域中的第一项出现。

该命令已经过 shell 转义,因此如果需要,你可以将其复制并在 shell 中手动执行。

中止

如果你不小心启动了错误的搜索/同步/替换,或者它耗时过长,你可以使用 Abort 操作提前中止。

切换搜索引擎

你可以使用 Swap Engine 操作切换搜索引擎。目前支持 ripgrep(默认)、astgrepastgrep-rules。 如果你只想在切换循环中包含部分引擎,可以配置可用引擎列表。

ripgrep 使用 rg CLI 命令进行搜索和替换。有关 CLI 选项和正则表达式语法的更多信息,请参阅 ripgrep 文档

astgrepastgrep-rulesast-grep CLI 命令的两个不同接口。astgrep 仅限于单个 模式,使用 astgrep run --pattern=<your_search_string>astgrep-rules 接受 YAML 输入以定义 规则,使用 sg scan --inline-rules=<your_rules_yaml> 运行,虽然更冗长但功能更强大。有关更多信息,请参阅 ast-grep 文档。选择 astgrep-rules 引擎时,grug-far 会尝试预填充合理的 YAML 样板代码。如果你之前一直在使用 astgrep 处理某个模式,然后切换到 astgrep-rules 引擎,grug-far 会将你现有的模式包含在规则中,以便你在此基础上继续构建。

关闭

完成后,建议使用配置好的键位(默认为 <localleader>c) 或直接 :bd 来关闭缓冲区,以节省资源,因为某些搜索结果可能相当庞大。 使用 Close 操作而不是直接 :bd 的优势在于,如果有正在进行的替换/同步,它会要求你确认,因为这些操作将会被中止。

注意:如果你使用 transient = true 选项打开 grug-far,该缓冲区将不被列出,并且在不使用时(即窗口关闭时)会完全删除自身。

🥪 食谱

以光标下的当前单词作为搜索字符串启动

:lua require('grug-far').open({ prefills = { search = vim.fn.expand("<cword>") } })

使用 ast-grep 引擎启动

:lua require('grug-far').open({ engine = 'astgrep' })

作为瞬态缓冲区启动,该缓冲区不被列出,并且在不使用时会完全删除自身

:lua require('grug-far').open({ transient = true })

启动,将搜索/替换限制在当前文件

:lua require('grug-far').open({ prefills = { paths = vim.fn.expand("%") } })

使用当前的可视选择启动,仅搜索当前文件

:<C-u>lua require('grug-far').with_visual_selection({ prefills = { paths = vim.fn.expand("%") } })

启动,将搜索限制在当前缓冲区的可视选择范围内

:GrugFarWithin

或者,如果你想完全使用 lua,可以将其作为键映射:

vim.keymap.set({ 'n', 'x' }, '<leader>si', function()
  require('grug-far').open({ visualSelectionUsage = 'operate-within-range' })
end, { desc = 'grug-far: Search within range' })

或者自动检测,仅在可视选择是按行选择时限制范围,否则使用选择内容预填充搜索输入:

vim.keymap.set({ 'n', 'x' }, '<leader>si', function()
  require('grug-far').open({ visualSelectionUsage = 'auto-detect' })
end, { desc = 'grug-far: Search within range' })

启动,使用 @/ 寄存器的值作为搜索查询,回退到可视选择

注意 @/ 寄存器保存了你上一次的 /* 等搜索查询。

vim.keymap.set({ 'n', 'x' }, '<leader>ss', function()
  local search = vim.fn.getreg('/')
  -- surround with \b if "word" search (such as when pressing `*`)
  if search and vim.startswith(search, '\\<') and vim.endswith(search, '\\>') then
    search = '\\b' .. search:sub(3, -3) .. '\\b'
  elseif search and vim.startswith(search, '\\V') then
    search = search:sub(3)
  end
  local inst = require('grug-far').open({
    prefills = {
      search = search,
    },
  })
  inst:when_ready(function()
    inst:goto_input('replacement')
  end)
end, { desc = 'grug-far: Search using @/ register value or visual selection' })

启动,预填充最后一条历史记录

vim.keymap.set({ 'n', 'x' }, '<leader>ss', function()
  ---@type grug.far.OptionsOverride
  local opts = {}
  local entry = require('grug-far').get_last_history_entry()
  if entry ~= nil then
    opts.prefills = entry
    opts.engine = entry.engine
    opts.replacementInterpreter = entry.replacementInterpreter
  end

  require('grug-far').open(opts)
end, { desc = 'grug-far: Search, pre-filling with last history entry' })

切换特定实例的可见性,并将标题设置为固定字符串

:lua require('grug-far').toggle_instance({ instanceName="far", staticTitle="Find and Replace" })

创建一个缓冲区局部键绑定,用于切换 --fixed-strings 标志

vim.api.nvim_create_autocmd('FileType', {
  group =  vim.api.nvim_create_augroup('my-grug-far-custom-keybinds', { clear = true }),
  pattern = { 'grug-far' },
  callback = function()
    vim.keymap.set('n', '<localleader>w', function()
      local state = unpack(require('grug-far').get_instance(0):toggle_flags({ '--fixed-strings' }))
      vim.notify('grug-far: toggled --fixed-strings ' .. (state and 'ON' or 'OFF'))
    end, { buffer = true })
  end,
})

创建一个缓冲区局部键绑定,用于打开结果位置并立即关闭 grug-far.nvim

vim.api.nvim_create_autocmd('FileType', {
  group = vim.api.nvim_create_augroup('grug-far-keybindings', { clear = true }),
  pattern = { 'grug-far' },
  callback = function()
    vim.keymap.set('n', '<C-enter>', function()
      require('grug-far').get_instance(0):open_location()
      require('grug-far').get_instance(0):close()
    end, { buffer = true })
  end,
})

创建一个缓冲区局部键绑定,用于跳回第一个输入

vim.api.nvim_create_autocmd('FileType', {
  group = vim.api.nvim_create_augroup('grug-far-keymap', { clear = true }),
  pattern = { 'grug-far' },
  callback = function()
    -- jump back to first input by hitting left arrow in normal mode:
    vim.keymap.set('n', '<left>', function()
      require('grug-far').get_instance(0):goto_first_input()
    end, { buffer = true })
  end,
})

在每次替换/同步修改文件之前运行命令(p4 edit

require('grug-far').open({ hooks = {
  on_before_edit_file = function(on_finish, file)
    return require('grug-far').spawn_cmd_async({
      cmd_path = 'p4',
      args = { 'edit', file.path},
      on_finish = on_finish,
    })
  end,
}})

NOTE: spawn_cmd_async 作为一个便捷功能提供,同时也处理中止操作(它返回中止函数), 但你可以当然实现自己的逻辑。

添加 neo-tree 集成以打开仅限于聚焦目录或文件的搜索

neo-tree 中创建一个热键 z,该热键将创建/打开一个 grug-far 的命名实例,其路径为当前聚焦文件或目录的目录。在第二次触发时,grug-far 实例的路径将被更新,其他字段保持不变。

Neo tree 懒加载插件配置

一个展示其运行效果的小视频:https://github.com/MagicDuck/grug-far.nvim/issues/165#issuecomment-2257439367

return {
  "nvim-neo-tree/neo-tree.nvim",
  dependencies = "nvim-tree/nvim-web-devicons",
  config = function()
    local function open_grug_far(prefills)
      local grug_far = require("grug-far")

      if not grug_far.has_instance("explorer") then
        grug_far.open({ instanceName = "explorer" })
      else
        grug_far.get_instance('explorer'):open()
      end
      -- doing it seperately because multiple paths doesn't open work when passed with open
      -- updating the prefills without clearing the search and other fields
      grug_far.get_instance('explorer'):update_input_values(prefills, false)
    end
    require("neo-tree").setup {
      commands = {
        -- create a new neo-tree command
        grug_far_replace = function(state)
          local node = state.tree:get_node()
          local prefills = {
            -- also escape the paths if space is there
            -- if you want files to be selected, use ':p' only, see filename-modifiers
            paths = node.type == "directory" and vim.fn.fnameescape(vim.fn.fnamemodify(node:get_id(), ":p"))
        or vim.fn.fnameescape(vim.fn.fnamemodify(node:get_id(), ":h")),
          }
          open_grug_far(prefills)
        end,
        -- https://github.com/nvim-neo-tree/neo-tree.nvim/blob/fbb631e818f48591d0c3a590817003d36d0de691/doc/neo-tree.txt#L535
        grug_far_replace_visual = function(state, selected_nodes, callback)
          local paths = {}
          for _, node in pairs(selected_nodes) do
            -- also escape the paths if space is there
            -- if you want files to be selected, use ':p' only, see filename-modifiers
            local path = node.type == "directory" and vim.fn.fnameescape(vim.fn.fnamemodify(node:get_id(), ":p"))
        or vim.fn.fnameescape(vim.fn.fnamemodify(node:get_id(), ":h"))
            table.insert(paths, path)
          end
          local prefills = { paths = table.concat(paths, "\n") }
          open_grug_far(prefills)
        end,
      },
      window = {
        mappings = {
          -- map our new command to z
          z = "grug_far_replace",
        },
      },
      -- rest of your config
    }
  end,
}

添加 oil.nvim 集成以打开仅限聚焦目录的搜索

oil.nvim 中创建一个热键 gs,用于创建/打开一个以当前聚焦目录为路径的 grug-far 命名实例。第二次触发时,将更新该 grug-far 实例的路径,其他字段保持不变。

Oil explorer 懒加载插件配置
return {
  "stevearc/oil.nvim",
  config = function()
    local oil = require "oil"
    oil.setup {
      keymaps = {
        -- create a new mapping, gs, to search and replace in the current directory
        gs = {
          callback = function()
            -- get the current directory
            local prefills = { paths = oil.get_current_dir() }

            local grug_far = require "grug-far"
            -- instance check
            if not grug_far.has_instance "explorer" then
              grug_far.open {
                instanceName = "explorer",
                prefills = prefills,
                staticTitle = "Find and Replace from Explorer",
              }
            else
              grug_far.get_instance('explorer'):open()
              -- updating the prefills without clearing the search and other fields
              grug_far.get_instance('explorer'):update_input_values(prefills, false)
            end
          end,
          desc = "oil: Search in directory",
        },
      },
      -- rest of your config
    }
  end,
}

添加 mini.files 集成以打开仅限于聚焦目录的搜索

mini.files 中创建一个热键 gs,用于创建/打开一个以当前聚焦目录为路径的 grug-far 命名实例。第二次触发时,将更新该 grug-far 实例的路径,其他字段保持不变。

MiniFiles 资源管理器懒加载插件配置
return {
  "echasnovski/mini.files",
  config = function()
    local MiniFiles = require "mini.files"

    MiniFiles.setup({
      -- your config
    })

    
    local files_grug_far_replace = function(path)
      -- works only if cursor is on the valid file system entry
      local cur_entry_path = MiniFiles.get_fs_entry().path
      local prefills = { paths = vim.fs.dirname(cur_entry_path) }

      local grug_far = require "grug-far"

      -- instance check
      if not grug_far.has_instance "explorer" then
        grug_far.open {
          instanceName = "explorer",
          prefills = prefills,
          staticTitle = "Find and Replace from Explorer",
        }
      else
        grug_far.get_instance('explorer'):open()
        -- updating the prefills without crealing the search and other fields
        grug_far.get_instance('explorer'):update_input_values(prefills, false)
      end
    end

    vim.api.nvim_create_autocmd("User", {
      pattern = "MiniFilesBufferCreate",
      callback = function(args)
        vim.keymap.set("n", "gs", files_grug_far_replace, { buffer = args.data.buf_id, desc = "Search in directory" })
      end,
    })
  end,
}

❓ Q&A

1. 获取 RPC[Error] ... 未找到 URI 的文档: file:///.../Grug%20FAR%20-%20...

你很可能正在使用 copilot.nvim,解决方法是在 copilot 配置中排除 grug-far 文件类型:

filetypes = {
  ["grug-far"] = false,
  ["grug-far-history"] = false,
  ["grug-far-help"] = false,
}

📦 类似插件 / 灵感来源