14-其他编辑技巧 - Some-soda

14-其他编辑技巧

第 14 章 其他编辑技巧

在我们深入探讨 LazyVim 启用的一些更“类似 IDE”的行为之前,我想收集一些能让你的编辑生活更有趣的技巧。本章有点像个大杂烩,包含了一些我无法放在其他地方的命令和插件。

14.1. 字数统计

使用 g<Control-g> 可以输出一条包含有关当前光标位置的一些有用信息:

word count dark

图 73. 字数统计

最值得注意的是,“Word 110 of 3179”(第 110 个单词,共 3179 个)告诉我这一章有超过 3000 个单词(显然我在写了更多单词后更新了这一节!)

14.2. 调换字符

你有多经常因为打字太快而意外地调换了两个字符?

只需使用 xp 将一个字符与其右侧的字符交换。例如,如果你输入了 ra 而本意是 ar,将光标放在 r 上并按下 xp

这不是一个特殊的自定义命令。它只是使用了默认的“删除字符” (x) 和“将最后删除的内容放在光标之后” (p) 命令,将字符从当前位置移动到下一个位置。你可以用类似的想法移动其他文本。例如,用 dwwP 移动一个单词,或者用 daWp 删除一个参数并将其移动到函数签名中的后面位置。

14.3. 注释和取消注释代码

LazyVim 自带一个用于在旧版 Neovim 中注释和取消注释代码的插件,但从 Neovim 0.10 开始,这变成了一个原生的 Neovim 功能。

切换注释的动词是 gc,后面可以跟一个动作或文本对象。所以 gc5j 将注释当前行及其下五行,而 gcap 将注释掉由换行符分隔的整个块。

译者注: gc (go comment/comment toggle)。

这个命令与 S 命令完美搭配,用于注释掉周围的文本对象。例如 gcSh 将在调用 S 后注释掉由 h 标签包围的函数。

要注释掉单行,请使用易于输入的快捷键 gcc。这个命令可以接受计数,所以 5gcc 将注释掉五行(比 gc4j 输入起来稍微容易一点)。

与大多数动词一样,gc 也可以应用于可视选择,例如 V5jgc

gc 动词实际上是一个切换开关,所以如果一行当前已被注释,它将取消注释而不是再次注释它。因此,gccgcc 是一个空操作。但是请注意,如果你的选择包含已注释和未注释的行,你最终会得到双重注释。这通常是你想要的:如果你临时注释掉一个包含其他注释的块,当你取消注释该块时,你可能希望原始注释保持注释状态。

作为快捷方式,如果你想在当前行的上方或下方添加一个新的注释行,而不是注释当前行,你可以使用 gcOgco。严格来说这是一个新的动词,但为了便于记忆,可以将其视为将 gc 与打开新行的动词(oO)结合起来。

14.4. 递增和递减数字

如果你的光标当前在普通模式下的一个数字上,你可以使用 Control-a 来增加该数字。这个命令相当智能,如果你的数字需要新的位数,它会做“正确”的事情。所以当你按 Control-a 在数字的任何位置时,9 变成 1099 变成 100

要递减数字,请使用 Control-x

译者注: Ctrl-a (add), Ctrl-x (好像没什么好的助记词),记不住就相信肌肉记忆吧,我也记不住。

我曾一度非常讨厌这两个快捷键,因为它们只是偶尔有用,但在它们有用的时候,我又记不住它们。所以我花了很长时间手动增加数字,并心里想着“我需要查一下那些数字递增命令”,但与这个帮助部分相关的唯一关键词就是快捷键本身!

最终我了解了 :helpgrep 命令,它允许你搜索帮助文档。早在我记住快捷键之前,我就记得 :helpgrep Adding and subtracting (搜索“加减”) 可以帮助我查到它们。

