13-…与替换 - Some-soda

13-…与替换

第 13 章 …与替换

Vim 有一个非常强大的查找和替换机制。它需要一些时间来适应。一方面,一旦你习惯了 Vim 替换的强大功能,就很难再回去了。另一方面,适应它可能需要一辈子的时间。

你肯定需要知道如何在 Vim 中进行老式的替换,但我想先讨论一个稍微简单一些的查找和替换版本,我发现自己越来越常用它。

13.1. Nvim-rip-substitute

Chris Grieser 的 nvim-rip-substitute 插件提供了一个查找和替换对话框,类似于大多数其他工具和编辑器。它不如 Vim 原生替换强大,但更容易使用。

没有针对 nvim-rip-substitute 的 Lazy Extra,所以你需要在你的 plugins 目录下添加一个新文件。我把它叫做 rip-substitute.lua,内容如下:

列表 30. Nvim-rip-substitute 配置

-- lua/plugins/rip-substitute.lua
return {
  "chrisgrieser/nvim-rip-substitute",
  -- 建议添加 event 或 cmd 触发加载
  -- event = "VeryLazy",
  -- cmd = "RipSubstitute",
  keys = {
    -- 将 g/ 映射到 Normal 和 Visual 模式下启动替换
    {
      "g/",
      function()
        require("rip-substitute").sub()
      end,
      mode = { "n", "x" }, -- n=Normal, x=Visual
      desc = "Rip Substitute (区域/文件替换)",
    },
  },
}

添加此插件后重启 Neovim。它添加了一个新的 g/ 快捷键。我认为 g 是“不适合放在其他地方的东西”的前缀,而 / 与前一章的“搜索” / 相对应。

现在当你按下 g/ 时,你会在窗口的右下角得到一个搜索和替换小部件:

rip substitute dark

图 56. Rip Substitute 小部件

有两个字段,当小部件弹出时,Search 字段获得焦点。在里面输入任何你想搜索的内容(如果你之前选择了少量文本,它可能会被预填充)。

这个字段使用正则表达式语法,但谢天谢地,它不是 vim 正则表达式语法。它使用现代正则表达式语法,这种语法伴随着 rip-grep,如果你在任何现代编程语言中使用过正则表达式,应该会很熟悉。如果你不熟悉,用 Escape 切换到普通模式,然后按 R 键在有用的 regex101 网站上打开当前的正则表达式!

要在 SearchReplace 字段之间切换,请在普通模式下使用 jk 键,就像在任何 vim 窗口中切换行一样。你甚至可以使用像 o 这样的命令在下一行进入插入模式。

一旦你按你的喜好安排好了搜索和替换字符串,并验证了弹出的实时预览,请在普通模式下使用 Enter 键或在插入模式下使用 Control-Enter 组合键来执行替换。

默认情况下,rip-substitute 对整个文件执行查找和替换。如果你在调用 g/ 快捷键之前先进行了行可视选择,它将改为在选定的行内进行搜索和替换。

这可能会令人困惑,因为如果你开始时选择了一个字符范围(可以跨越多行),它将改为使用选定的文本来预填充 Search 字段。如果你开始时没有选择,Search 字段将改为用你打开小部件时光标下的单词填充。

你可以在搜索字符串中捕获变量,然后在替换部分通过数字 $1, $2 等重用它们,其中捕获组按其左括号从左到右编号。

这允许你执行像这样的替换:

rip substitute capture dark

图 57. Rip Substitute 捕获

在这个例子中,我们匹配单词 barbazban 中的任何一个,并且根据匹配到的是哪一个,分别将它们替换为 foofoozfoon

Rip-substitute 也会记录你的历史记录。在普通模式下使用上下箭头键选择之前的替换并根据需要进行编辑。

我已经把 rip-substitute 作为我日常查找和替换的主力。它足够强大,可以满足我大部分的需求,而且它的默认行为比我们接下来要介绍的 substitute 命令要理智得多。话虽如此,rip-substitute 不能做所有事情,所以你仍然需要时不时地深入研究下面的内容。

13.2. Substitute 命令

替换功能比 Vim 甚至 Vi 都要早;它可以追溯到传奇人物 Ken Thompson 开发的传奇编辑器 ed。他撰写了关于正则表达式的原始论文(以及许多其他基础工具),所以我怀疑 ed 是正则表达式在实践中首次被使用的地方。

