grug-far.nvim
Find And Replace plugin for neovim
Grug 查找!Grug 替换!Grug 开心!
✨ 特性
- 使用
rg或ast-grep的全部功能进行搜索 - 使用
rg或ast-grep的几乎全部功能进行替换。例如,对于rg,某些标志如--binary和--json等被列入黑名单,以防止意外输出。当使用此类标志时,UI 会发出警告并阻止替换。 - 自动防抖搜索,或在退出插入模式(以及普通模式更改)时手动搜索
- 在 quickfix 列表中打开搜索结果
- 在结果输出的行上按下
<Enter>时,跳转到匹配项的文件/行/列(键位可配置)。 - 使用可配置的键位内联编辑结果行,并将其同步回其源文件位置。
- 手动/自动保存搜索历史并重新加载
- 语法高亮的搜索结果
- 搜索结果折叠
- 多行搜索与替换
- 在由可视选择标记的缓冲区范围内进行搜索/替换
- 在“滚动”浏览结果时“预览”结果源
- 使用 lua 解释的替换对每个匹配项进行替换
搜索:
替换:
更多截图:
Rg 向你展示它的用法
帮助:
在缓冲区范围内搜索
历史:
Ast-grep 多行搜索与替换:
Ast-grep debug-query
使用 lua 解释替换的 Ripgrep
使用 vimscript 解释替换的 Ast-grep
Ast-grep 规则
视频
linkarzu 贴心地制作了一段视频,展示了部分功能的实际运行效果(如果你觉得有帮助,别忘了点赞以感谢他的辛勤付出):
🤔 哲学
- 力求降低心智负担。 你可以执行的所有操作以及尽可能多的帮助都直接呈现在你面前(部分可配置)。Grug 经常忘记如何使用捕获组,或者哪个标志符起什么作用。
- 透明性。 不试图隐藏底层工具。例如,来自
rg的错误消息会原样显示,因为当你写错正则表达式时,它们其实相当友好。你可以逐步学习rg或ast-grep标志符,或者利用在 CLI 中运行它时已有的知识。你甚至可以输入--help标志符来查看完整的rg帮助,或使用--debug-query=ast标志符来调试你的ast-grep查询。Grug 喜欢! - 复用肌肉记忆。 不试图阻止任何类型的缓冲区编辑,例如删除行等。这类操作很容易出错,一旦出错,Grug 就无法在编写大型正则表达式的过程中修改文本。Grug 很生气!!仅确保优雅恢复,以维护基本 UI 完整性(这得益于 extmarks 的魔力)。恢复应该只需简单的撤销操作即可。
- uniformity. 仅使用一个工具来同时执行搜索和替换操作,以保持行为的一致性。例如,不会将
rg与其他工具(如sed)组合使用,尽管rg并不直接支持替换功能。在编写正则表达式时,不应需要担心兼容性差异。此外,这也为使用许多高级的rg标志(例如不同的正则表达式引擎)打开了大门,而这些在混合环境中是无法实现的。目前存在一个小的例外,这是因为ast-grep目前不支持类似--glob标志的功能,因此我们必须通过rg来过滤文件,但希望未来能够解决这一问题。
⚡️ 需求
- Neovim >= 0.11.0 (对于 nvim 0.10 请使用 tag 1.6.3)
- BurntSushi/ripgrep >= 14 受支持,>= 15 推荐
- 一个 Nerd Font (可选)
- ast-grep (可选) 如果你希望使用
ast-grep搜索引擎。推荐版本 >=0.36。 - 使用 nvim-web-devicons 或 mini.icons 以支持文件图标 (可选)
如果你遇到意外问题,请运行 :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,例如 botright、aboveleft 等,以及可视范围。
在可视模式下,该命令会用当前的可视选区预填充搜索字符串。
注意,如果你希望在可视选区内部进行搜索和替换,应改用 :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 操作切换到所需的替换解释器,
例如 lua 或 vimscript。
例如,使用 lua 解释器,这将允许您在 Replace: 输入中编写多行 lua 代码,基本上就是 lua 函数的主体。
您可以使用 match 来引用每个匹配项,并且需要 return 您希望作为替换值的值。
对于 astgrep 引擎,您还可以通过访问
vars 表来访问元变量。例如,$A 由 vars.A 引用,$$$ARGS 由 vars.ARGS 引用。
对于 vimscript 解释器,情况类似。
将结果行同步回源文件
可以将结果区域中各行的文本同步回其源文件。这允许在 grug-far 缓冲区中自由编辑结果,甚至包括旧的 %s/foo/bar。
有 3 种类型的操作可以完成此同步:
Sync Line- 同步当前行Sync All- 同步所有行Apply Next/Apply Prev- 同步当前行/差异并智能地从结果缓冲区中删除它
只有当某一行与源文件相比发生了某种变化时,才会发生同步,即正在发生替换或您已手动编辑了该行。
删除结果行会导致它们被排除在 Sync All 操作的同步范围之外。
在某些情况下,如果您想排除特定文件
或某些特定匹配项,这是一种很好的细化替换的方法。
注意: 在进行多行替换(--multiline 标志)时,同步功能被禁用
注意: 如果您希望在使用空字符串进行替换时同步功能正常工作,请将 --replace=
添加到标志中。
注意: 同步功能仅由 ripgrep 引擎支持。以下关于同步与替换之间差异的说明
是 ripgrep 引擎特有的:
如果你不编辑结果列表,Sync All 和 Replace 具有等效的结果,除了一个情况。
当你使用 --multiline 和 --multiline-dot-all 标志进行多行替换时,同步将不起作用,因此你
必须使用 replace。本质上区别在于 Replace 在每个文件上运行 rg --replace=... --passthrough,
并且完全不依赖于结果区域中的内容。Sync All 基于结果区域中的内容进行逐行
同步。
前往 / 打开 / 预览结果位置
当光标放置在结果文件路径上时,你可以在普通模式下按 <enter> 前往该文件(Goto 操作的默认键位)。
当它放置在结果匹配行上时,你将前往匹配的文件/行/列。默认情况下,文件缓冲区
在你打开 grug-far 之前所在的最后一个窗口中打开,这通常是另一个垂直分割窗口。
如果你希望执行相同操作,但让光标保持原位,你可以改用 Open 操作。
注意: 对于 Goto 和 Open 操作,如果之前输入了 <count>,则使用对应于 <count> 结果行的
位置,而不是当前光标行。如果你希望
对 <count> 有视觉指示,可以设置选项 resultLocation.showNumberLabel = true。
为了平滑地按顺序 Open 每个结果位置,你可以使用 Open Next(<down> by default) 和 Open Prev(<up> 默认) 操作。
如果你希望保持缓冲区布局,可以改用 Preview 操作,它将在浮动窗口中打开位置。
在 quickfix 列表中打开结果行
结果行可以在快速修复列表中打开。删除结果行将导致它们不被包含在内。
注意: 执行多行替换时(--multiline 标志),快速修复列表操作将被禁用。
历史记录
grug-far 可以跟踪你的搜索历史。这可以通过手动使用
History Add 操作添加历史记录条目,或在某些成功的操作(如 Replace 和 Sync 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(默认)、astgrep 和 astgrep-rules。
如果你只想在切换循环中包含部分引擎,可以配置可用引擎列表。
ripgrep 使用 rg CLI 命令进行搜索和替换。有关 CLI 选项和正则表达式语法的更多信息,请参阅 ripgrep 文档。
astgrep 和 astgrep-rules 是 ast-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,
}
📦 类似插件 / 灵感来源
- nvim-spectre: 缓冲区中查找和替换的 OG 插件,绝佳的灵感来源!
- telescope.nvim: 从那里借用了
rg健康检查 :P - lazy.nvim: 使用了他们精美的
README.md作为模板 - plugin-template.nvim: 超级好用的模板,本插件基于它构建!
