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

为什么重新整理这份笔记

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

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

先记住一句话:

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

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

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

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

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

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

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

几个入口的区别很实用:

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

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

命令 含义
:w 保存当前缓冲区
:q 关闭当前窗口;有未保存修改时会拒绝
:wqZZ 保存并退出
: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 单词内部 / 单词及周围空白 ciwdaw
i" / a" 双引号内部 / 连同双引号 di"ya"
i( / a( 圆括号内部 / 连同圆括号 ci(ya(
it / at HTML/XML 标签内部 / 连同标签 citdat
ip / ap 段落内部 / 连同周围空行 gqipyap

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
重复 / 反向重复最近的 ft ; / ,
上一个 / 下一个段落 { / }
配对的括号、方括号或花括号 %
文件开头 / 文件末尾 / 第 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
19 最近几次行删除或较大删除的历史队列 "2p
- 最近一次小于一行的删除 "-p
az 有名寄存器;写入时用大写名称表示追加 "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. 使用 ft、文本对象或搜索定位,不依赖固定列数;
  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

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

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

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

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

一个 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.vimjavascript.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/ef/;%ciwdi"yap
  2. 接着 8 分钟:搜索与重复。/n/N 找目标,完成一次修改,再用 . 复用;用 u<C-r> 往返观察撤销树。
  3. 接着 8 分钟:寄存器。 依次执行 yyddpu"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 协作:挂起、恢复与外部命令

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot atlas attention axis-angle bang-bang belief encoder blender bode cadquery calibration camera calibration chrome cmake cmakelists cnn colcon conan control cpp cpu d435i dagger data_struct db depth camera design-pattern direct collocation dots economics eigen elevation map factory-pattern fcpx fiducial marker figure finance forge fourier fov freecad gae gazebo gdb git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel mac math matlab matrix memory mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pixhawk pixhawk 6c policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor realsense reinforcement learning representation learning reward tuning rnn robot robot parkour robotics ros ros2 rsl_rl rtb security shell signal-processing sim-to-real simulation socket soft dynamics constraints spot stairs stl stm32 tcp-ip teacher policy teacher student teacher-student temporal convolution thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl z-transform zero-shot transfer 中文输入 交叉编译 人形机器人 依赖管理 分支管理 四旋翼 四足机器人 实验诊断 强化学习 机器人 机器人控制 机器人视觉 构建系统 深度学习 深度相机 点云 版本控制 神经网络 自主回充 航模 视觉定位 训练曲线 足式机器人 输入法 配置类 采购记录 音乐 飞控
知识共享许可协议