学习使用 Vim&Neovim 的 text-object

你可能不知道什么是 text-object,但我相信你可能已经在使用了只是你自己没有意识到。比如,在写代码的时候,我们经常想要修改函数调用的入参。比如我们在下面这段代码中,想要修改成 bar(3, 2, 1),而你的光标停留在 () 里面

python

def foo():
    ...
    res = bar(1, 2, 3)
                # ^
                # The position of your cursor(on `,`)
    ...

你会如何修改呢?如果你用 Vim/Neovim 已经很熟练了,你多半会不假思索地敲下 ci) 或者 ci(,然后就可以快速敲下新的入参了。这里的 i( 或者 i) 就是所谓的 text-object 了

警告
不好的做法:用 F( 找到左括号,然后用 x 一个字符一个字符删除
技巧
:h text-objects 查看更多帮助信息

简单来说,text-object 是文本对象,对应一块区域的文本,而且这一块文本包含结构在里面,所以它们可以当成一个 text-object 来处理,给很多操作带来方便

Vim/Neovim 自带的 text-object 包含两种情况,由两个字母组成:

  1. 第一个字母要么是 i 要么是 a
    1. 简单来说,i 指的是 text-object 本身,而 a 则还包含了下一个/上一个空格或者是成对字符
    2. 记忆i = (i)nsidea = (a)round
  2. 第二个字母有两种情况
    1. 成对字符中的一个
      1. 成对字符:{}, (), [], '', "", <tag></tag>,其中 <tag></tag>t 代指,比如删除 <tag>foobar</tag> 里面的 foobar 你只需要将光标放在 tag 里面并按下 dit
      2. e.g. a{ = (around) {,选中 {} 内的所有东西(包括 {}
    2. (w)ord, (s)entences, (p)aragraphs
      1. 每一种都可以和 i 或者 a 搭配使用,所以会有 iw, aw, is, as, ip, ap 这几种

前面我们提到,text-object 对应的文本包含一定的结构,那么什么文本天然有结构信息呢?那就是代码,代码是高度结构化的文本,受到对应语法的严格限制

在写代码的时候,我们常常想要直接删掉整个函数体重写。如果函数体是用 {} 括起来的,那么通常我们可以利用 ci{ 完成这个任务,只是需要注意光标所在的位置,要确保函数体的 {}最靠近光标所在位置的,这有点恼人

示例

Rust 代码的函数体就是用 {} 括起来的,所以删掉函数体我们可以考虑将光标停在 ifi 上,然后用 ci{ 就好了

rust

fn fib(n: i32) -> i32 {
 // The position of your cursor(on `i`)
 // v
    if n == 1 || n == 2 {
        1
    } else {
        fib(n - 1) + fib(n-2)
    }
}

但如果函数体并不是用 {} 括起来的呢?比如 Python 代码,此时 ci{ 就不能用了

python

def fib(n: int):
    if n == 1 or n == 2:
        return 1

    return fib(n - 1) + fib(n - 2)

考虑到代码是高度结构化的,是否存在某个特殊的 text-object 让我们可以选中函数体并整个删除呢?这正是 nvim-treesitter-textobjects 要做的事情,它是基于 nvim-treesitter 的 Lua 插件。它通过对代码的 AST 的分析,帮我们定义了函数体、循环体等 text object,也叫做 syntax aware text object,因为它跟编程语言的语法有关系

技巧

查看对应的 commit 了解文件变更,以及我是如何组织配置文件的

你的文件组织方式可能跟我不同,但我想下面的内容理解起来没有什么困难 :)

如果你也用的是 lazy.nvim 的话,只需要在插件列表里面追加

lua

require("lazy").setup({
    ...
	{
		"nvim-treesitter/nvim-treesitter-textobjects",
		dependencies = "nvim-treesitter/nvim-treesitter",
		config = function()
			require("config.nvim-treesitter-textobjects")
		end,
	},
    ...
})

config 目录下新增 nvim-treesitter-textobjects.lua 文件添加如下的配置(大部分代码被我省略了,完整文件在这里

lua

local is_ok, configs = pcall(require, "nvim-treesitter.configs")
if not is_ok then
	return
end

configs.setup({
	textobjects = {
		select = {
            ...
			keymaps = {
				-- outer: outer part
				-- inner: inner part
				["af"] = "@function.outer",
				["if"] = "@function.inner",
				["ac"] = "@class.outer",
				["ic"] = "@class.inner",
				["al"] = "@loop.outer",
				["il"] = "@loop.inner",
			},
			include_surrounding_whitespace = true,
		},
	},
    ...
})

根据我的个人需要,我设置了 6 种 text-object,分别是 af, if, ac, ic, al, il,分别用于处理函数、类和循环,在官方仓库的 README 里你可以看到更多支持的 syntax aware text-object

接下来我们就可以用 dif 方便地在函数体里的任意位置删掉整个函数体了(只要确保你的光标在函数体内),还是方便很多的!

text-object 将文本组织成对象,我们可以方便地对齐进行操作。在 nvim-treesitter-textobjects 的加持下,编程语言的各种结构也可以被当成是 text-object,使用下来非常方便🍺