VIM 是系统运维、开发和文本处理工作中不可或缺的高效工具。它以模式化操作、组合式命令和精准的文本对象控制著称,虽初学略有门槛,但一旦掌握,编辑效率将显著提升。本手册面向网络运维人员,聚焦 VIM 在日常配置管理、日志分析和脚本编写中的实用场景,摒弃繁杂理论。

本文档从最基础的文件编辑流程入手,逐步深入到移动命令 (Motion)、文本对象 (Text Object)、操作符 (Operator) 与可视模式 (Visual Mode) 等核心概念,帮助读者建立 VIM 的结构化编辑思维。同时涵盖搜索替换、多窗口管理、外部命令集成等进阶技巧,力求在真实运维环境中做到手不离键盘,眼不看鼠标。

学习 VIM 不是背诵快捷键,而是理解其核心语法:动词+范围=操作。

注:本文档借助了 AI 进行润色和纠错。


缩写

  1. {char}:输入一个字符;
  2. {count}:输入一个数字,用于 motion 的重复执行。count 的数字即为 motion 重复的次数;
  3. {motion}:输入一个移动命令;
  4. {n}:等价于 {count}
  5. {nb-char}:非空白字符 (no blank char);
  6. {txt-obj}:文本对象 (text object),由文本对象前缀+对象类型组成;

前言

下面对最基本的文件编辑过程进行展示。我会先打开一个空白文件,在输入一些文本之后再保存。

(1) 首先通过 vim <filename> 命令,用 VIM 打开一个文件 (如果该文件不存在,则新建)。

[root@localhost ~]# vim test.txt

(2) 刚打开一个文件时,VIM 默认处于 COMMON MODE。通过下按 i 键进入 INSERT MODE,在插入模式 (- INSERT -) 中可以编辑文档,例如输入以下文字:

This is a test file.
And this is the first time to using "vim".
It's easy to use "vim".
I like to using it, do you like it?

(3) 编辑完毕,在命令模式执行保存。按下 Esc 键退出插入模式,然后输入 :wq 和回车即可进行保存操作。

This is a test file.
And this is the first time to using "vim".
It's easy to use "vim".
I like to using it, do you like it?
~
~

:wq

(4) 此时,test.txt 文档的内容已被修改。

[root@localhost ~]# cat test.txt
This is a test file.
And this is the first time to using "vim".
It's easy to use "vim".
I like to using it, do you like it?

上面的就是全部的编辑过程。

另外,在移动命令 (Motion) 和文本对象 (Test object) 的学习过程中,我们会用到 c 命令。c 是一个更改 (change) 操作符,用于删除指定范围的文本并进入插入模式。它的语法是 c{motion}/c{txt-obj}。例如,我们尝试在下面的例子中尝试执行 cw,结果就是,整个 The 都被删掉了,并且 VIM 从 NORMAL 模式进入到了 INSERT 模式:

1 The quick brown fox jumps over the lazy dog.
 
1 The_quick brown fox jumps over the lazy dog.

The 会被删除掉,是因为 w 这个移动命令会让光标移动到下一个词 (word) 的开头,配合 c 的删除效果,导致了整个光标所在的单词 (word) 都被删除的结果。换而言之,c 命令可以让我们清楚的看到文本对象的作用范围。


VIM 基本概念

主要模式与模式间切换

最主要的模式为普通模式 (NORMAL)、插入模式 (INSERT) 以及命令模式 (COMMAND)。

  • 普通模式:主要执行快捷命令,譬如光标移动、增删查等动作;
  • 插入模式:在普通模式按下部分按键可以进入插入模式,此时可以进行文本输入;
  • 命令模式:在普通模式下可以通过 :CONNAND 的方式来执行命令。

下面是各个模式最经典的进入方式:

ESC               COMMON Mode
i                 INSERT Mode
:                 COMMAND Mode
v                 V-Block Mode
/                 Match Mode

文档编辑过程中,最必要的模式是插入模式 (NORMAL)。因为一般模式下,只能删,不能增。如果要进行完整的修改操作,需要进入到编辑模式。甚至可以这么说,只需要会打开文件 vim <filename>、进入插入模式 i、退出插入模式,ESC 以及关闭文件 :wq 这四个操作,你就可以说你会使用 VIM 进行文档编写了。为此 VIM 提供了灵活的进入插入模式的方法:

i                 从当前光标处进入插入模式 (insert)
I                 并置光标于行首,并进入插入模式 (INSERT)
a                 追加模式,置光标于当前光标之后 (after)
A                 追加模式,置光标于当前行尾 (AFTER)
o                 在当前行之下新加一行,并进入插入模式
O                 在当前行之上新加一行,并进入插入模式

普通模式是模式切换的桥梁。绝大多数情况下,模式切换操作的流程都是先切换到普通模式然后再切到另一个模式。例如从插入模式切换到命令模式,就需要先按下 ESC 键,然后再按下 : 键。

