# Vim 是一门编辑文本的语言

> source: 文章/vim-language/index.md

# Vim 是一门编辑文本的语言

说起 Vim，大多数人先想到的是一个梗：进去了就出不来。`:q` 是很多人学会的第一个、也是唯一一个 Vim 命令。

这篇文章想讲的是梗后面的东西。Vim 的按键不是一袋快捷键，而是一门**小语言**：有动词，有名词，有数量词；句子可以重复，可以录下来变成函数，可以对整个文件的每一行执行。学会这门语言之后，编辑文本就不再是「把光标挪过去、选中、按删除」，而是**说出你想要的变换**。

文章里的灰框都是能按的 Vim。点一下框里的文字，就可以直接用键盘输入；手机上会弹出键盘，`Esc` 有一个单独的按钮。它背后是一个在浏览器里运行的小 Vim，每一种行为都和 Neovim 逐键对照过。

<!-- fold -->

## 一、先活下来：模式

打开 Vim，按下 `x`，屏幕上不会出现一个 x，光标下的字却消失了。这是第一个让人困惑的地方：**Vim 打开时处在普通模式（Normal mode），这时每个键都是命令，不是字。**

想打字，先按 `i`（insert），进入插入模式，这时它就和普通的编辑器一样了。打完按 `Esc`，回到普通模式。

| 键 | 做什么 |
|---|---|
| `i` | 在光标前插入 |
| `a` | 在光标后插入（append） |
| `I` / `A` | 在行首 / 行尾插入 |
| `o` / `O` | 在下面 / 上面开一个新行 |
| `Esc` | 回到普通模式 |

为什么要这么麻烦？因为写程序、写文章的时候，大部分时间并不是在打新字，而是在**读、跳、改**。既然改得比写得多，那就让「改」成为默认状态，让每个键都派上用场。

试一试：在 Hello 后面加一个逗号。`e` 跳到这个词的末尾，`a` 在它后面插入，打一个逗号，再按 `Esc`。

```vim-try
@title 加一个逗号
@keys ea,<Esc>
@goal Hello, world
Hello world
```

框下面会记下你按了几个键。在普通的编辑器里，这件事要先用鼠标点到 o 和空格之间；这里四个键就够了，手也不用离开键盘。

## 二、移动：从一格到一个词

最基本的移动是 `h` `j` `k` `l`：左、下、上、右。这四个键挨在一起，就在右手底下。

但一格一格走太慢了。Vim 的移动是按**文本的结构**走的：

| 键 | 去哪里 |
|---|---|
| `w` / `b` | 下一个词的开头 / 上一个词的开头 |
| `e` | 这个词的末尾 |
| `0` / `^` / `$` | 行首 / 第一个非空字符 / 行尾 |
| `f`字符 | 本行下一个这个字符（find） |
| `t`字符 | 走到这个字符前面一格（till） |
| `;` / `,` | 重复上一次的 `f` `t`，往前 / 往后 |
| `gg` / `G` | 文件开头 / 结尾 |
| `}` / `{` | 下一段 / 上一段 |

看一遍它们怎么走。按「播放」，或者一次按一个键：

```vim-play
@title 按结构移动
@keys wwbe$0f,;;
calculate(alpha, beta, gamma)
```

`w` 停在 `(` 上，是因为在 Vim 眼里，字母数字是一类，标点是另一类，两类之间就是词的边界。`f,` 跳到第一个逗号，`;` 再跳到下一个。

## 三、动词加名词

现在到了 Vim 最关键的一步。普通模式里有几个键是**动词**（Vim 叫它 operator）：

| 动词 | 意思 |
|---|---|
| `d` | 删除（delete） |
| `c` | 改（change）：删掉，然后进入插入模式 |
| `y` | 复制（yank） |
| `gU` / `gu` | 变大写 / 变小写 |

动词单独按下去什么也不会发生，它在等一个**名词**：说明作用在哪一段文本上。上一节的每一个移动都可以当名词用：

