Vim 实战:从编辑语言到工程导航

为什么重新整理这份笔记

以前的 Vim 笔记是边学边记的:寄存器在一篇、宏在另一篇,搜索、分屏和格式化又散落在几十条技巧里。单条命令虽然能查到,真正编辑时却很难回答两个问题:这个命令适合什么场景,以及它为什么能和别的命令组合?

这次把 2019、2020、2024 年的通用笔记和寄存器专文合并为一条学习主线,并纠正其中已经过时或不准确的内容。重点不是背快捷键,而是建立一套可以迁移到代码、配置文件和普通文本的编辑方法。

先记住一句话:

Vim 的核心不是“快捷键很多”,而是用“操作符 + 范围 + 重复”描述一次编辑。

如果只想快速开始,可依次阅读“编辑语言 → 寄存器 → 批量修改 → 工程导航”;每章末尾都可以直接在临时文件里练习。

想解决的问题 首选工具 典型命令
改一个有边界的对象 操作符 + 文本对象 ciw、di"、yap
重复刚才的修改 点命令 .
保护已经复制的内容 专用寄存器 "0p、"_d
对很多行做同一种动作 :normal :%normal! A;
重复一串依赖上下文的动作 宏 qa…q、@a、@@
跨文件查找并逐项处理 quickfix :vimgrep、:cnext
把文本交给系统命令 filter / Shell :%!sort -u

1. 模式不是门槛,而是在选择“接下来要表达什么”

Vim 最常用的四种模式分别承担不同角色:

模式 用途 常用入口 返回普通模式
普通模式 Normal 移动、删除、复制、组合命令 启动 Vim 后默认进入 已在其中
插入模式 Insert 输入新文字 i、a、o、O <Esc> 或 <C-[>
可视模式 Visual 先选择范围,再操作 v、V、<C-v> <Esc>
命令行模式 Command-line 保存、替换、搜索、批处理 :、/、? <Esc>

不要长时间停留在插入模式里寻找方向键。完成一小段输入后回到普通模式,下一步便可以继续用“动作”表达意图。

几个入口的区别很实用:

  • i 在光标前插入,a 在光标后插入;
  • o 在下方新建一行,O 在上方新建一行;
  • gi 回到上次离开插入模式的位置继续输入;
  • 插入模式中的 <C-o> 可以临时执行一次普通模式命令,执行后自动回到插入模式。

保存和退出只需先掌握这一组:

命令 含义
:w 保存当前缓冲区
:q 关闭当前窗口;有未保存修改时会拒绝
:wq 或 ZZ 保存并退出
:q! 或 ZQ 放弃当前修改并退出

这里的“窗口”和“文件”不是同一个概念,后文会专门说明。

插入模式也有不依赖插件的补全

Vim 自带的补全足以处理许多轻量场景。进入插入模式后:

命令 补全来源
<C-n> / <C-p> 当前补全来源中的下一个 / 上一个候选
<C-x><C-f> 文件名
<C-x><C-l> 整行
<C-x><C-]> tags
<C-x><C-o> 当前文件类型的 omni completion;需要相应 omnifunc

补全菜单出现后可用 <C-n>、<C-p> 选择,<C-y> 接受,<C-e> 取消。LSP 或补全插件可以提供更丰富的数据源,但这些内置按键仍有助于理解补全菜单的基本行为。

2. 把命令看成一门编辑语言

普通模式下最有价值的结构是:

[次数] + 操作符 + [次数] + 动作或文本对象

常用操作符只有几个:

操作符 含义 示例
d 删除并写入寄存器 dw 删除到下一个单词
c 修改:先删除,再进入插入模式 ciw 修改当前单词
y 复制(yank) yap 复制当前段落
> / < 增加 / 减少缩进 >ip 缩进当前段落
= 按文件类型规则重排缩进 =ap 重排当前段落
gq 按 textwidth 格式化文本 gqap 格式化当前段落

