学习使用 Vim&Neovim 的 text-object
引言
你可能不知道什么是 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,因为它跟编程语言的语法有关系
查看对应的 commit 了解文件变更,以及我是如何组织配置文件的
你的文件组织方式可能跟我不同,但我想下面的内容理解起来没有什么困难 :)
如果你也用的是 lazy.nvim 的话,只需要在插件列表里面追加
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
文件添加如下的配置(大部分代码被我省略了,完整文件在这里)
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,使用下来非常方便🍺