再学一次Vim

再学一次Vim

Last Updated
Revised3316 Jun 22, 2024
Published
Apr 30, 2023
Text
🎶NOW LISTENING🎶
Author
Evan Gao
music
告白ライバル宣言・鎖那
(这篇文章的部分图片比较糊。这是由于文章写的比较早,之后更换图床导致图片按 的压缩比被多次压缩,最终 next/image 还会再压一遍。
(From his family)
It is with a heavy heart that we have to inform you that Bram Moolenaar passed away on 3 August 2023.
Bram was suffering from a medical condition that progressed quickly over the last few weeks.
Bram dedicated a large part of his life to VIM and he was very proud of the VIM community that you are all part of.
(Aug, 5)With all the respect, RIP Bram.

 
有一位伟人曾经说过:
/r/ProgrammerHumor/
/r/ProgrammerHumor/
(7.30更新)还有一位伟人曾经说过
It's kinda true.
It's kinda true.
 

前言 Preface

The problem with learning Vim is not that it's hard to do—it's that you have to keep doing it.

为何要写作本文?

关于Vim的教程,无论是中文互联网,或是英文,都已经数不胜数了。然而,大部分的教程都只是把Vim的键位,它的操作等全部罗列出来,忽略了Vim自身的操作逻辑和设计哲学——忽略了开发者的良苦用心。这正踩了许多初学者的雷——「明明我就是不习惯Vim的操作和键位,罗列出来是想让我死记硬背吗」 而他们中的许多人就此对Vim望而却步。
本文希望从Vim的操作逻辑和设计哲学(Vim’s Philosophy)入手,极力避免死记硬背,使每一位读者都能一学就会,让这篇小作品成为您最后一篇Vim教程
 
在此之前,我们有几点需要说明
  • 如果您还不知道什么是Vim的话——
    • Vim是一款免费开源的文本编辑器,最新版本9.0,衍生品有Neovim(本人使用)
  • 本文仅仅涉及Vim的操作——即诸如插件、美化、配置、VimScript之类的知识本文并不会涉及——网上大把。
  • 学习Vim,不是非用Vim不可——不可否认,由于vscode, sublime, xcode编辑器等现代的、易用的编辑器的出现,Vim无论是从友好程度、插件数量、外观设计、配置友好程度等方面都较之落后⁰。然而,Vim仍然受人追捧的重要原因就是它操作的高效与便捷,因此上述的编辑器大多都提供Vim模式,例如vscode的Vim插件;xcode内置vim模式——在实际应用的层面上来说,Vim模式+现代编辑器是很舒适高效的搭配。如果您想要 Hackerman 的感觉,那么也不妨使用vim-airline 或是Emacs-Evil(对于新手来说Doom Emacs是个不错的选择)。
知乎的快捷键,导航如 j/k / gg 等都是受了Vim的影响( ⇧/ 调出)
知乎的快捷键,导航如 j/k / gg 等都是受了Vim的影响( ⇧/ 调出)
  • 边学边用——由于个人表达能力的问题,有许多知识无法表述清楚,如果遇到这样的情况,不妨打开Vim自己试试看。
  • 坚持使用——使用Vim是熟练工种。特别是在肌肉记忆形成之后,我们的编辑效率能够最大化。
  • 记原文词——大多数关键词是我直接从英语译来,考虑到权威性,我们应该要记住更准确的英语原文。
  • 肌肉记忆——初见Vim的人可能被其的复杂性所吓倒,但实际上在一段时间的使用后绝大多数人都能够形成肌肉记忆(归功于Vim操作方法上精妙的设计)。并且在熟练之后,还会习惯性地在其他的地方(word、typora、notion等,虽然它们并不支持)使用这套操作。
  • 再次扣题——本文标题不是Mastering Vim in One Tutorial——不要奢望看完了之后就搞懂了Vim。事实上,Mastering靠的是日常的连续使用,是日积月累的过程。
Vim
VSCodeVimUpdated Sep 12, 2024
💡
vscode的插件能够直接读取诸如.vimrc 等vim配置文件,几乎能够无缝迁移。这也是本人目前使用的方案,强烈推荐
注0
固然有 coc.nvim 这类插件框架助力Vim的现代化(我也在用),但值得注意的是这框架本身就是受了vscode的启发,思路相同——基于node设计的插件框架。虽然这方便了 language server 的实现和开发者跨平台开发插件,对于用户来说配置仍然过于麻烦——即门坎过高,不够便捷。既然基于同样的思路——对用户来说为什么不直接用vscode呢?

为何还要学Vim?

Q:2023年了,还有必要学Vim吗?
A:有。——无论什么年代,打字都是最快捷准确的输入方法(除非语音输入能做到100%正确,但即便如此代码之类也不适合语音输入)。而Vim的设计初衷就是为了使手不离开键盘、不使用方向键(位置尴尬,甚者如HHKB的键盘直接取消了方向键)。在手不离开键盘的基础上,Vim设计了一套直觉性的(Intuitive)的方案(语言)来达到这一点。
手不用离开键盘,这对于每一个码字的文字工作者来说是多么美妙的事情啊——Vim做到了。
 
最后,请记住一点:
请像说中文一样使用Vim吧。

导入:基本配置

 我的.vimrc
我的.vimrc
vim的环境配置文件叫做 .vimrc ,和其他的dotfiles一样,放在 ~ 下最好。
如上文所述,我们并不介绍Vim如何配置,但对初学者来说,最好在配置里加上以下几条,以便使用。( " 表注释)
imap jk <ESC> "insert mode下用jk代替ESC键 map <F7> :tabp<CR> "多分页情况下 map <F8> :tabn<CR> "利用 F7 F8左右切换分页 syntax on "语法高亮 set number "显示绝对行数 set hlsearch "搜索高亮(搜索高亮不会消失,需通过:noh解除高亮) set ignorecase "忽略空格 set incsearch "立即显示搜索结果(输入时即显示) set clipboard+=unnamed,unnamedplus "复制到系统剪贴版(Windows, macOS, Linux)
(注:不建议自动复制到系统剪贴板。因为Vim中的删除操作本质上是将数据移至寄存器(因此Vim不存在「剪贴」一说)。设置此项后每次删除都将复制到系统剪贴板,颇为不便)

关于 Leader

当我们执行自定义的快捷键时,需要加上Leader键前缀。
let mapleader = " " "Leader键设为空格 nmap <Leader>c :set rnu!<CR>
Vim默认的Leader键是 \ ,这颇不方便。设置Leader键时,一种广为接受的方案是将其设置为键盘上最大的、离手指较近的键。此处使用空格,方便操作。
nmap <Leader>c :set rnu!<CR> : 在normal mode下切换绝对行数与相对行数的显示。当我们想要切换时,输入 <Space> + c 即可切换。

调整 LCTRL

macOS中交换 Caps Lock 和 L^
macOS中交换 Caps Lock L^
调整^键对于使用Emacs的人来说是家常便饭。但个人以为每个人用电脑时都应该调整其键位:原来的位置、大小太难受了。
习惯上,我们交换Caps Lock L^ 键——前者太相比后者太不常用了——而且前者更大——对于我这样手小的人来说能够很轻易按到。映射键位的方法有很多,试简单分列成以下三种:
  1. 可编程键盘(对键盘硬编程,最快捷,诸如罗技等厂商的键盘一般都有。)
  1. 系统级修改(e.g. macOS修改系统设置、Windows修改注册表(Sharpkey),后者较为不安全)
  1. 第三方软件(e.g. AHK 所有的输入要先经过它处理,变相增加输入延迟(更不要提Raw Input了)玩游戏可能导致卡顿)
REALFORCE 键盘编程辅助程序
REALFORCE 键盘编程辅助程序
苹果JIS规格的键盘默认交换了这两个键。

一、Vim是一门语言——Vimish

The ‘i’ in vIm stands for INTUITIVE.
要想做到只用一把键盘代替鼠标和上下文菜单的操作,有两条路可走:
一、设计大量、复杂的快捷键。
二、利用一种描述性的语言分解鼠标的动作。
Vim的创始人Bram Moolenaar选择了后者,于是乎才有了它的经久不衰。
特别地,仿照语言学中的「语素」这一概念,我擅自将Vim语言的最小单位(a,c,d,i…)定义为语素。别着急,我们将在之后解释。今后本文的「语素」一词均服从上述定义。
💡
Vim是一门语言,我们像使用自然语言一样使用他。

动词 (operator)

操作
释义
d, delete
删除
c, change
更改
v/V, visually select
按字符/行为单位选中
y/Y, yank
复制所选/整行(不用c是因为被change占用了)
在vim中运行:h operator 有更详细的信息
💡
Vim内置了很全面的帮助文档,善用 :h 。它就像shell中的 man 一样好用。

介词 (modifier)

关键词
释义
i, inside
…中
a, around
…周围
<INT>, integer
整数
t, to(exclusive)
到某字符之前
f, to(inclusive)
到某字符之上
/ or ?, search¹
从前/后开始搜索
注1
  1. vim的搜索支持regex。需要注意的是当我们谈到regex,我们通常指PCRE。而Vim的语法稍有不同。e.g. \b在vim不可用。等价的写法是用\<\> 分别表示词头和词尾。在vim里运行 :h /ordinary-atom 有更细致的说明。
 

名词 (motion & text objects)

名词在Vim语句中通常作宾语——是动词的受词
注意 我们在此不谈先名词的motion特性,后文叙述。
关键词
释义
w, word
单词
句子,()也表示其本身
段落,{}也表示其本身
t, tag
标签,e.g. HTML, XML
b, block
块,相当于圆括号(parenthesis)
注2,3
  1. 名词作为动作(motion)时,使用()而不是s(和substitute冲突)
  1. 名词作为动作(motion)时,使用{}而不是p(和paste冲突)

实例

操作,直译
释义
d10w, delete 10 words
删除10个单词
diw, delete inside word
删除光标位置上的单词,将光标移到下一个单词
daw, delete around word
删除光标位置上的单词,将光标移到下一个单词开头
ciw, change inside word
同上,进入编辑模式
yis, yank inside sentence
复制光标位置上的句子
ct(, change to (
删除光标到下一个(之间的全部内容,进入编辑模式
ci" , change inside "
修改”“内的内容,对 ( { [ < " ' 都有效
可以看出,我们怎样说话,就怎样构建vim语句。
如此,我们避免了重复移动光标的繁琐操作。学会避免重复按键,是一名成熟的vim使用者的体现。当旁人正懊恼地重复敲击着方向键之时,你优雅的通过一句话便轻松的达到了目的。Vim语和自然语言一样,随着使用次数的增加,不需多加思考就能下意识的组织语言。
关于 OperatorMotion 的详细内容请参阅 :h 04.1

二、文件操作

简单介绍:什么是 buffer

在内存不溢出的情况下,几乎所有的文本编辑器在打开文件时都会把文件内容载入内存中。内存中的文件副本就叫做buffer。buffer的概念始终贯穿vim。
e.g. 利用红框框起来的部分是一个个buffer。
notion image
buffer ≠ tab. 在vim中同时存在tab和buffer两个概念。他们并不相同。

具体操作

Obsidian的vim模式是人们常津津乐道的:
notion image
上面问题的答案就在本节。以下是一些简单的文件操作:
操作
释义
:w, write
将buffer写入当前文件
:q, quit
关闭文件
:wq
write & quit,写入并退出
:q!
buffer和原文件有差异,不写入直接退出,强制退出
:x, ZZ
:saveas
:e
打开文件或目录(目录可以进入explorer模式)
:o
打开最近编辑的文件列表
:bd, buffer delete
删除buffer(关闭当前buf),如果有更改,则警告
:bw, buffer wipe
删除buffer,无论有无更改,不警告
:bp/:bn
切换到上/下一个buffer
:tabp/:tabn
切换到上/下一个tab
:tabe
打开/新建tab
:tabc
关闭tab
我们在此不细谈tab/window/buffer的区别,读者对buffer不需太过在意,只需掌握上述最基本的tab操作即可。
(可能再写一篇文章专门讲)
map <F7> :tabp<CR> map <F8> :tabn<CR> " 注:f7,f8和vscode默认的快捷键冲突。 " 如果使用vscode和vim插件, " 建议直接使用vscode自带的快捷键 ^⇥ 和 ^⇧⇥
上述命令可以达到左右切换tab的操作,方便多文件编辑。
💡
map 是键位到字符串的字面义替换。因此,如果需要按下键直接执行命令,需要在结尾增加 <CR> 亦即回车键。
注4,5
  1. 并不完全同上,对于:w,无论buffer和原文件有无差别,总是写入并更新文件修改时间;对于:x和ZZ,如果无差别则不写入,不更新文件修改时间。
  1. 用:w亦可做到。 :w <path>
对于Obisidian的问题,回答就是 q!

三、搜索&跳转

现代人必备的技巧之一就是「善用所有的搜索功能」。对于绝大多数应用,搜索的快捷键是 ^F 。前面说过,Vim使用 /? 来搜索。搜索是vim中极为基础但同时又极为重要的操作,是快速移动光标到目标位置的不二之选。
相关操作如下:
操作
释义
/ or ?{Pattern}
后/前搜索,后接目标字符串或regex
t
跳到某个字符跟前(up to)
f
跳到某个字符上(onto)
*
搜索当前光标上的单词
n/N
前往下/上一个搜索结果
;/,
前往下/上一个跳转结果(针对 t & f
Vim的搜索策略是从当前位置开始搜索,因此 / + <Enter>将跳到这之后最近的匹配结果;而 ? + <Enter>将跳到这之前最近的匹配结果。得益于「最近」这一特点,我们想要快速移动光标使用搜索特别方便。
上面(注1)说过,Vim中的正则和一般使用的PCRE, BRE, ERE不同,它有自己的语法。通过 :h perl-patterns 可以查看语法上的区别。(若编译Vim的时候加上了 +perldo 或使用Neovim,可以通过 :perldo 使用PCRE.)此外,Vim的Regex有四种模式,称为 magic nomagic very magic 等,通过 :h magic 可以查看。——总之Vim的regex语法不是工业标准。
(我习惯使用.Net的regex语法,因此Vim的regex至今我也用的不习惯,特别是涉及到复杂的pattern时。)
此外,Vim还有更强大的搜索功能: :vimgrep (:vim),简单说来强大之处在于能搜索多文件(和 grep 不同它仍使用Vim的regex)。Vim也可以调用外部 grep (通过 :grep),虽然解决了语法问题,但是体验比较差。

四、光标的移动——Motion

编辑文本最基本的操作——移动光标。当我们拥有VIm这把利器的时候,一切都变得简单了起来。
我们将能移动光标的操作都定义为Motion。
一般来说,Motion在Vim语句中,常常作状语修饰Operator。
在自然语言中,我们常常使用介词+宾语来作为状语——介宾短语。仿照这个定义,我擅自定义(modifier + noun)的组合为一个介宾短语,修饰谓语(Operator)。看起来,它也符合Motion的定义。但是在Vim中对此特别做了区分。我们将这些介宾短语称作Text Objects。e.g. iw ip. 我们将在后文对此进行说明。

最基本的操作:上下左右

操作
释义
k/j
上/下移一行
j/h
右/左移一位
之所以如此设计,是由于方向键右下角的位置过于反人类,而 jkl; 则是我们右手闲置时放的位置,只需轻轻动动手指即可以随意移动光标。尽管Vim力图减少重复按键,但即便是重复按键, hjkl 较之方向键都要舒适的多。

行内移动

操作
释义
0
移到开头
$
移到结尾
^
移到开头第一个非空格字符
t
跳到某个字符跟前(up to)
f
跳到某个字符上(onto)

跳词

操作
释义
w/W
跳到下一个词,大写W去除了某些分隔符,一并跳过
b/B
跳到上一个词,大写B作用同上
e
跳到词尾
 
💡
Vim经常通过大小写的区分来实现更多功能,而大小写的功能通常都具有很大相关性。

句、段内移动

操作
释义
(/)
向前/后移一句
{/}
向前/后移一段

屏幕内移动

注意 我们采用vim和emacs表示法: ^X<C-X>
操作
释义
H, M, L
移动到屏幕顶端/中间/底端
gg, G
移动到文件开头/结尾
<C-U>/<C-D>
向上/下移动半个屏幕(即窗口纵像素/2)
<C-F>/<C-B>
PgDn , PgUp
<C-E>/<C-Y>
向上/下滚一行
:<INT>
跳到某行
:(+/-)<n:INT>
向下/上跳n行(设置为相对行数时实用)
💡
Vim经常通过两次相同操作来实现更多功能。e.g. gg dd

反复横跳

不小心跳到别的地方了,想要回去怎么办—— Vim’s got you covered.
仅在normal mode有效
<C-i>/<C-o>
跳回上个位置/再跳回去
关于更详细的信息,请查阅 :h motion.txt
以上这些总结起来,就可以说是Vim Motion的基础用法了。

自此,最基础的光标操作,常用操作都已介绍完毕。接下来,我们介绍Vim中基础的文字编辑手段。

五、四种模式

(严格说来Vim至少有6种模式,剩下的有Replace Mode, Binary Mode. 这两个模式相对不重要,前者我们将在后文介绍;后者略过)
三种模式之间的转换(Made w/ Mermaid)
三种模式之间的转换(Made w/ Mermaid)
模式是Vim的基础之一,通过模式的区分,相同的操作在不同的模式下就有了不同的意义,有许多操作只在特定模式下才有用(e.g. 上述的大部分操作仅在Normal/Visual mode中可用。),模式的存在有效地降低了Vim许多操作的混乱性。
网上大多数教程通常以模式打头,但在我看来这其实并不合适。——模式是Vim所特有的,对于没有经验的初学者来说,这是反直觉的(counter-intuitive)。正如上述Obisidian图片所述:
…this option might make it look like Obsidian has stopped working.
这大概是因为Vim默认进入Normal mode,而此模式下不像其他编辑器一样直接能编辑文本,造成了「程序未响应」的错觉。
我认为介绍模式最好的入口点就是从Normal → Insert mode之时,因此我将其排版在此。

Normal Mode

关于Normal/Insert Mode,有以下几个要点需要叙述:
  • Vim默认进入Normal Mode,且大概是不可更改的(我也没试过,一般也没必要)
    • Normal Mode也叫Command Mode,顾名思义是一般用来输入操作(语句)的模式。
  • Insert Mode是Vim和其他编辑器最相似的地方。进入Insert Mode后可以正常的编辑文件(严格来说是buffer,这时还未写入文件)。
  • Visual Mode是用来选定文本(Visual Select)的,和其他编辑器选定文本差不多——只不过前者强大得多。
    • Visual Mode和Normal Mode差不多,只不过你移动光标时留下了「轨迹」——即选定文本产生的高亮效果。
    • 此模式下的逻辑是针对你选定的文本进行各种操作

Ex Mode

  • Ex Mode⁶是用来输入命令的地方,即按下 : 键会进入的模式。自然,我们上面介绍过的以 : 打头的操作都是在这个模式下进行的。在Ex Mode下编辑文件对于以下情况可能适用:
    • 写脚本时
    • 网络不好的时候需要批处理操作
    • 键盘的 esc^ 这类功能键坏掉了——说实在不如换个键盘
    • (本文提到的所有Ex命令都以 : 打头)
    • 一行多个命令要用 | 分隔开 e.g. :%s/\r//g | wq
    • 读者可能注意到了,本文的「命令」一词特指Ex 命令。尽管如此,我还是要再次强调。与此相对,「语句」和「操作」两词我常常混用,可以等同。语感上「操作」「语素」,「语句」则是多个「语素」构成的复杂操作。

Replace Mode

  • 替换模式和Insert Mode差不多,只不过它会将当前光标下的字符替换为你键入的字符,且连续进行。在替换大段文字时十分有用
    • Normal Mode 下用 R 可以进入
    • <BS> 不是删除字符,而是撤消最近一次的替换操作
注6
  1. Unix中最古老的编辑器是 eded 启发了 ex , 在后者的基础上又产生了 vi (ex’s VIsual mode), vim 又师承vi。事实上Ex Mode就是模拟 ex 的操作。
 
 

七、Normal模式下的语素行为

还记得前面我们讲过的「语素」这一概念吗?不记得的话你还是寄了吧 刚讲过
对于没有语言学基础的人,首先给出「语素」在语言学下的定义:
语素是语言中最小的有意义或有语法功能的单位。 e.g. 对于汉语,「东、南、西、北、白、中、发」就是一组单音节语素。
由上述定义,我们可以得知Vim语言的语素同样如此(behaves the same)。我们首先叙述Normal模式下这些语素的意义(操作, movement)。

更改文本

语素(motion)
释义
i/I, insert
在光标之前/行首插入字符
a/A, append
在光标之后/行末插入字符
o/O, open
在当前行下方/上方新开一行
r/R, replace
替换光标上的字符,大写R替换后进入Insert Mode
c<Motion>, change <Motion>
按某种<Motion>来更改,e.g. cw, ct:, cf?
C
从光标位置开始更改直到行末
s, substitute
从光标位置开始以字符为单位替换(请看下文例子)
S
替换一整行
~(tilda)
改变大小写
e.g.
  • 修改光标上的句子: cis
  • 修改 furbarfoobar : 光标挪到 f 上,输入3sfoo or 光标挪到 u 上,输入 ctboo

删除文本

语素(Motion)
释义
x/X, exterminate
删除光标上/左侧的字符
d<Motion>, delete <Motion>
按某种<Motion>来删除⁷,e.g. dw , dt.
dd
删除当前行
D
从当前位置开始删除直到行末
J, join
删除当前行的下一行。即第(n,n+2)中间的第n+1行
注7
  1. 一般情况下, Vim所指的「删除」只是将字符串从buffer移动到「寄存器」(register)。因此,通过 d , c , s , x 删除的文本,和 y 复制的文本都会被移动(复制)到默认寄存器 "" 。同理, p 默认从 "" 拉取文本。因此,「剪贴」在Vim中的操作和「删除」一致。
    1. 寄存器相关的知识请参看 :h reg 。基础配置中关于剪贴板的设定就运用了相关知识。
 
交换上下两行: ddp

撤消&重做

语素(Motion)
释义
u/<C-r>
撤消/重做
撤消最多可到最后一次保存时的状态。

语句(和命令)的简单重复

尽管我们说过,一个熟练的Vim使用者应该尽量减少重复语句的使用——这并不代表我们能够完全避免重复的操作(此外,省时第一。如果光是想出一个少重复但复杂的方法就花了比实践简单但繁琐的方法还长的时间,那就是舍本逐末,脱裤子放屁了)。为此,Vim通过 . 来重复上次操作。
e.g. 上次操作是 dw ,再删五个词:
5......
除此之外,Vim还可以通过 & 来重复命令。
💡
如果你还不能做到尽量减少重复操作(事实上很多人都不能),至少应该要熟练运用上述知识。这是非常省时的。
e.g. 对于下列以C, M开头的人名末尾打 * 号。
notion image
/^[CM]<CR> A* "在行尾附上* n. "一直执行直到所有匹配者都修改完毕
当然,还有更快捷的方法,这里先不作解释:
:g/^[CM]/norm A*

八、Text Objects

我们前面曾提到过Text Objects这个概念。它可以用一句话来形容——更复杂的noun。
一般来说,我们下面提到和Text Objects相关的操作,要求光标在对象本身之上。
e.g. 你发现光标正在一个单词之上,你希望删掉这个单词(我们用红色表示光标所处位置, 蓝色表示删除): Cleopatra VII
执行以下操作得到的结果是——
  • dw :CleopatraVII
  • diwCleopatra<Space>VII
  • dawCleopatraVII
对比 i / ai 即宾语(motion)本身, a 包括宾语和它之后的空格。
可以看出,Text Object能使得动词的行为更加复杂。
💡
不用太纠结什么时候该用 a 什么时候该用 i 。差别不是很大,不值得耗费那些思考时间。
在上面我们曾经列举过一些Text Objects。为了明确,再举出几个常用的:
  • 单词i / a + w
  • 引号i / a + ' / " —— 字符串
  • 标签i / a + t —— XML HTML
  • 括号i / a + ( / [ / { —— 函数、数组/列表/集合…
  • 关于所有的Text Objects,参看 :h text-object
关于这部分的详细信息,见 :h 04.8

九、标记

标记是Vim中用来(手动)记录光标位置的功能,而光标本身不会显示在界面上。
我们曾在上面(四)叙述过光标的移动。本章所述的标记亦是一种移动光标的方式(部分标记),其具有高自由度,十分强大。
概念时间——标记在Vim中称为Mark-Motion. 自然,他具有(继承,更OOP一点)motion的特性(有一些特例不是),同时也是名词
一般的Vim标记由52个大小写字母中的1个表示:
  • [a-z]:小写标记作用域为当前文件(buffer,严格地)
  • [A-Z]:大写标记,又叫文件标记,一个文件只能有一个。跨文件使用。
操作
释义
m<Mark>
在当前位置做名为<Mark>的标记
'<Mark>
跳到<Mark>所在行的第一个非空白字符
`<Mark>
跳到<Mark>的位置
d'<Mark>
从当前行开始删除直到<Mark>所在行,闭区间
d`<Mark>
从当前位置开始删除直到<Mark>位置,左开右闭(exclusive)
:marks <Mark>s
列出当前所有(指定)标记的详细信息
:delm <Mark>s
删除<Mark>
:delm!
删除所有[a-z]标记,很明显这只在当前文件中起效
💡
依样画葫芦, c y 可以类比 dOperators(谓语)都可以。
💡
建议使用易记的字母。e.g. b for bottom, t for top.
e.g. 假设我们正在编辑 名为.zshrc 的文件,在某处同时设置标记a和A. 当我们下次编辑 .zshrc 时,可以通过 'a 跳至该处;在编辑别的文件时,通过 'A 可以打开 .zshrc 并跳至 a (也是 A)指向的位置。
此外,Vim也自动设置了一些标记,这些标记同样适用于上面的命令,试分列如下:
标记
释义
.
当前buffer上次修改处
"
当前buffer上次退出时的位置
[0-9]
跳到最后一次(指关闭Vim)修改的文件,依照0-9向前迭代
'
跳跃前的位置的那行
`
跳跃前的位置
[]
跳到上次change yanked
<>
跳到上次visual select
关于更详细的信息,请查看 :h mark-motions .

十、文本替换

搜索与替换是两个不可分割的功能。上面我们已经了解了简单的搜索操作,现在介绍Vim中强大的文本替换功能。我们已经知道, :s —— substitution 是替换命令。不过,实现搜索替换的功能不止有此,我们一一叙述。
The quick brown fox jumps over the lazy dog, rushing to the fence.
的所有 the 删除, jumps 改成rolls:
:%s/the//g :s/jumps/rolls
(7.30更新)将所有的Twitter替换为X:
:%s/[Tt]witter/X/g
使用过 sed 的读者可能对此十分熟悉。Vim的替换语法与 sed 大致相似⁸
:[range]s /{regex}/{replacement}/[flag]
由上面的例子:
  • % :替换范围设为整个文件(整个buffer,严格地说)
  • [regex] :默认不分大小写
  • [replacement] :为空则为删除
  • g :即global,和 sedg 一样,对本行的所有匹配结果进行替换。 cf. %
注8
  1. sed 本身受 ed 启发,基于 ed 的脚本功能开发。所以它和Vim其实具有亲缘关系。语法相似是必然的。

先验知识:Range

Range是Vim的范围参数,具体来说指的是行数的范围
在Vim中有一些Ex命令和Operator可以接受范围参数, s 和我们上面说的创建标记m 就是其中的一些。指定 Range 能让我们进行更精确的操作。在上面的例子中, % 就是一个范围参数。下面列举一些常用的范围参数:
Ranges
释义
<UINT>
行数(非负整数)
.
当前行(和shell的 . 当前目录类似)
$
最后一行
'<Mark>
标记<Mark>处
'<MARK>
标记<MARK>处,当标记在另一文件内时不能作为范围参数
/{Regex}/
匹配{Regex}的行,向后搜索
?{Regex}?
匹配{Regex}的行,向前搜索
\/
上次搜索匹配的下一行
\?
上次搜索匹配的上一行
\&
上次替换操作匹配的下一行
若表示区间,使用 , 分隔始点和终点(闭区间):
  • 3,5 :第3~5行
  • .,$ :当前行~最后行
除此之外,通过 +- 可以指定相对行数:
  • 3,+4 :第3和后面的4行 (+4 就是 3 + 4 = 7)
  • 3,+4-1 :第3和后面的3行
Range是基础知识,它贯穿Vim使用中的方方面面,完全有必要掌握。
关于详细的信息,参见: :h range

Global

不止有 :s 可以进行替换, :g 也可以(后者还能做更多)。
注意g 非彼 g ——之前的 g 是在替换文本之后的, sed 中的 g ——它表示是对本行的所有匹配结果进行替换;此处的 :g 指的是对整个buffer进行匹配和后续操作
我们曾在第七章中提到过下述的替换方法:
当然,还有更快捷的方法,这里先不作解释: :g/^[CM]/norm A*
现在,我们就来叙述 :g 命令。
:g (global)是接收regex,对当前文件所有的(因此叫global)匹配(或不匹配)的文本行执行某些操作的命令。
:[range]g(!)/{pattern}/[cmd]
:g 的语法
其中 (!) 表示取反(即所有不匹配的);[cmd] 表示某个命令此指Ex Mode下可以使用的命令⁹ e.g. d y)。如果要使用Normal Mode下的命令(也是最常用的),在[cmd] 处需要使用 norm ,正如上面例子。
注9
  1. e.g. Normal Mode下的 c 和Ex Mode下的 :c ,虽然都是change,但具体行为是不同的。

norm

在很多情景中,我们会在Ex Mode中用到Normal Mode的操作. 然而由于前者的许多命令和后者的许多操作同名,需要区分。因此Vim引人了norm关键字作为标记。在 norm 后由一个空格隔开的就是Normal Mode下的输入。
e.g. 给[3,15]行加上一个 * 号: :3,15norm A*
由上面的例子,显然 norm 也可以接收我们上面说过的Range参数。
详细信息参见 :h norm
我们回到Global的使用:实际上不少情况下 :g 较之 :s 更好用,因此我们可以说两者是一种互补的关系。下面看一个例子:
e.g. 沿用之前的人名列表,对所有C、M开头的名字将所有的 ar 替换为 ur
替换
替换
  • 如果用 :g 实现:
:g/^[CM]/s/ar/ur/g ~~~~~~~~~^~~~~~~~^ " 这里的s就是substitute. " 这里的g就是单行的global
十分简单,注意 :g 是如何搭配 :s 一起使用的。
  • 但如果用 :s 实现:
:%s/^[CM]/\=substitute(getline('.'), 'ar','ur','g')
substitute() getline() 是VimScript中的函数
就显得异常麻烦,可读性也不强,还涉及到了VimScript的内容,超出了本文的范围。
💡
对于需要匹配某些特征,但是又不需要对这些特征作出修改,而是修改同一行内其他的地方,使用 :g 最为方便。
关于更多内容,参见 :h global
文本替换还有一些途径,我们此处叙述了最常用的两种。而事实上这两种途径对于绝大多数场景都足够了。对于更复杂的问题,由于Vim语句的可读性有限,不适合长句,应该考虑用 VimScript, awk 或 perl 解决,不要把时间浪费在这上面。

十一、Another 'g'

Some Vim intrinsics are totally obscure even after years of happy vimming.
Vim怎么有这么多的 g ——我相信这是很多人都有的问题。我想这大概是因为在键盘(只考虑QWERTY)上 g 在左手食指旁边,方便而不易误触。上一章我们叙述了Ex Mode中的 :g ;现在我们将要叙述Normal Mode中的 g
不知您是否记得,其实我们之前在第四章介绍过g的两种用法:
gg:跳回开头 (补充:<INT>gg可以跳到第<INT>行) G:跳到结尾(补充:<INT>G可以跳到第<INT>行)
  • 此外, ga (get ascii)可以显示光标下的字符的ASCII值
  • 此外, <INT_1>GV<INT_2>G 可以选择[<INT_1>,<INT_2>]范围的行。
上面的命令是我最常使用的。
事实上,g可以表示goto,也可以表示get,还可以表示更多。Normal Mode下的g没有特别的用处,只是作为一个前缀。因为按键有限(本质),有许多不常用的操作(当然上面的还是常用的)就失去了独占按键的资格。我们用g作为前缀来invoke这些操作。我们可以将之类比为一种 namespace.
——可能最适合g的名字是 Generic.
像这样的 namespace 在Vim中还有一些,如 z [
通过 :h g :h z :h [ 可以查询到其中各种操作的用途。

十二、Visual Mode

Vim的Visual Mode很贴合它的名字(Live up to its name):有许多之前需要通过命令实现的结果可以以一种交互式(interactive)的方式实现,很「亲民」。同时,它也增强了我们目前学到的各种操作的威力——现在能对所有选定的内容批量执行操作了,更精确,更简单
首先介绍三种Visual Mode变体:
  • Visual Mode
    • Visual v —— character-based(按字符选择)
    • Visual Line V —— line-based(按行选择)
    • Visual Block <C-v> —— block-based(按矩形区域选择,若是处理矩阵型的数据很方便)
      • o 可以将光标移到矩形区域对角线另一端,cf. $ 将光标(此处是矩形区域的边)移到行末
图示:Visual Block
图示:Visual Block
之前讲过, v 本身也是动词——我们可以搭配modifier来使用他:
  • vi" :选取两个引号之间的内容
  • 若光标在嵌套括号之内:
T FindNearest(CoordT x, CoordT y) const { assert(this->Count() > 0); CoordT xy[2] = { x, y }; return this->FindNearestRecursive(xy, this->root, 0).first; }
Part of a K-D Tree implementation.
  • 如上例,假设光标位于 { x, y } 中:
    • vi{ :选定括号内的代码
    • v2i{ :选定上一级括号内的代码,即函数体
    • 依此类推
在Visual Mode下,我们的熟悉的Motions都会用于更改选定的内容或选定区域本身。譬如按 w 可以以词为单位扩大选定区域、以 hjkl 向四个方向按字母扩大选定区域;再对选定的区域执行某些操作( d c y

作为Range

我们上面叙述过Range这一范围参数。显然Visual Mode能够选定内容,可以作为一种范围参数来使用。这为我们修改文本提供了更多的可能性,同时还具备其他模式不具备的交互性。

搭配重复操作

e.g. Toggle Comment
export interface PageError { message?: string statusCode: number }
修改前
//export interface PageError { // message?: string // statusCode: number //}
修改后
(这方法目前在vscode中不管用)
  1. 在首行添加 //
  1. 选中剩下三行
  1. 我们上面曾提到 norm ,这时就派上用场了:
    1. 执行 :'<,'>norm. 即可
这是Range和重复命令 . 的经典搭配。
不妨在 .vimrc 中加入以下内容:
vnoremap . :norm.<CR>
这时候我们在步骤2之后按下 . 即可。
当然,这自然不是最简便的方法,我们先往下看。

Multicursor

还是看上面的例子,一种更简便的方法是采用Visual Block Mode:
  1. <C-v> 进入Visual Block Mode,选定这4行
  1. I 进入Insert Mode,光标会移动到选定的第一行行首
  1. 输入 //
  1. 回到Normal Mode( Esc or jk,如果你和我的键位一样)
当然咯,折腾了这么久,还不如vscode的⌘/来的快,我的建议仍是怎样快怎样来)
Multicursor——正如其字面义,有多个光标,能够同时批量对这些光标下的文本进行操作。但Vim自带的Multicursor仅仅可用于Visual Block Mode中,若需要更强大的功能则需要依靠插件
再看一个华而不实的例子:
构造一个递增的奇数列
  1. 键入1, V 选中这行, Y 复制 注意 因为我们没有换行,所以如此操作在行末增加了一个换行符
  1. 100p<C-v>99j 复制100遍,进入Block Mode,选中
  1. g<C-a> 每行在前一行的基础上 + 1
当然,这并不能体现Vim的优越性。上面的解决方案明显不如
for i in range(1,202,2): print(i)
w/ Python
Or if you are a man of culture
(loop for x from 1 to 202 by 2 do (print x))
w/ Common Lisp (Prism.js没有CL的代码高亮,只好退而求其次用Lisp的)
因此,我个人认为,也一直强调Vim的操作应该是直觉性的,不需要太多的思考,一切向生产力看齐——当你认为一些操作可能降低自己的效率的时候就不必吊死在Vim上了。

Indentation

在生产环境中,保持一致的代码风格比较重要,而代码缩进正是其中的重要部分。我们可以通过Multicursor来对选定部分的代码进行简单的缩进操作。
  • >> 可以向右缩进
  • << 可以向左缩进
缩进量则是根据 .vimrc 中的设置。默认情况下是一个 <Tab> 或八个 <Space> .
Vim在不装相关插件的情况下的Multicursor的功能是颇为孱弱的,并不能实时看到多个光标,不是很清晰明了。而Vscode的Vim插件则自带了货真价实的Multicursor功能,在此就不多赘述。
Multicursor in vscode
Multicursor in vscode

十三、Increment & Decrement

有时候我们需要对文本中的数字进行加减操作,通过Vim的快捷键可以很方便的实现这一点:
  • [count]<C-a> :光标上数字增加[count]
  • [count]<C-x> :光标上数字减少[count]
上面的操作无论是Normal还是Visual Mode都可以直接使用。
在刚刚的Multicursor叙述中我们曾经用过一个操作: g<C-a> ,与上面的相比,此处的 g (又是g!Vim最乱的键之一)表示操作具有上下文关联的特性——根据Vim手册的介绍——
Add [count] to the number or alphabetic character in the highlighted text. If several lines are highlighted, each one will be incremented by an additional [count] (so effectively creating a [count] incrementing sequence).
这告诉我们增加/减少的值将在上一行的基础上进行。因此若还是考虑上面那个例子,将有如下的结果:
1 3 5 7 9
g<C-a>
1 3 3 3 3
<C-a>
不仅如此,我们甚至可以对字母、二进制、八进制、十六进制进行加减操作:
  • a(A) —— <C-a> ——> b(B)
  • 0b001 —— 3<C-a> ——> 0b100
  • 0x0f —— 16<C-a> ——> 0x1f
不仅如此,通过设置 nrformats ¹⁰,我们还有更多的选择。较为琐碎,只放在注解处。
注10
  1. nrformatsnf 参数告诉Vim如何对待数字和字母的。可选值有(标红为默认启用):
      • alpha —— 启用则字母可以加减
      • octal —— 启用则八进制数(形如 02 这类有一前导零者)可以加减
      • hex —— 启用则十六进制数(形如 0x02)可以加减
      • bin —— 启用则二进制数(形如 0b10)可以加减
      • unsigned —— 启用则将所有十进制数视为无符号整数加减,无论它前面是什么符号
      总的说来和沿袭了 C/C++ 的格式习惯
      我个人使用 nf=alpha,hex,bin .
Vim不自带高精度运算,因此超过 (u)int64(unsigned)long long 范围的加减操作将被舍入到最近的合法数字而不造成Overflow.
基本上,递增递减最常用之处就是构造有序列表。

十四、宏Macro

和其他程序中的「宏」一样,Vim中的宏单纯是记录所有的操作并复现而已。相当于加强版的 . 。还记得我们前面曾经提到的 Register 「寄存器」吗?Vim将录制的操作也存在其中。当然这两种寄存器并不能画等号。
同之前一样,不同的寄存器用一个字符来区分,因此我们可以将操作存储在 [a-z] 这26个寄存器中。下面介绍具体的使用方式,我们用 <Reg> 表示寄存器。
  • q<Reg> :开始录制宏,并存入 <Reg>
    • q :再次输入来结束录制
录制宏 a 中。 注意左下的 recording 字样
录制宏 a 中。 注意左下的 recording 字样
  • <INT>@<Reg> :回放 <INT> 遍宏
关于如何对待重复性、程序性的工作,我们上面已经介绍过了一些强大的方法,而「宏」和这些的区别就是复杂度。后者可实现的行为的复杂度更高,即能够处理更多的问题。
💡
搭配前叙之Visual Mode和norm,我们能够针对选定区域套用录制的宏,也就是 :norm <INT>@<Reg>

附、零散知识

我们已经正式叙述完了Vim使用方法的主体部分,以下是一些零散知识,因为普遍比较应用而不好划分范围,因而单独分列。这部分的说明比较简略。

换行问题

Windows/DOS下的换行是 CRLF 亦即 \r\n 而类Unix下则是 LF 亦即 \n 。因此有时候会在类Unix下的Vim中看见 ^M ,其代表的就是 CR
错误地用LF模式打开了CRLF的文件将会出现 ^M
错误地用LF模式打开了CRLF的文件将会出现 ^M
CRLF → LF (仅在LF模式下打开CRLF文件有效)
:%s/\r// "删除末尾的\r 或者 :%s/^M// "其中^M就是字符本身,而不是转义符\r
还可以
:set ff=unix | wq
LF → CRLF
因为无法直接替换¹¹,所以只能
:set ff=dos | wq
当然用 sed awk tr col perl之类的亦可以实现,vscode也能直接修改,很方便。
注11
(懒得翻译,但就是这个原因)
If you enter a value of 10, it will end up in the file as a 0. The 10 is a <NL>, which is used internally to represent the <Nul> character. When writing the buffer to a file, the <NL> character is translated into <Nul>. The <NL> character is written at the end of each line. Thus if you want to insert a <NL> character in a file you will have to make a line break.

键入特殊字符

有时候会需要键入不方便打出的特殊字符,Vim在这方面也有充分的支持。

^V / ^Q

Insert next non-digit literally. It's also possible to enter the decimal, octal or hexadecimal value of a character.
Insert Mode下输入 <C-v><C-q> (前者基本被占用了,都是用后者,两者等价),后面跟着
  • 数字——将键入数字(八、十、十六进制)对应的字符(Unicode)
    • <C-q>065
    • <C-q>u0041
    • <C-q>x41
    • 都将键入A(U+0041, 65)
  • 字符——将键入字符对应的数字对应的字符——字符本身
    • <C-q>A 将键入A
  • 控制符(如上面的CR,以^起头)——将键入控制符本身
    • <C-q><C-m> 将键入 ^M\r

Digraphs (^K)

上面的方法不是太方便,需要记住编码才能够有效使用。因此Vim提供了一个更方便的方案来处理这项工作。这是Vim的Intuitive又一体现。
Digraphs是一张字符映射表,将两个普通字符的排列(称Digraph)映射到1305个特殊字符上,因此我们只需要键入两个普通字符就能键入特殊字符了。而这两个字符不是任意排列的,套用汉字「六书」的概念的话,大概是用到了「象形」「会意」,便于记忆。
  • :h digraphs-default 可以了解具体的规则,此处不列举。
  • :dig 可以查看整张表
  • <C-k> 后跟对应Digraph
:dig in Vim
:dig in Vim
:dig in VScodeVim
:dig in VScodeVim
e.g. U+00B4 ACUTE ACCENT (´)
Acute accent(尖音符) 作为一种Diacritic(变音符) 在拉丁、西里尔、希腊书写系统中都广泛使用。由于英法百年战争所以英语也吸收了不少的法语(Français, aka. Ape Language)外来语,e.g. cliché résumé 因此使用还是十分普遍的。
<C-k>e'
  • ' (Apostrophe,回车左边的)在Digraph表中指代(长得像´ ,因此字母+ ' 可以得到对应字母的尖音符,很容易记忆,思想上类似死键(Dead Key)
(当然前提是对应字母有尖音符)
macOS下直接键入⌥e+字母即可。⌥本质代替了一部分Alt Gr的功能,十分方便。

Reference

多读Vim自带的手册,其中有最全和最准确的说明。这也是我为什么常在文中指出对应的entry.
(本文布局谋篇参考对象,必读,作者从09年写到21年,The Legend👇)
还有一些零散资料(不全)

结语

这篇文章只是概括了Vim的主要功能和操作,然而我也洋洋洒洒写了一万多字。但是其中的内容其实都很好理解,写这篇文章只是为了昭告三点:
  • Vim十分可学
  • Vim十分强大
  • Vim十分快捷
我相信,对于我们这般天天打字的工作者来说,首先要熟练掌握自己的文字编辑器,才能抓住一念之间的奇思妙想。无论学习使用Vim是为了装逼还是提高生产力还是想要提升自己的能力,我希望这篇文章都能有一些微薄的可取之处。

动笔
二三,四,三零
完工
七,一零