第2章 普通模式
普通模式是Vim的自然放松状态,如果本章看起来出奇的短,那是因为几乎整本书都在讲如何利用普通模式,而本章只涉及其中的一些核心概念以及通用技巧。
其他文本编辑器大部分时间都处于类似Vim插入模式的状态中,因此对Vim新手来说,把普通模式(normal mode)当成默认状态看起来很奇怪。在技巧7 中,我们将以一个画家的工作区作为类比,来解释其原因。
许多普通模式命令可以在执行时指定执行的次数,这样它们就可以被执行多次。在技巧10中,我们将结识一对用于加减数值的命令,并且会看到这两条命令如何与次数结合在一起,进行简单的算术运算。
指定执行的次数可以减少按键个数,但并不是说你一定要为此目的而这样做。我们将会看到一些例子,在这些例子中,简单地重复执行一条命令,要比花时间去计算想要执行多少次更好。
普通模式命令的强大,很大程度上源于它可以把操作符与动作命令结合在一起。在本章的最后,我们将看到这种结合所达到的效果。
技巧7 停顿时请移开画笔
对不习惯 Vim 的人来说,普通模式看上去是一种奇怪的缺省状态,但有经验的Vim用户却很难想象还有其他任何方式。本节使用了一个比喻来说明为什么Vim要采用这种方式。
你估计画家会花费多少时间用画笔在画布上作画?毫无疑问,这因人而异,但是,如果这占了画家全部工作时间的一半还要多的话,我会觉得非常诧异。
想一下除了画画外,画家还要做哪些事情。他们要研究主题,调整光线,把颜料混合成新的色彩。而且,在把颜料往画布上画时,谁说他们必须要用画笔?画家也许会换用刻刀来实现不同的质地,或是用棉签来对已经画好的地方进行润色。
画家在休息时不会把画笔放在画布上。对Vim而言也是这样,普通模式就是Vim的自然放松状态,其名字已经寓示了这一点。
就像画家只花一小部分时间涂色一样,程序员也只花一小部分时间编写代码。绝大多数时间用来思考、阅读,以及在代码中穿梭浏览。而且,当确实需要做修改时,谁说一定要切换到插入模式才行?我们可以重新调整已有代码的格式,复制它们,移动其位置,或是删除它们。在普通模式中,我们有众多的工具可以利用。
技巧8 把撤销单元切成块
在其他编辑器中,输入一些词后使用撤销命令,可能会撤销最后输入的词或字符。然而在Vim中,我们自己可以控制撤销的粒度。
u 键会触发撤销命令,它会撤销最新的修改。一次修改可以是改变文档内文本的任意操作,其中包括在普通模式、可视模式以及命令行模式中所触发的命令,而且一次修改也包括了在插入模式中输入(或删除)的文本,因此我们也可以说,i{insert some text}<Esc> 是一次修改。
在不区分模式的文本编辑器中,输入一些单词后使用撤销命令,有两种可能。一种是它可能会撤销最后输入的字符;另一种做得更好点,它可能会把字符分成块,使每次撤销操作删除一个单词而不是一个字符。
在Vim中,我们自己可以控制撤销命令的粒度。从进入插入模式开始,直到返回普通模式为止,在此期间输入或删除的任何内容都被当成一次修改。因此,只要我们控制好对 <Esc> 键的使用,就可使撤销命令作用于单词、句子或段落。
那么,应该多久离开一次插入模式呢?这是个人喜好的问题,不过我喜欢让每个“可撤销块”对应一次思考过程。在写这段文字时(当然是在Vim中写的),我经常在每句话的结尾停顿一下,想一想接下来该写什么。不管停顿的时间有多短,每次停顿都是一个自然的中断点,提示我该退出插入模式了。当我准备好继续写时,按 A 命令就可以回到原来的地方继续写作。
如果我认为已经走错了方向,我会切换到普通模式,然后按 u 撤销。每次做撤销时,文字都按我最初书写时的思路,被切分成条理清晰的块,也就是说我可以很容易地试着写一两句话,如果感到不合适的话,随后按一两下键就可以将其舍弃。
当处于插入模式时,如果光标位于行尾的话,另起一行最快的方式是按<CR>。不过有时我更喜欢按 <Esc>o,这是因为我有预感,也许在撤销时我想拥有更细的粒度。如果听起来这不太好理解,不必担心,当你对Vim越来越熟悉时,就会感到切换模式越来越轻松。
一般来讲,如果你停顿的时间长到足以问“我应该退出插入模式吗?”这个问题,那么就退出吧。
在插入模式中移动光标会重置修改状态
当我提到撤销命令会回退从进入插入模式到退出此模式期间输入(或删除)的全部字符时,我略过了一个小细节。如果在插入模式中使用了 <Up> 、<Down> 、<Left>或 <Right> 这些光标键,将会产生一个新的撤销块。你可以把这想象为先切换回普通模式,然后用 h、j、k 或 l 命令对光标进行了移动,唯一区别是我们并没有退出插入模式。这也会对 . 命令的操作产生影响。
技巧9 构造可重复的修改
Vim对重复操作进行了优化,要利用这一点,我们必须考虑该如何构造修改。
在Vim中,要完成一件事,总是有不止一种方式。在评估哪种方式最好时,最显而易见的指标是效率,即哪种手段需要的按键次数最少(又名VimGolf)。然而,在平局时该如何选择获胜者呢?
在下例中,假设光标位于行尾处的字符“h”上,而我们想要删除单词“nigh”:
normal_mode/the_end.txt
The end is nigh
反向删除
因为光标已经位于单词末尾,我们可以先反向删除该词。
按 db 命令删除从光标起始位置到单词开头的内容,但会原封未动地留下最后一个字符 “h”,再按一下 x 键就可以删除这个捣乱的字符。这样,整个操作的 Vim高尔夫得分是3分。
正向删除
这一次,让我们尝试一下正向删除。
我们先用 b 命令把光标移到单词的开头,移动好后,就可以用一个 dw 命令删掉整个单词。这一次的Vim高尔夫得分也是3分。
删除整个单词
到目前为止,已有的两种方式都要先做某种准备工作或清理工作。另外,我们也可以使用更为精准的aw文本对象(text object),而不是用动作命令(参见:h aw):
你可以把 daw 命令解读为“delete a word”,这样比较容易记忆。在技巧51 和技巧52中我们将介绍更多关于文本对象的细节。
决胜局:哪种方式最具重复性?
我们尝试了3种不同的方式来删除一个词:dbx、bdw以及daw。每种情况的Vim高尔夫得分都是3分。那么我们要怎么回答这个问题:“哪种方式最好?”
还记得吗,Vim对重复操作进行了优化。让我们再回顾一下这3种方式,这一次我们跟着用一次 . 命令,看看会发生什么。我建议你自己也亲自试一下。
反向删除方案包含两步操作:db 命令删除至单词的开头,而后 x 命令删除一个字符。如果我们跟着执行一次 . 命令,它会重复删除一个字符( . = = x )。我不觉得这有什么价值。
正向删除方案也包含两步。这一次,b 只是一次普通的移动,而 dw 完成修改。此时用 . 命令会重复 dw,删除从光标位置到下个单词开头的内容。不过因为我们刚好已经在行尾了,并没有“下一个单词”,所以在这个场景里 . 命令没什么用。不过,至少它代表了一个更长点的操作(. = = dw)。
最后的方案只调用一个操作:daw。这个操作不仅仅删除了该单词,它还会删除一个空格,因此光标最终会停在单词“is”的最后一个字符上。如果此时我们使用 . 命令,它会重复上次删除单词的命令。这一次,. 命令会做真正有用的事情(. = = daw)。
结论
daw 可以发挥 . 命令的最大威力,因此我宣布它是本轮的获胜者。
要想充分利用 . 命令,事先常常需要进行一番周详的考虑。如果你发现自己要在几个地方做同样的小修改,就可以尝试构造你的修改,让它们能够被 . 命令重复执行。要识别出这类机会需要进行一定的实践,不过一旦你养成了使修改可重复的习惯,那么你就会从 Vim 这里得到“奖赏”。
有时,我并没有看到用 . 命令的机会,然而在做完一次修改后,我发现要做另一次同样的操作,这时候,我脑海里会浮现出 . 命令,而它也已经准备好为我效力了。每当遇到这种情况时,我都会开心地笑起来。
技巧10 用次数做简单的算术运算
大多数普通模式命令可以在执行时指定次数,我们可以利用这个功能来做简单的算术运算。
很多普通模式命令都可以带一个次数前缀,这样Vim就会尝试把该命令执行指定的次数,而不是只执行一次(参见:h count)。
<C-a> 和 <C-x> 命令分别对数字执行加和减操作。在不带次数执行时,它们会逐个加减,但如果带一个次数前缀,那么就可以用它们加减任意整数。例如,如果我们把光标移到字符5上,执行 10<C-a>就会把它变成15。
但是如果光标不在数字上会发生什么?文档里说,<C-a> 命令会“把当前光标之上或之后的数值加上[count]”(参见:h ctrl-a)。因此,如果光标不在数字上,那么 <C-a> 命令将在当前行正向查找一个数字,如果找到了,它就径直跳到那里。我们可以利用这一点简化操作。
下面是一段CSS片段:
normal_mode/sprite.css
.blog, .news { background-image: url(/sprite.png); }
.blog { background-position: 0px 0px }
我们要复制最后一行并且对其做两个小改动,即用“news”替换单词“blog”,以及把“0px”改为“-180px”。我们可以运行 yyp 来复制此行,然后用 cw 来修改第一个单词。但我们该怎么处理那个数值呢?
一种做法是用 f0 跳到此数字,然后进入插入模式手动修改它的值,即i-18<Esc>。不过,运行 180<C-x> 则要快得多。由于我们的光标不在要操作的数字上,所以该命令会正向跳到所找到的第一个数字上,从而省去了手动移光标的步骤。让我们看看整个操作过程:
在本例中,我们只复制了一行并做出改动。但是,假设你要复制10份,并对后续数字依次减 180。如果要切换到插入模式去修改每个数字,我们每次都得输入不同的内容(-180,然后-360,以此类推)。但是如果用 180<C-x> 命令的话,对后续行也可以采用相同的操作过程。我们甚至还可以把这组按键操作录制成一个宏(参见第 11章),然后根据需要执行多次。
数字的格式
007 的后面是什么?不,这不是詹姆斯·邦德的恶作剧,我是在问:如果对 007加1,你觉得会得到什么结果。
如果你的答案是008,那么当你尝试对任意以0开头的数字使用 <C-a> 命令时,也许会感到诧异。像在某些编程语言中的约定一样,Vim把以0开头的数字解释为八进制值,而不是十进制。在八进制体系中,007 + 001 = 010,看起来像是十进制中的10,但实际上它是八进制中的8,糊涂了吗?
如果你经常使用八进制,Vim 的缺省行为或许会适合你。如果不是这样,那么你可能想把下面这行加入你的vimrc里:
set nrformats=
这会让Vim把所有数字都当成十进制,不管它们是不是以0开头的。
技巧11 能够重复,就别用次数
在处理某些特定工作时,使用次数可以使按键次数变得最少,不过我们并不是非得这样不可。我们需要认真考虑次数与重复各自的优缺点。
假设在缓冲区里有如下文字:
Delete more than one word
我们想把这段文字改为“Delete one word”,也就是说,要像这段文字里所讲的那样删除两个单词。
有几种方式可以达到这一目的,d2w 和 2dw 都可以。使用 d2w,我们先调用删除命令,然后以 2w 作为动作命令,我们可以把它解读为“删除两个单词”;然而 2dw做的相反,这一次,次数作用于删除命令,而动作命令只跨越一个单词,我们可以把这解读为“做两次删除单词的操作”。抛开语义不讲,无论哪种方法,结果都是相同的。
现在,让我们考虑另外一种方式,即 dw.。这可以解读为“删除一个单词,然后重复上次的操作”。
概括一下,我们的3种选择d2w、2dw或者dw. 都是3次按键,不过哪一种最好呢?
根据我们的讨论,d2w 和 2dw 是相同的,在执行完两者中的任一个后,我们可以按 u 键撤销,这样两个被删除的单词又会回来。或者,我们不是用撤销,而是用 .命令重复执行它,这就会删除后面的两个单词。
对于 dw. 的情形,按 u 或 . 的结果会有细微的差别。这里的修改是 dw,即删除一个单词。因此,如果想恢复这两个被删除的单词,必须撤销两次,按 uu(或者,如果你愿意,也可以按 2u)。按 . 则只删除后面的一个单词,而不是两个。
现在假设我们原本是想删除3个单词,而不是2个。由于判断出了点差错,我们执行了 d2w 而不是 d3w,那接下来怎么做?我们不能使用 . 命令,因为那会总共删除4个单词。因此,我们或是先撤销而后修正次数(ud3w),或是继续删除下一个单词(dw)。
现在考虑另一种方案,如果我们在第一处地方用的是dw. 命令,那么我们只要再多重复一次 . 命令就行了。因为我们最初的修改只是简单的 dw,因此u 命令和 . 命令都具有更细的粒度,每次只作用于一个单词。
现在假设我们想删除7个单词,我们可以运行 d7w,或是 dw......(即dw 后面跟6次 . 命令)。计算一下按键的次数,哪个命令胜出是很显而易见的。不过你真地确信自己数对了次数吗?
计算次数很是讨厌,因此我宁愿按6次 . 命令,也不愿意只为减少按键的次数,而浪费同样的时间去统计次数。如果我多按了一次 . 命令怎么办?没关系,只要按一次 u 键就可以回退回来。
还记得吗,我们的口诀是(参见技巧4):执行、重复、回退。这里就是在把它付诸行动。
只在必要时使用次数
假设我们想把文字“I have a couple of questions”改为“I have some more questions”,可以用下面的方式做:
在此场景中,使用 . 命令的意义不大,我们可以删除一个单词,然后再用 . 命令删除另一个,但随后我们还得切换到插入模式(例如,使用 i 或 cw)。对我来说这么做很不顺手,我反而更愿意用次数。
使用次数的另一个好处是:它保留了一个干净、连贯的撤销历史记录。完成这次修改后,我们按一下 u 键就可以撤销整个修改,这和技巧8中的讨论是一致的。
对于是用次数风格(d5w)还是用重复风格(dw....)也有同样的争论,因此我的偏好看起来似乎不太一致。对此,你要总结自己的观点,这取决于你怎么看保留干净撤销历史记录的价值,以及你是否觉得用次数令人生厌。
技巧12 双剑合璧,天下无敌
Vim的强大很大程度上源自操作符与动作命令相结合。在本节,我们将看到它是如何工作的,并考虑其寓义。
操作符 + 动作命令 = 操作
d{motion} 命令可以对一个字符(dl)、一个完整单词(daw)或一整个段落(dap)进行操作,它作用的范围由动作命令决定。c{motion}、y{motion}以及其他一些命令也类似,它们被统称为操作符(operator)。你可以用:h operator来查阅完整的列表,表2-1总结了一些比较常见的操作符。
表2-1 Vim的操作符命令
g~、gu和gU命令要用两次按键来调用,我们可以把上述命令中的 g 当做一个前缀字符,用以改变其后面的按键行为,进一步的讨论请参见本技巧最后的“结识操作符待决模式”部分。
操作符与动作命令的结合形成了一种语法。这种语法的第一条规则很简单,即一个操作由一个操作符,后面跟一个动作命令组成。学习新的动作命令及操作符,就像是在学习Vim的词汇一样。如果掌握了这一简单的语法规则,在词汇量增长时,就能表达更多的想法。
假如我们已经知道如何用 daw 删除一个单词,然后又学到 gU 命令(参见 :h gU)。它也是个操作符,所以我们可以用 gUaw 把当前单词转换成大写形式。如果我们的词汇进一步扩充,学会了作用于段落的 ap 动作命令,就会发现我们可以进行两个新的操作:用 dap 删除整个段落,或者用 gUap 把整段文字转换为大写。
Vim的语法只有一条额外规则,即当一个操作符命令被连续调用两次时,它会作用于当前行。所以 dd 删除当前行,而 >> 缩进当前行。gU 命令是一种特殊情况,我们既可以用 gUgU ,也可以用简化版的 gUU 来使它作用于当前行。
扩展命令组合的威力
使用Vim缺省的操作符和动作命令,我们能够执行的操作的数目是巨大的,然而,我们还可以通过自定义动作命令及操作符来进一步扩充其数目。让我们想想这寓示着什么。
自定义操作符与已有动作命令协同工作
随同 Vim 发布的标准操作符集合相对比较少,但我们可以定义新的操作符。TimPope的commentary.vim插件提供了一个很好的例子,此插件为Vim所支持的编程语言增添了注释及取消注释的命令。
注释命令以 \\{motion} 触发,它会切换指定行的注释状态。它是一个操作符命令,因此可以把它和所有动作命令结合在一起。\\ap 将切换当前段落的注释状态,\\G 会把从当前行到文件结尾间的所有内容注释掉,而 \\\ 则注释当前行。
如果你对如何创建自定义操作符感到好奇,可以先阅读一下文档:h:map-operator。
自定义动作命令与已有操作符协同工作
Vim缺省的动作命令集已经相当全面了,但是我们还是可以定义新的动作命令及文本对象来进一步增强它。
Kana Natsuno 的textobj-entire 插件是一个很好的例子,它为Vim增加了两种新的文本对象ie 和 ae,它们作用于整个文件。
如果想用 = 命令自动缩进整个文件,我们可以执行 gg=G (就是说,先用 gg 跳到文件开头,然后用 =G 自动缩进从光标位置到文件结尾的所有内容)。但是如果我们安装了textobj-entire插件的话,简单地执行 =ae 就可以了。运行这条命令时光标在哪儿并不重要,因为它总是作用于整个文件。
注意:如果我们同时安装了commentary和textobj-entire插件,就可以把它们放在一起使用。例如,执行 \\ae 会切换整个文件的注释状态。
如果你对如何创建自定义动作命令感到好奇,可以由阅读:h omap-info开始。
结识操作符待决模式
普通、插入及可视模式很容易辨识,但是Vim还有另外一些很容易被忽视的模式,操作符待决模式(Operator-Pending mode)就是一个例子。每天我们无数次地使用它,但通常它只持续不到一秒时间。举个例子,在我们执行命令 dw 时,就会激活该模式。这一模式只在按 d 及 w 键之间的短暂时间间隔内存在,一眨眼工夫就不见了。
如果我们把Vim想象成有限状态机,那么操作符待决模式就是一个只接受动作命令的状态。这个状态在我们调用操作符时被激活,然后什么也不做,直到我们提供了一个动作命令,完成整个操作。当操作符待决模式被激活时,我们可以像平常一样按 <Esc> 中止该操作,返回到普通模式。
很多命令都由两个或更多的按键来调用(查阅:h g、:h z、:h ctrl-w ,或者:h [,可以看到一些例子),但在多数情况下,头一个按键只是第二个按键的前缀。这些命令不会激活操作符待决模式,相反,可以把它们当成命名空间(namespace),用来扩充可用命令的数目。只有操作符才会激活操作符待决模式。
你也许想知道,为什么要有一个完整的模式,专门用于操作符和动作命令之间的短暂瞬间,而命名空间命令则仅仅是普通模式的一个扩充?好问题!这是因为我们能够创建自定义映射项来激活或终结操作符待决模式。换句话说,它允许我们创建自定义的操作符及动作命令,从而让我们可以扩充Vim的词汇。