另外,<C-[> 也能用于返回普通模式。这是因为 Esc 键的 ASCII 码是 27 (0x1B),而按下 <C-[> 组合键时也会向终端发送 ASCII 码 27。因此,对 VIM (或任何终端程序) 来说,这两个输入是完全相同的字节流。甚至对于极少数老旧终端或 SSH 客户端来说,可能使用 <C-[> 组合键比使用 Esc 键还要可靠一些。

另外在这里对文件的保存和退出方法进行初步介绍。

# -- COMMAND --
:q                在没有进行文档修改的情况下,不保存退出
:q!               在进行过文档修改的情况下,不保存强制退出
:w                保存文档现有修改
:wq               保存并退出 (添加 `!` 强制保存并退出 `:wq!)

# -- NORMAL --
ZZ                保存退出。等价于 `:x` 
ZQ                不保存退出。等价于 `:q!` 

定位与范围 (Motion & Text Object)

在 VIM 中,定位靠移动命令 (motions),如 w$};范围选择则依赖文本对象 (text objects),如 iwa"i{Motion 描述光标怎么走,常用于导航或作为操作的终点;Text Object 描述结构化文本单元,不依赖光标精确位置,直接作用于单词、句子、括号块等逻辑结构。二者配合操作符,构成 VIM 精准编辑的基础。


VIM 命令结构

VIM 的核心命令遵循统一语法:

Syntax:
    [Count][Operator][Text-Object / Motion]

Options:
    - Counts        计数。重复后续动作,如 `3dw` 删除三个单词;
    - Operators     操作符。执行动作,如 `d`(删除)、`y`(复制)、`c`(更改);
    - Motions       移动命令 (如:`w``G`)。定义作用区域。
    - Text Objects  文本对象 (如:`iw``ap`)。定义作用区域。

Example:
    ci"             更改当前双引号内的内容
    d2j             删除当前行及向下两行
    yG              复制从当前位置到文件末尾

这种组合式设计让少量按键即可完成复杂编辑。


文本对象(text objects)

文本对象是 VIM 中非常强大的概念,它允许我们以一种结构化的方式来操作文本内容。不同于简单的字符或行操作,文本对象能够理解代码或文档的结构,比如一个单词、一个句子、一个段落,或者一对括号之间的内容。掌握文本对象可以更加精准和高效地编辑文本。

文本对象的基本语法是由一个范围标识符和一个对象类型组成:

(a/i){wWspbBt'"[<}

范围标识符的含义放到下一个小节再详细描述,这里先对各种对象类型进行枚举:

  • w/W:单词 (word)
  • s:句子 (sentence)
  • p:段落 (paragraph)
  • b/B 小括号/大括号 (block),等价于 ( )/{ }
  • t:XML/HTML 标签块 (tag)
  • '/"/`:各种引号
  • ( )/[ ]/{ }/< >:各种括号

文本对象前缀

范围标识符分为两种:i (inner) 和 a (around)。i 表示选择对象的内部内容,不包含外围的边界符号;a 则表示选择包含边界符号的完整对象。举个例子,当代码结构如下,并且光标位于第二行 e 的位置,位于一对圆括号内。此时使用 ci( 会删除括号内的所有内容但不删除括号本身;而使用 ca( 则会连同括号一起删除。

1 def foo():
2     print("Hello, World!")
 
Des.  print("Hello, World!") -> ci(
      print("Hello, World!") -> ca(

这种设计模式适用于各种成对结构,包括方括号 [ ]、花括号 { }、引号 " "、HTML 标签 <a> </a>等。在编程时,这种操作方式能极大提高效率。想象你需要修改一个函数的参数列表,只需要将光标放在括号内,输入 ci( 即可删除所有参数并进入插入模式,而不必一个个字符去删除。

同样的逻辑也适用于字符串修改,ci"ci' 可以快速修改引号内的内容,ci` 可以修改反引号内的内容,这在 shell 脚本或 Markdown 代码块中很有用。


单词对象类型(word)

最常见的文本对象是单词 (word) VIM 中对单词的定义有两种:一种是 w (word,短词),以空格或标点符号作为分隔符;另一种是 W (WORD,长词),只以空格作为分隔符。下方是一个示例,当代码结构如下,并且光标位于第二行 e 的位置。此时的 w 代表着 Hello,而 W 代表着 print("Hello,

1 def foo():
2     print("Hello, World!")
 
Des.  print("Hello, World!") -> cw, word
      print("Hello, World!") -> cW, WORD

这种细微的区别在处理包含标点的文本时特别有用,比如处理 URL 或文件路径时,W 往往比 w 更符合实际需求。


句子对象类型(sentence)

s 代表句子 (sentence),以句号 (.)、问号 (?) 或感叹号 (!) 加上空格 (<space>) 作为句子的结束。因此下方的段落只有两个句子。

1 Of threats of Hell and Hopes of Paradise!
2 One thing at least is certain—This Life flies;
3 One thing is certain and the rest is Lies;
4 The Flower that once has blown for ever dies.

Tips: (1) 在第 1 行执行 `vis` 会选中黄色范围;
(2) 在 2-4 行执行 `vis` 会选中绿色范围;
(3) 这是因为 2、3 行的结尾是 `;` 而不是 `.` `?` `!` `<space>`

段落对象类型(paragraph)

段落也是常见的文本对象。p 代表段落 (paragraph),VIM 以空行作为段落的分隔;

1 Of threats of Hell and Hopes of Paradise! 1 Of threats of Hell and Hopes of Paradise! 2 One thing at least is certain—This Life flies; 2 One thing at least is certain—This Life flies; 3 One thing is certain and the rest is Lies; 3 4 The Flower that once has blown for ever dies. 4 One thing is certain and the rest is Lies; 5 The Flower that once has blown for ever dies. Tips: VIM 通过数 “空行” 来判定段落。根据 VIM 的语法,左边是一个段落,右边是两个段落

cip 可以删除整个段落的内容,cas 则会删除整句包括后面的标点。这些命令在编辑文章或文档时特别高效。


成对类文本对象类型

标签 (Tag)

标签类文本对象在编辑 HTML 或 XML 时非常有用。cit 可以修改标签内的内容,而 cat 会删除包括标签本身在内的整个元素。光标只需要在标签内部的任意位置,VIM 就会自动识别匹配的开始和结束标签。这种智能识别大大简化了网页代码的编辑工作。

1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Index</title> 6 </head> 7 <body> 8 <h1>Hello, World!</h1> <h1>Hello, World!</h1> -> cit <h1>Hello, World!</h1> -> cat 9 </body> 10 </html>

Tips: `cit` 只会删除标签包裹住的内容,`cat` 则会连同标签一起删掉
Code Point   BIN                                   Hex          Character
----------   -----------------------------------   --------     ---------
U+0041       00000000 00000000 00000000 01000001   00000041     A
U+000A       00000000 00000000 00000000 00001010   0000000A     LF (NL line feed, new line)
U+007F       00000000 00000000 00000000 01111111   0000007F     DEL (delete)
U+0080       00000000 00000000 00000000 10000000   00000080     PAD (Padding Character
U+4E2D       00000000 00000000 01001110 00101101   00004E2D     中
U+1F60A      00000000 00000001 11110110 00001010   0001F60A     😊
U+10FFFF     00000001 00001111 11111111 11111111   0010FFFF     Unicode 最大合法码点(未分配字符)

Tips: UTF-32 编码方式空间效率极低。
   上面的例子可以看出,4 Byte 定长的 UTF-32 存在极大的资源浪费。

各种成对的括号、引号也可以通过上面的方式进行匹配。

1 data = { 2 "list": [1, 2, (3, 4)], -> ci[ 3 "nested": {"key": 'value'}, -> ca{ 4 "string": "Hello" -> ci" 5 } 6 print(data["list"][2][0]) -> ci(

文本对象的一个强大之处在于它们可以与各种操作符组合使用。di[ 删除方括号内容,ya( 复制包括括号在内的完整对象,ci" 修改字符串,>i} 对花括号内的代码块增加缩进。这种组合模式让 VIM 的编辑能力变得极其灵活。你不需要移动光标到特定位置,只需要告诉 VIM 你想操作什么类型的对象,VIM 就会自动处理范围选择。

值得注意的是,文本对象的匹配是基于语法规则的,而不仅仅是简单的字符查找。VIM 能够识别嵌套结构,并且会优先选择内层结构。当光标位于嵌套的括号内时,ci( 会作用于最内层的括号对。如果你想要操作外层的括号,可以先移动光标到外层括号上,或者使用 c3( 这样的计数语法来选择第三层的括号。这种智能匹配机制确保了操作的准确性,避免了误删内容的情况。

理解并熟练使用文本对象是掌握 VIM 的关键一步。从简单的单词修改到复杂的代码块重构,文本对象都能提供高效的操作方式。初学者可以先从 ciwci"ci( 这几个最常用的命令开始练习,逐渐将它们融入到日常编辑习惯中。一旦你习惯了用结构化的思维来思考文本编辑,就会发现 VIM 的效率优势是其他编辑器难以比拟的。


移动命令(motions)

最基本的移动命令 (motion) 是字符级别的移动。hjkl 分别对应左、下、上、右四个方向的移动。初学者可能会觉得用方向键更直观,但坚持使用 hjkl 有两个明显优势:

  1. 手不需要离开主键盘区域,编辑速度更快;
  2. 这些键位于键盘中间位置,手指移动距离最短,长期使用可以减少疲劳。数字前缀可以重复这些移动,比如 5j 会向下移动五行,3l 会向右移动三个字符。这种计数机制适用于几乎所有的移动命令。

除此之外,VIM 还有许多其他移动命令。移动命令是 VIM 导航系统的基础,它们告诉光标如何在文档中移动。单独使用时,移动命令只是改变光标位置;但与操作符结合后,它们就变成了强大的范围选择工具。理解移动命令的工作原理对于高效使用 VIM 至关重要,因为几乎所有的编辑操作都依赖于精确定位。

移动命令 (motion) 有两个隐含属性,影响 dcy 等操作的范围。一个是移动类型 (Inclusive / Exclusive),另一个是操作颗粒度 (Character-wise / Linewise)。

Inclusive vs Exclusive

操作符与移动命令的组合遵循一个重要的语法规则:操作符+移动=作用于从当前位置到移动目标之间的文本。这个规则看似简单,但理解它背后的 inclusive 和 exclusive 移动类型差异很重要。参考如下的示例。w 命令的作用是使光标移动到下个单词开头,e 命令的作用是使光标移动到下个单词结尾。虽然 ew 移动命令最终停留的位置不一样,但用在 c{motion} 命令下的时候,结果却是一致的。这是因为,e 属于包含式移动 (inclusive motion,简称 i-motion),作用范围从光标左侧开始,到跳转后的光标位置右侧结束;而 w 属于排除式移动 (exclusive motion,简称 e-motion),作用范围从光标左侧开始,到跳转后的光标位置左侧结束。

📚 官方定义来自 :help inclusive: A character motion is inclusive when the last character of the motion is included in the operation. It is exclusive when the last character is not included.

1 The quick brown fox jumps over the lazy dog.
2 The quick brown fox jumps over the lazy dog. -> `e`
3 The quick brown fox jumps over the lazy dog. -> `w`
 
- The_quick brown fox jumps over the lazy dog. -> `ce`
- The_quick brown fox jumps over the lazy dog. -> `cw`

更直观的说,e (i-motion) 的作用范围包含目标位置,而 w (e-motion) 的作用范围不包含目标位置。这种细微的差异在选择精确范围时很重要,VIM 的设计已经为大多数场景选择了合理的默认值,我们只需要了解在少数情况下可能需要调整操作方式。目标位置的意思是 “光标最终停留的字符”

⚠️ 需要特别注意的是:$inclusive,但它包含的是行尾之后的换行符 (newline),所以 d$ 不会删除换行符,而 d_D 才会删到行尾内容 (不含换行)。

Character-wise vs Linewise

Vim 的移动命令不仅有 inclusive/exclusive 之分,还根据其操作的颗粒度分为两类:字符级 (character-wise) 和 行级 (linewise)。这一属性决定了操作符 (如 dyc) 作用时是以字符为单位还是以整行为单位进行处理。

  1. 字符级移动命令 (Character-wise) 的操作范围精确到单个字符。大多数基础移动命令属于此类,例如:hlwf{char}^$。字符级移动命令在删除、复制或修改时,不会自动包含整行,也不会在结果中引入额外的换行。
  2. 行级移动命令 (Linewise) 的操作范围以整行为单位。无论光标在行内哪个位置,操作都会作用于完整的行 (从行首到行尾,含换行符)。常见的行级移动命令包括:j、kG、gg} / {HML

⚠️ 注意:jk 本身在普通移动时是字符级的,但一旦与操作符结合 (如 djyk),它们就变成行级移动命令。

1 Of threats of Hell and Hopes of Paradise! -> `d3j`
2 One thing at least is certain—This Life flies;
3 One thing is certain and the rest is Lies;
4 The Flower that once has blown for ever dies.

Tips: (1) `d3j` 表示删除含光标所在行的下面 3 行;
(2) 由于 `d` 操作符,原本为字符级的移动命令变成了行级

单词级 (word)

单词级 (word) 移动是使用频率最高的导航方式之一。单词级移动命令中,小写命令 (如 w) 和大写命令 (如 W) 的区别是:它们对单词的定义不同。小写版本以非空白字符序列作为单词,而大写版本只以空格作为分隔符。这意味着在处理类似 file-name.txt 这样的字符串时,w 会把它当作三个单词,而 W 会把它当作一个整体。

w                 跳到下一个单词开头,按标点或单词分割
W (shift-w)       跳到下一个字首,长跳,如 "end-of-line" 被认为是一个字
b                 跳到上一个单词开头 (backward),是 `w` 的反向版本
B                 跳到上一个字 (BACKWARD),长跳
e                 跳到当前/下一个单词 (非空字符串) 结尾 (end)
ge                跳到上一个单词的最后一个字符 {nb-char},是 `e` 的反向版本
E                 跳到下一个字尾 (END),长跳
{n}w              跳转到第 n 个单词开头。等价于按 n 次 `w`

行内移动 (line)

行内移动提供了在同一行内快速定位的能力。

0                 跳转至行首,跳转至第 0 个字符
^                 跳转至本行第一个单词的第一个字母位置 (不能为空字符) 
$                 跳转至行尾

f (find) 命令可以在当前行内向右搜索指定字符 (char) 并跳转到它,比如 f; 会找到下一个分号并跳转过去。F 则是向左搜索。分号 ; 可以重复上一次的 fF 搜索,继续向同一方向查找下一个匹配;逗号 , 则向相反方向查找。t(till)命令类似于 f,但它会跳转到目标字符的前一个位置,这在需要删除到某个字符之前的内容时特别有用。

f{char}         向右搜索,光标会跳到搜索到的第一个字符
F{char}         向左搜索,光标会跳到搜索到的第一个字符
;               表示在检索结果中跳转,方向与检索方向一致
`f{char}` 下使用 `;` 则向右跳转,`F{char}` 则向左跳转
,               表示在检索结果中跳转,方向与检索方向一致
`f{char}` 下使用 `,` 则向左跳转,`F{char}` 则向右跳转
t{char}         向右搜索,光标会跳到搜索到的第一个字符
T{char}         向左搜索,光标会跳到搜索到的第一个字符
1 def foo():
2     print("Hello, World!")
              ^^       ^
              12       3

Tips: (1) 输入 `tl` 后跳转至此处;
(2) 输入 `fl` 后跳转至此处,
也是输入 `tl` 再按下 `;` 后跳转的位置;
(3) 输入 `fl` 再按下 `;` 后跳转的位置。

行间移动 (row)

行间移动帮助你在多行文档中快速导航。最基础的是通过 j/k 来上下换行。但这种换行不会改变列位置 (垂直的),如果要进行下 (上) 移一行,并跳转到行首。的操作,原本需要 j^/k^ 两个按键,但实际上存在 “快捷键”:

+ (shift-=)       光标下移一行,并跳到该行第一个非空白字符 (non-blank)
                  `+``<Enter>` 是等价的
-                 光标上移一行,并跳到该行第一个非空白字符 (non-blank)
{n}j              向下移动 n 行
{n}k              向上移动 n 行

此外还有 G (goto) 命令。G 命令在不同上下文中有不同行为:

gg                跳到文件最顶部 (第一行)
                  拓展命令 ggvG 全选 (解释:选中最顶部到最底部。即全选)
G (shift+g)       跳到文件最底部
{n}G              跳到第 n 行 (`10G` 跳转到第 10)

但当需要在大文件中快速定位时,百分比移动会更高效。并且这种基于百分比的导航方式让你对文件位置有直观的把握。

{n}%              按照文件篇幅的百分比进行跳转
50%               跳转到文件正中间的行
10%               跳转到文件开头附近
90%               跳转到接近结尾的位置

屏幕级 (scream)

屏幕相关的移动命令让光标能够相对于当前可见屏幕进行定位。

zt                将当前行置顶 (top)。可以简写为 `H` (head)
zz                将当前行居中。可以简写为 `M` (middle)
zb                将当前行置底 (botten)。可以简写为 `L` (last)
<C-b>             向后翻整页 (backward)
<C-f>             向前翻整页 (forward)
<C-u>             向上翻半页 (up)
<C-d>             向下翻半页 (down)
<C-e>             下移一行,单行滚动 (相当于窗口上移)
<C-y>             上移一行,单行滚动 (相当于窗口上移)

句子/段落移动 (text object)

() 分别移动到上一个和下一个句子的开头,同样地 {} 移动到段落的边界。这些命令在编辑长文档时非常有用,可以让你按照文档的逻辑结构而非简单的行号来导航。但很可惜的是,这种导航方式只在处理自然语言文本的时候比较好用,因为句子的结束符号为句号 (.)、问号 (?) 、感叹号 (!) 和空格 ( <space>),段落的结束符号为空行 (blank line),这两种模式在编写代码时不太可控,尤其是使用句子进行跳转时。

(                 到句首
)                 到句尾
{                 到段首
}                 到段尾

代码块一般使用如下的跳转方式:

[[                跳到上一个代码块(函数/类)开头
]]                跳到下一个代码块开头
[{                跳到当前代码块的起始 {(跳过同级块)
]}                跳到当前代码块的结束 }
[#                在 #if / #endif 等预处理指令间跳转
]#                在 #if / #endif 等预处理指令间跳转

搜索移动 (match mode)

搜索移动是另一种强大的导航方式。输入 / 后跟搜索文本可以向下搜索,? 则向上搜索。找到匹配后,n(next)跳转到下一个匹配项,N 跳转到上一个。* 命令可以搜索光标所在单词的下一个出现位置,# 则搜索上一个出现位置。这种基于内容的导航方式比记住行号更加直观和高效,特别是在阅读或修改代码时,你经常需要跳转到某个变量或函数的其他使用位置。

/<pattern>      向下搜索匹配的文本 (pattern)
?<pattern>      向上搜索匹配的文本 (pattern)
n               按照检索的顺序,光标跳到下一个 (next) 匹配项的首字母处
                这个顺序与检索使用的是 `/` 还是 `?` 有关
                如果是 `/<pattern>` 则向后跳转,`?<parttern>` 则向前
N               按照检索的顺序,光标跳到前一个匹配项的首字母处
                跳转规则与 `n` 完全相反
\c              匹配时忽略大小写,接在 `<pattern>` 后面使用
                e.g. `/hello\c` 即可匹配 `Hello`, `hEllo`, `HEllO`
\C (shift-c)    匹配时大小写敏感,接在 `<pattern>` 后面使用
                e.g. `/Hello\C` 仅能匹配 `Hello`

小技巧:/ 和 ? 共享同一个搜索历史,按 n/N 时方向由最后一次搜索决定

大招来了,其实你不用辛辛苦苦的输入这么长一串 <pattern> 来进行匹配,只需要按两个键即可 (* / #):

1 def foo():
2     print("Hello, World!")
*    搜索光标下的单词 (向下)。在上面的例子中等价于 `/print`
#    搜索光标下的单词 (向上)。在上面的例子中等价于 `?print`

标记 (mark)

标记 (marks) 功能允许你在文档中设置书签,以便快速返回。小写字母标记 (a-z) 是局部标记,每个缓冲区独立;大写字母标记 (A-Z) 是全局标记,可以在不同文件间跳转。这种机制让你在编辑大型项目时能够在多个位置之间快速切换,而不需要记住具体的行号。标记对应的是某个行列号,而非某个文本对象

m<tag>            可以在当前位置设置标记 (tag)
`<tag>            可以跳转回该位置
'<tag>            跳转到标记所在行的第一个非空字符

跳转历史

跳转历史记录了你在文档中的移动轨迹。<C-o> (old) 回退到上一个位置,<C-i> 则前进到下一个位置。这种类似浏览器的前进后退功能让你可以自由地在不同位置间穿梭,而不用担心迷路。特别是在使用 gd (跳转到定义) 或搜索命令后,你可能需要快速返回原来的位置,这时 <C-o> 就非常实用。

<C-o>             回退到上一个位置  
<C-i>             前进到下一个位置

操作符详解(operators)

操作符 (operators) 是 VIM 中执行实际编辑动作的命令。它们与移动命令和文本对象结合使用,形成 VIM 核心的 “操作符+移动” 语法。这种语法设计让 VIM 能够用简洁的按键组合表达复杂的编辑操作,理解操作符的工作原理是掌握 VIM 编辑能力的关键。

最基本的操作符是 d (delete),它用于删除文本。d 命令本身不直接执行删除,而是等待你指定一个范围。这个范围可以由移动命令文本对象提供。例如,dw 删除从当前位置到下一个单词开头的内容。一个重要的细节是,所有被 d 删除的内容都会被复制到寄存器中,这意味着删除操作在 VIM 中本质上是剪切操作,你可以随时用 p 命令将删除的内容粘贴回来。这种设计让误删的风险大大降低,因为几乎所有删除都可以通过撤销或粘贴来恢复。

操作符的一个强大特性是可以与数字前缀组合使用。3dw 会删除三个单词,5yy 复制五行,10>> 将十行增加缩进。数字前缀告诉 VIM 重复执行操作符+移动的组合,这种机制让批量编辑变得非常简洁。

更进阶的是,你可以用点号 (.) 重复上一次的操作,如果上一次操作是 ciw 修改一个单词,移动到另一个单词后按 . 会自动执行相同的修改操作。这种重复机制在需要进行多次相似编辑时特别高效。例如下面的段落。先进行 gUw 操作会将 The 转变成纯大写单词 THE。然后移到下一个单词首字母 进行 . 操作,重复这一过程,即可将整行文本全部转换为大写。

1 The quick brown fox jumps over the lazy dog.
                       ↓                     
2 THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.

掌握操作符的关键是理解 “先告诉 VIM 你想做什么,再告诉 VIM 对什么做” 的思维模式。这与传统编辑器 “先选择,再操作” 的模式相反,需要一些时间适应。但一旦形成了这种思维习惯,便会发现 VIM 的操作符系统异常强大和一致。同样的操作符可以作用于任何移动范围,从单个字符到整个文件,这种统一性大大减少了需要记忆的命令数量。


剪切、复制与粘贴操作符

删除 (Delete)

d 的含义是删除 (Delete),即将指定范围的文本内容删除,并复制到 VIM 的默认寄存器中。是的,在 VIM 中没有纯粹的删除操作,删除其实是剪切

在普通模式下,d 本身不是一个完整命令,必须与一个 移动命令(motion)文本对象 (text object) 结合使用,因此主要有 d{motion}d{text-object} 两种语法形式。另外,还存在命令 dd 用于行级别的删除。

(1) d{motion}:根据光标的移动范围进行删除。例如:

dw                删除到词尾 (delete word)
db                删除光标左边的单词
d$                从当前光标删除到行尾
d^                从当前光标删除到行首
df{char}          删除从当前字符到指定字符 (char) 的数据
d1G               删除光标所在行到第一行的所有数据
dG                删除光标所在行到文档末尾的所有数据
D                 从当前光标字符处,删除到行末
                  等价于 d$
x                 删除当前光标所在的字符,等价于 `dl`
                  所以 `{n}x` 等价于 `d{n}l`
xp                交换光标所在的字符和它后面的一个字符
{n}X (shift+x)    反向删除当前光标左边的字符,等价于 `d{n}h`
                  实际上是两步操作,由于 VIM 中的所有删除行为实质上都是剪切操作。
                  所以能够先 `x` 删除当前字符,再 `p` 粘贴到下一个位置

(2) d{text-object}:基于结构化文本单元进行删除,不依赖光标在该单元中的具体位置。例如:

diw               删除当前单词 (不含空格)
dap               删除整个段落 (含空行)

(3) dd 用于删除整行,语法为 {n}yy。例如:

dd                删除光标所在行
{n}dd             向下删除光标所在行在内 n 行

复制 (yark)

y 的含义是复制 (yank),即将指定范围的文本内容复制到 VIM 的默认寄存器 (或指定寄存器) 中,以便后续粘贴使用。在普通模式下,y 本身不是一个完整命令,必须与一个 移动命令(motion)文本对象 (text object) 结合使用,因此主要有 y{motion}y{text-object} 两种语法形式。另外,还存在命令 yy 用于行级别的复制。

(1) y{motion}:根据光标的移动范围进行复制。例如:

yl                复制当前光标上的字符 (char)
y2j               复制当前行及向下两行
y^                复制当前字符串到行首
y$                复制当前字符串到行末 
y1G               复制光标所在行到第一行的所有数据
yG                复制光标所在行到末行的所有数据
yf{char}          从当前字符复制到指定字符处 (char)

(2) y{text-object}:基于结构化文本单元进行复制,不依赖光标在该单元中的具体位置。例如:

yw                向右复制一个词 (word)
yiw               复制当前单词内部文本
yb                向左复制一个单词,不包含自身
yi(               复制 `( )` 内的内容
yip               复制当前段落

(3) yy 用于复制整行 (等价于 Y),语法为 {n}yy。例如:

2yy               复制从光标所在行起向下 2Y (shift-y)       完全等价于 `yy`

粘贴 (paste)

p                 在光标所在行的右侧、或下方粘贴  
P (shift-p)       在光标所在行的左侧、或上方粘贴

VIM 的复制系统比简单的剪贴板更强大,还它支持多个命名的寄存器,你可以用 "ayy 将行复制到寄存器 a,用 "ap 从寄存器 a 粘贴,这样可以同时保存多个不同片段而不会相互覆盖。这部分属于进阶内容,本文档暂不涉及。


替换、撤销与重复操作符

r{char}           替换单个字符 (`rx` 将当前字符改为 x)
R                 进入连续替换模式,一直替换光标所在字符,直到按下 ESC
u                 撤销上一次更改 (undo)
<C-r>             重做 (redo, 取消撤销)
U (shift+u)       撤销对当前行的所有更改 (慎用,有局限性)
.                 重复上一命令

删除、插入操作符(Change)

c (Change) 相当于在 d 操作之后直接进入插入模式。

cw                删除光标所在字符到词尾的内容,并进入编辑模式
ciw               删除当前词 (word, change inner word),并进入编辑模式
c^                擦除光标至非空行首,并进入编辑模式
c$                擦除光标至行尾,并进入编辑模式
C (shift-c)       擦除从当前位置至行末的内容,并进入编辑模式
                  等价于 `c$` 或者 `Di`
s                 删除当前字符 (char) 并进入编辑模式
                  等价于 `cl`
S (shift-s)       删除光标所在行并进入编辑模式
                  等价于 `cc`

cc 用于删除一整行然后进入插入模式,语法为 {n}cc。例如:

cc                删除光标所在行并进入编辑模式
5cc               向下删除包含光标所在行在内的 5 行,并进入编辑模式

另外,在插入模式下可以通过 <C-N> 进行代码补全。


大小写转换操作符

~ 可以将光标位置的单个字符 (char) 进行大小写转换:

~                 切换光标所在字符的大小写 (大写变小写,小写变大写)

u 操作符可以使目标范围的所有字符转为小写,U 操作符则是反过来,可以使目标范围的所有字符转为大写。语法和示例如下:

  • gu{text-object / motion}:当前选中的单词转换为小写
  • gU{text-object / motion}:当前选中的单词转换为大写
gUaw              将当前词转为大写
gU$               从光标到行尾转大写
guG               从光标到文件末尾转小写
guf{char}         从光标到下一个 {char} 字符转为小写。({char})

折行与行合并操作符

zf                创建折行 (zip-fold)
                  例如 `zfap` 折叠一个段落
zo                打开折行 (zip-open)
zc                关闭折叠 (zip-close)
J                 将下一行合并到当前行。(两行之间用空格连接)
gJ                将下一行合并到当前行。(两行之间无空格)

状态

<C-g>             当前行信息
g <C-g>           字数统计
ga                显示当前字符的 ASCII/Unicode 值

视觉选择(Visual Mode)

v                 进入字符级可视模式 (VISUAL)
V (shift-v)       进入行级可视模式/选中多行/水平选取 (VISUAL-LINE)
<C-v>             进入列级可视模式/选中多列/垂直选取 (VISUAL-BLOCK)
gv                重新选中上一次的可视区域

视觉选择的语法是 v{motion}/v{txt-obj}。下面可视模式下的常用操作 (motion/txt-obj):

d                剪切(删除并放入寄存器)
y                复制(yank)到默认寄存器
c                更改(先删除选中内容,进入插入模式)
p/<C-p>          粘贴(覆盖选中内容,仅在 Visual Block 中有效)
>                增加缩进(向右缩进)
<                减少缩进(向左缩进)
u                转为小写
                 `<C-u>` 表示转为大写
~                切换大小写
:                对选中行范围执行 Ex 命令(自动插入 :'<,'>
`:'<,'>s/foo/bar/g`

高效选择技巧 (无需先进入可视模式)

viw               选中当前光标所在单词(不含空格。(visual inner word)
vaw               选中单词及后续空格。(visual a word)
vit               选中当前标签内的内容 (如 HTML/XML 的 <div>`...`</div>)
vat               选中整个标签 (`<div>...</div>`)
vib               选中括号内的内容 (( `...` ))
vab               选中整个括号对,含括号本身。(`( ... )`)
vip               选中当前段落 (paragraph)

列块模式 <C-v> 的典型用途:

  1. 批量插入:选中多行开头,按 Shift-i,输入文本,按 Esc,所有行同时插入
  2. 批量删除前 N 列<C-v> x / <C-v> d
  3. 对齐数字/注释:选中多行末尾,按 $ 对齐到行尾,插入注释。

文件与项目管理(Command Mode)

文件操作

打开/切换文件

:edit <file>      打开指定文件。简写为 :e <file>
:edit .           打开当前目录的文件浏览器 (需 netrw 插件)。简写为 :e .
:edit *.txt       查找当前目录以 txt 结尾的文件
                  使用 tab 补全提示,左右切换选择文件
:edit **/*.txt    查找当前项目 (多个目录中) 所有带 .txt 后缀的文件
:edit <path>      列出目录下的所有文件,edit 后传目录名参数
                  例如 `:edit ./` 代表列出当前目录所有文件
:find *.txt       同样是查找以 .txt 结尾的文件

保存与退出

:q                退出当前编辑 (quit)
                  如果对缓冲区进行过修改,则会提示
:q!               强制退出 (不保存)
:w [path]         `:w` 将缓冲区写入当前文件,即保存 (write)
                  `:w <path>` 按照路径 (path) 另存为
:wq               保存并退出 (write & quit)
                  `:wq!` 表示强制保存退出,可以保存只读文件 (readonly)
:x                保存并退出。等价于 `:wq`

(1) wqwq! 的区别

  • 有些文件设置了只读 (readonly),只读文件的内容一般是不能被修改的。因此,只读文件不能用 :wq 命令来保存并退出,但是文件的最高权限者 (owner/root) 可通过 :wq! 来强制执行。

(2) :x:wq 的区别

  • :wq 强制性写入文件并退出 (存盘并退出 write and quite)。即使文件没有被修改也强制写入,并更新文件的修改时间。
  • :x 写入文件并退出。仅当文件被修改时才写入,并更新文件修改时间;否则不会更新文件修改时间。
  • 这两者一般情况下没什么不一样,但是在编程方面,对编辑源文件可能会产生重要影响。因为文件即使没有修改,:wq 强制更新文件的修改时间,这样会让 make 编译整个项目时以为文件被修改过了,然后就得重新编译链接生成可执行文件。这可能会产生让人误解的后果,当然也产生了不必要的系统资源花销。不过像是版本控制的软件一般首选还是比较文件内容,修改时间一般不加以理会。

(3) :q:q! 的区别

  • 如果文件有修改,:q 会提示 “有修改,是否退出”,输入 y 退出。而 :q! 会强制直接退出,并且不对当前缓存进行保留。

缓冲区与窗口管理

缓冲区管理 (buffer)

:buffers          查看缓冲区列表。可以简写为 `:ls`
                  - `%` 代表当前活动窗口显示的缓冲区
                  - `#` 上一个活动的缓冲区 (可用 `<C-^>` 快速切换回去)
                  - `a` active:该缓冲区正显示在某个窗口中
                  - `h` hidden:缓冲区已加载但未显示 (因你设置了 `:set hidden`)
                  - `u` unlisted:未列入列表 (如临时缓冲区)
:biffer {n}       切换缓冲区。n 为缓冲区编号。
                  可以简写为 `:b <n>`,其中 `:b #` 表示切换到上一个缓冲区
                  `:b #` 等价于 NORMAL 模式的 `<C-^>` 
:bd [n]           删除当前缓冲区。`:bd`。后可接想要删除的缓冲区编号 n
                  如果缓冲区有未保存的修改,默认会报错,需使用 `:bd!` 强制删除      
:sball            每个缓冲区开一个水平窗格 (split all buffers)
:vert sball       垂直分屏打开所有缓冲区

:buffers 输出示例:

  1 #h   "main.c"                 line 42
  2 %a   "utils.h"                line 10
  3      "README.md"              line 1

窗口管理 (window)

:split [file]     水平分屏打开文件 (split pane),简称为 `:sp`
:vsplit [file]    垂直分屏打开文件 (vertical split pane),简称为 `:vsp`
:only             关闭其他窗口,仅保留当前窗口
:qall             关闭所有窗口并退出 VIM
<C-w> h/j/k/l     在窗口间移动光标 (上下左右)
<C-w> t/b         在窗口间移动光标 (顶部/底部)
<C-w> =           使所有窗口等宽等高  
<C-w> q           关闭当前窗口 (quit)

TIPS:如果需要用多个分屏同时打开多个文件,可以使用命令:vim -o <file_1> <file_2> ... <file_n>


外部命令与终端集成

:!<cmd>           使用 `:!` 命令来执行一个外部命令
:read !<cmd>      将命令执行结果插入到光标所在的位置,可简写为 `:r !<cmd>`
:write !<cmd>     把当前文件所有内容传递给外部命令执行,可简写为 `:w !<cmd>`
                  例如 `:w !python`
:!<shell>         启动子 shell,用 exit 或 <C-D> 返回
                  例如 `:!bash``:!powershell`
:terminal         新建一个水平分屏,在分屏中打开一个新终端 (terminal)
                  可简写为 `:term`
                  使用 `<C-w> N` 可以将焦点切换回 VIM 窗口,结束使用
:vert term        在 VIM 的右侧打开一个垂直分割的终端窗口

文本读写与恢复

:r [file]         在当前光标所在行的下面读入 file 文档的内容
:e!               将文档还原成最原始状态
n1,n2 w [file]    将n1到n2的内容另存为 file 这个文档

查找与替换(Ex 模式)

VIM 的替换命令通用格式为::[range]substitute/pattern/replacement/[flags],其中 :substitute 可以简写为 :s

Syntax:
    :[range]s/pattern/replacement/[flags]

Options:
    - [range]         指定操作的行范围 (可选)
    - s/substitute    表示 “替换” 操作
    - pattern         要查找的字符串 (支持正则表达式)
    - replacement     替换成的新内容
    - [flags]         标志位,如 `g` (全局)`c` (确认)
Example:
    :s/old/new/g      仅当前行替换第一次匹配
    :s/old/new/g      对当前行的所有匹配结果进行替换
    :%s/old/new/g     搜索整个文件,将所有的 `old` 替换为 `new`
    :%s/old/new/gc    搜索整个文件,将所有的 `old` 替换为 `new`
                      每次都要你确认是否替换 (*c=confirm*)
    1,55s/old/new/g   在第 1 行和第 2 行间查找 `old` 这个字符串并替换为 `new`
    1,$s/lod/new/gc   从第 1 行到最后一行,查找 `old` 这个字符串并替换为 `new`
                      在替换前需要用户确认

另外,分隔符 / 可以换成其他字符(如 #|),避免与内容冲突,例如:

:%s#/home/user#/opt/data#g

显示与辅助设置

:set number       显示当前行号,可以简写为 `:set nu`
:set nonumber     隐藏当前行号,可以简写为 `:set nonu`
:set autoindent   自动缩进
:syntax enable    语法高亮
:set hlsearch     查找结果高亮显示
:set warp         自动换行
:set incsearch    立即显示当前输入匹配的
:set ignorecase   忽略大小写
:h :w             帮助

高级操作

代码编辑中的操作

插入模式常用操作

>>                将当前行向右缩进一个单位
                  通常是一个制表符 (Tab) 宽度
<<                将当前行向左缩进一个单位,减少缩进
==                自动调整当前行的缩进,根据代码语法规则自动对齐 (对编程时调整代码格式很有用)
ggvG==            全文件范围内自动调整缩进
<C-n>             代码补全

命令模式常用操作

:!{cmd command}   执行终端命令,例如 ipconfig / python <file.py>

标签与函数跳转(Navigation with Tags),需提前运行 ctags -R . 生成 tags 文件

gd                跳转到当下标识符的定义处 (goto definition)
<C-]>             跳转到光标下标识符的定义(需 tags 文件)  
<C-t>             返回上一个标签位置 (retrun)
:tag funcnam      手动跳转到函数定义

文本对象选择与编辑

(1) 选择括号内内容 (Visual Mode)

# 以 ( ) 为例,同理适用于 [ ] 和 { }
vi(               选中小括号内的内容 (不含括号)。与 `vi)` 等价
va(               选中小括号内的内容 (含括号)。与 `va)` 等价

(2) 删除/修改括号内容

# 以 ( ) 为例,同理适用于 [ ] 和 { }
di(               删除小括号内内容 (不含括号)。与 `di)` 等价
da(               删除小括号及内容 (含括号)。与 `da)` 等价

(3) 其他组合示例

# v 开启视觉模式;f 开启行内检索
vf{char}d         从当前位置选中到下一个字符 x 并删除
vf{char}y         同上,但执行复制

括号匹配

%                 在匹配的括号间跳转。支持 ( )[ ]{ }

代码块导航 (Code Block Navigation)

[[                跳到上一个代码块(函数/类)开头
]]                跳到下一个代码块开头
[{                跳到当前代码块的起始 {(跳过同级块)
]}                跳到当前代码块的结束 }
[#                在 #if / #endif 等预处理指令间跳转
]#                在 #if / #endif 等预处理指令间跳转

注释与标识符查找

# -- NORMAL --
[/                跳到当前注释块的开始
]/                跳到当前注释块的结束
[I                在当前文件中查找光标下标识符的所有出现位置
[|                查找标识符

大小写转换

适用于变量命名规范、常量定义等场景。

# -- NORMAL --
~                 切换单个字符 (char) 的大小写
{n}~              切换 n 个字符 (char) 的大小写
g~w               切换当前单词 (word) 大小写 (大小写互换)
gUw               将当前单词 (word) 转为大写
guw               将当前单词 (word) 转为小写
gU$               从光标到行尾转为大写
guG               从光标到文件末尾转为小写

缩进操作 (Indentation)

<> 操作符用于调整缩进,这在编程时特别实用。

>>                增加当前行的缩进
>{n}j         增加当前行,以及下 n 行的缩进
>}                对当前段落 (花括号块) 整体增加缩进
                  同理适用于 `[ ]``( )`
>G                从当前行到文件末尾全部增加缩进
<<                减少缩进 (用法同 `>`)
==                自动修正当前行缩进
=G                从当前行到文件末尾自动格式化缩进

外部命令过滤文本 (Filter through Shell)

! 操作符允许你通过外部命令来过滤文本。通过执行任意的 shell 命令,来实现强大文本处理能力。这是 VIM 与系统 shell 集成的强大功能之一。

!!                用外部命令处理当前行,并替换为输出结果
                  按下后命令行位置显示 `:.!`,等效于 `:!`
!}                对当前段落 (到下一个空行为止) 执行外部命令
                  `!!` 的变体,范围更广
!}jq              格式化 JSON (需安装 jq)
!}sort            对段落内容按行排序

寄存器(Registers)

"                 访问寄存器 (如 `"ayy` 将当前行复制到寄存器 a)
:reg              查看所有寄存器内容
"+p               从系统剪贴板粘贴 (需 VIM 支持 `+clipboard`)
"*p               X11 选择缓冲区 (Linux)

宏与自动化(Macros)

qa                开始录制宏到寄存器 a  
q                 停止录制  
@a                执行寄存器 a 中的宏  
@@                重复上一次执行的宏

参考文档


附件 A:练习程序

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VIM 练习专用 Python 文件
包含变量、函数、循环、条件判断等基础结构,适合练习删除、复制、替换、跳转等操作
"""

# 基础变量定义(练习单词/字符编辑)
name = "VIM Learner"
age = 25
score = 98.5
is_student = True

# 列表和字典(练习多行编辑)
fruits = ["apple", "banana", "orange", "grape", "watermelon"]
student_info = {
    "name": "Zhang San",
    "age": 20,
    "major": "Computer Science",
    "grades": [85, 90, 92, 88]
}

# 简单函数(练习行操作、跳转)
def calculate_average(numbers):
    """计算列表的平均值"""
    if not numbers:
        return 0.0
    total = sum(numbers)
    average = total / len(numbers)
    return average

# 循环和条件判断(练习块操作)
def process_fruits(fruit_list):
    """遍历水果列表,打印不同类型的水果"""
    for index, fruit in enumerate(fruit_list):
        if len(fruit) > 5:
            print(f"长名称水果 [{index}]: {fruit.upper()}")
        else:
            print(f"短名称水果 [{index}]: {fruit.lower()}")

# 主程序(练习复制粘贴、移动代码块)
if __name__ == "__main__":
    # 练习:将下面的注释取消,并修改变量值
    # print("姓名:", name)
    # print("年龄:", age)
    # print("平均分:", calculate_average(student_info["grades"]))
    
    # 练习:调用函数并修改参数
    process_fruits(fruits)
    
    # 练习:添加新的代码块(如新增一个函数)
    pass

练习题:

 1  请把 /etc/init.d/iptables 复制到 /root/ 目录下,并重命名为 test.txt
 2  用 vim 打开 test.txt 并设置行号
 3  分别向下、向右、向左、向右移动 5 个字符
 4  分别向下、向上翻两页
 5  把光标移动到第 49 行
 
 6  让光标移动到行末,再移动到行首
 7  移动到 test.txt 文件的最后一行
 8  移动到文件的首行
 9  搜索文件中出现的 iptables 并数一下一共出现多少个
10  把从第一行到第三行出现的 iptables 替换成 iptable

11  还原上一步操作
12  把整个文件中所有的 iptables 替换成 iptable
13  把光标移动到 25 行,删除字符 “$”
14  还原上一步操作
15  删除第 50 行

16  还原上一步操作
17  删除从 37 行到 42 行的所有内容
18  还原上一步操作
19  复制 48 行并粘贴到 52 行下面
20  还原上一步操作 (按两次 u)

21  复制从 37 行到 42 行的内容并粘贴到 44 行上面
22  还原上一步操作 (按两次 u)
23  把 37 行到 42 行的内容移动到 19 行下面
24  还原上一步操作 (按两次 u)
25  光标移动到首行,把 /bin/sh 改成 /bin/bash

26  在第一行下面插入新的一行,并输入 “# Hello!”
27  保存文档并退出