Vim 的哲学第四篇姗姗来迟,狗血的原因我就不多说了,好消息是我将为这个系列带来一些动态演示。原本我打算录视频的,但是文章都写了那么些篇了,现在再录视频似乎晚了些,所以我研究了一下如何录制高质量的 GIF 动画(第三方软件都不好用,最后我还是用 QuickTime 和一段脚本来完成录制,挺酷的~)。接下来先奉上第一弹:
查漏补缺
上一期的基础配置我遗漏了一个蛮重要的选项:shiftround
,这个选项真的很贴心很好用,遗憾的是官方文档对此语焉不详。我特意 Google 了一下才发现挺少有人解释这个选项的。现在我也忘记了当初我是怎么知道它的,然后我发现用文字也还挺难解释清楚,所以还是看动画吧:
round 在这里应该是取整的意思。当你的缩进不成倍时,开启这个选项将会让 Vim 自动帮你把周围的缩进化零为整,你就不需要手动去填/删空格了。顺便提前讲一下,缩进的指令是 <
或 >
键,它们支持移动指令(马上讲到),也支持数字前缀。对当前行执行缩进是 <<
或 >>
,也就是连按两次。我们在以后会详细介绍关于缩进的知识。
OK,加上它在你的基础配置里,我们开始新的旅程!
移动与编辑
现在我们知道使用 Vim 的一个很重要的原则就是远离鼠标,远离可视化的定位装置(当然除了键盘以外),原因我就不再赘述了。在此原则之下,必然要有一些方式来帮助我们选中目标然后做出我们希望的更改,也就是移动和编辑。
在常规模式里有两个很重要的概念,一个叫做“动作”(Motions),另一个叫做“操作”(Operators)。动作,是指你能让光标移动到哪里,而操作则需要结合动作来决定你可以对文本做什么。通过二者的结合,我们可以逐渐体会 Vim 是如何贯彻保持简单这一原则的。
hjkl
虽然简单,但是在很多场合之下它们太没效率了,对么?从现在开始我希望你记住:我们通常不用 hjkl
做大量的光标移动。如果你发现你经常抽疯似的按这四个键在屏幕里来回移动,那你就已经错了!这四个键真正的主要用处是作为其他动作和操作的辅助键来使用的,随着我们的深入学习你会越来越理解这一点。
那么除此之外我们还有什么选择呢?
横向移动 :help left-right-motions
当我们以行/段为单位来审视我们的文本时,Vim 为我们提供了一些横向移动的快捷动作。
移动指令 | 移动效果 |
---|---|
0 |
移动光标至行首 |
$ |
移动光标至行尾 |
^ |
移动光标至行首的第一个非空白符的字符 |
g_ |
移动光标至行尾的最后一个非空白符的字符 |
f{char} F{char}
|
向前(右)或向后(左)移动光标至指定的字符({char} )处,光标停留在该字符之上 |
t{char} T{char}
|
向前(右)或向后(左)移动光标至指定的字符({char} )处,光标停留在该字符前/后面 |
通过上面这个表格,我们可以获取到以下信息:
- Vim 的许多指令都是成对儿的(非常重要),学习各种指令时尝试成对儿去练习会事半功倍。
- 空白符(空格,Tab)在 Vim 里是很重要的,许多命令都有不同的版本来应对有/没有空白符的状况。这主要是为了满足不同的人群习惯,比如说码字儿的不是很在乎多一两个空格,但是写代码的就不一样了。
- 有些命令脱胎于正则表达式(相信你注意到了),所以结合正则来学习也能帮助你理解它们。
- 最后两行的那几个移动指令可能有一点不好理解,但是它们实在是太有用了,请你亲自试一下。以后我再深入介绍它们。
这还没完,对于横向移动来说,最麻烦的是当“回绕”(wrap)出现的时候。所谓回绕是指,当一行(段)的字符数目超过屏幕的可视宽度范围时,编辑器会将超出的部分自动转移到下一行来显示,但是这并非换行,也就是说没有插入换行符,只是在显示上不让字符超出屏幕的最大宽度范围而已。
这个特性当然不是 Vim 特有的,几乎所有的编辑器都支持回绕,可是 Vim 对待回绕是和其他编辑器完全不同的,尤其是在编辑长文档时会让你觉得有些古怪。我不会在本章详细介绍关于回绕的一切,因为本系列面向的读者主要是程序员。对于编写代码这样的工作,只要你遵循良好的编码规范(你应该这么做),那就很少会出现应对回绕的情况。所以目前为止你学会上述四个动作指令就足够了。未来的某一天我会专门写一篇如何打造专业的 Markdown 编辑功能,在那时候我们再回过头来好好谈谈回绕。
现在我列出横向移动时如果遇到回绕行我们可以做什么,这样你可以自己试一下:
移动指令 | 移动效果 |
---|---|
gh gj gk gl
|
让光标在回绕行内做四方向移动 |
g0 |
移动光标至当前回绕行的行首 |
g$ |
移动光标至当前回绕行的行尾 |
g^ |
移动光标至当前回绕行的行首的第一个非空白符的字符 |
gm |
移动光标至当前回绕行的中间位置(或尽可能接近中间的位置) |
关于 g
键
你或许已经注意到 g
键的多次出现了,似乎它和其他指令相互配合可以产生许多新指令。没错,在 Vim 中有那么几个“万能的”指令修饰键,g
是其中之一。如果你好奇还有多少指令是用 g
来修饰的,你可以键入 :help g
来查看一个列表。另外你应该知道这份列表其实是索引文档的一部分,你可以时常打开索引(:help index.text
)来考察下自己对 Vim 到底有多熟悉。
纵向移动 :help up-down-motions
纵向移动的指令比较多,我还是先介绍几个简单并且最有用的:
移动指令 | 移动效果 |
---|---|
gg |
让光标跳转到文档的最开始处 |
G |
让光标跳转到文档的最后一行 |
{count}G :{count}
|
让光标跳转到指定的行号,即 {count} 所代表的行号 |
{count}% |
让光标跳转到指定的百分比位置,比如说第一行是 0%,最后一行是 100% 等 |
H |
让光标移动到当前屏幕的顶部(High Position),不滚屏 |
M |
让光标移动到当前屏幕的中部(Middle Position),不滚屏 |
L |
让光标移动到当前屏幕的底部(Low Position),不滚屏 |
{count}
是一个前缀标记,意思是这个指令前面可以追加数字,比如说如果你键入 25G
,那么光标就会移动到当前文档的第 25 行去。这个特性非常重要,Vim 的强大和灵活在很大程度上都仰仗类似的前缀特性。
G
和 {count}G
其实是同一个指令,只是带上数字前缀与否会产生不同的效果,所以我分开写了。这是第一次,一旦你了解了这个特点,以后我就没必要分开了。
gg
和 G
是一对经典的搭档,许多非常有用的操作都是它们俩配合完成的。举个例子,《Vim 的哲学》所有的文字都是在Vim 里完成的,每一次到最后我都要把它们复制粘贴到 SegmentFault 博客的发表页面做最后的检查并且发布,如何全部复制过去?有很多办法,我一般选择如下两种:
- 如果我使用 MacVim(GUI 环境),那么我先
command + a
,然后"+y
或者"*y
- 如果我使用 CLI Vim(命令行环境),就变成一串指令:
gg"+yG
,拆开看:gg
+"+y
+G
喂喂,command + a
是全选吧,这是 Vim 的指令吗?你别忽悠我喔~
我可没有说你只能用 Vim 的内置指令呀,了解你所处的环境,在不影响效率,不打乱节奏的前提下善于利用一切可以利用的工具,这不正是极客的特质之一吗?只不过我不是每次都有机会用到 MacVim 的,所以 gg
+ G
的经典组合还是必须要掌握的。至于 "+y
,这个涉及到寄存器(:help registers
)的知识,我们稍后就会讲到,别急。
以词为单位的移动 :help word-motions
hjkl
是以字符为单位来移动的,横向和纵向移动基本上都是以句/段为单位来使用的,然而有些情况下我们需要的是介于二者之间的移动单位,也就是以词为单位。以词为单位使得我们可以更精确(也是更具语义化)的移动光标,并且要比逐个字符的移动要快得多。
这里所说的词特指的是像英语那样的以空格(有时候也会是别的符号)为分隔符的词——很遗憾,中文分词是很大的挑战,Vim 没有内置这一功能。像这样的词都可以分出词头和词尾,比如 word 这个词,词头是 w,词尾是 d。
于是在 Vim 中的词组移动也按照词分头尾的特性分成了两组:
移动指令 | 移动效果 |
---|---|
w b
|
向前(右)或向后(左)移动光标至下一个词组的词头位置 |
e ge
|
向前(右)或向后(左)移动光标至下一个词组的词尾位置 |
一些朋友喜欢寻找每一个指令对应的含义,这样有助于形象记忆。事实上,这些含义也不是我编造出来的,内置的文档里都有很详细的描述,比如说上面这几个分别是:
-
w
:Words forward -
b
:words Backward -
e
:forward to the End of word -
ge
:backward to the End of word
你可能在想:我干嘛要关心词头和词尾,好麻烦啊!这个问题其实无关于 Vim 的哲学,而是语言的哲学。
你看,咱们的汉语和英语是完全不同的两个语种。在汉语里词与词的界限是靠意义来划分,书写形式则不太重要。用汉语写一句话,你可以在词组之间添加空格或者不添加空格,基本上不会对读者产生影响(除了个别会产生歧义的特例)。而英文及其他类似语言则不然,它们加或不加空格的差别大了去了!一段英文如果没有空格,那几乎就是无法阅读的。因此,对于母语为类似语种的人群来说,空格所划分出的词头与词尾是自然而然,司空见惯的事情,他们一点也不会觉得奇怪。不幸的是,作为程序员的我们也(被迫)得使用英语作为我们的主要书写语言,因此你必须习惯去辨识和使用词头与词尾。一旦你习惯了,你会发现它们非常有用。
为了让你看到正确使用词头词尾的效果,我放一张图给你对比一下差别。在这张图里,我分别演示了四种操作:
- 在句首,向右删除两个单词,使用词头作为动作指令(
w
) - 在句首,向右删除两个单词,使用词尾作为动作指令(
e
) - 在句尾,向左删除两个单词,使用词头作为动作指令(
b
) - 在句尾,向左删除两个单词,使用词尾作为动作指令(
ge
)
每一步操作之后,注意观察光标停留的位置和最终的效果:
这些结果或许是你期望的,也可能不是,但这不要紧,没有哪一种是绝对正确或错误的,重要的是你需要了解它们之间的差别,于是你可以在必要的时候选择正确的方式。
不过故事还没完,以上四个指令各自还有一个变体,分别是:W
,B
,E
,gE
。要了解它们的作用,我们得先聊一下词的定界符。
对于词和词之间,空格是唯一的区隔标准吗?很显然不是。像这样的词:i_am_a_word
,Vim 会视为 1 个词,但是 i-am-a-word
,Vim 则会视为 7 个词!这是因为 Vim 允许你为其指定可以被视作词组定界符的字符,于是当 Vim 遇到这些字符的时候,就会认为是一个词的结束。默认情况下,_
不是定界符,所以它会被视作一个词的组成部分。
然而有些时候我们希望把这些定界符也当作词组的一部分,这样我们可以移动的快速一点,这时大写版本的词组移动指令就派上用场了,它们永远都只把空白符(空格、TAB、EOL)视作词组的定界符。
你会觉得自己定义定界符很酷吧?我会把它放在高级设置那一篇来讲。
基础编辑 :help operator
Vim 内置了 15 个编辑指令(还有一些变体),但是一般来说我们用不到那么多。在本节我们来学习其中的五种(共计 10 个):
删除 :help d
如果你把光标对准某个字符,然后按下 d
(delete),你会发现什么都没有发生?不要惊讶,编辑操作是要配合移动指令来干活的,我之前花大力气介绍一堆移动指令不是漫无目的的不是?
OK,精彩的来了。当你按下 d
,Vim 会说:“好的伙计,你想要删除对吧?接下来请告诉我你要删什么?”
如果你要删一个词,按下 dw
,也就是 delete word。
如果你要删除两个词,按下 d2w
或者 2dw
,它们的效果是一样的,但是它们代表的含义略有差别:
-
d2w
意思是:删除 -> 2 个 -> 词 -
2dw
意思是:2 次 -> 删除 -> 1 个词
在这个例子里,两种操作的结果不会产生歧义,所以你能得到一样的效果。但是以后你会发现在某些特定的条件下,数字前缀在不一样的地方会产生不一样的效果(不只局限于删除操作)。所以,正确的理解操作的含义是有必要的,请记住:
理解操作的含义,而不是背诵操作的顺序。就好像你说话说的是你想要表达的意思,而不是字词的某种排列组合。有些时候你颠倒字词的顺序不会影响你要表达的意思,因为不存在歧义,但有些时候则正好相反,切记切记!
如果我要删除一整行怎么办?简单:dd
。
那如果我想要从光标的位置开始一直删除到行结束呢?那还用我教你?d$
!不过 Vim 还有另外一个版本等价于 d$
,它是:D
。
哦,那这么说如果我使用 0d$
或者 0D
,就是和 dd
等价的咯?
啊哈~聪明的童鞋,很抱歉你错了!但是不怪你,这是一个很重要的区别,我们来单独看一下示范:
看明白了吗?其实差异是非常明显的,dd
是连同行尾的行结束符(EOL)一起删除的,所以粘贴的时候也会连着行结束符一起粘贴;而 0D
/0d$
则不会包含行结束符。
你可能还纳闷呢,不是演示删除的吗?为什么删掉的东西还能再粘贴回来呢?嗯,可能删除这个词不太恰当,如果改叫剪切是不是忽然就觉得贴切起来了?
Vim 的删除操作和我们常见的剪切非常相似,事实上 Vim 的删除指的是从你的眼前把目标文字移除到寄存器中,之后你还可以从寄存器里把删除的部分再粘贴回来。Vim 拥有一堆各式各样的寄存器,擅于使用寄存器可以让你的编辑工作变得异常轻松。今后我们会单独介绍寄存器的进阶使用。
让我来问你一个问题:如果要删除一个字符该怎么办?你或许已经了解到 x
可以删除光标所在的那个字符,X
可以删除光标左边的那个字符,但是你是否知道这两个功能也是从 d
演化出来的呢?试着找找答案吧。
改写 :help c
在你执行完删除之后,紧接着按下 i
(insert),你就等于在改写之前删除的内容了。如何把这两步简化成一步?答案就是 c
(change)了。对于 c
,真的没什么好讲的,你学会了 d
就等于学会了 c
,因为 c
就等于 d
完了紧接着 i
而已。
而且其他相关联的操作也是类似的,比如说:
-
c2w
:改写两个词 -
cl
:改写光标所在位置的字符,并且s
等价于cl
-
cc
:改写光标所在的一整行,并且S
等价于cc
-
c^
/c$
:从光标所在位置开始一直改写到行头/行尾,并且C
等价于c$
s
比较少用,因为通常改一个字符我们会使用 r
,也就是替换(replace),但是 s
在编辑中文的时候有妙用,容我在这里卖个关子,等到打造专业 Markdown 编辑器的时候再说(实在是不能都说了,要不然这篇结束不了了)。
S
比 cc
多节省一次按键,而且也比较好按(在标准键位上),所以推荐用 S
来代替 cc
。
复制 :help y
复制是几个基础编辑操作里怪癖略多的一个。首先是它的命名,y
是 yank 的首字母,但是 yank 又是什么?它和拷贝(复制)有什么关系呢?
这也和寄存器有关。你看,Vim 的复制/剪切(删除)/粘贴操作都是基于它底层的寄存器的,由于 c
已经被改写(change)占用了,而 Vim 的复制实质上是把目标文本拉拽(yank)到寄存器中备用,所以……好了你知道了就是了,咱不解释那么多,反正 y
就是复制了,爱咋咋地~
另外一个怪癖出在 Y
身上,按照之前删除和改写的经验,你一定会认为 Y
就等同于 y$
呗,(Vim 乱入:“呵呵,图样图森破!你以为我会让你这么轻易就掌握诀窍吗,少年?”)可是很不幸你又错了。这一回,Y
又和 yy
等价了……
我一直都没闹清楚为什么到了复制这里就和删除/改写不一样了,就连官方的帮助文档都是这么说的:
如果你希望
Y
是从光标处复制到行尾(这样更合乎逻辑,不过不兼容 Vi),你可以使用:map Y y$
看起来唯一的原因就是为了和老 Vi 兼容,但是我们完全不在乎这一点!后面的 :map Y y$
是键位映射,虽然我们还没讲到,不过这一句你已经可以把它放到你的 .vimrc
里了,重启 Vim 之后你会发现 Y
的表现和 C
D
它们保持一致了,谢天谢地!
粘贴 :help p
粘贴就单纯多了,只有两个指令:
操作指令 | 移动效果 |
---|---|
p |
自光标所在位置向右粘贴默认寄存器里的内容 |
P |
自光标所在位置向左粘贴默认寄存器里的内容 |
默认寄存器里的内容取决于你在粘贴前最后的编辑动作,有可能是删除或复制的一段文本,也有可能是其他的。由于我们还没有详细介绍强大的寄存器功能,你或许会偶尔感到有些不便,在这里我先介绍一个最常用的技巧:
有时候,我们需要完成如下操作:
- 在某处复制或删除(剪切)了一些文本
- 在另外一处删除一些文本
- 把之前复制或删除(剪切)的文本粘贴到这里
你看,这实际上是要用 A 处的文本来替换 B 处的文本,但由于默认寄存器只保留了最后一次的复制/删除(剪切)内容,所以当你完成第 2 步的时候,你在第 1 步准备好的文本已经没了……大多数人是这么做的:
- 在某处复制或删除(剪切)了一些文本
- 来到另外一处,先把这些文本粘贴到空白的地方
- 把需要替换的文本删除
- 清理多余的空白(如果需要的话)
实际上,我们可以让 Vim 不把指定的内容放入默认寄存器,这样就不会覆盖预先准备好的内容了,这等同于彻底删除而不是剪切。Vim 的默认寄存器是 ""
(也叫匿名寄存器,:h quotequote
),它保存常规的复制/删除等操作的内容,Vim 还有一个名字很酷的寄存器叫做:黑洞寄存器(Blackhole Register,:h quote_
),它的按键是 "_
。如果你在键入任何操作之前先输入 "_
,操作的结果将不会被任何寄存器保留下来,就好像丢入了一个深渊黑洞,再也回不来了……(好伤感 T_T)
因此,我们可以这么玩:
- 在某处复制或删除(剪切)了一些文本
- 使用黑洞寄存器删除需要替换的内容,例如删除一行:
"_dd
- 直接粘贴,搞定!
我把这个过程也录了下来,你可以对照看看:
我真是爱死这玩意儿了!不过你要知道,就上例而言黑洞寄存器不是唯一的办法,说不定你更喜欢别的操作组合,比如下面这个:
这一套“组合拳”没有用黑洞寄存器,它的好处是如果我反悔了,我还可以撤销之前的操作把被替换的内容找回来。整个过程的按键顺序是这样的:y$
-> gt
-> gP
-> D
。
请允许我用更加具有语义的方式来重复一遍上面的操作:
-
y$
:从光标所在位置(行首)复制到行尾(不包括换行符) -
gt
:切换至下一个标签页 -
gP
:自光标位置向左粘贴刚才复制的内容,结束之后把光标向右移动一个字符(这就是g
的作用,为了把末尾的.
保留住。你也可以使用Pl
实现一样的目标) -
D
:自光标位置删除到行尾
我希望你理解我这样重复一遍的原因,它包含了体现 Vim 哲学的三个侧面:
- 每一步都保持简单的颗粒操作
- 从不死记硬背,而是去表达你的意图,用你自己的方式
- 条条大路通 Vim,何必死撞一棵树?
好吧,第三点纯粹是我在胡扯,哈哈。
大小写转换
大小写转换其实不算什么大事,本来我也犹豫还要不要介绍一下,但是考虑到这个在编程的时候还挺有用的,于是索性一并说了吧,反正也不多……
操作指令 | 移动效果 |
---|---|
~ |
转换光标所在字符的大小写(严格来说,这不是一个操作指令) |
g~ |
转换字符的大小写(这个才真的是) |
gu |
强制转换成小写 |
gU |
强制转换成大写 |
解释一下头两个,~
不是操作指令,是因为它没办法和移动指令结合,它就只会转换当前光标所在位置的那个字符。如果你有多个字符需要转换,你就只能一个一个按过去。g~
才是转换大小写的正式版,它可以结合移动指令。比方说按下 g~3j
会把往下 3 行的字符大小写都转换了(小写变大写,大写变小写)。不过 Vim 有一个选项叫做 tildeop
(:h tildeop),它默认是关闭的,如果你开启它,~
就会变成和 g~
一样了。这选项我记得很熟,因为我经常在团队里做重构工作,这种改写命名的活儿一再重复,我索性就把 ~
变成真正的操作指令了。
另外,毫无意外的,它们几个都有直接操作当前一整行的快捷版本,分别是:g~~
guu
gUU
。
趣味知识:你知道 ROT13 加密编码吗?这可能是世界上最简单的加密手段了,有意思的是 Vim 也内置了 ROT13 编码/解码功能。闲来无事的时候可以拿来逗别人玩哦!切换 ROT13 编码/解码的操作指令是:
g?
(:help g?)
缩进与排版
操作指令 | 移动效果 |
---|---|
gq |
自动应用排版规则 |
gw |
自动应用排版规则(光标位置不变) |
= |
自动应用缩进规则 |
< >
|
手动应用缩进规则(左右两个方向) |
前面两个在编写代码时不太常用,倒是在编写文档时能发挥作用,因此它们不是重点,请自行查阅文档并尝试。
后面两个就比较常用了,所谓“自动应用缩进规则”,前提是你得有可用的缩进规则。Vim 内置了非常多种语言的缩进规则,那些没有内置的也基本上都可以在网上找到合适的缩进规则插件。此前我们也在基础设置里打开了 filetype indent on
,所以此时如果你打开一份源码文件,然后按下 gg=G
,“唰”的一下——整个世界清静了。
还记得吧?gg
是去文件的最开始处,G
则是去文件的最后一行,所以这条命令的含义是:“从文件的开始处应用自动缩进规则直到文件的最后一行”。当然你可以不必总是对整个文件进行自动缩进,之前我们提到过的移动指令都可以搭配使用,之后我们还要介绍更加强大灵活的文本对象选择指令,搭配上自动缩进那叫一个如虎添翼~
至于 <
和 >
就没什么新鲜的了,手动缩进呗!缩进的宽度是由 shiftwidth
指定的,咱们上次已经设置过了的有木有?另外它们也有针对当前行的快捷版本,你已经知道了,是吧?
想知道你的 Vim 内置了那些语言的缩进规则?键入这条命令:
:e $VIMRUNTIM/indent
Bonus
你现在可以尝试把这些最常用的指令应用在你的日常工作里了,如果你能坚持去寻找最有效率的操作方式,你终将会明白其实根本用不着装太多的插件。我很乐意进一步帮助你,所以你如果在使用中有任何疑问请不要客气尽管询问我,我也喜欢看看有什么新的挑战,所以来吧~
其实,第四篇本来想直接讲文本对象的,但是我担心新手会看不太懂,于是把文本对象一再往后挤。挤到现在才发现,天啊!这篇太长了,实在是不能再继续下去了。于是,我们只好对文本对象说拜拜了~咱们下期再见!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。