- `dw`：删除到下一个词的开头
- `d$`：删到行尾
- `dt)`：删到 `)` 前面
- `cw`：把这个词改掉
- `y}`：复制到这一段结束

再加上**数量词**：`3w` 是往后走三个词，`d3w` 就是删掉三个词。

```vim-try
@title 删掉中间三个词
@keys wd3w
@goal one five six
one two three four five six
```

这里有一个乘法。如果有 5 个动词、20 个移动，你记住的是 25 个词，得到的却是 100 句话。而且以后每学会一个新的移动，它自动就能和所有动词组合；每学会一个新动词，它自动就能作用在所有移动上。普通编辑器的快捷键做不到这一点：`Ctrl+Shift+K` 删一行，但没有办法让它「删三个词」。

两个常用的缩写：动词按两遍表示整行。`dd` 删除这一行，`yy` 复制这一行，`cc` 改掉这一行。`p` 把复制或删掉的东西贴到光标后面，所以 `ddp` 是把这一行和下一行交换。

## 四、文本对象：不管光标在哪

`dw` 有一个问题：它从光标开始删。如果光标在一个词的中间，删掉的只是后半个词。

**文本对象**（text object）解决了这个问题。它不是「从这里到那里」，而是「光标所在的那个东西」：

| 对象 | 是什么 |
|---|---|
| `iw` / `aw` | 一个词 / 一个词连同后面的空格 |
| `i(` / `a(` | 括号里面 / 连同括号 |
| `i"` / `a"` | 引号里面 / 连同引号 |
| `i{` `i[` `i<` | 其他括号，同理 |
| `ip` / `ap` | 一段 / 一段连同后面的空行 |

`i` 是 inner（里面），`a` 是 around（连同边界）。这就是这门语言里最常用的一组句子：`diw` 删掉这个词，`ci"` 改掉引号里的内容，`da(` 连括号一起删掉。

下面这张表可以随便组合。光标停在 world 的中间：先点一个动词，再点一个对象，看看每一句话的意思。

```vim-grid
@title 动词 × 对象
@at 0 12
@ops d c y gU v
@objs iw aw i" a" i( a(
say("hello world", f(x, y))
```

注意 `i(` 选中的是外面那一层 `say( … )` 里的全部内容：光标在 `"hello world"` 里，而这段文字在 `say` 的括号里，不在 `f` 的括号里。Vim 按括号的实际嵌套关系来找。

两道练习。第一道，把函数的参数全部换成 `x`：

```vim-try
@title 换掉所有参数
@keys ci(x<Esc>
@goal calculate(x)
calculate(alpha, beta, gamma)
```

光标在行首，并不在括号里面。较新的 Vim 和 Neovim 会往后找这一行里的下一对括号，所以 `ci(` 照样能用。

第二道，改掉引号里的字：

```vim-try
@title 改一句提示
@keys ci"new<Esc>
@goal print("new")
print("old message")
```

## 五、点号：把一句话再说一遍

`.` 是 Vim 里最不起眼、也最有用的键：**重复上一次修改**。

「上一次修改」指的是一整句话：`ciwX<Esc>` 是一句，`A,<Esc>` 也是一句。所以 `.` 重复的不是最后一个键，而是你最后说的那句话。

配合搜索就更方便了。`*` 搜索光标下的这个词，`n` 跳到下一处。下面这道题：把所有的 red 换成 X。

```vim-try
@title 一处改了，其余的用 .
@keys *ciwX<Esc>n.n.
@goal X green X blue X
red green red blue red
```

参考答案的节奏是：`*` 找到下一个 red，`ciwX<Esc>` 改掉它，然后 `n.` `n.`：跳过去，再说一遍。改错了用 `u` 撤销，`Ctrl-r` 重做。

再来一道：给每一行末尾加一个逗号。`A,<Esc>` 改好第一行，然后 `j.` 两次。

```vim-try
@title 每行加逗号
@keys A,<Esc>j.j.
@goal apple 3,
@goal pear 10,
@goal fig 7,
apple 3
pear 10
fig 7
```

