vim 被称为编辑器之神,学习难度很大,但是熟练掌握后可以更高效地敲代码,因此有必要学一下,但是没必要鼓捣各种插件。 主要参考:【Vim】可能是B站最系统的Vim教程

vim 介绍与版本

vim 是古董编辑器 vi 的升级版,在现代的 Linux 发行版中通常自带 vim,无需手动安装,虽然版本通常不是最新的,但是也足够使用。在很多发行版中都将 vi 命令链接到 vim 命令,因此 vi 命令和 vim 命令通常是等价的,都是在调用 vim 编辑器。

在 vim 中输入 :version 可以查看版本信息,例如

1
2
3
VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Nov 22 2021 19:31:05)
Included patches: 1-3582
Compiled by <https://www.msys2.org/>

vim 的各种功能模块化,在编译安装时可以选择精简其中一部分功能,至少有几种安装模式:tiny, small, normal, big, huge,显然 huge 版本的功能最完整。

:version 的输出中包括一个列表,其中描述了当前编译的 vim 支持或不支持的功能,例如 +clipboard 意味着剪贴板功能被编译支持了,-clipboard 意味着剪贴板特性没有被编译支持。

极简使用

打开或新建文件

在命令行直接输入vim filename即可打开或新建指定的文件:

  • 对于已存在的文件会打开它;
  • 对于不存在的文件,会创建一个临时文件,如果退出时选择保存,则会真实地创建该文件。

如果直接输入 vim,不接任何文件名,vim 可能会给出如下的欢迎界面,有概率出现著名的标语:帮助乌干达的贫困儿童。

在 vim 中可能看见某些行只有 ~,这表示该行不存在,即文本行数小于显示屏幕行数,如果行首只有 @,则表示该行有内容存在,但是软换行导致目前无法完整显示。

修改文件

进入vim之后无法直接编辑文件,因为处于 Normal 模式,此时除了上下左右键之外,还可以使用 hjkl 进行光标移动。 在 Normal 模式下,几乎所有的按键(以及按键组合)都有特殊含义,并不是通常意义下的在光标所在位置进行输入,而是进行某些操作,可以参考下面的速查表。

在 Normal 模式可以输入 ia 进入 Insert 模式(下方状态栏默认会显示 -- INSERT --),此时可以像记事本一样正常进行编辑:在光标所在位置插入或删除对应字符。

在 Insert 模式使用 <Esc> 可以退回到 Normal 模式。

关闭、保存和退出

在编辑完成之后,退出 vim 的操作首先要明确所处的模式,假设当前处于 Normal 模式中(输入 : 就会进入 Command 模式),有如下几种退出方式:

  • :q (quit) 直接退出,只适合于当前文件没有被改动时,如果文件有改动,则会报错无法退出,因为必须处理对文件的修改;
  • :wq (write and quit) 保存并退出,也可以拆成两个命令:保存 :w,退出 :q
  • :q! 加感叹号表示强制操作,如果不希望对保存文件的改动,就可以强制进行不保存的退出。

使用 :x 也可以保存并退出,而且和 :wq 相比有细微的差别,它仅仅会在文件真正改动时才写入文件。 使用 :w newfilename 可以把改动另存为新的文件。 使用 :w n1,n2 newfilename 可以把 n1 行到 n2 行的内容存储到指定的文件中。

对于不熟悉 vim 的用户,几乎不可能体面地关闭 vim,不管你按什么按键。一个冷笑话是:如何生成一串完全随机字符串? 让新手退出 vim。

只读状态

如果在 Linux 中使用 vim 打开缺少写入权限的文件,会自动进入 readonly 状态,当然也可以通过 vim -R 或者别名 view 手动指定为 readonly 状态。

1
2
vim -R filename
view filename

readonly 状态也可以在 vim 中进行切换

1
2
:set readonly
:set noreadonly

readonly 状态代表 buffer 可以被修改,但是不能把改动写入到文件中,即不允许普通的写入命令 :w,强制写入命令 :w! 可以突破 vim 自身的限制, 但是如果对文件确实缺少写入权限,:w! 仍然会失败。

vim 还有一个比 readonly 更强的 modifiable 属性,决定 buffer 是否可以被修改,但是通常不需要使用。