ed 中的替换功能是如此强大,以至于它竟然流传了半个多世纪。它不仅是现代 Vim 和 Neovim 中的主要搜索和替换机制,而且在使用 sed(流编辑器,ed 的续作)通过 shell 脚本自动化任务时也很流行。

LazyVim,像往常一样,增强了替换命令,主要是通过在你输入时向你显示更改的实时预览。

因为它是一个 ex 命令(ex 代表“extended ed”,就像它的兄弟 vi 后来被重写为“vi improved”一样),你可以通过进入命令模式(用 :)来访问替换。你可以输入 :substitute,但每个人都把它缩短为 :s,因为 a) 它有效,b) 为什么要输入比需求的还要多的东西呢?

然后,不要按回车,输入一个 /。这只是一个分隔符,用来分隔你正在发出的命令(ssubstitute)和你正在搜索的术语。

现在输入搜索模式。这可以是任何 Vim 正则表达式,就像我们在第 12 章为普通搜索介绍的那样。

在这里你可以看到我已经在编辑器中输入了 :s/pattern,并且 pattern 在我的光标所在的那一行上被高亮了:

s pattern dark

图 58. Substitute 模式

接下来,输入另一个 / 来分隔模式替换内容,然后输入任何你想要替换成的字符串。LazyVim 会实时更新搜索词的所有实例为替换词,这样你就可以预览它看起来会是什么样子。在这里,我将把 pattern 替换为 FOOBAR

s replace dark

图 59. Substitute 替换字符串

现在按 Enter 完成命令并确认替换。所以查找和替换就像 :s/模式/替换内容<Enter> 一样简单。没那么糟,是吧?

也许不是,但我们还没完。远着呢。首先,那个命令只会替换 pattern 的第一个实例,并且只有当该模式恰好在光标所在的同一行时才会替换。

在 substitute 命令后使用 / 是惯例,但如果你要替换的内容包含很多 / 字符(例如 Unix 路径),你可以使用另一个字符,如 + 作为分隔符,以避免需要用 \/ 转义一堆 / 字符。例如,可以使用 :s+/home/dustyphillips/+/home/yourname/+ 来代替 %s/\/home\/dustyphillips\//\/home\/yourname\//

13.2.1. Substitute 范围

许多 Neovim ex 命令可以在前面加上一个行的范围,该命令将作用于这个范围。范围的语法可能有点令人困惑,直到今天,如果我做任何非标准的操作,我仍然需要用 :help range 查阅它。

最简单的范围是 .,代表“当前行”。它看起来像 :.s/模式/替换内容:s 之间的 . 就是范围。不过,你通常不会费心去写它,因为 . 或“当前行”是默认范围。

你可能会使用的第二常见的范围可能是 %。它代表“整个文件”。如果你习惯了大多数编辑器或文字处理器中的查找和替换对话框,你可能期望它默认搜索“整个文件”。但它不会,如果你想在整个文件中进行查找和替换,你需要使用 :%s/模式/替换内容(可能像下一节描述的那样在末尾加上 /g)。

你也可以设置一个特定的行号,例如 :5s/模式/替换内容 来替换第 5 行的单词 pattern。但我通常会用 5G 将光标移动到第五行,然后进行默认范围的替换。

“范围”这个名字意味着你可以覆盖多行的序列,你确实可以用逗号分隔开始和结束位置。所以,例如,:3,8s/… 将在第 3、4、5、6、7 和 8 行(选择在两端都包含)执行替换:在这里,我开始了一个模式,它高亮了第 3 到 8 行的单词 hello,但没有高亮其他行:

range 3 8 dark

图 60. 范围 3-8(含)

你也可以使用像 'a 这样的标记(如前一章所述)来定义范围的开始或结束。

你最常用的方式是使用 '<,'>,它指定了“最近一次可视选择”的范围。幸运的是,你不必经常输入那些字符,因为如果你使用例如 Shift-V 后跟光标移动来选择一些文本,然后输入 :,Neovim 会自动将该范围复制到命令行中。

这意味着如果你想“在当前可视选择的文本中执行替换”,你只需选择文本然后输入 :s/…。范围将被插入到冒号和 s 之间,所以你会得到 :'<,'>s/…