这就是为什么 Vim 用户在意「用一句话完成一次修改」：一句话就能重复，几个零碎的动作就不能。

## 六、组合起来：像写程序一样编辑

到这里，其实已经可以把 Vim 当作一门编程语言来看了：

| 编程语言 | Vim |
|---|---|
| 一次函数调用 | 一句命令，比如 `ci"` |
| 再调用一次 | `.` |
| 循环 | 数量词，`3.` |
| 定义一个函数 | 录一个宏 |
| 对列表里每一项做同一件事 | `:g` 和 `:normal` |
| 表达式 | `:s` 替换里的 `\=` |

### 宏：把按键录下来

`qa` 开始录制，之后按的每个键都存进寄存器 `a`；再按 `q` 停止。`@a` 把这串键重放一遍，`@@` 重放上一次的宏，`2@a` 重放两遍。

宏就是一个函数：它接受「光标所在的位置」作为参数，然后执行一段固定的编辑。写宏的诀窍是**让它结束在下一次开始的地方**，比如最后按一个 `j` 走到下一行。

下面把每一行改成 JSON 的写法。在第一行录宏：`qa`，`I"<Esc>` 在行首加引号，`ea":<Esc>` 在词尾加引号和冒号，`A,<Esc>` 在行末加逗号，`j` 下一行，`q` 停止。然后 `2@a`。

```vim-try
@title 录一个宏，用三次
@keys qaI"<Esc>ea":<Esc>A,<Esc>jq2@a
@goal "apple": 3,
@goal "pear": 10,
@goal "fig": 7,
apple 3
pear 10
fig 7
```

如果不知道有多少行呢？那就给一个很大的数，`99@a`。宏里任何一步失败（比如已经到了最后一行，`j` 走不动了），整个宏就会停下来。**失败就是循环的终止条件。**

```vim-play
@title 99@a 在最后一行自己停下
@keys qadwjq99@a
apple 3
pear 10
fig 7
```

还有一点：宏存在寄存器里，寄存器里就是普通的文本。`"ap` 可以把宏贴出来，像改代码一样改它，再存回去。在 Vim 里，**操作本身也是可以编辑的数据**。

### :g：对每一行做同一件事

按 `:` 进入命令行。这里的命令来自 Vim 的前身 ex 编辑器，它们作用在**行**上，前面可以加范围：`%` 是整个文件，`2,5` 是第 2 到第 5 行，`.` 是当前行。

`:g/模式/命令` 的意思是：对每一个匹配这个模式的行，执行这条命令。`:v` 反过来，对不匹配的行执行。

```vim-try
@title 删掉所有 TODO 行
@keys :g/TODO/d<CR>
@goal keep a
@goal keep c
@goal keep e
keep a
TODO fix b
keep c
TODO fix d
keep e
```

`:g` 后面可以接任何命令，包括 `:normal`：把一串普通模式的按键当成命令，在每一行上执行一遍。`:%normal A;` 就是给每一行末尾加分号。这正好是函数式编程里的 `map`。

### :s 和 \=：在替换里算数

`:s/模式/替换/` 是替换，加 `g` 替换一行里的所有匹配，前面加 `%` 作用到整个文件。替换部分如果用 `\=` 开头，后面就是一个**表达式**，`submatch(0)` 是匹配到的文本。

下面这道题：把每个数量翻倍。模式 `\d\+` 匹配一串数字。

```vim-try
@title 数量翻倍
@keys :%s/\d\+/\=submatch(0)*2/<CR>
@goal apple 6
@goal pear 20
@goal fig 14
apple 3
pear 10
fig 7
```

最后一道题，把前面学过的东西放在一起。没有唯一的答案，试着用尽量少的键：

```vim-try
@title 综合练习
@keys :g/TODO/d<CR>ggci"new<Esc>:%s/3/6/<CR>
@goal name = "new"
@goal size = 6
TODO remove me
name = "old"
TODO remove me too
size = 3
```

