5月27日 15:06

Vim 的文本对象怎么用?

为什么你一直在用 dw 而不是文本对象

很多 Vim 用户学了 dw 删除单词、dd 删除行,就觉得够用了。但当你需要删除一对括号里的内容、改写一个 HTML 标签内的文字、或者复制整个段落时,还在用 dw 一个个删就太慢了。文本对象(Text Object)是 Vim 里最被低估的特性——它让你不移动光标就能对"语义单元"做操作,而不只是对"字符位置"做操作。

i 和 a:文本对象的核心逻辑

所有文本对象都遵循一个模式:操作符 + i/a + 对象标识

  • i = inner,选"内部",不包含分隔符
  • a = a/around,选"一个",包含分隔符(有时还包含周围空格)

举一个最直观的例子,假设光标在 hello 上:

shell
say hello world
  • diwsay world(只删单词,留两个空格)
  • dawsay world(删单词加后面空格,句子依然通顺)

记住这个区别,后面所有文本对象都是同一套逻辑。

单词:iw / aw / iW / aW

iw 选中当前单词(不含空格),aw 选中当前单词加相邻空格。

wW 的区别在于分词规则:

  • w 按标点和空白分词,hello-world 是三个单词
  • W 只按空白分词,hello-world 是一个整体

实际场景:代码里改变量名用 ciw,删除函数参数用 daw

vim
" 光标在 foo 上 const result = foo + bar ciw→ 输入 baz const result = baz + bar

句子:is / as

is 选中当前句子内部,as 选中句子含后面的空格。Vim 以 .!? 后跟空格或换行来识别句子边界。

日常编辑中用得不算多,但在写文档或 Markdown 时,cis 可以快速重写一句话,yas 可以复制整句去引用。

段落:ip / ap

ip 选中当前段落(不含前后空行),ap 选中段落加上下面的空行。

段落在 Vim 中的定义是:由空行分隔的连续非空行。这在编辑 Markdown、邮件、纯文本文档时特别好用:

  • vip 可视选中当前段落
  • yap 复制整段
  • dap 删除整段(包含段间空行,删除后不会多出空行)
  • gqip 对当前段落重新排版

括号家族:i( / a( / i{ / a{ / i[ / a[

这是写代码用得最多的一组。所有成对括号都支持 i/a 变体:

文本对象选中范围典型用法
i(i)括号内内容,不含括号ci( 改写函数参数
a(a)包含括号本身da( 删除整个括号及内容
i{i}花括号内内容vi{ 选中函数体
a{a}包含花括号本身ya{ 复制整个块
i[i]方括号内内容ci[ 改写数组元素
a[a]包含方括号本身da[ 删除整个数组

注意:光标不需要在括号上,只要在括号包围的范围内即可。所以你可以站在函数体中间,直接 ci( 改写参数列表。

引号家族:i" / a" / i' / a' / i / a

和括号类似,引号也有完整的 i/a 支持:

文本对象选中范围典型用法
i"双引号内内容ci" 改写字符串值
a"包含双引号da" 删除整个字符串
i'单引号内内容ci" 改写字符
a'包含单引号ya' 复制含引号
`i``反引号内内容`yi`` 复制模板字符串内容
`a``包含反引号`da`` 删除模板字符串

前端开发时 ci" 改写属性值、`ci`` 改写模板字符串内容,是最高频的操作之一。

HTML 标签:it / at

it 选中标签内部内容,at 选中包含标签本身。

html
<p class="intro">Hello Vim</p>
  • 光标在 Hello 上,dit<p class="intro"></p>(删内容留空标签)
  • 光标在 Hello 上,dat → 整行消失(连标签一起删)
  • cit → 删内容并进入插入模式,直接输入新内容

这对编辑 HTML、XML、JSX/Vue 模板非常实用。配合 vat 先选中标签块再操作,比手动找开闭标签快得多。

与操作符组合:真正的威力所在

文本对象单独用(如 iw)没有意义,它必须和操作符组合才能发挥作用。常见的操作符:

  • d delete:diw 删单词、dib 删括号内、dit 删标签内
  • c change:ciw 改单词、ci" 改字符串、cip 改段落
  • y yank:yiw 复制单词、yi{ 复制花括号内容
  • v visual:viw 选中单词、vip 选中段落
  • > / < 缩进:>ip 缩进段落、>a{ 缩进整个块
  • gU / gu 大小写:gUiw 单词转大写

也可以加数字前缀:2daw 删除两个单词,3ip 选中三段。

一些高频组合速查:

shell
ciw — 改写当前单词 ci" — 改写双引号内字符串 ci( — 改写括号内参数 cit — 改写 HTML 标签内容 diw — 删除当前单词 dib — 删除括号内内容(b 等同于 (dat — 删除整个 HTML 标签 yiw — 复制当前单词 yi{ — 复制花括号内内容 vip — 选中当前段落

自定义文本对象与 vim-textobj-user

Vim 内置的文本对象已经覆盖了大部分场景,但你可能会想定义自己的——比如选中函数名、选中注释块、选中 CamelCase 的某个部分。

vim-textobj-user 是 Kana Natsuno 写的插件,提供了一个声明式的 API 来创建自定义文本对象,不用手写复杂的 onoremap 映射。

最简单的用法——用正则定义一个文本对象:

vim
call textobj#user#plugin('line', { \ '-': { \ 'select-i': 'il', \ 'select-a': 'al', \ 'pattern': '.* ', \ }, \ })

这样就有了 il(行内)和 al(含换行)两个文本对象,可以用 vildal 等操作。

社区基于 vim-textobj-user 构建了大量插件,常用的有:

  • vim-textobj-commentic/ac 选中注释块
  • vim-textobj-functionif/af 选中函数体
  • vim-textobj-entireie/ae 选中整个文件
  • vim-textobj-indentii/ai 选中同缩进层
  • vim-textobj-anyblock:自动匹配最近的括号/引号对

如果你用 Neovim,还可以通过 Lua 和 treesitter 定义更智能的文本对象(如 @function.inner@class.outer),这是另一个话题了。

常见实战场景

改函数参数:光标在参数列表中间,ci( 清空参数重新输入。

改字符串值:光标在字符串上,ci" 删值进插入模式,不用移光标到引号里。

删 HTML 标签但保留内容dat 会连标签一起删。如果只想删标签保留内容,可以 vat 选中后手动删标签行,或者用 vim-surround 插件的 dst(delete surrounding tag)。

复制整个代码块yi{ 复制花括号内所有内容,比 V{ 手动选快得多。

重排段落:写 Markdown 时 gqip 对当前段落自动换行重排。

批量缩进>ap 缩进当前段落,>a{ 缩进整个代码块。

在可视化模式下确认范围:不确定文本对象会选中什么?先用 vi( 看看高亮范围,确认后再换成 di(ci(

从今天开始用

如果你之前只用 dwdd,从这几个操作开始:

  1. ciw — 改写当前单词(日常最频繁)
  2. ci" — 改写字符串内容
  3. ci( — 改写括号内容
  4. dit — 删除标签内容
  5. vip — 选中当前段落

不用一次全记,先把 ciw 用熟,你会发现越来越不想回到逐字符操作了。文本对象的本质就是:告诉 Vim 你要操作"什么",而不是"从哪到哪"。

标签:Vim