如果你的大脑能承受一些递归的混乱,你甚至可以用一个搜索模式来指定范围的一端!在下面的例子中,当我开始替换时,我的光标在第 5 行:

pattern range dark

图 61. 模式范围

替换命令是 :,/hello-10/s/hello/foo。里面所有的正斜杠让它很难读(看起来像 Unix 文件路径!),但实际上写起来很容易。让我们从左到右分解它:

  • : 是普通模式下“开始一个 ex 命令”的触发器。
  • :, 之间没有任何东西,所以范围的开始是当前行(本例中是第 5 行)。
  • 第一个 / 是表示“范围的结束是当前光标位置之后匹配某个模式的第一行”的一种更简洁的方式。
  • hello-10 是我们用来定义范围结束的搜索模式。
  • 第二个 / 标记模式的结束。所以我们的完整范围是 ,/hello-10/,意思是“从当前行到包含 hello-10 的那一行”。
  • s 表示我们想在该范围内的行上执行替换。
  • /hello/foo 是模式“hello”和替换内容“foo”,就像任何替换一样。

你可以用 Vim 范围做很多其他事情,但事实是,它们中的大多数只存在于支持过时的编辑模式。你可能会发现 %'<,'>,/模式/ 覆盖了你 95% 的用例。通读一遍 :help range 以确保你知道还有哪些其他类型的语法可用,并且在上述方法不足以满足需求的罕见情况下,不要害怕去查阅它们。

13.2.2. 标志 (全局和忽略大小写替换)

你可以在任何替换命令的末尾(在最后一个 / 之后)添加“标志”来修改搜索和替换的行为。你最常用的标志是 g,代表“global”(全局)。你使用它的频率会很高。

默认情况下,substitute 只替换行上模式的第一个实例。所以如果我有一个充满过于欢快的单词 hello hello 的文件,那么替换 :%s/hello/foo 将只替换每行的第一个实例:

non global substitute dark

图 62. Substitute 高亮 (非全局)

但如果我附加 /g,它将替换每行所有的 hello

global substitute dark

图 63. Substitute 高亮 (全局)

我之前提到过,极其常见的“替换文件中所有内容”的用例是 :%s/模式/替换内容/g% 是“每一行”,g 表示“每一行中的每一个实例”。

有将近一打的标志,但唯一其他有用的标志是 iI 和(很少用的)c。前两个分别显式忽略大小写或禁用忽略大小写来搜索术语,你只需要根据你的 options.lua 中是否设置了 ignorecase(在 LazyVim 中默认为 true)来使用其中一个。c 标志表示确认 (confirm),如果你想在一个大文件中进行替换但你知道想要跳过其中一些时很有用。你会看到每一个建议的更改,并可以逐一接受或拒绝它们。

标志可以组合使用,所以 :%s/hello/foo/gc 将进行全局替换,并对每一个进行确认。

13.2.3. 便捷的 Substitute 快捷方式

你不需要记住这一节,但一旦你习惯了替换,你可能会注意到一些操作相当重复单调,你希望能更快地输入它们。通读这些提示,以便在你对 :substitute 更熟悉时记得去查阅它们。