## 七、设计哲学

回头看，Vim 的设计可以归结为几个判断。

**编辑比书写多。** 所以默认是普通模式，打字反而要先说一声 `i`。模式不是负担，而是把整个键盘都变成了命令。

**一门语言，而不是一组快捷键。** 动词、名词、数量词彼此正交，学会 n 个动词和 m 个名词，就有 n × m 句话可说。新东西加进来，会自动和已有的东西组合。

**可以重复的才是好的修改。** `.`、数量词、宏，都建立在「一次修改是一句完整的话」上面。Vim 鼓励你用能重复的方式说话，而不是用一串零碎的动作把事情凑出来。

**操作也是数据。** 宏是寄存器里的文本，`:normal` 把按键当作命令，`:g` 把命令当作对每一行执行的函数。一串按键就是一段程序，人能读，也能写。

**手不离开主键区。** 这一点有它的历史。vi 是 Bill Joy 在 1976 年写的，当时他用的终端是 ADM-3A：这台终端的 `H` `J` `K` `L` 键上就印着方向箭头，`Esc` 键在今天 `Tab` 键的位置，伸手就能按到。网络很慢，每一个键都要省，所以命令都很短。Vim 是 Bram Moolenaar 在 1991 年发布的 vi 的改进版，名字原本是 Vi IMitation，后来改成 Vi IMproved。

**文本有结构。** 词、句子、段落、括号、引号，Vim 都把它们当作可以直接点名的东西，而不是一串需要用鼠标框出来的字符。

## 八、和现代编辑器差在哪

今天的大多数编辑器，比如 VS Code，用的是另一种模型。

**先选中，再操作。** 用鼠标或者 `Shift` 加方向键选中一段，再按删除、输入或者快捷键。这是「名词 → 动词」的语序，好处是你总能先看到自己要动的是哪一块。Vim 是「动词 → 名词」，说出来就执行。有意思的是，后来的 Kakoune 和 Helix 两个编辑器，保留了 Vim 的语言，却把语序换了回来：先选，再动词，兼顾了「看得见」和「能组合」。

**快捷键是一袋按钮。** `Ctrl+D` 选中下一个相同的词，`Alt+↑` 把这一行往上移，`Ctrl+Shift+K` 删一行。每个功能一个快捷键，彼此不能组合，于是功能越多，要记的组合键就越多。Vim 的词汇量小得多，句子却多得多。

**多光标和宏。** 现代编辑器处理「同样的修改做很多次」，用的是多光标：所有地方同时改，改的过程看得见。Vim 用的是 `.`、宏和 `:g`：先在一处说清楚，再让它在别处重复。多光标要求所有目标都在屏幕上、都能选中；宏可以跑过一万行，也可以在条件不满足时自己停下。

**鼠标和菜单。** 现代编辑器让你不用学就能用，这是真正的优点。Vim 的代价是开头的那道门槛：先要学会模式，学会第一批动词和名词。但门槛过去之后，你的能力随着学会的东西相乘，而不是相加。

所以 Vim 并没有过时，它的思想已经散到别处去了：几乎每一个主流编辑器都有 Vim 模式；浏览器里有用 `j` `k` `f` 浏览网页的插件；这个网站本身，在文章里按 `j` `k` 走段落、`yap` 复制一段、`V` 选中几段，用的也是同一套语法。

而在 AI 也来写代码、改文本的今天，「一串按键就是一段程序」又多了一层意思：一句 `:g/TODO/d` 人能读懂，机器也能生成，这比描述「在第 3 行点一下、拖到第 5 行」要清楚得多。

<!-- fold -->

## 再往下学

- 在终端里输入 `vimtutor`，有一个大约半小时的交互教程。
- 在 Vim 里输入 `:help`，然后 `:help text-objects`、`:help motion.txt`、`:help :g`，这些文档写得非常好。
- 每次发现自己在重复按同样的键，就停下来想一想：这句话能不能说得更短，能不能用 `.` 重复？

最后，退出是 `:q`。