但实际上这些快捷键有一个助记符:Control-a 是“Add”(增加),这很容易记住。Control-x 有点难,但既然你有了 Control-a,你就能用 :help CTRL-a 查到它。我不确定这对其他人是否有帮助,但我把 x 想象成“划掉(‘cross’ out)一个数字来减去”。

使用 g<Control-a>g<Control-x> 可以在连续行上递减数字,并且每行在计数的基础上额外增加。当你在处理编号列表时,这很有用。假设你想创建一个包含 10 个项目的列表。首先输入 o1.<esc> 创建一行写着 1.。然后输入 9. 重复该命令 9 次。现在你有了:

列表 40. 一个傻列表

1.
1.
1.
1.
1.
1.
1.
1.
1.
1.

你可以使用 V'[ 来选择刚刚插入的 9 行,因为 '[ 标记是上次更改文本的第一个字符。(译者注:'] 标记是最后一个字符,可能更有用,或者直接用 V9j)。现在输入 g<Control-a> 来递增它们,你最终得到:

列表 41. 一个更聪明的列表

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

对于仅仅几个看起来很奇怪的按键来说还不错:o1.<Esc>9.V'[g<Control-a>!(译者注:如上,选择部分可能有更优方式)。

如果你需要在列表的中间插入一个新的条目,添加该条目,选择包含剩余条目的行,然后按 Control-a 使它们同步。

Neovim 会智能地只递增它在行上遇到的第一个数字。这意味着很容易例如操作一本书的大纲,即使它包含多个数字。考虑这个虚构的、与本书大不相同的书的大纲:

列表 42. 一些书的章节

Chapter 1: Intro and Install
Chapter 2: 1 Weird modal editing trick
Chapter 3: The numbered marks 1-9
Chapter 4: Navigating things
...

假设我想把第 1 章分成两个不同的章节:“Intro”和“Install”。我可以简单地使用普通的文本插入来添加新章节,像这样:

列表 43. 添加一个新章节

Chapter 1: Intro
Chapter 2: Install
Chapter 2: 1 Weird modal editing trick
Chapter 3: The numbered marks 1-9
Chapter 4: Navigating things
...

然后我可以使用 <Shift-V>} 来选择最初编号为 2 及以上的所有章节。当我按下 Control-a 时,章节号会递增,但 1 Weird trick 中的 1 不会受到影响,编号标记指示符也不会。

列表 44. 同步数字

Chapter 1: Intro
Chapter 2: Install
Chapter 3: 1 Weird modal editing trick
Chapter 4: The numbered marks 1-9
Chapter 5: Navigating things
...

14.4.1. Dial.nvim Extra

如果递增和递减快捷键听起来有点像那种一个月才有用一次的奇怪厨房小工具,你可能想考虑安装来自 :LazyExtraseditor.dial extra。

这个 extra 安装了 dial.nvim 插件,它允许你递增和递减许多其他很酷的东西。我主要用它来切换布尔表达式(Control-aControl-x 都会将 true 切换为 false,反之亦然),但它也可以递增单词(“first”递增为“second”)、月份(“December”递增为“January”)、版本号、Markdown 标题等等。如果你需要,甚至可以用你自己的模式来扩展它。

14.5. 更改缩进

>< 快捷键可以在普通模式下用来增加或减少文本缩进。最常见的是,你会将它们成对使用(如 <<>>)来更改当前行的缩进。然而,你也可以更改任何动作范围的缩进。另一个常见用法是 >Sx,用某个标签 x 来缩进一个 treesitter 实体,而 >ap 将缩进由空行分隔的整个段落。

译者注: > (indent), < (dedent)。

当涉及到使用计数时,这些动词可能会有点令人困惑。你可能期望 2>> 将当前行缩进两个级别,但实际上,它会将两行缩进一个级别。

当你想在一个命令中更改多个缩进级别时,你需要求助于可视模式。将当前行缩进五个缩进宽度,最快的方法是用 v5>,相比之下输入十个大于号要慢得多。这适用于任何可视选择,所以你可以用例如 va{5> 将整个块缩进五个级别。

通常,你只想“使缩进对于这门编程语言是正确的”。如果 conform.nvim 配置正确,最简单的方法就是保存文件。LazyVim 默认启用了保存时格式化,如果它能找到格式化器,就会使用它。你也可以使用 gq 配合动作或选择(最常见的是 gqag 格式化整个文件)来应用格式化。

然而,如果你不想保存,或者没有使用 conform.nvim,你也可以使用 = 动词。= 的行为有点取决于编程语言,但它通常会将缩进引擎应用于可视选择(或动作选择)的行,就好像你按了 enter 开始一个新行一样。最终结果是所有行都将按照某种“正确”的定义被“正确地”缩进。

译者注: = 通常用于自动调整选中行的缩进。

你也可以在不离开插入模式的情况下调整缩进。Control-tControl-d 快捷键将在插入文本时增加和减少当前行的缩进。助记符是“add tab”(添加制表符)和“dedent”(减少缩进)。

14.6. 重新排版文本 (Reflowing Text)

我在写这本书的时候经常使用 gw 命令。它有效地在八十字限制处(或任何标尺数字,可用 :set textwidth=<数字> 配置)重新包裹 (w 代表 wrap) 所有文本,而不断开单词。

译者注: gw<动作> 用于根据 textwidth 设置重新格式化指定范围的文本。

最常见的是,我使用 gww 来重新包裹当前行,使其在适当的位置换行,或者 gwip 来重新包裹整个段落。但 gw 可以与任何动作或可视选择一起工作。要重新包裹整个文件,请使用 gwig

这个命令在很大程度上依赖于换行符的存在。实际上,任何两个连续的行都将被连接成单行(如果它们能容纳在 80 个字符内)。对我来说,这意味着如果我忘记在标题后放一个换行符,我的第一个段落就会和标题连在一起,这显然不是我想要的。

14.7. 通过外部程序过滤

你也可以将文本通过管道传递给任何遵循良好 Unix 规范的外部程序:即在 STDIN 上处理输入并将其输出到 STDOUT 的程序。为此,在可视模式下选择你想要通过管道传递的文本。然后输入 !。这将打开命令窗口,并将可视选择作为范围传入,是 :'<,'>! 的快捷方式。然后输入路径上的命令,选定的文本将被该命令的输出替换。

译者注: ! (感叹号) 在可视模式或与范围一起使用时,允许将选定的文本作为标准输入传递给外部命令,并用该命令的标准输出来替换选定的文本。

以下是一些例子,假设安装了一些常见的 Unix 工具:

  • !grep -v a 将用相同的文本替换选择内容,但任何包含字母“a”的行将被移除。
  • !tr -s ' ' 将调用 translate 命令,将所有多个空格的实例替换为单个空格。
  • !jq 将用 jq 格式化 json 文本。
  • !pandoc -f markdown -t html 是通过从更简单的 Markdown 语法开始快速编写 HTML 的便捷方式。
  • !./my-custom-script 将通过你编写的任意脚本传递命令。
  • !python ./something.py 将通过你编写的 Python 脚本传递命令。

如果你想运行一个命令而不修改文本,不要提供范围。例如,:!mkdir foo 将运行 mkdir 命令而不会覆盖你的文件内容。

我认为这个功能没有被大量使用是很可惜的。许多内置于 Neovim 或作为插件提供的功能,同样可以很容易地成为处理管道输入和输出的 CLI 程序。仅举一例,Neovim 自带的 :sort 命令在我看来只是增加了编辑器的臃肿,而 !sort 同样可以很好地运行外部的 sort 工具。

14.8. 拼写检查

你可以使用 <Space>us 启用或禁用拼写检查。启用后,拼写检查器无法识别的错误会像诊断信息一样用波浪线标出。但你必须使用 [s]s 在拼写错误之间跳转,而不是使用诊断快捷键 [d]d

要让 Vim 给出单词的拼写建议,请使用 z=。这几乎是你所能想到的最难记的了,所以把它写下来。如果你能记住它在 z 菜单而不是 Space 菜单中,你至少可以在菜单中再次找到它。拼写建议会弹出一个带编号的菜单;输入一个数字将单词替换为该拼写。

14.9. 插入模式快捷键

如果你在插入模式,并且想在返回插入模式之前执行单个普通模式操作,你可以使用 ctrl-o。执行一个普通模式命令,你会立即回到插入模式。我不太明白这个的意义,因为 Control-o<command> 增加了两次按键,<Escape><command>i 也是如此。

在插入模式下,如果你按下 Control-a,它将插入你在上一个插入模式会话中插入的任何文本。这类似于访问 ". 寄存器。

要在插入模式下访问其他寄存器,请使用 Control-r。这将弹出寄存器菜单,你可以通过按相应的键插入任何这些寄存器。所以在插入模式下的 Control-a 类似于 Control-r.。要从剪贴板插入,请使用 Control-r 然后按 +

CTRL-U 快捷键将移除当前行上自你进入插入模式以来添加的所有字符。所以在单行编辑中,它类似于撤销操作,但如果你的插入包含了 <Enter>,撤销将只作用于一行。

有些人喜欢在插入模式下绑定到一个不常见的字符序列。最常见的建议是将 jk 绑定到 Escape,或者将 ;; 绑定到 Control-O,但你可以做任何你喜欢的组合。前者允许你在不按 EscapeControl 的情况下切换到普通模式,后者允许你临时执行单个普通模式操作并返回到插入模式。它们在按键次数上并没有变少,但它们是容易按到的键。

如果你想探索这个,打开你的 keymaps.lua 文件并添加以下行:

列表 45. 插入模式键映射

-- lua/config/keymaps.lua
-- 将 jk 映射为 Escape (退出插入模式)
vim.keymap.set("i", "jk", "<Esc>", { desc = "普通模式" })
-- 将 ;; 映射为 Ctrl-o (执行一次普通模式命令)
vim.keymap.set("i", ";;", "<C-o>", { desc = "普通模式单次操作" })
  • 注意

    如果你喜欢用 jk 操作离开插入模式,max397574/better-escape.nvim 插件将消除每次你在插入模式下按 j 时发生的延迟。

这里重要的是第一个参数是 "i"。这告诉 Neovim 键映射应该在插入模式而不是普通模式 ("n") 下发生。你也可以使用 "o" 表示操作符待决模式,v 表示可视和选择模式,等等。

在普通文本和编码中,; 键很少后跟除 <Space><Enter> 之外的任何字符,所以它很适合用作各种插入模式操作的前缀。

不过,不要用这种技术来将一个文本序列扩展成另一个不同的文本序列。为此,你最好使用缩写或片段,这是接下来两节的主题。

14.10. 缩写 (以及文件类型配置)

Vim 缩写自编辑器早期就已存在。它们是一种简单的方式,可以在不离开插入模式的情况下,让“快捷”词语扩展成完全不同的东西。

要创建一个临时缩写,只需使用命令 :iabbr <快捷词> <扩展内容>。你可以使用 Vim 的快捷键语法在扩展内容中表示特殊字符,如 <Enter><Tab>。你甚至可以使用例如 <Left> 在缩写文本内重新定位光标。

译者注: :iabbr (insert mode abbreviation)。

例如,考虑这个命令:

列表 46. 缩写命令

:iabbr ifmain if __name__ == "__main__":<Enter>main()<Left>

它将在插入模式下输入 ifmain<Space> 时,将文本扩展为以下内容,并将光标放在 main 后面的括号内:

列表 47. 建议的 If Main 扩展

if __name__ == "__main__":
    main( )

iabbr 中的 i 表示它将在插入模式下工作,abbr 是“abbreviate”(缩写)的缩写。

请注意,我不必在 Enter 后显式添加任何缩进,因为 Python 缩进引擎会为我处理。还要注意,我在 ifmain 后输入的 <Space> 被插入到了括号之间。如果你需要扩展一个缩写而不添加空格,请改用 Control-] 快捷键来触发扩展。

如果你需要插入单词 ifmain 而不扩展它们,请键入 ifmain<Escape> 返回到普通模式而不进行扩展。

这个缩写只在我关闭编辑器之前存在。要使其永久生效,我需要将其添加到我的 LazyVim 配置中。通常,缩写只在单一文件类型的上下文中才有意义,所以我将它们收集在 autocmds.lua 中,使用类似这样的语法:

列表 48. If Main 缩写

-- lua/config/autocmds.lua
-- 创建一个自动命令组 (可选但推荐)
local abbr_group = vim.api.nvim_create_augroup("Abbreviations", { clear = true })

-- 为 Python 文件类型创建自动命令
vim.api.nvim_create_autocmd("FileType", {
  group = abbr_group, -- 将自动命令添加到组中
  pattern = { "python" }, -- 匹配的文件类型
  callback = function()
    -- 定义 Python 相关的缩写
    vim.cmd('iabbr ifmain if __name__ == "__main__":<CR>main()<Left>') -- 使用 <CR> 代表 Enter
    vim.cmd("iabbr frang for i in range():<CR><Esc>F(i")
    -- 其他 Python 缩写...
  end,
})

-- 为其他文件类型添加更多自动命令...
-- vim.api.nvim_create_autocmd("FileType", {
--   group = abbr_group,
--   pattern = { "javascript", "typescript" },
--   callback = function()
--     vim.cmd('iabbr clog console.log()<Left>')
--   end,
-- })

译者注: 使用 vim.api 创建自动命令是 Neovim 中更现代、更推荐的方式。使用自动命令组 (augroup) 可以更好地管理自动命令,防止重复定义。<CR><Enter> 在 Vim 命令中的标准表示。

frang 缩写展示了另一个巧妙的技巧:你可以使用字符串 <Esc> 进入普通模式并移动光标。我使用了 F( 来“查找上一个左括号”,然后用 irange() 括号内进入插入模式。

Vim 缩写已经存在很久了,并且效果很好。我仍然使用它们(可能因为我老了),但世界在很大程度上已经转向使用代码片段 (snippets) 了。

14.11. 代码片段 (Snippets)

LazyVim 自带 cmp-nvim-lsp 插件 (译者注:原文是 blink.cmp,但 LazyVim 默认使用 nvim-cmp 及其相关源,cmp-nvim-lspcmp-vsnipcmp-luasnip 等处理补全和片段),提供了我们之前见过的高速补全界面。在其他补全项中,它连接到 Neovim 0.10+ 的内置代码片段功能 (译者注:Neovim 本身不内置片段引擎,需要插件如 vim-vsnipLuaSnip)。它可以加载 VS Code 风格的代码片段

默认情况下,nvim-cmp (译者注:替代 blink.cmp) 会在你输入时弹出一个简单的菜单,包含一堆补全项。例如,如果我在 Python 文件中输入 if,我看到的是这样:

blink cmp dark

图 74. Cmp 菜单 if

列表显示了可能的补全项。我可以用箭头键或 Control-nControl-p 在列表中上下移动光标(jk 在这里不起作用,因为我仍在插入模式)。大多数补全项旁边会弹出一个预览框,包含文档或补全示例。

我暂时禁用了 LSP 以隐藏此截图中非片段的结果。

这个片段是由 FriendlySnippets 插件创建的,这是一个随 LazyVim 一起提供的大量有用片段的集合。(另请注意,有一个 ifmain 片段,很像我上面显然实际上不需要定义的那个缩写!)

如果我然后按下 Control-y 键,它会确认一个补全(或者如果你使用 LazyVim 默认设置,则按 Enter;或者如果你像我一样配置了 nvim-cmp,则按 右箭头),该片段就会插入到我的编辑器中:

snippet inserted dark

图 75. 插入的片段

编辑器当前处于“选择”模式 (Select mode),这是一种不常见的模式,表面上类似于可视模式。在 LazyVim 的默认配置中,除了接受一个片段之外,我不知道有任何其他方法可以进入选择模式!所以我们不会在片段上下文之外详细讨论这个模式。

关键点是“condition”当前被高亮显示,我可以立即开始输入来覆盖它,几乎就像我在插入模式一样。一旦条件被替换,我可以按 <Tab> 键,在选择模式下,这表示“跳转到片段中的下一个字段”。现在 if 内部的 pass 被高亮显示了。

<Tab> 键只有在片段引擎(如 LuaSnipvim-vsnip)处于一个有字段的片段中时才会这样工作。

14.11.1. 定义新片段

如果 FriendlySnippets 的片段对你来说还不够,你可以使用现在无处不在的 VS Code 片段语法定义你自己的片段,并在你的片段引擎中加载它们。作为一个快速示例,以下是如何为一个样板 Svelte 组件创建片段:

  1. 如果目录不存在,创建 ~/.config/nvim/snippets/ 来存放你的片段。这是 nvim-cmp (通过其片段源插件) 查找片段的默认位置之一。
  2. 如果 ~/.config/nvim/snippets/package.json 文件不存在,则创建它。它需要包含所有片段文件的列表。在这种情况下,我们将添加 svelte:

列表 49. 片段 package.json

{
  "name": "personal-snippets",
  "contributes": {
    "snippets": [
      {
        "language": ["svelte"], // 可以是数组,支持多种语言
        "path": "./svelte.json"
      }
      // 添加更多语言和路径...
    ]
  }
}

可以通过打开该类型的文件并输入 :set ft? 命令来找到给定文件类型的语言名称。

  1. 创建一个与路径匹配的 json 文件,即 svelte.json。给它以下内容:

列表 50. 片段定义

{
  "Boilerplate Component": {
    "prefix": "scri", // 触发词
    "description": "Basic svelte boilerplate", // 描述
    "body": [ // 片段主体,数组的每个元素是一行
      "<script lang=\"ts\">",
      "\t$1", // $1 是第一个制表位 (tab stop)
      "</script>",
      "",
      "${2:<div></div>}", // $2 是第二个制表位,带有默认占位符文本
      "",
      "<style>",
      "\t$3", // $3 是第三个制表位
      "</style>"
    ]
  }
}

如果你不熟悉 VS Code 片段语法:

  • prefix 是你在插入模式下输入以触发片段的字符串。在这种情况下,它是 scri
  • description 是在预览窗格中描述它的字符串。
  • body 是片段中行的列表。
  • $1$2$3 代表片段中的“制表位”(tab stops)。
  • ${2:<div></div>} 代表一个带有可以被覆盖的占位符内容的制表位。

如果我重启 Neovim 并加载一个 svelte 文件,我可以输入 scri 来插入这个片段。默认输出看起来像这样:

列表 51. 片段输出

<script lang="ts">
  $1
</script>

<div></div>

<style>
  $3
</style>

译者注: 光标会首先停在 $1 处,你可以输入内容,然后按 Tab (通常需要配置 nvim-cmp 或片段引擎的跳转键) 跳到 $2 (即 <div></div>,可以覆盖),再按 Tab 跳到 $3

14.12. 总结

本章介绍各种编辑技巧,从字数统计和调换字符开始,然后转向管理注释、缩进和格式化。

最后,我们介绍了古老但不过时的缩写语法和 LazyVim 自带的片段引擎。

在下一章中,我们将开始讨论一些完全不同的东西:LazyVim 中的版本控制。