配置命令

可以设置显示绝对行号:set number(或者简写为:set nu),效果如下图

可以设置:set nonumber关闭行号。(一般的配置项都可以通过加no前缀来取消) 可以使用:set number?来查询当前行号是开启还是关闭的,会显示numbernonumber。(一般的配置项都可以通过加?查询状态)

还可以设置显示相对行号:set relativenumber,光标所在的行记作 0,上下相邻的行记作 1,以此类推,效果如下图

绝对行号和相对行号可以同时出现,效果如下图

其它命令类似,例如:

  • :set cursorline可以高亮光标所在的行,这里的高亮有可能是改成红色背景,也有可能是整行添加下划线。
  • :! command可以暂时返回命令行界面执行指令,然后回车可以回到 vim,例如:! ls快速查询目录。

在 Command 模式中输入相关的设置命令,仅仅对于当前窗口有效。 如果希望在每次打开 vim 时都自动采用某些设置,则应该将其写入配置文件中,通常用户自定义的配置文件为~/.vimrc(对于 Windows 的 gvim,可能叫做_vimrc),配置文件例如

1
2
set number
set cursorline

在配置文件中,以双引号"开头的行被视作注释。

工作模式

vim 包括三种主要模式

  • Normal 模式
  • Insert 模式
  • Command 模式

以及一个临时的 Visual 模式。

启动 vim 会直接进入 Normal 模式,此时的按键不会被视作输入字符,而是视作命令,例如输入i会切换到 Insert 模式,进行正常的文本编辑状态;输入英文冒号:会切换到 Command 模式,此时光标会留在最底行,可以输入复杂的命令。

