07-对象与操作符待决模式 - Some-soda

07-对象与操作符待决模式

第 7 章 对象与操作符待决模式

我们目前学到的导航和动作命令非常宝贵,但 Neovim 还自带了几个更高级的动作,可以极大地提升你的编辑工作流。LazyVim 通过各种插件提供的其他强大导航功能进一步补充了这套动作。

例如,如果你编辑的是文本而不是源代码,你会发现按句子和段落导航很有用。句子基本上是指以 .?! 后跟空白结尾的任何内容。

句子的快捷键对我来说是最难记住的。我用得很少,以至于还没形成肌肉记忆,而且我记不住什么好的助记符。

我吊足胃口了吗?注意了,因为你会忘记这个。要向前移动一个句子(到句子结束标点后的空白之后的第一个字母),在普通模式下输入 )(右括号)命令。要移动到当前句子的开头,使用 (。再次按下括号可以移动到下一个或上一个句子,或者如果你想移动多个句子,可以添加计数。

我讨厌这个命令是 (,因为它感觉应该移动到,你知道的,括号!但它不是;它是按句子移动。由于 .!? 字符在正常的软件开发中很少表示“句子”,所以我直到开始写书时才不怎么用它(我一直告诉自己不会再投入写书了,但这从未持久)。

除了在标点符号后跟空白处停止外,按句子导航也会在“段落边界”停止,也就是说“空行”。如果你使用带计数的句子导航,这可能会打乱你的导航,因为你需要为每个段落以及通常定义句子的标点符号添加额外的步骤。

不过,我确实一直使用段落动作。段落被定义为两个空行之间的所有内容,这个概念在编程上下文中是有意义的。大多数开发者会用空行分隔逻辑上相关的语句来组织他们的代码。向上或向下移动一个“段落”的命令是花括号 {}。如果你需要向前或向后跳转多个段落,它们像往常一样可以前缀计数。

同样,你可能期望 { 跳转到花括号,所以它表示“空行”而不是这个,有点烦人,但一旦你习惯了它,你可能会经常用到它。

7.1. Unimpaired 模式

LazyVim 提供了许多其他可以使用方括号访问的动作。完全内化它们需要一些时间,但幸运的是,你可以通过按单个 [] 来获取菜单。就像句子和段落动作一样,方括号允许你移动到上一个或下一个某物,只不过这个某物取决于你在方括号后输入的键。

总的来说,这些成对的导航技术有时被称为“Unimpaired 模式”,因为它们让人想起一位著名的 Vim 插件作者 Tim Pope 开发的基础性 Vim 插件 vim-unimpaired。LazyVim 没有直接使用这个插件,但该插件的精神得以延续。

译者注: vim-unimpaired 插件的核心思想是为许多常见的操作提供成对的、易于记忆的快捷键,通常使用 [] 作为前缀。例如 [f]f 在文件间切换,[q]q 在 quickfix 列表项间跳转等。LazyVim 实现了类似的功能。

如果我输入 [ 然后暂停等待菜单,我看到的是这样:

unimpaired menu dark

图 25. Unimpaired 模式菜单

并非所有这些都与导航相关,其中一个只是因为我为它启用了 Lazy Extra 才在那里。我们将在这里介绍与动作相关的,并在后续章节中介绍其他大部分。

首先,用于处理 (<{ 的命令比它们看起来要微妙得多。它们不会盲目地跳转到下一个(如果你以 ] 开头)或上一个(如果你使用 [)圆括号、尖括号或花括号。如果你想那样做,你可以直接使用 f(F(

相反,它们将跳转到下一个未匹配的圆括号、尖括号或花括号。这实际上意味着像 [( ]} 这样的按键表示“跳出”。所以如果你在一个被 {} 包围的代码块中间,你可以很容易地使用 ]} 跳转到该块的末尾,或者使用 [{ 跳转到它的开头,无论该对象内部存在多少其他由花括号界定的代码块。这在各种编程上下文中都很有用,所以花些时间来习惯它。

作为快捷方式,你也可以使用 [%]%,其中 % 键基本上是“任何包围我的括号”的占位符。它们将跳转到你当前所在的任何圆括号、花括号、尖括号或方括号的开头或结尾。

译者注: % 是 Vim 内建命令,用于在匹配的括号((), {}, [])之间跳转。[%]% 可能是 LazyVim 添加的,模拟类似行为但确保跳到块的开始/结束。

最后一个(方括号)很重要,因为与其他不同,[[]] 不会跳出方括号,所以如果你需要跳出它们,使用 [%]% 是你唯一的选择。

译者注: Vim 内建的 [[]] 通常用于按函数或段落(以 { 或空行分隔)跳转,具体行为可能取决于文件类型。LazyVim 重新映射了它们。

7.1.1. 按引用跳转

与你可能期望的跳出方括号不同,易于输入的 [[]] 被保留用于一个更常见的操作:跳转到光标下变量的其他引用(在同一文件中)。

这个功能通常使用当前语言的语言服务器,所以它通常比盲目搜索更智能。只有该函数或变量的实际使用处会被跳转到,而不是像搜索操作那样跳转到其他变量、类型或注释中间的该单词实例。

当你移动光标时,LazyVim 会自动高亮文件中其他的变量实例,这样你就能轻易看到 ]][[ 会将光标移动到哪里。

7.1.2. 按语言特性跳转

[c]c[f]f[m]m 快捷键允许你通过跳转到上一个或下一个类/类型定义、函数定义或方法定义来在源代码文件中导航。这些功能的用处有点取决于你使用的语言以及该语言的语言服务(Language Service)的配置方式,但它在常见语言中工作得很好。

译者注:

  • [c/]c: 跳转到上一个/下一个class (类) 或类型定义。
  • [f/]f: 跳转到上一个/下一个function (函数) 定义。
  • [m/]m: 跳转到上一个/下一个method (方法) 定义 (通常在类内部)。

默认情况下,这些快捷键都跳转到上一个或下一个类/函数/方法的开头。如果你想跳转到末尾,只需添加一个 Shift 键:[C]C[F]f[M]M 就能带你到那里。(译者注:原文 ]f 可能是笔误,应为 ]F)。

注意,这些与“跳出”行为不同:如果你在当前编辑的函数内部定义了一个嵌套或匿名函数或回调,]*F* 快捷键将跳转到嵌套函数的末尾,而不是跳转到你当前所在函数之后的那个函数的末尾。

我个人不怎么使用这些快捷键,因为有其他方法可以在文档中导航符号,我们稍后会讨论。但是如果你正在编辑一个大函数,并且想快速跳转到文件中的下一个函数,]*f* 可能比使用你需要计算的计数的 j,甚至比 Control-d 后跟 s 进入 seek 模式更快地带你到那里。

7.1.3. 跳转到缩进结束

如果你正在处理基于缩进的代码(如 Python)或深度嵌套的基于标签的标记(如 HTML 和 JSX),你可能会发现 [i]i 这对键很有帮助。

这些是作为 snacks 插件套件的一部分,通过 snacks.indent 提供的。这个插件帮助可视化文件中的缩进级别。以下是我最近处理的一个 Svelte 组件的例子:

indent guides dark

图 26. 缩进指南

这段 Svelte 代码使用两个空格进行缩进。每个缩进级别都有一条(在我的主题中是)灰色的垂直线来帮助可视化该缩进级别的开始和结束位置,并且“当前”缩进级别以不同的颜色高亮显示。

此外,该插件添加了 unimpaired 命令 [i]i 来跳出当前缩进级别;它将跳转到当前高亮的任何缩进线的顶部或底部。

我在编辑 Python 代码和 Svelte 组件时一直使用这个功能。在其他语言中我用得较少,因为 [%]% 往往能让我更接近接下来需要去的地方。但是缩进指南的视觉反馈可能非常有用,即使在括号密集的语言中也是如此;我可能会对我会“跳出”到哪个花括号感到惊讶,但缩进指南总是很明显。

7.1.4. 跳转到诊断信息

我不知道你怎么样,但是当我写代码时,我倾向于在其中引入很多错误。根据语言的不同,LazyVim 要么是预先配置好的,要么可以被配置来给我大量关于这些错误的反馈,通常是以波浪下划线的形式。

如果你看不到波浪下划线,回到第 1 章,选一个更好的终端。

这些波浪下划线通常是通过与编译器、类型检查器、linter(代码风格检查器),甚至拼写检查器的集成创建的,具体取决于语言。其中一些是错误,一些是警告,一些是提示。有些只是干扰,但大多数是改进代码的机会。

因为我在代码中引入问题的能力是如此惊人地有才华(译者注:俺也一样),我需要执行的一个常见导航任务是“跳转到下一个波浪线”。总的来说,这些被称为diagnostics(诊断信息),所以组合键是 [d]d。如果你只想关注错误而忽略提示和警告,可以使用 [e]e (error)。类似地,[w]w 快捷键仅在警告 (warning) 之间导航。

如果你正在编辑的语言启用了拼写检查,或者你用 <Space>us 明确启用了它,可以使用 [s]s (spell) 跳转到拼写错误处。当我开始写这本书时,这让我很困惑,因为我期望 ]d 能带我到拼写错误单词下的波浪线,但它不会。我需要用 ]s

译者注: <Space>us 是 “UI Spellcheck” 或类似命令的快捷键。

最后,如果你在代码中使用 TODOFIXME 注释,你可以使用 [t]t (todo) 在它们之间跳转。

注意,与之前大多数 ][ 快捷键不同,无法将诊断跳转与动词结合使用。所以 d[d 不会删除从当前位置到最近诊断信息处的文本。这(可能)只是 LazyVim 定义快捷键时的一个疏忽。

7.1.5. 跳转到 Git 修订块

这实际上是我最喜欢的方括号对:[h]h 允许你跳转到下一个 git “hunk”(块)。如果你不熟悉这个词(或者如果你来自认为它指代帅哥的那一代),“git hunk”仅仅指文件中包含尚未暂存或提交的修改的部分。

译者注: Git Hunk 是 git diff 输出中的一个基本单元,表示一处连续的更改(添加、删除、修改)。

我的很多编辑工作都涉及在一个大文件中仅在三四个地方进行编辑。例如,我可能在文件顶部添加一个 import,在文件的其他地方向函数调用添加一个参数,并在第三个地方更改接收该参数的函数。一旦开始编辑,我可能需要在这些位置之间来回跳转。]*h*[h 对此非常完美,我不需要记住我的跳转历史或添加命名标记(本质上是书签)来做到这一点。

更好的是,LazyVim 为你提供了一个简单的视觉指示器,标明文件中哪些行已被修改,这样你就能知道它将要跳转到哪里。看看这个截图:

git hunks dark

图 27. Git Hunk 指示器

在左侧,行号的右边,你可以看到我插入了两行的地方有一个绿色的竖条,我更改了一行的地方有一个橙色的条,还有一个小的红色箭头表示我删除了一行。(它还在我修改引入错误的行的行号左侧显示了一个诊断波浪线和一个红色的圆圈叉)。如果我将光标放在文件顶部并输入 ]*h* 三次,我将在那三个地方之间跳转。

与诊断信息类似,[h]h 不能与动词结合使用。

7.2. 文本对象

将动词与动作结合非常有用,但将这些相同的动词与对象(objects)而不是动作结合通常更有帮助。Vim 自带了几个常见的对象,例如单词、句子以及括号内的内容。LazyVim 添加了一大堆其他的文本对象。

译者注: 文本对象 (Text Objects) 是 Vim 中一个极其强大的概念。它们允许你对结构化的文本块(如一个单词、一个句子、引号内的内容、括号内的内容、一个函数、一个 XML 标签等)进行操作,而无需精确地将光标移动到它们的开始和结束位置。

对象的语法是 <动词><上下文><对象>。动词是你已经学过的用于处理动作的相同动词,所以它们可以是 dcgU 等。

上下文(context)总是 ai。如你所知,这是从普通模式进入插入模式的两个命令。但是如果你已经输入了一个动词,如 dc,你严格来说已经不在普通模式了!

你处于所谓的“操作符待决模式”(Operator Pending Mode)。你熟悉的导航按键通常在操作符待决模式下也是允许的,这就是你可以在动词后执行动作的真正原因。但是如果插件维护者忽略了定义操作符待决的键映射,你就会遇到可以导航但不能执行动词的情况。

译者注: 操作符待决模式 (Operator-Pending Mode) 是当你输入一个操作符(动词,如 d, c, y)后,Vim 等待你输入一个动作或文本对象来确定操作范围时的状态。

在操作符之后切换到插入模式没有意义,所以 ai 按键意味着完全不同的事情。通常,你可以将它们视为 around(周围)和 inside(内部)(尽管在我脑海里我总是直接念作“a”和“in”)。区别在于 a 操作倾向于选择 inside 选择的所有内容加上一点取决于所定义对象的周围上下文。

例如,一个常见的对象是圆括号:(。如果你输入命令 di(,你将删除一对匹配的圆括号内的所有文本。但如果你输入 da(,你将删除圆括号内的所有文本以及两端的 ()

译者注:

  • i (inner): 选择对象内部的内容,不包括边界(如括号、引号)。
  • a (around): 选择对象周围的内容,包括边界。

要查看 LazyVim 中许多可能的文本对象列表,请键入 da 并暂停。我看到的是这样:

operator pending dark

图 28. 操作符待决菜单

接下来让我们详细介绍其中的大部分。

7.2.1. 文本化对象

操作符 wsp 用于对整个单词、句子或段落执行操作,定义如前:单词是连续的非标点符号字符,句子是任何以 .?! 结尾的内容,段落是由两个换行符分隔的任何内容。

译者注:

  • w: word (单词)
  • s: sentence (句子)
  • p: paragraph (段落)

aroundinside 上下文对于这些对象的区别在于是否也影响周围的空白。

例如,考虑以下文本片段,并想象我的光标当前位于第二句中单词 handful 中间的 | 字符处:

列表 18. 一个纯文本段落

This snippet contains a bunch of words. There are a hand|ful of
sentences.

And two paragraphs.

如果我想在那个位置删除单词 handful,我可以输入 bde 跳转到单词后面,然后删除到单词末尾。或者我可以使用 inside word 文本对象并输入 diw

译者注: diw = delete inner word (删除内部单词)。

无论哪种方式,我最终都会在 aof 之间留下一个额外的空格,因为 diw 是在单词 inside,不触及周围的空白。

如果我改为输入 daw,它将删除该单词和一个周围的空格字符,因此之后 aof 之间只有一个空格,一切都能正确对齐。

译者注: daw = delete around word (删除单词及其周围空格)。

还有一个 W(大写)操作符,其含义与按词导航时的大写 W 类似:它将删除两个空白字符之间的所有内容,而不是将标点符号解释为单词边界。

类似地,我可以从同一光标位置使用 disdas 来删除整个“There are a handful of sentences.”句子。前者不会触及 The 之前或 . 之后的任何空白,而后者会正确地同步空白。

译者注: dis/das = delete inner/around sentence。

最后,我可以用 dipdap 删除整个段落。区别在于,前一种情况下,被删除段落后的空行仍然存在,但在 around 模式下,它将移除多余的空行。

译者注: dip/dap = delete inner/around paragraph。

通常,当我用 c 动词更改单词、句子或段落时,我使用 i,因为我想用其他需要周围空白的东西替换它。但是当我用 d 删除文本对象时,我使用 a,因为我不打算替换它,所以我希望空白表现得好像那个对象从未存在过一样。

7.2.2. 引号和括号

对象 "' 操作由双引号、单引号或反引号包围的文本字符串。如果你使用命令 ci",你最终会处于两个引号之间的插入模式,字符串内部的所有内容都被移除。然而,如果你使用 da",它将连同引号一起删除。

译者注:

  • di" / da": delete inner/around double quotes.
  • di' / da': delete inner/around single quotes.
  • di``` / da``` : delete inner/around backticks.

作为快捷方式,你可以使用字母 q 作为文本对象,LazyVim 会找出最近的引号,无论是单引号、双引号还是反引号,并删除该对象。我个人不使用这个,但我猜它在处理双引号时可以节省一次按键。

类似地,如果你想将动词应用于包含在圆括号或花括号、尖括号、方括号中的整个块,你只需键入其中一个括号字符。考虑这些例子:di[da(ci{ca<。与引号一样,i 版本将保持周围的括号完整,而 a 版本将删除整个内容。

译者注:

  • di( / da(: delete inner/around parentheses (). (也可用 di) / da))
  • di{ / da{: delete inner/around curly braces {}. (也可用 di} / da})
  • di[ / da[: delete inner/around square brackets []. (也可用 di] / da])
  • di< / da<: delete inner/around angle brackets <>. (也可用 di> / da>)

选择最近的封闭括号或圆括号类型的快捷方式是 b 对象。(助记符是“bracket”括号)。

译者注: dib/dab (delete inner/around block) 通常指 (){} 块,具体取决于上下文或插件配置。

这些实际上可以与计数一起工作,所以如果你想,可以删除“第三层包围的花括号”而不是“最近的包围花括号”。不过,我总是记不住把计数放在哪里!如果你的记性比我好,语法是将计数放在 ai 之前。所以例如,d2a{ 将删除第二层最近的花括号集合内的所有内容。我不确定这是否说得通,所以这里有一个可视化示例:

列表 19. 一个奇怪的小类

class Foo {
    function bar() {
       let obj = {fizz: 'buzz'} // <- 光标在此
    }
}

如果我的光标在 fizz'buzz' 之间的冒号上,那么你可以预期以下效果:

  • di{ 将删除 fizz: 'buzz' 但保留周围的花括号。
  • c2i{ 将移除整个 let obj = ... 行,并将我的光标留在定义函数体的花括号内的插入模式。
  • c2a{ 会做同样的事情,但会移除那些花括号,所以我只剩下一个没有函数体的 function bar()
  • d3i{ 将移除整个函数,留给我一个空的 Foo 类。

你也可以删除某些标点符号之间的内容。例如,ci*ca_ 对于替换 Markdown 文件中标记为粗体或斜体的文本内容很有用。

如果你想对整个缓冲区进行操作,请使用 agig 文本对象。所以 cag 是清除所有内容并重新开始的最快方法,而 yig 将复制缓冲区,以便你可以将其粘贴到 pastebin 或聊天机器人中。g 可能看起来是个奇怪的选择,但它与 ggG 跳转到文件开头或结尾的事实具有对称性。如果你需要一个助记符,可以把 yig 想成“yank in global”(全局复制)。

7.2.3. 语言特性

LazyVim 添加了一些有用的操作符,用于对整个函数或类定义、对象以及(在 HTML 和 JSX 中)标签执行命令。总结如下:

  • c: 对 class (类) 或类型执行操作。
  • f: 对 function (函数) 或方法执行操作。
  • o: 对 “object” (对象,这个助记符有点牵强) 执行操作,例如块、循环或条件语句。
  • t: 对类 HTML 的 tag (标签) 执行操作(适用于 JSX)。
  • i: 对 “scope” (作用域) 执行操作,本质上是一个indentation (缩进) 级别(仅当安装了前面提到的 mini.indentscope extra 时可用)。

译者注: 这些通常由 nvim-treesitter-textobjects 或类似插件提供,需要相应的 TreeSitter 语法解析器支持。例如 dif/daf 删除函数内部/周围,dit/dat 删除标签内部/周围。

7.2.4. Git Hunks

还记得我们用 Unimpaired 模式讨论的 git hunks 吗?你同样可以用 h 对象对整个 hunk 执行操作。所以快速撤销一次添加的一种方法就是输入 dih。但你可能不会经常这样做,因为有更好的处理 git 的方法,我们将在第 15 章讨论。

译者注: dih/dah 删除内部/周围的 git hunk。

7.2.5. 下一个和上一个文本对象

如果你已经在想要操作的对象内部,文本对象功能很棒,但 LazyVim (使用名为 mini.ai 的插件) 进行了配置,使得你甚至可以对仅靠近你光标位置的对象进行操作。

译者注: mini.ai 插件增强了 ai,允许它们在光标不在对象内部时,智能地选择光标之前或之后的文本对象。

安装后,可以通过在你想要访问的对象前加上 ln 来访问下一个和上一个文本对象。(译者注:原文描述可能有误,mini.ai 通常是自动寻找,不需要额外前缀。作者可能描述的是另一个插件或自定义行为。标准 Vim 或常见 textobj 插件没有 ln 前缀。)

译者注: 修正/澄清: 经过查证,mini.ai 插件本身并不使用 ln 前缀来选择下一个/上一个对象。它的核心功能是让 a/i 即使光标不在对象内部也能工作(智能选择附近的对象)。选择下一个/上一个特定类型的文本对象通常需要不同的插件或映射,例如 nvim-treesitter-textobjects 可能提供类似 ]m/[m (下一个/上一个方法) 或 ]f/[f (下一个/上一个函数) 的动作,但这些是动作,不是文本对象选择器。原文作者可能混淆了概念或描述了非标准行为。以下保留原文的例子,但读者需注意其准确性。

再次考虑 Foo.bar Javascript 类:

列表 20. 那个奇怪的类又来了

class Foo {
    function bar() { // <- 光标在此行的 '{' 上
       let obj = {fizz: 'buzz'}
    }
}

如果我的光标在 function bar 行的 { 上,我可以输入 cin{ 来删除 fizz: 'buzz' 对象的内容并将光标置于插入模式。我只需一个额外的 n 按键就能省去整个导航过程。(译者注:如上所述,cin{ 在光标位于外层 { 时,标准行为或 mini.ai 行为通常是删除 let obj = {fizz: 'buzz'}。要删除内层 {} 的内容,需要将光标移动到内层或使用更精确的 textobj 插件功能。n 前缀不是标准用法。)我觉得这是一个非常巧妙的功能,但我往往会忘记它的存在……希望在这里写下它能帮助我记住!

7.3. 寻找周围对象

那个给了我们 Seek 模式的 flash.nvim 插件还有另一个绝活:文本对象的圣杯。在指定一个动词后,你可以使用 S 键(不需要 ia)来看到光标周围主要代码对象周围出现的一堆成对的标签。

译者注: 这是 flash.nvim 提供的对象选择功能,通过 S 触发。

举个例子,我将再次依赖那个 Foo 类。我把光标放在 : 上,然后输入 cS。该插件识别出光标周围的各种对象,并在每个对象的两端放置标签:

seek object dark

图 29. Seek 周围对象

这张图片中的标签是绿色的,并且(通常)按从“最内层”到“最外层”的字母顺序排列。与 Seek 模式的主要区别在于每个标签都是成对出现的;有两个 a 标签,两个 b 标签,依此类推。文本对象就是那些标签之间的任何内容。

如果我接下来按的字符是 a(或按 Enter 接受默认值),那么我将更改定义 obj 的花括号内的所有内容。如果我按 b,它也会替换那些花括号。按 c 将更改整个赋值语句,d 将更改函数的内容。按 e 也会替换花括号,f 则更改整个函数定义。g 标签是类的内容,而 h 则更改整个类。

当你需要更改、删除或复制一个不直接映射到任何其他对象的复杂结构时,这是一个非常有用的工具。

7.3.1. 远程寻找周围对象

S 操作符待决模式对于操作光标周围的对象很有用,但如果你的光标当前不在你想要选择的对象内部,它就不够用了。你可以使用 s 导航到对象内部,然后用 S 选择它,但你可以通过改用 R 操作符来节省几次按键。

译者注: R 是 flash.nvim 提供的远程对象选择功能。

助记符为“Remote”(远程)的 R 容易使用,但难以解释。它是一个操作符待决操作,所以你需要先输入一个动词,然后是 R(与 S 一样,不需要 ia)。

此时,LazyVim 本质上处于 Seek 模式,所以你可以输入搜索字符串的几个字符来查找屏幕上任何地方的匹配项。然而,flash.nvim 不会在你搜索的字符串的任何匹配项处显示单个标签,而是会自动切换到周围对象模式,并显示包围匹配位置的所有结构的成对标签。

锦上添花的是,你也可以在不使用环绕模式的情况下对任何类型的对象执行远程 seek。在这种情况下,你需要输入一个动词,后跟一个小写 r(它仍然表示“remote”)。这也会让你进入 Seek 模式,你可以开始输入匹配的字符。单个标签(普通的 Seek 模式,而不是环绕 Seek 模式)会弹出,你可以输入一个字符将光标临时移动到该标签,就像普通的 Seek 模式一样。但是当你的光标到达那里时,它会自动再次置于操作符待决模式。所以你现在可以输入任何其他操作符,例如 awi(。一旦操作完成,你的光标将移回进入远程 Seek 模式之前的位置。

作为一个具体的例子,命令 drAth2w 将删除从带有标签 h 的单词 “At” 开始的两个单词,然后将你的光标跳回到你开始删除之前的位置。换句话说,它与命令 sAthd2w<Control-o> 相同,后者将 seek 到标签 h 处的单词 “At”,然后删除两个单词,并使用 Control-o 跳回到你之前的历史记录位置。远程命令稍微短一些,但这是我倾向于忘记使用的另一个命令。我的大脑在弄清楚“删除”模式之前就进入了“移动光标”模式,所以当我意识到我本可以远程完成它时,已经太晚了。

7.4. 操作包围对

我们已经看到了用于操作引号或括号对内部内容的文本对象,但是如果你想保留内容但更改包围对呢?

也许你想把双引号字符串如 "hello world" 改成单引号的 'hello world'。或者你正在把 obj.get(some_variable) 方法查找改成 obj[some_variable] 索引查找,需要把包围的圆括号改成方括号。

LazyVim 自带了 mini.surround 插件来实现这种行为,但它默认没有安装。它是一个推荐的 extra,所以如果你按照我的建议安装了所有推荐插件,你可能已经有了它。

译者注: mini.surround 插件提供了添加、删除、替换包围字符(如括号、引号、标签)的功能,类似于经典的 vim-surround 插件。

7.4.1. 添加包围对

添加包围对的默认动词是 gsa。这会将你的编辑器置于操作符待决模式,现在你必须输入动作或文本对象来覆盖你想要用某物包围的文本。完成该对象的输入后,你需要输入你想要用其包围的字符,例如 "()。后两者的区别在于,虽然两者都会用圆括号包围文本,但 ( 还会圆括号内放入额外的空格。

译者注: gsmini.surround 的默认前缀。gsa = Go Surround Add。

这听起来可能很复杂,但在看到一些例子后应该就明白了:

  • gsai[( 将选择方括号内的内容(使用 i[)并在方括号内放置由空格分隔的圆括号。所以如果你开始时有 [foo bar] 并输入 gsai[(, 你最终会得到 [( foo bar )]
  • gsai[) 做同样的事情,只是没有添加空格,所以同样的 [foo bar] 会变成 [(foo bar)]
  • gsaa[) 将把圆括号放在方括号外面,因为你用 a[ 而不是 i[ 进行选择。所以这次,我们的例子变成了 ([foo bar])
  • gsa$" 将用双引号包围当前光标位置和行尾之间的所有文本。
  • gsaSb' 将用单引号包围你在 S 操作后用标签 b 选择的文本对象。
  • gsaraa3e* (译者注:这个例子非常复杂且可能不准确) 将用星号包围远程对象(r),该对象以 a 开头(a),被标记为 aa),后跟接下来的三个单词(3e),星号放在这三个单词的两端。

根据上下文,可能需要输入很多字符,但这通常比独立导航到并更改对的两端所需的字符要少。

7.4.2. 删除包围对

删除一对稍微容易一些,因为你不需要指定文本对象。只需使用 gsd 后跟你想要移除的任何一对的指示符。

译者注: gsd = Go Surround Delete。

所以如果你想删除光标周围的 [],你可以使用 gsd[

如果你想删除深度嵌套的元素,你需要把计数放在 gsd 动词之前。所以使用 2gsd{ 来删除当前光标位置之外的第二层花括号。例如,如果你的光标在字符串 {abc {def}}def 内部,输入 2gsd{ 会得到 abc {def},保留了 def 周围的“内层”花括号,但移除了整个内容周围的第二层外层花括号。

7.4.3. 替换包围对

替换与删除类似,只是动词是 gsr,并且你需要输入现有字符之后输入你想要替换成的新字符。

译者注: gsr = Go Surround Replace。

所以如果你有文本 "hello world" 并且光标在其中,你可以使用 gsr"' 将双引号更改为单引号:'hello world'

7.4.4. 导航包围字符

对包围对或对的全部内容执行操作很方便,但有时你只想将光标移动到对的开头或结尾。你通常可以使用 Seek 模式、Find 模式或 Unimpaired 模式命令(如 [( )来做到这一点,但如果你需要,还有其他更具语法意识的命令。

最简单的一个已经内置于 Vim 很长时间了。如果你的光标当前位于圆括号、方括号或花括号对的开始或结束字符上,只需按 % 即可跳转到另一端的配对项。如果你在普通模式下不在配对字符上使用 %,它将跳转到最近的封闭类配对对象。但这只适用于括号,因此不包括引号等任意配对。

mini.pairs 插件(译者注:应为 mini.ai 或 mini.pairs,mini.surround 主要负责修改)带有 gsfgsF 快捷键,可用于将光标移动到相关字符。我不使用这些,因为 mini.ai 插件使用 g[g] 快捷方式提供了类似的功能。这两个快捷方式都需要后跟一个字符类型,例如 g[( 将跳回最近的包围开圆括号,g]] 将跳到最近的闭方括号。如果你给它一个计数,它将跳出那么多层的包围对。

译者注: mini.ai 提供了 g[g] 用于跳转到当前文本对象(由 a/i 自动选择的那个)的开始和结束边界。

7.4.5. 高亮包围字符

如果你只是需要再次检查包围字符的位置,你可以使用类似 gsh( 的命令,其中 h 表示“highlight”(高亮)。这有时可以作为使用计数的删除或替换操作的预演,这样你就可以再次检查你操作的是你认为的那些对。

译者注: gsh = Go Surround Highlight。

7.4.6. 额外功能:XML 或 HTML 标签

mini.surround 插件主要用于处理成对的字符,但它也可以操作类 html 的标签。

假设你有一段文本,想用 p 标签包围它。串联起命令 gsaapt。即 gsa 表示“添加包围”,后跟 ap 表示“around paragraph”(段落周围)。所以我们要在段落周围添加一些东西。我们不说要添加的是引号或括号,而是说要添加的是 t,代表tag(标签)。

mini.surround 会理解你想要添加一个标签,并弹出一个小提示窗口让你输入想要添加的标签。输入你想要创建的标签 p。你不需要尖括号;只需要标签名:

surround tag dark

图 30. 用标签包围

如果你想要添加的标签有属性,你可以将它们添加到提示符中。mini.surround 很聪明,知道属性只放在开标签上。

surround tag attrs dark

图 31. 包围标签属性

7.4.7. 修改快捷键

我喜欢 mini.surround 的行为。我经常使用它。以至于我很快就厌倦了重复输入 gs。我决定用 ; 替换 gs,这样我就可以输入 ;d;r 来代替 gsdgsr。对于添加包围,我决定利用双击按键容易输入的特点,所以我用 ;; 来代替 gsa 甚至 ;a

为了让这个正常工作,我还必须修改 flash.nvim 配置以移除 ; 命令。(默认情况下,; 键可以用作 ft 键的“查找下一个”行为,但 flash 的设计方式使得你不需要一个单独的键来实现它;只需再次按 ft 即可)。

如果你想做同样的事情,只需在 config/plugins 目录下创建一个新的 Lua 文件,命名为你想要的任何名称(我的是 extend-mini-surround.lua)。

文件的内容将是:

列表 21. 配置 Mini.surround 和 Flash

-- lua/plugins/extend-mini-surround.lua
return {
  -- 配置 mini.surround
  {
    "echasnovski/mini.surround",
    opts = {
      -- 自定义快捷键映射
      mappings = {
        add = ";;",         -- 添加: ;;
        delete = ";d",        -- 删除: ;d
        find = ";f",          -- 查找右边: ;f
        find_left = ";F",     -- 查找左边: ;F
        highlight = ";h",     -- 高亮: ;h
        replace = ";r",       -- 替换: ;r
        update_n_lines = ";n", -- 更新 N 行: ;n (这个功能可能不常用)
      },
    },
  },

  -- 配置 flash.nvim,移除默认的 ';' 和 ',' 查找重复键
  {
    "folke/flash.nvim",
    opts = function(_, opts) -- 使用函数形式修改 opts
      -- 保留默认模式设置,但只保留 f, F, t, T 键
      opts.modes = opts.modes or {}
      opts.modes.char = opts.modes.char or {}
      opts.modes.char.keys = { "f", "F", "t", "T" }
      -- 确保移除了 ',' (如果 flash 默认包含的话)
    end,
  },
}

因为我们正在修改两个插件,我把两个 Lua 表放在一个包装的 Lua 表里面,Lazy.nvim 很聪明,能将其解析为多个插件定义。第一个将 mappings 传递给传入 mini.surround 的 opts。这些将替换 LazyVim 为该表定义的默认快捷键(那些以 gs 开头的)。

第二个定义也传递了一个自定义的 opts 表。它用一个只定义了 fFtT 的新表替换了默认的 keys(包括 ;,)。(译者注:使用函数形式修改 opts 更安全,避免完全覆盖其他 flash 配置)。

如果我知道 ; 被 flash.nvim 重新绑定了,我可以通过阅读 LazyVim 网站 上 flash.nvim 的配置并查看需要覆盖什么来找到这个解决方案。然而,我当时没能弄清楚 ; 在哪里被定义了,最后在 LazyVim GitHub Discussions 上寻求了帮助。那里的人们真的很有帮助,如果你有任何问题,我鼓励你去打个招呼。

7.5. 总结

在本章中,我们学习了 LazyVim 通过其对 Unimpaired 模式的重新实现提供的一系列高级代码动作技术。然后我们学习了什么是文本对象,并快速了解了 LazyVim 提供的许多文本对象。

接着我们介绍了非常有用的 S 动作,它可以用来动态选择文本对象,以及 Rr 的远程变体。

最后我们介绍了几种新的动词,可用于处理成对的包围字符,如圆括号、方括号和引号。

在下一章中,我们将介绍剪贴板交互和寄存器,以及一个全新的可用于文本选择的可视模式。