动作描述“走到哪里”,例如 w 到下一个词、$ 到行尾、} 到下一段。文本对象描述“这个结构是什么”:

文本对象 范围 常见组合
iw / aw 单词内部 / 单词及周围空白 ciw、daw
i" / a" 双引号内部 / 连同双引号 di"、ya"
i( / a( 圆括号内部 / 连同圆括号 ci(、ya(
it / at HTML/XML 标签内部 / 连同标签 cit、dat
ip / ap 段落内部 / 连同周围空行 gqip、yap

i 可以理解成 inside,a 可以理解成 around。光标只要位于对象内部,通常不必先移动到对象开头。

使用 ciw 修改一个单词,再用点命令复用相同编辑

动态图 1:ciw 把“修改当前单词”作为一个完整动作;. 在下一个目标上复用这次修改。

点命令为什么重要

. 重复的是“最近一次可重复的修改”,不是最近按下的单个键。例如:

ciwrequest_timeout<Esc>

整段会成为一次修改。之后移动到另一个同类单词按 .,即可再次执行相同修改。它适合“动作相同、目标由自己逐个选择”的任务。

需要特别区分:. 不等于重复宏;重复最近执行的宏应使用 @@。

一分钟练习

新建一个临时文件,输入:

timeout = read_timeout + write_timeout

把光标放到第一个 timeout 内,执行 ciwlimit<Esc>;再用 w 移动到后面的单词,尝试 .。即使这次不需要真的把三个变量改成同名,也要观察“一次修改”的边界。

3. 移动、搜索和跳转:按目标尺度选择命令

不要把所有移动都压在 hjkl 上。文本越大,越应该使用更大尺度的动作。

行内和结构移动

目标 命令
行首 / 第一个非空字符 / 行尾 0 / ^ / $
下一个词首 / 上一个词首 / 当前或下一个词尾 w / b / e
本行下一个字符 x 的位置 / 前一格 fx / tx
重复 / 反向重复最近的 f、t ; / ,
上一个 / 下一个段落 { / }
配对的括号、方括号或花括号 %
文件开头 / 文件末尾 / 第 42 行 gg / G / 42G

G 只是跳到文件末尾,不会增加缩进。若要从当前位置处理到文件末尾,应把它作为范围,例如 >G 增加一级缩进,或 =G 重排缩进。

搜索不是最后一步,也可以成为下一条命令的范围

命令 含义
/pattern / ?pattern 向后 / 向前搜索
n / N 同方向 / 反方向重复搜索
* / # 向后 / 向前搜索光标下的完整单词
:nohlsearch 暂时清除搜索高亮

搜索后可以直接结合点命令:先用 * 找到同名词,再完成一次修改,之后用 n 选目标、. 执行修改。

完整单词在 Vim 正则中可写为:

/\<timeout\>

当表达式中反斜杠太多时,可以用 very magic 模式降低阅读成本:

/\v(user|admin)_[0-9]+

跳回“刚才的位置”

Vim 会记录比浏览器历史更细的导航信息:

需求 命令
沿跳转列表后退 / 前进 <C-o> / <C-i>
跳到更早 / 更新的修改位置 g; / g,
在当前位置建立标记 a ma
跳到标记 a 所在行 / 精确位置 'a / `a
打开光标下的文件名 gf

执行 gf 进入另一个文件后,通常用 <C-o> 回来。df 是“删除到字符 f”,不是文件跳转命令。

4. 寄存器:解决“刚复制的内容为什么被覆盖了”

Vim 的复制、删除和修改都会与寄存器交互。p 默认读取无名寄存器,因此下面的操作经常带来意外:

  1. yy 复制一行;
  2. dd 删除另一行;
  3. p 粘贴时得到刚删除的行,而不是先前复制的行。

这是寄存器设计的结果,不是复制失败。yy 同时更新无名寄存器和复制专用寄存器 0,随后的 dd 会覆盖无名寄存器,但不会覆盖 0,所以此时用 "0p 即可取回最近一次未显式指定寄存器的 yank。

复制后删除导致默认粘贴变化,并用 0 号寄存器取回复制内容

动态图 2:p 读取无名寄存器;"0p 明确读取最近一次 yank。

实用寄存器速查表

寄存器 用途 示例
" 无名寄存器,默认被复制、删除和修改使用 p 等价于 ""p
0 最近一次未显式指定其他寄存器的 yank "0p
1 至 9 最近几次行删除或较大删除的历史队列 "2p
- 最近一次小于一行的删除 "-p
a 至 z 有名寄存器;写入时用大写名称表示追加 "ayiw、"Ayiw
_ 黑洞寄存器,丢弃删除内容 "_dd
+ / * 系统剪贴板 / 选择缓冲区,具体映射因平台而异 "+y、"+p
: / . / % 最近 Ex 命令 / 最近插入文本 / 当前文件名 插入模式中 <C-r>%
= 表达式寄存器 插入模式中 <C-r>=2+3<CR>
/ 最近一次搜索模式 :let @/ = '' 清空它

用下面的命令观察真实状态,比猜测更可靠:

:registers
:registers 0 a +

若 "+y 无效,先检查当前 Vim 是否带剪贴板支持:

:echo has('clipboard')
:version

macOS 的 Cmd-V、Linux 的主选择区以及终端自身的粘贴并不总是等同于 Vim 的 + 寄存器;跨应用复制时明确使用 "+y / "+p,行为更容易推理。

5. 从一次修改扩展到一百次

批量编辑前先判断任务属于哪一种:

任务形状 工具 判断标准
同一修改,目标由人逐个选择 . 每个位置都执行完全相同的修改
同一普通模式动作应用到每行 :normal! 每行可从相同初始条件开始
文本模式可直接描述 :substitute / :global 能写出可靠的搜索模式
多步动作依赖当前行内容 宏 需要移动、编辑、再移动到下一项
目标来自跨文件搜索或编译结果 quickfix + :cdo 已经有一组位置列表

宏:录制的是按键语言

录制到寄存器 a 的基本流程为:

qa      开始录制到寄存器 a
… 完成一条记录的全部操作
q 停止录制
@a 执行宏 a
@@ 重复最近执行的宏
5@a 连续执行五次宏 a

录制并重复一个 Vim 宏,把多行名字转换为带引号的列表项

动态图 3:好的宏从稳定位置开始,并在下一条记录的稳定位置结束。

宏更可靠的三个习惯:

  1. 开始时用 0、^ 或一次搜索把光标放到可预测位置;
  2. 使用 f、t、文本对象或搜索定位,不依赖固定列数;
  3. 让宏末尾移动到下一条记录,便于 5@a 连续执行。

录错后不必全部重来。有名寄存器就是文本,可以执行 :put a 把宏放入缓冲区修改,再用 0"ay$ 以字符方式写回;也可以先查看 :registers a。

:normal!:对一组行执行普通模式命令

给整个文件的每一行末尾加分号:

:%normal! A;

使用 normal 命令批量给多行补分号,再一次撤销整个批处理

动态图 4:% 是全文件范围,normal! 在每一行执行 A;;! 表示忽略自定义映射。

范围同样能交给不同的 Ex 命令:

:'<,'>s/^/- /
:10,20normal! ==
:g/^TODO/normal! A;

第一条用替换命令给刚才选择的行添加 - 前缀;后两条分别处理第 10 到 20 行,以及所有以 TODO 开头的行。

可视模式选择后,o 在选区两端切换活动端点,gv 重新选择上一次可视区域。这对修正选区边界很方便。

替换与 :global

常用替换模板是:

:%s/old/new/gc
  • %:整个文件;省略时只处理当前行;
  • g:替换每行中的所有匹配,而非每行第一个;
  • c:逐项确认。

路径中斜杠很多时,可以换分隔符:

:%s#/old/path#/new/path#gc

:global 先选出匹配的行,再对每一行执行 Ex 命令:

:g/DEBUG/d
:v/^#/d
:g/TODO/normal! A;

三条命令分别删除包含 DEBUG 的行、删除所有不以 # 开头的行,以及只给包含 TODO 的行追加分号。

这里的 |(例如 :vimgrep … | copen)是连接两条 Ex 命令,不是把前一条命令的标准输出通过 Shell 管道传给后一条。

6. 缩进、格式化、折叠和长行提示

缩进与文本格式化不是一回事

需求 命令
重排当前行 / 当前段落 / 全文缩进 == / =ap / gg=G
当前行增加 / 减少一级缩进 >> / <<
按 textwidth 格式化当前段落 gqap
格式化可视选区 选中后按 gq

= 使用当前文件类型的缩进规则;如果结果不合理,先检查 :set filetype? indentexpr? cindent?,而不是继续叠加手工空格。

现代终端通常支持 bracketed paste,Vim 会识别一次外部粘贴的边界。旧笔记里把 :set paste 当成默认步骤已经不合适;只有在终端或旧版 Vim 无法正确处理粘贴时,才把它作为兼容排查手段,并在完成后立刻 :set nopaste。

折叠命令要分清开与关

命令 含义
zf{motion} 手工创建折叠
za 切换光标处折叠
zo / zc 打开 / 关闭一个折叠
zR / zM 打开 / 关闭所有折叠
zx 根据当前 foldlevel 更新折叠,并显示光标行

zc 的含义是 close,不是 open。若折叠来自语法或缩进,可查看 :set foldmethod?。

80 列是排版约束,不一定要用复杂正则

如果希望正文以 80 列换行,并在下一列给出视觉提示:

set textwidth=80
set colorcolumn=+1

只想提示、不想自动换行时,可把 textwidth 保持为 0,使用:

set colorcolumn=81

这比旧笔记里通过 :match 或 2match 拼接列号正则更清楚,也不会占用匹配高亮槽位。

7. Buffer、Window、Tab 分别是什么

很多“退出后文件怎么还在”“分屏为什么切不动”的困惑,都源于把三层对象混在一起:

对象 它代表什么 常用命令
Buffer 文件或临时文本在内存中的内容 :ls、:buffer、:bnext、:bdelete
Window 显示某个 buffer 的视口 :split、:vsplit、:close
Tab page 一组窗口布局 :tabnew、gt、gT

一个 buffer 可以同时显示在多个 window 中;关闭 window 不一定卸载 buffer;Vim 的 tab 更像“工作区布局”,不必把每个文件都放进一个 tab。

分屏工作流

:split other.txt       " 水平分屏
:vsplit config.yaml " 垂直分屏

窗口间移动:

<C-w>h  左    <C-w>j  下    <C-w>k  上    <C-w>l  右
<C-w>= 等分尺寸 <C-w>o 只保留当前窗口

也可用 <C-w>s、<C-w>v 拆分当前窗口。执行 :q 时关闭的是当前 window;当它是最后一个 window 时,Vim 才会退出。

8. 工程导航:把“找到结果”变成可遍历的列表

项目级编辑的关键不是一次跳到某处,而是形成闭环:

定位目标 → 打开上下文 → 修改/验证 → 跳到下一项 → 回到原位置

从 gf 跳转文件并返回,再用 vimgrep 和 quickfix 遍历项目结果

动态图 5:gf 与跳转列表适合单点导航;quickfix 适合管理一组跨文件结果。

文件名、标签和定义

  • gf 打开光标下的文件名,<C-o> 返回;路径解析可通过 :set path? 调整。
  • 生成 tags 后,<C-]> 跳到光标下标识符的标签,<C-t> 返回标签栈。
  • gd 跳到当前函数内的局部声明,gD 尝试跳到当前文件内的全局声明。
  • Vim/Neovim 的 LSP 插件通常能提供更精确的定义、引用和重命名;那是对导航数据源的增强,不会替代 buffer、window 和跳转列表这些基础模型。

:vimgrep 与 quickfix

在项目的 Python 文件中寻找 TODO,但先不自动跳到第一个结果:

:vimgrep /TODO/j **/*.py | copen

之后使用:

命令 含义
:cnext / :cprevious 下一个 / 上一个结果
:cc 显示当前结果的完整信息
:copen / :cclose 打开 / 关闭 quickfix 窗口
:cdo {cmd} 对每个结果执行一条 Ex 命令
:cfdo {cmd} 对结果涉及的每个文件执行一次命令

例如确认列表无误后,对每个结果所在行执行替换,并只保存确实发生变化的文件:

:cdo s/TODO/DONE/g | update

目标是一组已知文件而不是搜索结果时,可以使用参数列表:

:args **/*.py
:argdo %s/old_api/new_api/ge | update

:argdo 遍历参数列表,:bufdo 遍历已加载的 buffer,:cdo 遍历 quickfix 中的有效位置。替换命令中的 e 标志允许某个文件没有匹配项;第一次在真实项目使用前,仍应先检查文件列表并确保版本控制状态清晰。

:make 也会把编译器或测试工具的错误解析到 quickfix。用 :set makeprg? 和 :set errorformat? 可以查看它们如何连接。

与 Shell 配合,而不是反复退出 Vim

场景 命令
执行一条命令 :!git status
把命令输出插入当前文件 :read !date
让外部命令过滤全文 :%!sort -u
暂停 Vim 回到原 Shell <C-z>,完成后执行 fg
在 Vim 中启动终端缓冲区 :terminal(版本支持时)

这一部分涉及作业控制、标准输入输出、过滤器和平台差异,已保留为独立专题:Vim 与 Shell 协作:挂起、恢复与外部命令。

9. 一份可解释的最小配置

配置的目标是把稳定偏好写下来,而不是在不理解行为时先堆插件。下面这份 ~/.vimrc 可以作为起点:

set nocompatible
filetype plugin indent on
syntax enable

set number
set relativenumber
set incsearch
set hlsearch
set ignorecase
set smartcase

set expandtab
set tabstop=4
set softtabstop=4
set shiftwidth=4
set autoindent

set textwidth=80
set colorcolumn=+1
set splitright
set splitbelow
set undofile

tabstop 是 Tab 字符显示宽度,softtabstop 是编辑时一次 Tab/退格感受到的宽度,shiftwidth 是 >>、<< 和自动缩进使用的宽度,expandtab 则让插入的 Tab 展开为空格。四者不要只改一个就期待所有行为一起变化。

不同文件类型需要不同规则时,优先用局部选项。简单配置可写成:

augroup personal_filetype_settings
autocmd!
autocmd FileType python setlocal expandtab tabstop=4 softtabstop=4 shiftwidth=4
autocmd FileType javascript setlocal expandtab tabstop=2 softtabstop=2 shiftwidth=2
autocmd FileType make setlocal noexpandtab
augroup END

配置变多后,把内容分别放到 ~/.vim/after/ftplugin/python.vim、javascript.vim 等文件中更容易维护。文件里只写对应的 setlocal 即可。

Neovim 用户同样可以使用本文的编辑命令;它的主配置目录通常是 ~/.config/nvim/。插件、LSP 和 Lua 配置属于另一个层次,先把这里的编辑模型练熟,再决定哪些能力值得自动化。

配置出错时先隔离变量

vim --clean file.txt
vim -Nu NONE -n file.txt

进入 Vim 后可继续检查:

:scriptnames
:verbose set shiftwidth?
:verbose map <C-w>

verbose 会告诉你某个选项或映射最后在哪里被修改。旧笔记中的 Vundle、YouCompleteMe、固定 Vim 8.2 路径以及全局修改 PowerShell 执行策略都不再放进通用主线:它们依赖具体年代和环境,混在基础命令中反而会缩短笔记寿命。

10. 从真实场景反推命令

场景一:只改当前单词,不想先选中它

把光标放在单词内部:

ciw新内容<Esc>

如果下一个目标要做完全相同的修改,移动过去按 .。

场景二:复制一段模板,中途还要删除其他内容

复制后用 "0p 粘贴;临时删除且不想污染默认寄存器时用 "_d{motion}。若要长期保存多块内容,用 "ay{motion}、"by{motion} 分别写入有名寄存器。

场景三:只给某一段的每行添加前缀

行可视模式选中目标,执行:

:'<,'>s/^/- /

命令行中的 '<,'> 会在从可视模式按 : 时自动出现;^ 匹配行首,替换文本是连字符和一个空格。

场景四:把一列名字变成 Markdown 任务项

在第一行录制一个“移到行首 → 插入 - [ ] → 移到下一行”的宏,然后用计数执行。任务需要多个动作且每行结构一致,宏比复杂替换更直观。

场景五:逐个修复项目里的废弃接口

先用 :vimgrep 或 :grep 建立 quickfix 列表,用 :cnext 查看上下文;确认变化可机械执行后,再考虑 :cdo。搜索列表与编辑动作分开,回退和复核都更容易。

11. 一次 30 分钟的练习路线

不要在真实项目里第一次尝试宏。创建一个临时文件,分四轮练习:

  1. 前 5 分钟:移动与对象。 练 w/b/e、f/;、%、ciw、di"、yap。
  2. 接着 8 分钟:搜索与重复。 用 /、n/N 找目标,完成一次修改,再用 . 复用;用 u 和 <C-r> 往返观察撤销树。
  3. 接着 8 分钟:寄存器。 依次执行 yy、dd、p、u、"0p,打开 :registers 对照每一步。
  4. 最后 9 分钟:批处理。 用 :%normal! A; 处理多行并撤销;再录一个只处理一行的宏,用 3@a 执行。

第二天在真实工作中,只要求自己识别一次“这里可以用文本对象”以及一次“这里可以用点命令”。形成场景联想,比一天背完一张快捷键表更有效。

12. 最小速查表与常见误区

对象编辑  ciw  di"  yap  =ap  gqap
搜索跳转 / n N * # gf <C-o> <C-i>
重复撤销 . u <C-r> qa…q @a @@
寄存器 :reg "0p "_dd "ayiw "+y
批量处理 :%s/a/b/gc :%normal! A; :g/pat/d
窗口缓冲 :ls :bnext :split :vsplit <C-w>hjkl
工程列表 :vimgrep :copen :cnext :cprevious :cdo
格式折叠 gg=G >> << zo zc zR zM

最后再纠正几条很容易从旧资料中继承的误解:

  • zo 打开折叠,zc 关闭折叠;
  • G 跳到文件末尾,>G 或 =G 才是带范围的缩进操作;
  • . 重复最近修改,@@ 重复最近宏;
  • p 读取无名寄存器,不天然等于系统剪贴板;
  • buffer 是内容,window 是视口,tab page 是窗口布局;
  • :normal! 中的 ! 用来跳过自定义映射,适合可复现的批处理;
  • :set paste 是兼容排查手段,不应成为现代终端下的常驻设置;
  • Vim 内置加密不适合替代密码管理器、磁盘加密或可靠的 secrets 管理。

参考入口

Vim 自带帮助就是与当前版本最匹配的参考资料:

:help user-manual
:help operator
:help motion.txt
:help text-objects
:help registers
:help repeat.txt
:help visual-mode
:help :normal
:help quickfix
:help windows.txt
:help pattern.txt

这份主文负责编辑语言和工程工作流;涉及 Shell 作业控制、过滤器、外部构建工具的完整说明,请继续阅读:Vim 与 Shell 协作:挂起、恢复与外部命令。

知识共享许可协议