目录

Vim Text Object: 语义化编辑

更新

  • 2026-04-24:修改 nvim-treesitter-textobjects 的插件安装方式为 vim.pack.add(Neovim v0.12 新特性)

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

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

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

Warning

不好的做法:用 F( 找到左括号,然后用 x 一个字符一个字符删除

Tip

: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:每一种都可以和 i 或者 a 搭配使用,所以会有 iw, aw, is, as, ip, ap 这几种

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

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

Example

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

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{ 就不能用了

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,因为它跟编程语言的语法有关系

Tip

如果你想要看如何使用 Neovim v0.12 的 vim.pack 配置插件,那么你可以参考我的这篇文章

如果你已经用上了 Neovim v0.12 的 vim.pack 的话,那么你可以参考我的配置文件。在插件的安装上,使用 vim.pack.add API,内容如下所示

vim.pack.add({
	{ src = "https://github.com/nvim-treesitter/nvim-treesitter" },
	{ src = "https://github.com/nvim-treesitter/nvim-treesitter-textobjects" },
})
Tip

如果你对这里的 @function.outer 这样的写法感到陌生,可以参考我的另外一篇文章,里面谈到了 Tree-sitter 以及它的 Query 是如何工作的,这里其实是 Query 的写法

完整配置放在这会太长,我这里只放跟本篇文章最相关的部分

vim.keymap.set({ "x", "o" }, "af", function()
	require("nvim-treesitter-textobjects.select").select_textobject("@function.outer", "textobjects")
end)
vim.keymap.set({ "x", "o" }, "if", function()
	require("nvim-treesitter-textobjects.select").select_textobject("@function.inner", "textobjects")
end)
vim.keymap.set({ "x", "o" }, "ac", function()
	require("nvim-treesitter-textobjects.select").select_textobject("@class.outer", "textobjects")
end)
vim.keymap.set({ "x", "o" }, "ic", function()
	require("nvim-treesitter-textobjects.select").select_textobject("@class.inner", "textobjects")
end)

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

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

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