如果你将替换的模式部分留空(如 :s//替换内容/),它将默认为你上次搜索替换的任何模式。例如,如果你按顺序执行这些命令:

  • /foo 将搜索单词 foo
  • :s//bar 将用 bar 替换 foo
  • :s/baz/bar 将用 bar 替换 baz
  • :s//fizz 现在将用 fizz 替换 baz

这可以在你搜索一个术语然后决定要替换它时,或者当你在一个文件中替换了某些内容并想在另一个文件中再次替换它时节省一点输入。

如果你只使用 :s 而没有任何模式或替换内容,它将重复你上次做的模式替换。但请注意,它不会作用于相同的范围,所以如果你想完全重复它,你需要再次输入范围。

它也不会重复标志,但你(通常)可以将标志直接附加到 :s 后面。对于初学者来说,最常见的是 :%sg,它映射到“在整个文件上全局重复上一次替换”。当你输入 :s/长模式/长替换内容 并期望它进行全局替换,但实际上它只替换了当前行的第一个实例时,这很有用。:%sg 将按照你预期的方式重复替换。你也可能会想到用 :'<,'>sg 在上次的可视选择中进行替换。

别忘了你可以用 gv 重复上次的可视选择,以确认它确实选择了你期望的内容。

如果你想在替换内容中重用“模式匹配到的任何内容”,你可以在替换字符串中使用 \0。当你使用的正则表达式可能匹配不同的东西时,这特别有用。

例如,假设我有以下文件:

列表 31. 一个虚构的文件

hello world
Hello thrift shop
Hellish world

出于某种莫名其妙(但有教学意义)的原因,我想在第一个和第二个单词之间添加一个形容词。这可以通过命令 :%s/[hH]ell\S* /\0green / 来完成:

replace with pattern dark

图 64. 替换内容中使用模式

如果你对正则表达式不熟悉,那个命令可能有点吓人,所以我再分解一下:

  • :% 表示“对整个文件执行命令”
  • s/ 表示“要执行的命令是 substitute”
  • [hH] 表示“不区分大小写匹配 h”(见注释)
  • ell 表示“精确匹配三个字符 ell
  • \S 表示“匹配任何非空白字符”
  • * 表示“重复 \S 匹配零次或多次”,这将我们带到单词末尾。
  • / 包含一个空格,然后是搜索模式的结束
  • \0 表示“将上面模式匹配到的任何内容插入到替换中”
  • green 表示“将该文本直接插入到替换中”

如果你的 options.lua 中没有 vim.opt.ignorecase=false,那么[hH] 不是必要的。另一种方法是在模式末尾使用 /i 强制本次搜索忽略大小写。那么 [hH] 可以简化为 h

你甚至可以在替换中重用模式的一部分。为此,将你想要重用的部分放在 \(\) 之间。然后在替换部分使用 \1 来表示括号之间匹配到的任何内容。

译者注: \(\) 用于在 Vim 正则表达式中创建捕获组。

通过一个例子更容易理解。如果我们从上面相同的三个例子开始,我们可以使用替换 :%s/hell\(\S*\)/green\1 and blue\1/i 来产生以下无意义的替换:

reuse partial dark

图 65. 使用部分模式进行替换

\(\S*\) 匹配与 \S* 相同的内容,但它将结果存储在一个捕获组中。然后当我们在替换中想要重用这个捕获组时,我们使用 \1引用该匹配捕获到的任何内容。

你可能会从我们在这里使用数字猜出来,你可以拥有并引用多个捕获组,你的猜测是正确的!

13.3. 项目范围的搜索和替换

LazyVim 自带一个名为 Grug-far.nvim 的插件,用于在项目中的所有文件中进行全局查找和替换。没有 grug-far,你可能(不情愿地)会使用我提到过的 ed 的流式演进版本 sed 从命令行来完成这个任务。

在运行 Grug-far 之前,将你的文件提交到版本控制(Git)是个好主意。它所做的更改可能很难撤销。你可以逐个文件地撤销它,但不能一次性完成。所以确保 git reset --hard 不会导致你丢失任何非 Grug-far 完成的工作。

Grug-far 是一个围绕 ripgrep(我之前提到过的命令行工具)的轻量级 UI。但那个 UI 非常方便,因为 ripgrep 有一些晦涩的参数。

要显示 Grug-far UI,请使用键盘快捷键 <Space>sr,助记符 r 代表replace(替换)。一个窗口将在右侧打开:

grug far empty dark

图 66. 空的 Grug-far 窗口

你可以使用所有正常的 Vim 动作在这个窗口中导航,但你主要只需要 jk 在字段之间跳转。

搜索字段可以接受任何正则表达式。因为底层使用的是 ripgrep,所以它是一种稍微不那么晦涩的正则表达式语法。文件过滤器字段用于将你的搜索隔离到特定的路径或文件扩展名,并接受标准的 shell glob 语法。

当你填写表单条目时,Grug-far 会在一个实时更新的小部件中即时预览建议的更改:

grug far preview dark

图 67. 带预览的 Grug-far

在你插入搜索和替换文本后,你需要按 Escape 返回到普通模式。如果预览区域中的所有结果看起来都可接受,只需按 \r 执行replacement(替换)。(译者注:原文的 \r 可能是指 Enter 或特定映射,通常 UI 确认是 Enter)。

如果某些结果不符合你的需求,你也可以选择调整搜索结果。使用任何标准动作导航预览窗口。如果有你不想更改的匹配项,只需使用 dd 将它们彻底删除。或者,随时在预览窗口中编辑任何行,使其看起来像你想要的样子。

一旦你对预览满意,使用 \s 而不是 \r 来将你所做的更改sync(同步)到它们的原始源文件。(译者注:原文的 \s 可能是特定映射)。

你也可以通过将光标放在预览结果上并按 Enter 来跳转到该结果的源文件。

Grug-far 会记录你最近的搜索和替换操作,并允许你使用 \t 快捷键重新访问它们。用标准动作在它们之间导航,并使用 Enter 重用其中一个。

还有一个菜单可以通过 g? 弹出,里面有一些其他有用的快捷键,我留给你闲暇时细看。

13.4. Text-case 插件

Johnny Salas 的 text-case.nvim 插件提供了一组命令来快速更改文本的大小写。它有两个相关但不同的用例:

  1. 你想改变特定单词或选择的“形状”(shape) 到不同的形状而不改变单词本身。例如,这个插件可以快速将蛇形命名法 (snake-case) 转换为驼峰命名法 (CamelCase),或常量命名法 (CONSTANT_CASE)。
  2. 你想改变特定单词的“内容”(content) 为不同的内容而不改变其形状。例如,你可以用一个替换命令将 foo_barFooBarFOO_BAR 分别替换为 fizz_buzzFizzBuzzFIZZ_BUZZ

这是那种你不常使用,但在需要时可以节省大量时间的插件之一。

没有针对 text-case.nvim 的 lazy extra,但你可以按如下方式将其安装到你的 plugins 目录中:

列表 32. Text-case.nvim 配置

-- lua/plugins/text-case.lua
return {
  "johmsalas/text-case.nvim",
  -- 依赖 nvim-treesitter,确保它已安装并加载
  dependencies = { "nvim-treesitter/nvim-treesitter" },
  -- 建议延迟加载,除非需要立即使用其命令
  -- event = "VeryLazy",
  -- 如果不立即加载,需要指定命令以便 Lazy.nvim 知道何时加载
  -- cmd = { "TextCase...", "Subs..." }, -- 根据实际命令填写
  config = function()
    require("textcase").setup({})
    -- 设置快捷键 (可选,也可以在 keymaps.lua 中设置)
    -- vim.keymap.set("n", "ga", "<cmd>TextCaseOpenTelescope<CR>", { desc = "TextCase (ga)" })
  end,
}

译者注: 原文的配置示例缺少了必要的 require("textcase").setup() 调用,并且 lazy = falseconfig = true 不是推荐的 Lazy.nvim 配置方式。已更新为更标准的配置结构,并添加了依赖说明和加载建议。cmd 字段需要用户根据实际使用的命令填写。

lazy = false 是为了从插件获得某些交互式功能。(译者注:不推荐使用 lazy=false,应尽可能延迟加载。如果确实需要立即加载,就这样设置,但通常有更好的方法,如使用 eventcmd。)

安装后,你就可以开始更改大小写了。在某处写一个蛇形命名的单词,例如 fizz_buzz。将光标移动到该单词上(你甚至不必选择它)并输入 ga。注意 which-key 菜单如何弹出,显示你可以将其更改为的所有大小写形式:

text case menu dark

图 68. Text-case.nvim Which-key 菜单

选择你想要将单词转换成的大小写样式,就完成了!这种简写形式只在你要处理的单词没有空格时有效。如果你想将例如 fizz buzz 更改为 FizzBuzz,最简单的方法是在可视模式下选择这两个单词(例如,将光标移动到 f 并按 v2e),然后按 gap,其中 p 代表 “pascal” case (帕斯卡命名法)。当然,在你按下 ga 后,可视模式下的 which-key 菜单会显示其他大小写形式。

或者,你可以将光标放在起始的 f 上,然后按 gao 来指示 text-case 在你选择目标大小写之后进入 operator-pending (操作符待决) 模式。然后你可以使用任何动作(例如 2w)来告诉 text-case 它应该操作哪个块。

13.4.1. 大小写感知替换

text-case 插件还支持一个 :Subs 命令,其行为类似于 :substitute 命令,但它更改单词的方式总是保持其现有的大小写形式。

例如,考虑这个简单的 python 类:

列表 33. 大小写感知替换

class FooBar:
    fooBar: str

    def foo_bar(self):
        self.fooBar = "FOO_BAR"

选择整个类(用于寻找周围对象的 S 快捷键非常适合这个)并输入命令模式命令 :Subs/foo_bar/fizz_buzz。只需一次替换,整个类就会变成下面这样:

列表 34. 大小写感知替换

class FizzBuzz:
    fizzBuzz: str

    def fizz_buzz(self):
        self.fizzBuzz = "FIZZ_BUZZ"

这个功能在它有用的时候极其有用。需要它的场合相对不常见,但当你需要它时,没有其他工具能与之匹敌。

13.5. 对多行执行 Vim 命令

:substitute 命令不是唯一可以同时作用于多行的命令(使用范围)。事实上,如果你只想将几行写入一个单独的文件,你可以将范围传递给 :write。最简单的方法是在可视模式下选择范围,然后输入 :write <文件名>。Neovim 会自动将其转换为 :'<,'>write 并只保存那些行。

Neovim 没有好用的多光标支持(目前还没有)。历史上,Vim 编码者认为多光标模式是功能较弱、没有 Vim 模式的编辑器所需要的拐杖。最近,像 Kakoune 和 Helix 这样的实验性编辑器已经证明,多光标可以与模态编辑很好地集成。现代开发者喜欢多重选择,预计 Neovim 将来会提供原生的多光标支持(目前在路线图上列为 0.12+)。与此同时,确实多光标插件,但我发现它们笨拙且脆弱,建议目前避免使用它们。相反,你可以使用下面讨论的命令,或依赖其他 Vim 工具,例如重复录制(使用 q Q@@),或使用可视块模式(Control-v)配合插入或追加来修改多行。

13.5.1. Norm 命令

当你第一次使用 :norm 时,感觉会很奇怪。它允许你对多行执行一系列任意的 Vim 普通模式命令(包括像 hjklweb 这样的导航命令,以及像 dcy 这样的修改命令)。

译者注: :normal (或缩写 :norm) 命令用于在指定的行范围上执行后面跟的普通模式命令序列。

你甚至可以从 :norm 进入插入模式!但是你需要知道一个小秘密才能退出插入模式,因为当命令菜单可见时按 <Escape> 只会关闭命令菜单。你需要使用 Control-v<Escape>。当你在插入模式或命令模式时,Control-v 快捷键意味着“按字面意思插入下一个按键,而不是将其解释为命令”。终端通常将 Control-v<Escape> 渲染为 ^[.

例如,假设我们正在编辑以下文件:

列表 35. 一个虚构的文件

foo
Bar
fizz buzz
one two three

出于莫名其妙(但有教学意义)的原因,我们想对每一行执行以下操作:

  • 在行首插入单词 “HELLO” 并在后面加一个空格
  • 将行上第一个单词的首字母大写
  • 在每行的第一个单词后插入单词 “BEAUTIFUL”,并用空格包围
  • 在每行末尾附加单词 “WORLD”,并在前面加一个空格

首先输入 :%norm 打开一个命令行,范围作用于文件中的每一行 (%),然后是 norm 命令,后跟一个空格。

然后添加 IHELLO 在范围内的每一行开头插入文本 HELLO 。现在按 Control-v 然后按 Escape 将 escape 字符插入命令行。

现在输入 lgUl 将光标右移(使其位于第一个单词的开头),然后将右侧一个字符(即下一个单词的第一个字符)大写。

接下来是 e 跳转到单词末尾,后跟 a BEAUTIFUL 在该单词后追加一些文本。Control-vEscape 将插入另一个 escape 字符。

最后,添加 A WORLD 在行尾进入插入模式并添加文本 WORLD

因此,整个命令将是:

列表 36. Norm 命令

:%norm IHELLO <Control-v Escape>lgUlea BEAUTIFUL<ctrl-v Escape>A WORLD

视觉上,它看起来像这样,因为 Control-v Escape 按键被更改为 ^[

crazy normal mode command dark

图 69. 你为什么会想做这个?

最终结果:

列表 37. 应用 Norm 命令的结果

HELLO Foo BEAUTIFUL WORLD
HELLO Bar BEAUTIFUL WORLD
HELLO Fizz BEAUTIFUL buzz WORLD
HELLO One BEAUTIFUL two three WORLD

当然,不清楚你为什么会想执行这组确切的操作,但这希望能表明一切皆有可能!

第一次尝试应用命令时弄错是很常见的。只需使用 u 一次性撤销整个序列,然后输入 :<Up> (冒号后按上箭头键) 再次编辑命令行。

如果命令有点复杂,你在编辑它时可能会感到烦恼,因为你无法使用所有你习惯的 Vim 导航命令。所以现在是介绍 Vim 命令行编辑器的绝佳时机。

13.6. 命令行编辑器

当下方那个小小的命令行窗口(Cmdline window)获得焦点时,输入 Control-F 即可显示命令行编辑器。或者,如果你当前处于普通模式(Normal mode),输入 q:。后者与通常和 q 关联的“录制到寄存器”(record to register)命令无关,它指的是“打开可编辑的命令行窗口”。

译者注: 当你在普通模式下输入 :/? 时,下方会短暂出现一个命令行区域。此时按 Ctrl-F 可以打开完整的命令行窗口。q: 是在普通模式下直接打开该窗口的快捷键。

这个窗口基本上是普通命令行编辑器与普通 Vim 窗口结合并产生了一个具有超能力的魔法命令行窗口。

这个新的魔法窗口出现在当前缓冲区的底部,就在状态栏上方,它包含了你完整的命令行历史(包括搜索和替换命令):

command line window dark

图 70. 命令行历史窗口

使用 Control-u 向上滚动这个宝贝,你会看到你输入过的每一个命令。你甚至可以用 ? 来搜索它(向后搜索可能比向前搜索更有用,因为你的命令是按最近使用的顺序列出的)。

要运行任何旧命令,只需将光标导航到那一行并按 <Enter>。搞定!历史重演。

或者你可以在这个魔法命令行窗口底部的空行上输入一个全新的命令(记住 Shift-G 可以让你快速到达底部)。

不过,你会发现这个窗口极难退出。Escape 键不起作用,因为它被保留用于在窗口内编辑时切换回普通模式。秘诀是使用 Control-C 来关闭它,尽管其他关闭窗口的命令如 <Space>wq 也能工作。你甚至可以在命令行窗口内部运行 :q

最重要的是,你可以使用普通的 Vim 命令来编辑此窗口中的任何一行。只需导航到该行,使用你掌握的任何疯狂编辑技巧(包括其他命令模式命令,如 :s)使该行看起来如你所愿,返回普通模式,然后按 <Enter>。编辑后的命令将被执行。

13.7. 混合使用 Norm 命令与录制

回想一下,q 命令可以将一系列命令录制到一个寄存器中供以后回放。而 :norm 命令可用于将一系列命令应用于一个行范围。你可以粘贴(p)包含录制内容的寄存器,这意味着有几种方法可以稍后使用 :norm 将录制内容应用于一个行范围:

  • :<range>norm @q 将简单地在范围内的每一行上执行寄存器 q,因为 @q 就是执行寄存器 q 的命令。
  • :<range>norm <Control-r>q 会将寄存器 q 的内容复制到命令行窗口中,这样这些操作就会应用于每一行。
  • q:<range>norm <C-r>q<CR> (译者注:原文示例 q:<range>inorm <Esc>"qp 较复杂且易错,这里提供更直接的方式)打开命令行编辑器窗口,然后你可以输入 norm 并在其后通过 Ctrl-r q 插入寄存器 q 的内容,最后回车执行。

译者注:

  1. 第一种方法 (@q) 是最直接的,它将寄存器 q 中的内容作为普通模式命令序列在每一行执行。
  2. 第二种方法 (<C-r>q) 是将寄存器 q字面内容(即你录制的按键序列字符串)插入到 :norm 命令后面。这在你录制的命令包含特殊字符或需要在命令模式下解释时可能有用,但通常 @q 更常用。
  3. 第三种方法是先用 q: 打开命令行窗口,然后手动构造 :norm 命令并插入寄存器内容。这里简化了原书示例,直接用 <C-r>q 插入。<CR> 代表回车。

13.8. 全局命令 (:global)

:norm 命令作用于一个行范围,而 Neovim 的范围必须是连续的行。不可能在一个命令中对例如第 1 到 4 行和第 8 到 10 行执行命令,而跳过第 5 到 7 行(除非在不同的范围上运行两次 :norm)。

有时,你想在匹配某个模式(pattern)的每一行上运行一个命令。这就是 :global 命令发挥作用的地方。

:global 的语法本质上是 :<range>global/pattern/command,尽管你可以将其缩写为 :<range>g/pattern/command。这个模式就像任何 Vim 搜索或替换模式一样。

然而,command 部分有点奇怪。技术上讲,它是一个 “ex” 命令,这意味着“许多(但不是全部)跟在冒号后面的命令,但主要是那些你在日常编辑中不常用以至于很难记住的命令”。

译者注: Ex 命令是 Vim 源自 ex 编辑器的命令行命令。常见的如 :w (write), :q (quit), :s (substitute), :d (delete), :! (shell command) 等都属于 Ex 命令。:global 本身也是一个 Ex 命令,它后面可以跟其他 Ex 命令。

最常见的例子是“删除所有匹配模式的行”,你可以用 :%g/pattern/d 来完成。

译者注: % 代表整个文件范围。d 是删除行的 Ex 命令。

另一个流行的是 substitute(替换),你已经知道了。如果你在你的替换命令前加上 :%g/pattern,你可以让它只在匹配特定模式的行上执行替换。这个模式可以与替换本身使用的模式不同。考虑以下神秘的文本序列:

清单 38. 结合 Substitute 与 Global

:%g/^f/s/ba[rt]/glib

真是一团糟!这显然是为了易于编写,而不是易于阅读。如果我们想让它稍微易读一点,我们可能会写成 :%global/^f/substitute/ba[rt]/glib

译者注: :substitute 可以缩写为 :s

这个命令的意思是“对每个以 f 开头的行执行一个全局操作。在这种情况下,该操作应该是将 barbat 的每个实例替换为单词 glib。”

这与在范围中使用模式不同,例如 :,/foo/s/needle/haystack/。这个命令在光标所在行到第一个包含 foo 的行之间的所有行上执行替换,而 :%global/foo/s/needle/haystack/ 则在文件中每个包含单词 foo 的行上执行替换。

在我看来,:global 最有趣的用途是在匹配模式的行上运行一个普通模式(Normal mode)命令。这实际上意味着将 :global:normal 混合使用,如 :%g/pattern/norm <some keystrokes>

译者注: 注意 :global 后面跟的是 :normal 这个 Ex 命令,而不是直接跟普通模式的按键序列。

举个例子,这将在每个以 “hello” 开头的行的末尾插入单词 “world”:

global normal dark

图 71. 混合使用 Global 和 Normal

译者注: 图中命令为 :%g/^hello/norm A worldA 是普通模式命令,用于在行尾进入插入模式。

你也可以使用 global匹配模式的每一行上执行命令。只需使用 g!/vglobal/ (v 代表 invert)而不是 g/

g! 在处理日志文件时很有用,这些日志文件中的异常信息可能会随机换行。例如,一个基本的日志文件可能看起来像这样:

清单 39. 虚构的日志文件

2024-03-26T12:00:00 Something happened
2024-03-26T12:01:01 Something happened
2024-03-26T12:01:02 Something super bad happened
  Traceback:
    A bunch of lines I don't care about
2024-03-26T12:02:00 Something else happened
2024-03-26T12:03:58 Cool thing happened

在进一步处理之前,我可能想删除所有不以日期开头的行:

global non match dark

图 72. Global 反向匹配

译者注: 图中命令为 :g!/^\d\d\d\d-\d\d-\d\d/d

这可能有点令人费解,这已经成了反复出现的主题。每个 \d 表示“匹配一个数字”,而最后的 /d 表示“对选定的行执行删除操作”。g! 是重要的部分;它表示“选定的行是那些匹配该模式的行”。

我使用 :global 的频率远不如使用 :norm。但当我使用它时,它是一种极其高效的方式,可以在文件中引起大规模的更改。它需要一些时间来适应,你可能在头几次需要它的时候会查找语法,但它是你工具箱中一个非常棒的工具。

13.9. 总结

本章全部是关于批量编辑文本的。我们从使用 :s[ubstitute] ex 命令开始替换,然后浏览了使用 Grug-far 插件在多个文件中执行查找和替换的用户界面。

然后我们学习了如何使用 :norm:global 同时在多行上执行命令,并快速而全面地介绍了命令行编辑窗口。

在下一章中,我们将学习一些我无法放在其他任何地方的编辑技巧。