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 了
不好的做法:用 F( 找到左括号,然后用 x 一个字符一个字符删除
自带的 text-object
用 :h text-objects 查看更多帮助信息
简单来说,text-object 是文本对象,对应一块区域的文本,而且这一块文本包含结构在里面,所以它们可以当成一个 text-object 来处理,给很多操作带来方便
Vim/Neovim 自带的 text-object 包含两种情况,由两个字母组成:
- 第一个字母要么是
i要么是a- 简单来说,
i指的是 text-object 本身,而a则还包含了下一个/上一个空格或者是成对字符 - 记忆:
i = (i)nside,a = (a)round
- 简单来说,
- 第二个字母有两种情况
- 成对字符中的一个
- 成对字符:
{}, (), [], '', "", <tag></tag>,其中<tag></tag>用t代指,比如删除<tag>foobar</tag>里面的foobar你只需要将光标放在 tag 里面并按下dit - e.g.
a{=(around) {,选中{}内的所有东西(包括{})
- 成对字符:
- (w)ord, (s)entences, (p)aragraphs:每一种都可以和
i或者a搭配使用,所以会有iw, aw, is, as, ip, ap这几种
- 成对字符中的一个
扩展 text-object
前面我们提到,text-object 对应的文本包含一定的结构,那么什么文本天然有结构信息呢?那就是代码,代码是高度结构化的文本,受到对应语法的严格限制
在写代码的时候,我们常常想要直接删掉整个函数体重写。如果函数体是用 {} 括起来的,那么通常我们可以利用 ci{ 完成这个任务,只是需要注意光标所在的位置,要确保函数体的 {} 是最靠近光标所在位置的,这有点恼人
Rust 代码的函数体就是用 {} 括起来的,所以删掉函数体我们可以考虑将光标停在 if 的 i 上,然后用 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,因为它跟编程语言的语法有关系
如果你想要看如何使用 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" },
})
如果你对这里的 @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,使用下来非常方便🍺