几个模式的转换如下图,Normal 模式作为转换的中枢,在其他模式中可以通过 <Esc> 键可以返回 Normal 模式,也可以使用 <Ctrl+[> 替代(默认情况下,它和<Esc>键功能完全等价)。

从 Normal 模式出发

光标移动基础

在 Normal 模式中,为了保证操作的高效连贯,我们不希望双手离开主键区,主要使用按键 hjkl 来移动光标:(当然四个方向键也可以)

  • h:左移
  • j:下移
  • k:上移
  • l:右移

整个文件尺度的移动:

  • gg:移动到当前文件的第一行
  • G:移动到当前文件的最后一行

按照行号定位:

  • {lineno}gg/{lineno}G:跳转到第 lineno
  • {percent}% 移动到当前文件对应比例的位置,例如30%

在 Command 模式中,还有如下的命令可以按行跳转(需要回车执行):

  • :0 移动到当前文件的第一行;
  • :{lineno} 移动到当前文件的第 lineno 行;
  • :$ 移动到当前文件的最后一行;

屏幕尺度下的移动:

  • H:移动到屏幕顶端的行;
  • M:移动到屏幕中部的行;
  • L:移动到屏幕底部的行;

翻页:(尽量保持光标在屏幕中的相对位置不变)

  • <Ctrl-u>:(up) 将文本上移半页
  • <Ctrl-d>:(down) 将文本下移半页
  • <Ctrl-b>:将文本上移一页
  • <Ctrl-f>:将文本下移一页

光标行定位:

  • zz:光标行居中
  • zt:光标行置于屏幕第一行
  • zb:光标行置于屏幕最后一行

进入 Insert 模式

从 Normal 模式进入 Insert 模式,常见方法如下:

  • 插入:(insert)
    • i:在光标所在字符前开始插入(光标所处的字符会被挤到后面);
    • I:在光标所在行的行首开始插入,如果行首有空格则在空格之后插入。
  • 添加:(append)
    • a:在光标所在字符后开始插入(光标会自动后移一格);
    • A:在光标所在行的行尾开始插入。
  • 替换:(substitude)
    • s:删除光标所在的字符并开始插入;
    • S:删除光标所在行并开始插入
  • 插入新行:
    • o:在光标所在行的下面创建新行插入;
    • O:在光标所在行的上面创建新行开始插入。

大小写命令对应的操作不同,但是普遍具有相关性:

  • 如果小写的操作是字符尺度的,那么大写的操作通常是行尺度的;
  • 如果小写的操作具有方向性,那么大写通常是相反方向的操作。

进入 Command 模式

Normal 模式下,可以通过 : 进入 Command 模式,此时可以输入命令,例如:

  • :help:h 显示帮助(:h xxx显示对应条目的帮助)
  • :w 保存
  • :q 退出
  • :wq 保存并退出
  • :!{cmd}:利用shell执行外部命令,支持传递参数,提供一个临时界面展示命令的输出。

在 Command 模式,输入单词开头部分,即可可以使用<Ctrl-d>获取补全提示,也可以使用tab获取补全建议。

进入 Visual 模式

Normal 模式下,输入 v/V 进入 Visual 模式,常见操作:

  • 移动光标选中文本进行操作;(v以字符为单位,V以整行为单位)
  • x:剪切
  • y:(yank) 复制
  • d:(delete) 删除
  • p:(paste) 粘贴
    • 可以退出到 Normal 模式进行
    • 如果直接在选中一部分内容的状态下进行粘贴,会直接覆盖掉当前选中的内容

除了 <Esc>,再次输入 v 或者某些操作完成后也会自动退出 Visual 模式。

移动 (Motion)

vim 在 Normal 模式下提供了各种快捷键用于快速移动光标,这些按键所绑定的行为称为移动 (Motion)。

基于单词的移动

  • w/W:(word) 移动到(下一处)单词的首字母;
  • b/B:(back) 移动到(上一处)单词的首字母;
  • e/E:(end) 移动到(下一处)单词的尾字母;
  • ge:移动到(上一处)单词的尾字母; (e的反向操作)

大小写版本的 W/B/E 区别在于分词逻辑:大写操作只按照空格分词,例如 open-source 在小写操作中视作三个单词(- 也视作一个单词),但是在大写操作中视作一个单词。

基于单词的操作对于中文并不友好,因为单词的分词是简单地基于空格等符号所进行的,中文需要根据语义分词,这一点很难做到,可能会把整个句子视作一个单词。

行内搜索与移动

行内搜索:

  • f{char}/F{char}:跳转到本行的下一个/上一个 {char} 字符位置;
  • t{char}/T{char}:跳转到本行的下一个/上一个 {char} 字符位置之前;
  • ;:继续(或切换为)向后搜索
  • ,:继续(或切换为)向前搜索

注:

  • 行内搜索只能基于指定字符,除此之外,vim 还提供了一套支持模式匹配的全局搜索命令,见下文。
  • ;/, 在跳转时仍然限于本行内部,但是可以通过插件实现跨行跳转。

基于标记的移动

vim 提供了一套标记系统,可以在特定位置打上标记,从而基于标记快速移动:

  • m{mark}:在当前位置打一个标记 markmark 要求是小写字母,例如 mm
  • `{mark}:跳转到到标记 mark

内置的特殊标记:

  • ``:上次跳转前的位置
  • `.:上次修改的位置
  • `^:上次插入的位置

在 Normal 模式中,还可以使用 gi 来直接跳转到 Insert 模式中的光标最后位置,并且直接进入 Insert 模式。

基于文本的移动

  • (/):向前/向后跳转到一个句子的开头
  • {/}:向前/向后跳转到一个段落的开头
  • %:如果光标位于括号等配对符,会跳转到匹配的符号,否则向后跳转到下一个配对符

实用的局部跳转

  • 0/$:跳转到当前行开头/末尾(含空格)
  • ^/g_:跳转到当前行的第一个/最后一个非空白字符
  • +/-:跳转到上一行/下一行的第一个空白字符

粘贴

Vim 的粘贴命令为 p/P,它们会根据复制内容的类型自动选择进行“字符粘贴”或“行粘贴”,两者的区别是在光标后或光标前:

  • 如果复制的是字符或词:
    • p:粘贴在光标后
    • P:粘贴在光标前
  • 如果复制的是整行(如使用 yydd):
    • p:将复制内容插入到当前行的下方
    • P:将复制内容插入到当前行的上方

重复与撤销

  • .:重复上一次修改(批量操作常用)
  • u:撤销上一次修改
  • <Ctrl-r>:重做上一次修改(撤销的相反)

u 不同,大写的 U 对应的撤销更加彻底:恢复某一行进入编辑时的状态,这也意味着重复按 U 无效。

全局搜索匹配

文件中搜索:(pattern 可以是正则表达式)

  • /{pattern}:搜索模式 {pattern},并跳转到匹配的下一个位置;
  • ?{pattern}:搜索模式 {pattern},并跳转到匹配的上一个位置;
  • n:继续(或切换为)向后搜索
  • N:继续(或切换为)向前搜索
  • *:自动以当前光标所在单词为 pattern,向后搜索

进阶

重复移动

在移动前面加上数字就可以重复多次移动。

1
[count]{motion}

例如

  • 5j:向下移动 5 行;
  • 5k:向上移动 5 行;
  • 2w:向右移动 2 个单词;

Operator + Motion = Action

操作符和移动可以组成的一个完整编辑动作,移动前后的光标所组成的就是这次编辑所对应的范围。

1
{operator}{motion}

常见的重要操作符:

  • c:(change) 修改(删除并进入 Insert 模式)
  • d:(delete) 删除
  • y:(yank) 复制
  • v:(visual) 选中文本,进入 Visual 模式

这里并没有剪切操作,因为 vim 实现的删除操作其实就是剪切,它会把删除内容记录下来,可以被用于粘贴。

在操作符后面加上移动即可组成完整命令,例如:

  • dgg:删除到第一行
  • ye:复制到单词结尾
  • d$:删除到行尾
  • dt;:删除到遇到分号;

如果不确定{motion}的作用范围,可以先尝试v{motion},再执行c/d/y操作。

重复两次操作符通常是作用在当前行:

  • cc:修改当前行(删除并进入 Insert 模式)
  • dd:删除当前行
  • yy:复制当前行

但是 vv 会进入 Visual 模式然后退出。

由于历史兼容性等原因,这几个操作符的大写含义为:

  • C = c$:修改到行尾
  • D = d$:删除到行尾
  • Y = yy:复制整行(注意不是复制到行尾)

Count + Action

[count]{action} 代表重复 {action} 行为 [count] 次,这里的 {action} 也可以进一步展开,变成

1
[count]{operator}{motion}

例如:

  • 5j:向下移动5行
  • 3dw:删除3个单词
  • 2yy:复制2行
  • 4p:粘贴4次

这里的组合方式其实非常灵活,例如 {motion} 通常也可以换成 [count]{motion},例如:

  • 3dw 解释为 dw (删除到单词末尾)且重复3次;
  • d3w 解释为为按照 3w 的移动范围进行删除。

两者效果是一样的。

文本对象及其操作

vim 将如下文本结构视作文本对象:

  • 单词:w/W (word)(两者区别是单词的定义)
  • 句子:s (sentence)
  • 段落:p (paragraph)
  • 配对符定义的对象:(/)[/]{/}</>'/"

有两种基本格式:

  • i:(inner) 内部
  • a:(around) 完整对象,额外包括边界的空格或两边的配对符

⽂本对象使⽂本具有了结构化的语义,进而允许我们以语义对象为操作单元,进行更具体的操作。

1
[count]{operator}{textobjects}

例如:

  • diw:删除单词
  • ci(:修改小括号内部
  • yi{:复制大括号内部

但是这里的 {textobjects} 不能独立使用,而且重复数量只能加在 {operator} 前面。

如果不确定 {textobjects} 的作用范围,可以先尝试 v{textobjects},再执行 c/d/y 操作。

寄存器

Vim 提供了许多寄存器⽤于存放内容,可以理解为剪贴板, ⼀个字符对应⼀个寄存器(例如a-z0-9),不同的寄存器有对应的功能,具体细节非常复杂,下表只列举常用的寄存器。

名称 标识 内容或功能
无名寄存器 " 默认寄存器(上一次复制或删除的内容)
删除寄存器 0-9 文本复制和删除历史
小写寄存器 a-z 手动指定用于保存文本
大写寄存器 A-Z 用于追加内容到同名的小写寄存器
剪切板寄存器 + / * 系统剪贴板(需要 Vim 支持)
空寄存器 _ 删除但不存储(黑洞)

此外,还有一些存放特殊信息的只读寄存器:

  • %:当前文件名
  • .:上一次插入的内容
  • ::上一次执行的命令

通过 :reg {register} 可以查看对应寄存器的内容,使用 :reg 则会查看所有寄存器。

在复制/删除操作前加上"{register}就可以指定本次操作所使用的寄存器,例如:

  • "ayy:复制当前行内容,保存到a寄存器中;
  • "bdiw:删除单词,保存到b寄存器中;
  • "_dd:删除当前行,保存到空寄存器(也就是直接丢弃,不污染剪贴板)

在粘贴时可以指定使用的寄存器,例如:

  • "cp:粘贴,使用c寄存器的当前内容。

注:即使退出 vim,寄存器中的内容通常也不会丢失,而是会存储在 .viminfo 文件中。

宏指的是录制⼀系列键盘操作,并允许我们重放这些操作,常用于批量化操作文本。

基本用法:

  • q{register}:开始录制一段宏,将动作记录在寄存器 register 中;
  • 执行希望记录的动作,按 q 退出录制;
  • @{registetr}:重放寄存器 register 中的动作;

在使用一次 @{registetr} 之后,直接使用 @@ 就可以自动重放上一次宏操作,使用 [count]@{register} 可以自动重放 count 次。

注意:

  • . 命令对宏不⽣效, . 命令只记录上⼀次修改,⽽宏可能包含多次修改。
  • 由于宏的目标是重复操作,在动作序列中需要在开头或结尾跳转到需要编辑的位置,否则重放是错误的。

Command 模式

Ex Command 格式

Ex Command 的完整格式如下:

1
:[range] {excommand} [args]

其中:

  • [range]:指定操作的行范围,缺省时默认为当前行;
  • {excommand}:具体操作命令;
  • [args]:命令附带的参数。

range 与 address 表示

Command 模式下的命令都可以加上对应的行范围 rangerange 由一到两个 address 组成(两个之间用,分隔)。

下面是一些 address 的例子:

  • {lineno}:具体行号,例如 3 代表第3行,0 代表首行之上的虚拟行
  • $:最后一行
  • .:光标所在行
  • /{patttern}/:下一个 pattern 所在行

还支持简单的加减运算,例如:

  • .+3:当前行加上3行
  • $-3:倒数第4行

address 可以组合得到 ranges,例如:

  • 1, 3:1行到3行
  • ., .+4:当前行到后续4行(共5行)
  • $-3, $:倒数第3行到末尾行

除此之外,还有一些特殊范围:

  • %:整个文件的所有行
  • '<,'>:Visual 模式下选中范围的开头和结尾(如果在 Visual 模式下选中范围并输入 :,会自动补充为 :'<,'>

常见的 Ex Command

  • :[range] yank [x]:复制指定行(保存到寄存器 x 中)(yank 可以缩写为 y
  • :delete [x]:删除指定行(保存到寄存器 x 中)(delete 可以缩写为 d
  • :print:打印指定行(print 可以缩写为 p),注意这只是在底部展示一下对应内容,并不是粘贴到文本中。
  • :[range] copy {address}:把指定行插入到 address
  • :[range] move {address}:把指定行复制到 address
  • :[address] put [x]:把寄存器 x 的内容插入到 address 后面(可以插入第0行后面)

例如

  • :yank a:复制当前行,保存到寄存器 a 中;
  • :1, 3 delete a:删除1行到3行,保存到寄存器 a 中;
  • :$-3, $ print:打印倒数第3行到末尾行

Command 批量操作

normal 命令

normal 命令可以对 range 中的所有⾏执⾏ Normal 模式下的命令 commands

1
:[range] normal {commands}

例如:

  • :[range] normal .:⽤ normal 命令在指定的⾏上重复 . 记录的操作(先进行一次修改以记录到 .
  • :[range] normal @{register}:⽤ normal 命令在指定的⾏上重放宏 register

global 命令

global 命令可以对 range 中的所有包含 pattern 的⾏执⾏ Command 模式下的 Ex 命令 commands

1
:[range] global/{pattern}/[cmd]

其中 cmd 缺省时是打印print

例如:

  • :% global /TODO/delete:删除文件中所有包含TODO的行

替换命令

替换命令可以对 range 中的所有包含 pattern 的⾏操作,将 pattern 替换为 string

1
:[range]s/{pattern}/{string}/[flags]

其中的 flags 是可选的,用于调整匹配替换行为的细节:

  • g:替换所有匹配项(默认仅替换第一个匹配项)
  • i:忽视大小写(默认区分大小写)
  • c:每次替换时都要求确认,可以执行、跳过或退出
  • n:计数匹配项,不进行替换

一些例子:

  • :s/abc/xyz:替换当前行的 abc 字符串为 xyz
  • :s/abc/xyz/g:替换当前行的 abc 字符串为 xyz(替换所有匹配项)
  • :%s/abc/xyz/g:替换所有行的 abc 字符串为 xyz
  • :10,20s/abc//gn:计数指定范围内的 abc 字符串

补充

命令/操作符补充

命令补充:

  • ~:大小写翻转
  • J:拼接两行
  • r:快速替换单个字符
  • R:进入字符替换的模式(<Esc 退出)

操作符补充:(操作符不能单独使用,这里的 g 也不能单独使用)

  • gu/gU:转换为小写/大写
  • g~:大小写翻转
  • </>:调整缩进(例如 >><< 调整当前行的缩进)
  • =:智能调整缩进(例如 == 智能调整当前行的缩进,gg=G 先回到首行,然后智能调整整个文件)

vim 支持 bash 中常见的几个删除操作(甚至是在 Insert 模式下也支持!):

  • <Ctrl+u>:删除至行首
  • <Ctrl+w>:删除光标前的单词
  • <Ctrl+h>:删除光标前的字符(与 <Backspace> 相同)

Windows terminal + pwsh 对这些快捷键的支持好像有问题,<Ctrl+w>正常支持;<Ctrl+u>似乎不支持,可以用<Esc代替,也可以使用<Ctrl+a>全选后删除。

vim 有着非常详细的操作历史记录:

  • 使用 q: 可以查看 Command 模式的历史记录。
  • 使用 q/ 可以查看 Search 模式(正则匹配)的历史记录。

这些历史记录以及寄存器内容等会被存储在 .viminfo 文件中,再次打开 vim 时,这些内容会自动恢复。

从外部的粘贴

现在考虑在 windows 系统下基于 terminal+ssh 远程操作中的复制粘贴,此时显然无法使用 vim 内部的复制粘贴机制。

对于文本文件的大段粘贴,如果直接ctrl+v会出现很多异常:

  • 例如如果当前不是 Insert 模式,在开头的部分字符可能会被直接丢弃,直到出现 ia 等字符才会进入。
  • 例如可能产生异常的缩进,还可能将某些行自动注释掉。

这是因为vim将字符流视为用户从键盘进行的输入行为,并且视作命令进行处理。

可以通过下面的选项将 vim 设置为粘贴模式,此时就可以保持原样进行粘贴

1
:set paste

在执行ctrl+v之前,最好让 vim 保持在如下模式

1
-- INSERT (paste) --

在粘贴完成后关闭选项即可

1
:set nopaste

复制粘贴的过程实际上非常复杂,vim 和 nvim 的表现并不一致,使用各种 terminal,使用 tmux 都可能带来额外的麻烦,字符流在这些环节中传递,稍不注意就会出错,就像 windows 中的中文乱码一样麻烦。nvim 在这方面比 vim 更加友好,但是也有很多坑。

字母与常见含义的关联

字母 常见含义
a append:追加;around:包含边界(文本对象)
b back:返回;block:括号内容
c change:修改
d delete:删除
e end:结尾
f find:行内搜索
g goto:跳转(不能单独使用)
h 左移;help:帮助
i insert:插入;inner:内部(文本对象)
j 下移;join:合并行(J
k 上移
l 右移
m mark:标记
n next:下一个
o open:打开新行
p paste:粘贴;paragraph:段落(文本对象)
q quit:退出(还有录制宏等功能)
r replace:替换(单个字符)
s substitute:替换;sentence:句子(文本对象)
t till:行内搜索(跳转到目标前)
u undo:撤销
v visual:可视化
w write:保存;word:单词
x 删除单个字符
y yank:复制
z 滚动窗口