西洲渡

一只还没迈入IC领域的小菜鸡

聪明的学习Vim(上)

前言

本篇文章转载的GitHub上一个很火的项目
英文版本

Vim 语法

刚接触Vim时很容易被Vim许多复杂的命令吓到,如果你看到一个Vim的用户使用gUfV1GdG,你可能不能立刻想到这些命令是在做什么。这一章中,我将把Vim命令的结构拆分成一个简单的语法规则进行讲解。

这一章将是本书中最重要的一章,一旦你理解了Vim命令的语法结构,你将能够和Vim”说话”。注意,在这一章中当我讨论Vim语言时,我讨论并不是
Vimscript(Vim自带的插件编写和自定义设置的语言),这里我讨论的是Vim中normal模式的下的命令的通用规则。

如何学习一门语言

我并不是一个英语为母语的人,当我13岁移民到美国时我学习的英语,我会通过做三件事情建立我的语言能力:

  1. 学习语法规则
  2. 扩展我的词汇量
  3. 练习,练习,练习

同样的,为了说好Vim语言,你需要学习语法规则,增加词汇量,并且不断练习直到你可以把执行命令变成肌肉记忆。

语法规则

你只需要知道一个Vim语言的语法规则:

1
verb + noun # 动词 + 名词

这就类似与在英语中的祈使句:

  • Eat(verb) a donut(noun)
  • Kick(verb) a ball(noun)
  • Learn(verb) the Vim Editor(noun)

现在你需要的就是用Vim中基本的动词和名字来建立你的词汇表

词汇表

名词(动作 Motion)

我们这里将动作作为名词,动作用来在Vim中到处移动,他们也是Vim中的名词。下面列出了一些常见的动作的例子:

1
2
3
4
5
6
7
h	左
j 下
k 上
l 右
w 向前移动到下一个单词的开头
} 跳转到下一个段落
$ 跳转到当前行的末尾

在之后的章节你将学习更多的关于动作的内容,所以如果你不理解上面这些动作也不必担心。

动词(操作符 Operator)

根据:h operator,Vim共有16个操作符,然而根据我的经验,学习这3个操作符在80%的情况下就已经够用了

1
2
3
y	yank(复制)
d delete(删除)
c change 删除文本,将删除的文本存到寄存器中,进入插入模式

现在你已经知道了基本的动词和名词,我们来用一下我们的语法规则。假设你有下面这段文本:

1
const learn = "Vim";
  • 复制当前位置到行尾的所有内容:y$
  • 删除当前位置到下一个单词的开头:dw
  • 修改当前位置到这个段落的结尾:c}

动作也接受数字作为参数(这个部分我将在下个章节展开),如果你需要向上移动3行,你可以用3k代替按3次k

  • 向左拷贝2个字符:y2h
  • 删除后两个单词:d2w
  • 修改后面两行:c2j

目前,你也许需要想很久才能完成一个简单的命令,不过我刚开始时也是这样,我也经历过类似的挣扎的阶段但是不久我的速度就快了起来,你也一样。

作为补充,行级的操作符在文本编辑中和其他的操作符一样,Vim允许你通过按两次命令执行行级的操作,例如ddyycc来执行删除,复制或修改整个行。

我希望这些内容能够对你有用,但是到目前为止还没有结束,Vim有另一种类型的名词:文本对象(text object)

更多名词(文本对象)

想象一下你现在正在某个被括号包围的文本中例如(hello Vim),你现在想要删掉括号中的所有内容,你会怎样快速的完成它?是否有一种方法能够把括号中内容作为整体删除呢?

答案是有的。文本通常是结构化的,特别是代码经常被放置在小括号、中括号、大括号、引号等当中。Vim提供了一种处理这种结构的文本对象的方法。

文本对象可以被操作符使用,这里有两类文本对象:

1
2
i + object  内部文本对象
a + object 外部文本对象

内部文本对象选中的部分不包含包围文本对象的空白或括号等,外部文本对象则包括了包围内容的空白或括号等对象。外部对象总是比内部对象选中的内容更多,因此如果你的光标位于一对括号内部,例如(hello Vim)中:

  • 删除括号内部的内容但保留括号:di(
  • 删除括号以及内部的内容:da(

让我们看一些别的例子,假设你有这样一段Javascript的函数,你的光标停留在”Hello”上:

1
2
3
4
const hello = function() {
console.log("Hello Vim");
return true;
}
  • 删除整个”Hello Vim”:di(
  • 删除整个函数(被{}包含):di{
  • 删除”Hello”这个词:diw

文本对象很强大因为你可以在一个位置指向不同的对象,能够删除一对括号、函数体或整个单词的文本对象中的内容。此外,当你看到di(di{diw时,你也可以很好的意识到他们表示的是什么。

让我们来看最后一个例子。假设你有这样一些html的标签的文本:

1
2
3
4
5
<div>
<h1>Header1</h1>
<p>Paragraph1</p>
<p>Paragraph2</p>
</div>

如果你的光标位于”Header1”文本上:

  • 删除”Header1”:dit
  • 删除<h1>Header1</h1>dat

如果你的光标在”div”文本上:

  • 删除h1和所有p标签的行:dit
  • 删除所有文本:dat
  • 删除”div”:di<

下面列举的一些通常见到的文本对象:

1
2
3
4
5
6
7
8
9
10
11
w     一个单词
p 一个段落
s 一个句子
(或) 一对()
{或} 一对{}
[或] 一对[]
<或> 一对<>
t XML标签
" 一对""
' 一对''
` 一对``

你可以通过:h text-objects了解更多

结合性和语法

在学习Vim的语法之后,让我们来讨论一下Vim中的结合性以及为什么在文本编辑器中这是一个强大的功能。

结合性意味着你有很多可以组合起来完成更复杂命令的普通命令,就像你在编程中可以通过一些简单的抽象建立更复杂的抽象,在Vim中你可以通过简单的命令的组合执行更复杂的命令。Vim语法正是Vim中命令的可结合性的体现。

Vim的结合性最强大之处体现在它和外部程序结合时,Vim有一个过滤操作符!可以用外部程序过滤我们的文本。假设你有下面这段混乱的文本并且你想把它用tab格式化的更好看的一些:

1
2
3
4
Id|Name|Cuteness
01|Puppy|Very
02|Kitten|Ok
03|Bunny|Ok

这件事情通过Vim命令不太容易完成,但是你可以通过终端提供的命令column很快的完成它,当你的光标位于”Id”上时,运行!}column -t -s "|",你的文本就变得整齐了许多:

1
2
3
4
Id  Name    Cuteness
01 Puppy Very
02 Kitten Ok
03 Bunny Ok

让我们分解一下上面那条命令,动词是!(过滤操作符),名词是}(到下一个段落)。过滤操作符!接受终端命令作为另一个参数,因此我把column -t -s "|"传给它。我不想详细描述column是如何工作的,但是总之它格式化了文本。

假设你不止想格式化你的文本,还想只展示Ok结尾的行,你知道awk命令可以做这件事情,那么你可以这样做:

1
!}column -t -s "|" | awk 'NR > 1 && /Ok/{print $0}'

结果如下:

1
2
02  Kitten  Ok
03 Bunny Ok

666!管道竟然在Vim中也能起作用。

这就是Vim的结合性的强大之处。你知道的操作符动作,终端命令越多,你组建复杂操作的能力成倍增长。

换句话说,假设你只知道四个动作w, $, }, G和删除操作符(d),你可以做8件事:按四种方式移动(w, $, }, G)和删除4种文本对象(dw, d$, d}, dG)。如果有一天你学习了小写变大写的操作符(gU),你的Vim工具箱中多的不是1种工具,而是4种:gUw, gU$, gU}, gUG。现在你的Vim工具箱中就有12种工具了。如果你知道10个动作和5个操作符,那么你就有60种工具(50个操作+10个移动)。另外,行号动作(nG)给你了n动作,其中n是你文件中的行数(例如前往第5行,5G)。搜索动作(/)实际上给你带来无限数量的动作因为你可以搜索任何内容。你知道多少终端命令,外部命令操作符(!)就给你了多少种过滤工具。使用Vim这种能够组合的工具,所有你知道的东西都可以被串起来完成更复杂的操作。你知道的越多,你就越强大。

这种具有结合性的行为也正符合Unix的哲学:_一个命令做好一件事_。动作只需要做一件事:前往X。操作符只需要做一件事:完成Y。通过结合一个操作符和一个动作,你就获得了YX:在X上完成Y。

甚至,动作操作符都是可拓展的,你可以自己创造动作操作符去丰富你的Vim工具箱,Vim-textobj-user有一系列自定义的文本对象。

另外,如果你不知道我刚才使用的columnawk命令也没有关系,重要的是Vim可以和终端命令很好的结合起来。

聪明地学习语法

你刚刚学完Vim唯一的语法规则:

1
verb + noun

我学Vim中最大的”AHA moment”之一是当我刚学完大写命令(gU)时,想要把一个单词变成大写,我本能的运行了gUiW,它居然成功了,我光标所在的单词都大写了。我正是从那是开始理解Vim的。我希望你也会在不久之后有你自己的”AHA moment”,如果之前没有的话。

这一章的目标是向你展现Vim中的verb+noun模式,因此之后你就可以像学习一门新的语言一样渐进的学习Vim而不是死记每个命令的组合。

学习这种模式并且理解其中的含义,这是聪明的学习方式。

在文件中移动

一开始,通过键盘移动会让你感觉特别慢特别不自在,但是不要放弃!一旦你习惯了它,比起鼠标你可以更快的在文件中去到任何地方。

这一章,你将学习必要的移动以及如何高效的使用它们。 记住,这一章所讲的并不是Vim的全部移动命令,我们的目标是介绍有用的移动来快速提高效率。 如果你需要学习更多的移动命令,查看:h motion.txt

字符导航

最基本的移动单元是上下左右移动一个字符。

1
2
3
4
h   左
j 下
k 上
l 右

你也可以通过方向键进行移动,如果你只是初学者,使用任何你觉得最舒服的方法都没有关系。

我更喜欢hjkl因为我的右手可以保持在键盘上的默认姿势,这样做可以让我更快的敲到周围的键。 为了习惯它,我实际上在刚开始的时候通过~/.vimrc关闭了方向键:

1
2
3
4
noremap <Up> <NOP>
noremap <Down> <NOP>
noremap <Left> <NOP>
noremap <Right> <NOP>

也有一些插件可以帮助改掉这个坏习惯,其中有一个叫vim-hardtime。 让我感到惊讶的是,我只用了几天就习惯了使用hjkl

另外,如果你想知道为什么Vim使用hjkl进行移动,_这实际上是因为Bill Joy写VI用的Lear-Siegler ADM-3A终端没有方向键,而是把当做方向键_。

如果你想移动到附近的某个地方,比如从一个单词的一个部分移动到另一个部分,我会使用hl。 如果我需要在可见的范围内上下移动几行,我会使用jk。 如果我想去更远的地方,我倾向于使用其他移动命令。

相对行号

我觉得设置numberrelativenumber非常有用,你可以在~/.vimrc中设置:

1
set relativenumber number

这将会展示当前行号和其他行相对当前行的行号。

为什么这个功能有用呢?这个功能能够帮助我知道我离我的目标位置差了多少行,有了它我可以很轻松的知道我的目标行在我下方12行,因此我可以使用12j去前往。 否则,如果我在69行,我的目标是81行,我需要去计算81-69=12行,这太费劲了,当我需要去一个地方时,我需要思考的部分越少越好。

这是一个100%的个人偏好,你可以尝试relativenumber/norelativenumbernumber/nonumber 然后选择自己觉得最有用的。

对移动计数

在继续之前,让我们讨论一下”计数”参数。 一个移动可以接受一个数字前缀作为参数,上面我提到的你可以通过12j向下移动12行,其中12j中的12就是计数数字。

你使用带计数的移动的语法如下:

1
[计数] + 移动

你可以把这个应用到所有移动上,如果你想向右移动9个字符,你可以使用9l来代替按9次l。 当你学到了更多的动作时,你都可以试试给定计数参数。

单词导航

我们现在移动一个更长的单元:单词(word)。 你可以通过w移动到下一个单词的开始,通过e移动到下一个单词的结尾,通过b移动到上一个单词的开始,通过ge移动到前一个单词的结尾。

另外,为了和上面说的单词(word)做个区分,还有一种移动的单元:词组(WORD)。 你可以通过W移动到下一个词组的开始,通过E移动到下一个词组的结尾,通过B移动到前一个词组的开头,通过gE移动到前一个词组的结尾。 为了方便记忆,所以我们选择了词组和单词这两个词,相似但有些区分。

1
2
3
4
5
6
7
8
w		移动到下一个单词的开头
W 移动到下一个词组的开头
e 移动到下一个单词的结尾
E 移动到下一个词组的结尾
b 移动到前一个单词的开头
B 移动到前一个词组的开头
ge 移动到前一个单词的结尾
gE 移动到前一个词组的结尾

词组和单词到底有什么相同和不同呢?单词和词组都按照非空字符被分割,一个单词指的是一个只包含a-zA-Z0-9字符串,一个词组指的是一个包含除了空字符(包括空格,tab,EOL)以外的字符的字符串。 你可以通过:h word:h WORD了解更多。

例如,假如你有下面这段内容:

1
const hello = "world";

当你光标位于这行的开头时,你可以通过l走到行尾,但是你需要按21下,使用w,你需要6下,使用W只需要4下。 单词和词组都是短距离移动的很好的选择。

然而,之后你可以通过当前行导航只按一次从c移动到;

当前行导航

当你在进行编辑的时候,你经常需要水平地在一行中移动,你可以通过0跳到本行第一个字符,通过$跳到本行最后一个字符。 另外,你可以使用^跳到本行第一个非空字符,通过g_跳到本行最后一个非空字符。 如果你想去当前行的第n列,你可以使用n|

1
2
3
4
5
0		跳到本行第一个字符
^ 跳到本行第一个非空字符
g_ 跳到本行最后一个非空字符
$ 跳到本行最后一个字符
n| 跳到本行第n列

你也可以在本行通过ft进行行内搜索,ft的区别在于f会停在第一个匹配的字母上,t会停在第一个匹配的字母前。 因此如果你想要搜索并停留在”h”上,使用fh。 如果你想搜索第一个”h”并停留在它的前一个字母上,可以使用th。 如果你想去下一个行内匹配的位置,使用;,如果你想去前一个行内匹配的位置,使用,

如果想向前搜索”h”,可以使用Fh,使用;,保持相同的搜索方向搜索下一个匹配的字母。 注意,;不是总是向后搜索,;表示的是上一次搜索的方向,因此如果你使用的F,那么使用;时将会向前搜索使用,时向后搜索。

1
2
3
4
5
6
f   在同一行向后搜索第一个匹配
F 在同一行向前搜索第一个匹配
t 在同一行向后搜索第一个匹配,并停在匹配前
T 在同一行向前搜索第一个匹配,并停在匹配前
; 在同一行重复最近一次搜索
, 在同一行向相反方向重复最近一次搜索

回到上一个例子:

1
const hello = "world";

当你的光标位于行的开头时,你可以通过按一次键$去往行尾的最后一个字符”;”。 如果想去往”world”中的”w”,你可以使用fw。 一个建议是,在行内目标附近通过寻找重复出现最少的字母例如”j”,”x”,”z”来前往行中的该位置更快。

句子和段落导航

接下来两个移动的单元是句子和段落。

首先我们来聊聊句子。 一个句子的定义是以.!?和跟着的一个换行符或空格,tab结尾的。 你可以通过)(跳到下一个和上一个句子。

1
2
(   跳到前一个句子
) 跳到下一个句子

让我们来看一些例子,你觉得哪些字段是句子哪些不是? 可以尝试在Vim中用()感受一下。

1
2
3
I am a sentence. I am another sentence because I end with a period. I am still a sentence when ending with an exclamation point! What about question mark? I am not quite a sentence because of the hyphen - and neither semicolon ; nor colon :

There is an empty line above me.

另外,如果你的Vim中遇到了无法将一个以.结尾的字段并且后面跟着一个空行的这种情况判断为一个句子的问题,你可能处于compatible的模式。 运行:set nocompatible可以修复。 在Vi中,一个句子是以两个空格结尾的,你应该总是保持的nocompatible的设置。

接下来,我们将讨论什么是段落。 一个段落可以从一个空行之后开始,也可以从段落选项中字符对所指定的段落宏的每个集合开始。

1
2
{   跳转到上一个段落
} 跳转到下一个段落

如果你不知道什么是段落宏,不用担心,重要的是一个段落总是以一个空行开始和结尾, 在大多数时候总是对的。

我们来看这个例子。 你可以尝试着使用}{进行导航,也可以试一试()这样的句子导航。

1
2
3
4
5
6
7
8
Hello. How are you? I am great, thanks!
Vim is awesome.
It may not easy to learn it at first...- but we are in this together. Good luck!

Hello again.

Try to move around with ), (, }, and {. Feel how they work.
You got this.

你可以通过:h setence:h paragraph了解更多。

匹配导航

程序员经常编辑含有代码的文件,这种文件内容会包含大量的小括号,中括号和大括号,并且可能会把你搞迷糊你当前到底在哪对括号里。 许多编程语言都用到了小括号,中括号和大括号,你可能会迷失于其中。 如果你在它们中的某一对括号中,你可以通过%跳到其中一个括号或另一个上(如果存在)。 你也可以通过这种方法弄清你是否各个括号都成对匹配了。

1
%    Navigate to another match, usually works for (), [], {}

我们来看一段Scheme代码示例因为它用了大量的小括号。 你可以在括号中用%移动

1
2
3
4
5
6
(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else
(+ (fib (- n 1)) (fib (- n 2)))
)))

我个人喜欢使用类似vim-rainbow这样的可视化指示插件来作为%的补充。 通过:h %了解更多。

行号导航

你可以通过nG调到行号为n的行,例如如果你想跳到第7行,你可以使用7G,跳到第一行使用gg1G,跳到最后一行使用G

有时你不知道你想去的位置的具体行号,但是知道它大概在整个文件的70%左右的位置,你可以使用70%跳过去,可以使用50%跳到文件的中间。

1
2
3
4
gg      跳转到第一行
G 跳转到最后一行
nG 跳转到第n行
n% 跳到文件的n%

另外,如果你想看文件总行数,可以用CTRL-g查看。

窗格导航

为了移动到当前窗格的顶部,中间,底部,你可以使用HML

你也可以给HL传一个数字前缀。 如果你输入10H你会跳转到窗格顶部往下数10行的位置,如果你输入3L,你会跳转到距离当前窗格的底部一行向上数3行的位置。

1
2
3
4
5
H   跳转到屏幕的顶部
M 跳转到屏幕的中间
L 跳转到屏幕的底部
nH 跳转到距离顶部n行的位置
nL 跳转到距离底部n行的位置

滚动

在文件中滚动,你有三种速度可以选择: 滚动一整页(CTRL-F/CTRL-B),滚动半页(CTRL-D/CTRL-U),滚动一行CTRL-E/CTRL-Y)。

1
2
3
4
5
6
Ctrl-e    向下滚动一行
Ctrl-d 向下滚动半屏
Ctrl-f 向下滚动一屏
Ctrl-y 向上滚动一行
Ctrl-u 向上滚动半屏
Ctrl-b 向上滚动一屏

你也可以相对当前行进行滚动

1
2
3
zt    将当前行置于屏幕顶部附近
zz 将当前行置于屏幕中央
zb 将当前行置于屏幕底部

搜索导航

通常,你已经知道这个文件中有一个字段,你可以通过搜索导航非常快速的定位你的目标。 你可以通过/向下搜索,也可以通过?向上搜索一个字段。 你可以通过n重复最近一次搜索,N向反方向重复最近一次搜索。

1
2
3
4
/   向后搜索一个匹配
? 向前搜素一个匹配
n 重复上一次搜索(和上一次方向相同)
N 重复上一次搜索(和上一次方向相反)

假设你有一下文本:

1
2
3
4
5
let one = 1;
let two = 2;
one = "01";
one = "one";
let onetwo = 12;

你可以通过/let搜索”let”,然后通过n快速的重复搜索下一个”let”,如果需要向相反方向搜索,可以使用N。 如果你用?let搜索,会得到一个向前的搜索,这时你使用n,它会继续向前搜索,就和?的方向一致。(N将会向后搜索”let”)。

你可以通过:set hlsearch设置搜索高亮。 这样,当你搜索/let,它将高亮文件中所有匹配的字段。 另外,如果你通过:set incsearch设置了增量搜索,它将在你输入时不断匹配的输入的内容。 默认情况下,匹配的字段会一直高亮到你搜索另一个字段,这有时候很烦人,如果你希望取消高亮,可以使用:nohlsearch。 因为我经常使用这个功能,所以我会设置一个映射:

1
nnoremap <esc><esc> :noh<return><esc>

你可以通过*快速的向下搜索光标下的文本,通过#快速向前搜索光标下的文本。 如果你的光标位于一个字符串”one”上,按下*相当于/\<one\>
/\<one\>中的\<\>表示整词匹配,使得一个更长的包含”one”的单词不会被匹配上,也就是说它会匹配”one”,但不会匹配”onetwo”。 如果你的光标在”one”上并且你想向后搜索完全或部分匹配的单词,例如”one”和”onetwo”,你可以用g*替代*

1
2
3
4
*   向后查找光标所在的完整单词
# 向前查找光标所在的完整单词
g* 向后搜索光标所在的单词
g# 向前搜索光标所在的单词

位置标记

你可以通过标记保存当前位置并在之后回到这个位置,就像文本编辑中的书签。 你可以通过mx设置一个标记,其中x可以是a-zA-Z。 有两种办法能回到标记的位置: 用``x精确回到(行和列),或者用‘x`回到行级位置。

1
2
3
ma    用a标签标记一个位置
`a 精确回到a标签的位置(行和列)
'a 跳转到a标签的行

a-z的标签和A-Z的标签存在一个区别,小写字母是局部标签,大写字母是全局标签(也称文件标记)。

我们首先说说局部标记。 每个buffer可以有自己的一套局部标记,如果打开了两个文件,我可以在第一个文件中设置标记”a”(ma),然后在另一个文件中设置另一个标记”a”(ma)。

不像你可以在每个buffer中设置一套局部标签,你只能设置一套全局标签。 如果你在myFile.txt中设置了标签mA,下一次你在另一个文件中设置mA时,A标签的位置会被覆盖。 全局标签有一个好处就是,即使你在不同的项目红,你也可以跳转到任何一个全局标签上,全局标签可以帮助你在文件间切换。

使用:marks查看所有标签,你也许会注意到除了a-zA-Z以外还有别的标签,其中有一些例如:

1
2
3
4
5
6
7
"   在当前buffer中跳转回到上一次跳转前的最后一行
`` 在当前buffer中跳转回到上一次跳转前的最后一个位置
`[ 跳转到上一次修改或拷贝的文本的开头
`] 跳转到上一次修改或拷贝的文本的结尾
`< 跳转到最近一次可视模式下选择的部分的开头
`> 跳转到最近一次可视模式下选择的部分的结尾
`0 跳转到退出Vim前编辑的最后一个文件

除了上面列举的,还有更多标记,我不会在这一一列举因为我觉得它们很少用到,不过如果你很好奇,你可以通过: marks查看。

跳转

最后,我们聊聊Vim中的跳转你通过任意的移动可以在不同文件中或者同一个的文件的不同部分间跳转。 然而并不是所有的移动都被认为是一个跳转。 使用j向下移动一行就不被看做一个跳转,即使你使用10j向下移动10行,也不是一个跳转。 但是你通过10G去往第10行被算作一个跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'   跳转到标记的行
` 跳转到标记的位置(行和列)
G 跳转到行
/ 向后搜索
? 向前搜索
n 重复上一次搜索,相同方向
N 重复上一次搜索,相反方向
% 查找匹配
( 跳转上一个句子
) 跳转下一个句子
{ 跳转上一个段落
} 跳转下一个段落
L 跳转到当前屏幕的最后一行
M 跳转到当前屏幕的中间
H 跳转到当前屏幕的第一行
[[ 跳转到上一个小节
]] 跳转到下一个小节
:s 替换
:tag 跳转到tag定义

我不建议你把上面这个列表记下来,一个大致的规则是,任何大于一个单词或超过当前行导航的移动都可能是一个跳转。 Vim保留了你移动前位置的记录,你可以通过:jumps查看这个列表,如果想了解更多,可以查看:h jump-motions

为什么跳转有用呢? 因为你可以在跳转列表中通过Ctrl-oCtrl-i在记录之间向上或向下跳转到对应位置。 你可以在不同文件中进行跳转,这将是我之后会讲的部分。

聪明地学习导航

如果你是Vim的新手,这有很多值得你学,我不期望任何人能够立刻记住每样知识点,做到不用思考就能执行这需要一些时间。

我想,最好的开始的办法就是从一些少量的必要的移动开始记。 我推荐你从h,j,k,l,w,b,G,/,?,n开始,学习10个移动并且很达到很舒服使用它们的状态花不了很多时间。

为了让你更擅长导航,我有两个建议:

  1. 注意重复的动作。 如果你发现你自己在重复的使用l,你可以去找一个方法让你前进的更快,然后你会发现你可以用w在单词间移动。 如果你发现你自己的重复的使用w,你可以看看是否有一种方法能让你直接到行尾,然后你会想到可以用$。 如果你可以口语化的表达你的需求,Vim中大概就会有一种方法去完成它。
  2. 当你学习任何一个新的移动时,多需要花一定的时间直到你可以不经过思考直接完成它。

最后,为了提高效率你不需要知道所有的Vim的命令,大多数Vim用户也都不知道,你只需要学习当下能够帮助你完成任务的命令。

慢慢来,导航技巧是Vim中很重要的技巧,每天学一点并且把它学好。

输入模式

输入模式是大部分文本编辑器的默认模式,在这个模式下,所敲即所得。

在这一章节中,你将能够学到如何利用Vim输入模式中的特性来提升你的输入效率。

进入输入模式的方法

我们有很多方式从普通模式进入输入模式,下面列举出了其中的一些方法:

1
2
3
4
5
6
7
8
9
10
i    从光标之前的位置开始输入文本
I 从当前行第一个非空字符之前的位置之前开始输入文本
a 在光标之后的位置追加文本
A 在当前行的末尾追加文本
o 在光标位置下方新起一行并开始输入文本
O 在光标位置的上方新起一行并开始输入文本
s 删除当前光标位置的字符并开始输入文本
S 删除当前行并开始输入文本
gi 从当前缓冲区上次结束输入模式的地方开始输入文本
gI 在当前行的第一列的位置开始输入文本

值得注意的是这些命令的小写/大写模式,每一个小写命令都有一个与之对应的大写命令。如果你是初学者,不用担心记不住以上整个命令列表,可以从 ia两条命令开始,这两条命令足够在入门阶段使用了,之后再逐渐地掌握更多其他的命令。

退出输入模式的方法

下面列出了一些从输入模式退出到普通模式的方法:

1
2
3
<esc>     退出输入模式进入普通模式
Ctrl-[ 退出输入模式进入普通模式
Ctrl-c 与 Ctrl-[ 和 <esc>功能相同, 但是不检查缩写

我发现esc键在键盘上太远了,很难够到,所以我在我的机器上将caps lock 映射成了esc键。 如果你搜索Bill Joy(Vi的作者)的ADM-3A 键盘, 你会发现esc键并不是像现在流行的键盘布局一样在键盘的最左上方,而是在q键的左边,所以我认为将caps lock 映射成esc键是合理的。

另一个Vim用户中常见的习惯是用以下的配置方法在输入模式中把esc映射到jj或者jk

1
2
inoremap jj <esc>
inoremap jk <esc>

重复输入模式

你可以在进入输入模式之前传递一个计数参数. 比如:

1
10i

如果你输入“hello world!”然后退出输入模式, Vim将重复这段文本10次。这个方法对任意一种进入输入模式的方式都有效(如:10I, 11a, 12o

在输入模式中删除大块文本

当你输入过程中出现一些输入错误时,一直重复地用backspace来删除的话会非常地繁琐。更为合理的做法是切换到普通模式并使用d来删除错误。或者,你能用以下命令在输入模式下就删除一个或者多个字符:

1
2
3
Ctrl-h    删除一个字符
Ctrl-w 删除一个单词
Ctrl-u 删除一整行

此外,这些快捷键也支持在 命令行模式 和 Ex模式 中使用(命令行模式和Ex模式将会在之后的章节中介绍)

用寄存器进行输入

寄存器就像是内存里的暂存器一样,可供存储和取出文本。在输入模式下,可以使用快捷键Ctrl-r加上寄存器的标识来从任何有标识的寄存器输入文本。有很多标识可供使用,但是在这一章节中你只需要知道以(a-z)命名的寄存器是可以使用的就足够了。

让我们在一个具体的例子中展示寄存器的用法,首先你需要复制一个单词到寄存器a中,这一步可以用以下这条命令来完成:

1
"ayiw
  • "a 告诉Vim你下一个动作的目标地址是寄存器a
  • yiw 复制一个内词(inner word),可以回顾Vim语法章节查看具体语法。

现在寄存器a存放着你刚复制的单词。在输入模式中,使用以下的快捷键来粘贴存放在寄存器a中文本:

1
Ctrl-r a

Vim中存在很多种类型的寄存器,我会在后面的章节中介绍更多他们的细节。

页面滚动

你知道在输入模式中也是可以进行页面滚动的吗?在输入模式下,如果你使用快捷键Ctrl-x进入Ctrl-x子模式,你可以进行一些额外操作,页面滚动正是其中之一。

1
2
Ctrl-x Ctrl-y    向上滚动页面
Ctrl-x Ctrl-e 向下滚动页面

自动补全

Vim在进入Ctrl-x子模式后(和页面滚动一样),有一个自带的自动补全功能。尽管它不如intellisense或者其他的语言服务器协议(LSP)一样好用,但是也算是一个锦上添花的内置功能了。

下面列出了一些适合入门时学习的自动补全命令:

1
2
3
4
Ctrl-x Ctrl-l	   补全一整行
Ctrl-x Ctrl-n 从当前文件中补全文本
Ctrl-x Ctrl-i 从引用(include)的文件中补全文本
Ctrl-x Ctrl-f 补全一个文件名

当你出发自动补全时,Vim会显示一个选项弹窗,可以使用Ctrl-nCtrl-p来分别向上和向下浏览选项。

Vim也提供了两条不需要进入Ctrl-x模式就能使用的命令:

1
2
Ctrl-n             使用下一个匹配的单词进行补全
Ctrl-p 使用上一个匹配的单词进行补全

通常Vim会关注所有缓冲区(buffer)中的文本作为自动补全的文本来源。如果你打开了一个缓冲区,其中一行是”Chocolate donuts are the best”:

  • 当你输入”Choco”然后使用快捷键Ctrl-x Ctrl-l, Vim会进行匹配并输出这一整行的文本。
  • 当你输入”Choco”然后使用快捷键Ctrl-p,Vim会进行匹配并输出”Chocolate”这个单词。

Vim的自动补全是一个相当大的话题,以上只是冰山一角,想要进一步学习的话可以使用:h ins-completion命令进行查看。

执行普通模式下的命令

你知道Vim可以在输入模式下执行普通模式的命令吗?

在输入模式下, 如果你按下Ctrl-o,你就会进入到insert-normal(输入-普通)子模式。如果你关注一下左下角的模式指示器,通常你将看到-- INSERT -- ,但是按下Ctrl-o后就会变为-- (insert) --。 在这一模式下,你可以执行一条普通模式的命令,比如你可以做以下这些事:

设置居中以及跳转

1
2
3
Ctrl-o zz       居中窗口
Ctrl-o H/M/L 跳转到窗口的顶部/中部/底部
Ctrl-o 'a 跳转到标志'a处

重复文本

1
Ctrl-o 100ihello    输入 "hello" 100 次

执行终端命令

1
2
Ctrl-o !! curl https://google.com    运行curl命令
Ctrl-o !! pwd 运行pwd命令

快速删除

1
2
Ctrl-o dtz    从当前位置开始删除文本,直到遇到字母"z"
Ctrl-o D 从当前位置开始删除文本,直到行末

聪明地学习输入模式

如果你和我一样是从其他文本编辑器转到Vim的,你或许也会觉得一直待在输入模式下很有诱惑力,但是我强烈反对你在没有输入文本时,却仍然待在输入模式下。应该养成当你的双手没有在输入时,就退出到普通模式的好习惯。

当你需要进行输入时,先问问自己将要输入的文本是否已经存在。如果存在的话,试着复制或者移动这段文本而不是手动输入它。再问问自己是不是非得进入输入模式,试试能不能尽可能地使用自动补全来进行输入。尽量避免重复输入同一个单词。

点命令

在编辑文本时,我们应该尽可能地避免重复的动作。在这一章节中,你将会学习如何使用点命令来重放上一个修改操作。点命令是最简单的命令,然而又是减少重复操作最为有用的命令。

用法

正如这个命令的名字一样,你可以通过按下.键来使用点命令。

比如,如果你想将下面文本中的所有”let“替换为”const”:

1
2
3
let one = "1";
let two = "2";
let three = "3";

首先,使用/let来进行匹配。接着,使用cwconst<esc>来将”let”替换成”const”。第三步,使用n来找到下一个匹配的位置。最后,使用点命令(.)来重复之前的操作。持续地使用n . n .直到每一个匹配的词都被替换。

在这个例子里面,点命令重复的是cwconst<esc>这一串命令,它能够帮你将需要8次输入的命令简化到只需要敲击一次键盘。

什么才算是修改操作?

如果你查看点命令的定义的话(:h .),文档中说点命令会重复上一个修改操作,那么什么才算是一个修改操作呢?

当你使用普通模式下的命令来更新(添加,修改或者删除)当前缓冲区中的内容时,你就是在执行一个修改操作了。其中的例外是使用命令行命令进行的修改(以开头的命令),这些命令不算作修改操作。

在第一个例子中,你看到的cwconst<esc>就是一个修改操作。现在假设你有以下这么一个句子:

1
pancake, potatoes, fruit-juice,

我们来删除从这行开始的位置到第一个逗号出现的位置。你可以使用df,来完成这个操作,使用.来重复两次直到你将整个句子删除。

让我们再来试试另一个例子:

1
pancake, potatoes, fruit-juice,

这一次你只需要删除所有的逗号,不包括逗号前面的词。我们可以使用f,来找到第一个逗号,再使用x来删除光标下的字符。然后使用用.来重复两次,很简单对不对?等等!这样做行不通(只会重复删除光标下的一个字符,而不是删除逗号)!为什么会这样呢?

在Vim里,修改操作是不包括移动操作(motions)的,因为动作不会更新缓冲区的内容。当你运行f,x,你实际上是在执行两个独立的操作:f,命令只移动光标,而x更新缓冲区的内容,只有后者算作修改动作。和之前例子中的df,进行一下对比的话,你会发现df,中的f,告诉删除操作d哪里需要删除,是整个删除命令df,的一部分。

让我们想想办法完成这个任务。在你运行f,并执行x来删除第一个逗号后,使用;来继续匹配f的下一个目标(下一个逗号)。之后再使用.来重复修改操作,删除光标下的字符。重复; . ; .直到所有的逗号都被删除。完整的命令即为f,x;.;.

再来试试下一个例子:

1
2
3
pancake
potatoes
fruit-juice

我们的目标是给每一行的结尾加上逗号。从第一行开始,我们执行命令A,<esc>j来给结尾加上逗号并移动到下一行。现在我们知道了j是不算作修改操作的,只有A,算作修改操作。你可以使用j . j .来移动并重复修改操作。完整的命令是A,<esc>j

从你按下输入命令(A)开始到你退出输入模式()之间的所有输入都算作是一整个修改操作。Vim不仅允许你控制需要添加的文本的内容,还允许你控制在什么位置添加文本。你可以在选择在这些位置进行输入:光标位置前(i),光标位置之后(a),在下方插入一行(o),在上方插入一行(O),在当前行的末尾(A),或者在当前行的开始位置(I)。如果你想复习一下相关内容的话,可以看看输入模式(Insert Mode)这一章节。

重复多行修改操作

假设你有如下的文本:

1
2
3
4
5
6
7
8
9
10
let one = "1";
let two = "2";
let three = "3";
const foo = "bar";
let four = "4";
let five = "5";
let six = "6";
let seven = "7";
let eight = "8";
let nine = "9";

你的目标是删除除了含有”foo”那一行以外的所有行。首先,使用d2j删除前三行。之后跳过”foo”这一行,在其下一行使用点命令两次来删除剩下的六行。完整的命令是d2jj..

这里的修改操作是d2j2j不是一个移动操作,而是整个删除命令的一部分。

我们再来看看下一个例子:

1
2
3
4
zlet zzone = "1";
zlet zztwo = "2";
zlet zzthree = "3";
let four = "4";

我们的目标是删除所有的’z’。首先,在块可视化模式下使用Ctrl-vjj来选中前三行的第一个’z’字母。如果你对块可视化模式不熟悉的话也不用担心,我会在下一章节中进行介绍。在选中前三行的第一个’z’后,使用d来删除它们。接着用w移动到下一个z字母上,使用..重复两次之前选中加删除的动作。完整的命令为Ctrl-vjjdw..

你删除一列上的三个’z‘的操作(Ctrl-vjjd)被看做一整个修改操作。可视化模式中的选择操作可以用来选中多行,作为修改动作的一部分。

在修改中包含移动操作

让我们来重新回顾一下本章中的第一个例子。这个例子中我们使用了/letcwconst<esc>紧接着n . n .将下面的文本中的’let’都替换成了’const’。

1
2
3
let one = "1";
let two = "2";
let three = "3";

其实还有更快的方法来完成整个操作。在删除的时候,并不使用w,而是使用gn

gn是向前搜索和上一个搜索的模式(本例中为/let)匹配的位置,并且自动对匹配的文本进行可视化模式下的选取的移动操作。想要对下一个匹配的位置进行替换的话,你不再需要先移动在重复修改操作(n . n .),而是简单地使用. .就能完成。你不需要再进行移动操作了,因为找到下一个匹配的位置并进行选中成为了修改操作的一部分了。完整的命令为/letdgn..

当你在编辑文本时,应该时刻关注像gn命令这种能一下子做好几件事的移动操作。

聪明地学习点命令

点命令的强大之处在于使用仅仅1次键盘敲击代替好几次敲击。对于x这种只需一次敲击键盘就能完成的修改操作来说,点命令或许不会带来什么收益。但是如果你的上一个修改操作是像cgnconst<esc>这种复杂命令的话,使用点命令来替代就有非常可观的收益了。

在进行编辑时,思考一下你正将进行的操作是否是可以重复的。举个例子,如果我需要删除接下来的三个单词,是使用d3w更划算,还是dw再使用.两次更划算?之后还会不会再进行删除操作?如果是这样的话,使用dw好几次确实比d3w更加合理,因为dw更加有复用性。在编辑时应该养成“修改操作驱动”的观念。

点命令非常简单但又功能强大,帮助你开始自动化处理简单的任务。在后续的章节中,你将会学习到如何使用Vim的宏命令来自动化处理更多复杂的操作。但是首先,还是让我们来学习一下如何使用寄存器来存取文本吧。

寄存器

学习Vim中的寄存器就像第一次学习线性代数一样,除非你学习了他们,否则你会觉得自己根本不需要它们。

你可能已经在复制或删除文本并用pP粘贴它们到别处的时候使用过Vim的寄存器了。但是,你知道Vim总共有10种不同类型的寄存器吗?

在这一章节中,我会介绍Vim的所有寄存器类型,以及如何有效地使用它们。

寄存器的10种类型

下面是Vim所拥有的10种寄存器类型:

  1. 匿名寄存器(""
  2. 编号寄存器("0-9).
  3. 小删除寄存器 ("-).
  4. 命名寄存器 ("a-z).
  5. 只读寄存器 (":, ".,and "%).
  6. Buffer交替文件寄存器 ("#).
  7. 表达式寄存器 ("=).
  8. 选取和拖放寄存器("* and "+).
  9. 黑洞寄存器 ("_).
  10. 搜索模式寄存器 ("/).

寄存器命令

以下是一些存值到寄存器中操作:

1
2
3
y    复制
c 删除文本并进入输入模式
d 删除文本

其实还有更多的寄存器写入操作(比如sx),但是上面列出的是最常用的一些。根据经验看来,如果一个操作删除了文本,那么很有可能这个操作将移除的文本存入寄存器中了。

To put (paste) texts from registers, you can use:

想要从寄存器中取出(粘贴)文本,你可以用以下的命令:

1
2
p    在光标位置之后粘贴文本
P 在光标位置之前粘贴文本

pP都可以接受计数和一个寄存器标志作为参数。比如,想要把最近复制的文本粘贴10次的话可以用10p。想粘贴寄存器”a”中的文本,可以用"ap。想将寄存器“a”中的文本粘贴10次的话,可以使用10"ap

从某个特定寄存器中读取文本的通用语法是"x,其中x是这个寄存器的标识。

在输入模式中使用寄存器

在这一章节中你学到的东西在输入模式中也同样适用。想要获取寄存器”a”中的文本,通常可以使用"ap来进行。不过当你在输入模式下时,你需要运行Ctrl-r a。在输入模式下使用寄存器的语法是:

1
Ctrl-r x

其中x是寄存器标识。既然你现在已经知道如何存储和访问寄存器了,让我们学点更深入的吧。

匿名寄存器("")

想从匿名寄存器中获取文本,可以使用""p。 匿名寄存器默认存储着你最近一次复制,修改或删除的文本。如果再进行另一次复制,修改或删除,Vim会自动替换匿名寄存器中的文本。匿名寄存器和电脑上粘贴板的功能很接近。

默认情况下,p(或者P)是和匿名寄存器相关联的(从现在起我将使用p而不是""p来指代匿名寄存器)。

编号寄存器("0-9)

编号寄存器会自动以升序来进行填充。一共有两种不同的编号寄存器:复制寄存器(0)和其他编号寄存器(1-9)。让我们先来讨论复制寄存器。

复制寄存器 ("0)

如果你使用yy来复制一整行文本,事实上Vim会将文本存放两个寄存器中:

  1. 匿名寄存器 (p).
  2. 复制寄存器 ("0p).

在你又复制其他不同的文本后,Vim会自动替换匿名寄存器和复制寄存器中的内容。其他的任何操作都不会被存放在0号寄存器中。这可以为你提供方便,因为除非你再进行另一次复制,否则你已经复制的内容会一直在寄存器中,无论你进行多少次修改和删除。

比如,如果你:

  1. 复制一整行 (yy)
  2. 删除一整行(dd)
  3. 再删除另一行 (dd)

复制寄存器中的文本仍然是第一步中复制的文本。

如果你:

  1. 复制一整行 (yy)
  2. 删除一整行 (dd)
  3. 复制另一行 (yy)

复制寄存器中的内容则是第三步中复制的内容。

还有一个小技巧,在输入模式下,你可以使用Ctrl-r 0快速地粘贴你刚才复制的内容。

编号寄存器 ("1-9)

当你修改或者删除至少一整行的文本时,这部分文本会按时间顺序被存储在1-9号编号寄存器中。(编号越小时间距离越近)

比如,你有以下这些文本:

1
2
3
line three
line two
line one

当你的光标在文本“line three”上时,使用dd来一行一行地删除这些文本。在所有文本都已经删除后,1号寄存器中的内容应该是”line one”(时间上最近的文本), 2号寄存器则包含”line two”(时间上第二近的文本),3号寄存器中则包含”line three”(最早删除的文本)。普通模式下可以使用"1p来获取1号寄存器中的内容。

编号寄存器的编号在使用点命令时会自动增加。比如,如果你的1号编号寄存器("1)中的内容为”line one”, 2号寄存器("2)为”line two”, 三号寄存器("3),你可以使用以下的技巧来连续地粘贴他们:

  • 使用"1p来粘贴1号寄存器中的内容。
  • 使用. (点命令)来粘贴2号寄存器("2)中的内容。
  • 使用. (点命令)来粘贴3号寄存器("3)中的内容。

在连续地使用点命令时,Vim会自动的增加编号寄存器的编号。这个技巧对于所有的编号寄存器都适用。如果你从5号寄存器开始("5P), 点命令.会执行"6P,再次使用.则会执行"7P,等等。

小型的删除比如单词删除(dw)或者单词修改(cw)不会被存储在编号寄存器中,它们被存储在小删除寄存器("-)中,我将在接下来的一小节讨论小删除寄存器。

小删除寄存器("-)

不足一行的修改或者删除都不会被存储在0-9号编号寄存器中,而是会被存储在小删除寄存器 ("-)中。

比如:

  1. 删除一个单词 (diw)
  2. 删除一行文本 (dd)
  3. 删除一行文本 (dd)

"-p 会给你第一步中删除的单词。

另一个例子:

  1. 删除一个单词(diw)
  2. 删除一行文本 (dd)
  3. 删除一个单词 (diw)

"-p 会给出第三步中删除的单词。类似地, "1p 会给出第二步中删除的一整行文本。不幸的是我们没有办法获取第一步中删除的单词,因为小删除寄存器只能存储一个文本。然而,如果你想保存第一步中删除的文本,你可以使用命名寄存器来完成。

命名寄存器 ("a-z)

命名寄存器是Vim中用法最丰富的寄存器。a-z命名寄存器可以存储复制的,修改的和被删除的文本。不像之前介绍的3种寄存器一样,它们会自动将文本存储到寄存器中,你需要显式地告诉Vim你要使用命名寄存器,你拥有完整的控制权。

为了复制一个单词到寄存器”a”中,你可以使用命令"ayiw

  • "a告诉Vim下一个动作(删除/修改/复制)会被存储在寄存器”a”中
  • yiw复制这个单词

为了从寄存器”a”中获取文本,可以使用命令"ap。你可以使用以26个字母命名的寄存器来存储26个不同的文本。

有时你可能会想要往已有内容的命名寄存器中继续添加内容,这种情况下,你可以追加文本而不是全部重来。你可以使用大写版本的命名寄存器来进行文本的追加。比如,假设你的”a”寄存器中已经存有文本”Hello”,如果你想继续添加”world”到寄存器”a”中,你可以先找到文本”world”然后使用"Aiw来进行复制,即可完成追加。

只读寄存器(":, "., "%)

Vim有三个只读寄存器:.,:%,它们的用法非常简单:

1
2
3
.    存储上一个输入的文本
: 存储上一次执行的命令
% 存储当前文件的文件名

如果你写入”Hello Vim”,之后再运行".p就会打印出文本”Hello Vim”。如果你想要获得当前文件的文件名,可以运行命令"%p。如果你运行命令:s/foo/bar/g,再运行":p的话则会打印出文本”s/foo/bar/g”。

Buffer交替文件寄存器 ("#)

在Vim中,#通常代表交替文件。交替文件指的是你上一个打开的文件,想要插入交替文件的名字的话,可以使用命令"#p

表达式寄存器 ("=)

Vim有一个表达式寄存器,"=,用于计算表达式的结果。表达式是Vim中非常宏大的一个话题,所以我只会在这里介绍一些基础知识,我将会在之后的章节中进一步讲解更多关于表达式的细节。

你可以使用以下命令计算数学表达式1+1的值:

1
"=1+1<Enter>p

在这里,你在告诉Vim你正在使用表达式寄存器"=,你的表达式是(1+1),你还需要输入p来的到结果。正如之前所提到的,你也可以在输入模式中访问寄存器。想要在输入模式中计算数学表达式的值,你可以使用:

1
Ctrl-r =1+1

你可以使用@来从任何寄存器中获取表达式并用表达式寄存器计算其值。如果你希望从寄存器”a”中获取文本:

1
"=@a

之后输入<enter>,再输入p。类似地,想在输入模式中得到寄存器”a”中的值可以使用:

1
Ctrl-r =@a

你也能使用表达式寄存器来计算Vim脚本的值。如果你使用:let i = 1定义一个变量i,你可以用"=i获取到它的值,按下回车,再按下p。想在输入模式中获取到这个值的话可以运行命令Ctrl-r=i

假设你有一个方程:

1
2
3
function! HelloFunc()
return "Hello Vim Script!"
endfunction

你可以通过调用这个方程获取它的值。想要在普通模式中调用这个方程,你可以使用:"=HelloFunc(), 按下回车再按下p。而在输入模式下可以使用Ctrl-r =HelloFunc()

选取和拖放寄存器 ("*, "+)

你难道不觉得有些时候你需要从某些外部的程序中复制一些文本并粘贴到Vim中吗,或者反过来操作?有了Vim的选取和拖放寄存器你就能办到。Vim有两个选取寄存器:quotestar ("*) 和 quoteplus ("+)。你可以用它们来访问从外部程序中复制的文本。

如果你在运行一个外部程序(比如Chrome浏览器),然后你使用Ctrl-c(或者Cmd-c,取决于你的操作系统)复制了一部分文本,通常你是没有办法在Vim里使用p来粘贴这部分文本的。但是,Vim的两个寄存器"+"*都是和你系统的粘贴板相连接的,所以你可以使用"+p"*p来粘贴这些文本。反过来,如果你使用"+yiw或者"*yiw在Vim中复制了一些文本,你可以使用Ctrl-v(或者Cmd-v)。值得注意的是这个方法只在你的Vim开启了+clipboard选项时才有用,可以在命令行中运行vim --version查看这一选项。如果你看见-clipboard的话,则需要安装一下支持Vim粘贴板的配置。

你也许会想如果"*"+能办到的事完全相同,那为什么Vim需要两个不同的寄存器呢?一些机器使用的是X11窗口系统,这一系统有3个类型的选项:首选,次选和粘贴板。如果你的机器使用的是X11的话,Vim使用的是quotestar ("*)寄存器作为X11的首选选项,并使用 quoteplus ("+)作为粘贴板选项。这只在你的Vim配置里开启了xterm_clipboard 选项时才有效(vim --version中的+xterm_clipboard)。如果你的的Vim配置中没有 xterm_clipboard也不是什么大问题。这只是意味着quotestarquoteplus两个寄存器是可以互相替代的。

我发觉使用=*p或者=+p的话比较麻烦,为了使Vim仅使用p就能粘贴从外部程序复制的文本,你可以在你的vimrc配置文件中加入下面一行:

1
set clipboard=unnamed

现在当我从外部程序中复制文本时,我可以使用匿名寄存器p来进行粘贴。我也可以在Vim中复制文本后在外部程序中使用Ctrl-v来粘贴。如果你的Vim开启了 +xterm_clipboard设置,你或许会想同时也使用unnamedunnamedplus的粘贴板选项。

黑洞寄存器 ("_)

你每次删除或修改文本的时候,这部分文本都会自动保存在Vim的寄存器中。有些时候你并不希望把什么东西都往寄存器里存,这该怎么办到呢?

你可以使用黑洞寄存器("_)。想要删除一行并且不将其存储在任何寄存器中时,可以使用"_dd命令,它是和 /dev/null 类似的寄存器。

搜索模式寄存器 ("/)

为了粘贴你的上一个搜索询问(/?),你可以使用搜索模式寄存器("/)。使用命令 "/p就能粘贴上一个搜索的条目。

查看所有的寄存器

你可以使用:register命令来查看你的所有寄存器。如果你只想查看”a”,”1”和”-“寄存器的内容的话则可以使用命令:register a 1 -

有一个Vim的插件叫做 vim-peekaboo ,可以让你查看到寄存器的内容,在普通模式下输入"@ 即可,或者在输入模式中输入Ctrl-r。我发现这个插件相当的有用,因为大多数时候我是记不住我的寄存器中的内容的。值得一试!

执行寄存器

命名寄存器不只可以用来存放文本,你还可以借助@来执行宏命令。我会在下一章节中介绍宏命令。如果你将文本”Hello Vim”存放在寄存器”a”中,并且之后你在同一个寄存器里记录了一个宏命令 (qa{macro-commands}q),那么这个宏命令将会覆盖之前存储的文本”Hello Vim”(你可以使用@a来执行寄存器中存储的宏命令)。

清除寄存器

从技术上来说,我们没有必要来清除任何寄存器,因为你下一个使用来存储文本的寄存器会自动覆盖该寄存器中之前的内容。然而,你可以通过记录一个空的宏命令来快速地清除任何命名寄存器。比如,如果你运行qaq,Vim就会在寄存器”a”中记录一个空的宏命令。还有一种方法就是运行命令:call setreg('a',''),其中’a’代表的就是寄存器”a”。还有一种清除寄存器的方法就是使用表达式:let @a = ''来将寄存器的值设为空的字符串。

获取寄存器中的内容

你可以使用:put命令来粘贴任何寄存器的内容。比如,如果你运行命令:put a,Vim就会打印出寄存器”a”的内容,这和"ap非常像,唯一的区别在于在普通模式下命令p在当前光标位置之后打印寄存器的内容,而:put新起一行来打印寄存器的内容。

聪明地学习寄存器

恭喜你成功地坚持到了最后!这一章有非常多的内容需要消化。如果你感觉被新的知识淹没,你要知道你并不孤单,当我最初开始学习Vim寄存器时也有这种感觉。

我并不认为你必须现在就记得所有的知识点。为了提高我们的生产效率,你可以从使用以下三类寄存器开始:

  1. 匿名寄存器("").
  2. 命名寄存器 ("a-z).
  3. 编号寄存器 ("0-9).

既然匿名寄存器是默认和pP,你只需要学习两个寄存器:命名寄存器和编号寄存器。之后如果你需要用到其他的寄存器时你再逐渐地学习其他寄存器的用法,不用急,慢慢来。

普通人的短期记忆都是有极限的,大概每次只能记住7个信息。这就是为什么在我的日常编辑中,我只用3到7个命名寄存器的原因,我没有办法记住整整26个寄存器的内容。我通常从寄存器”a”开始用,之后用寄存器”b”,以字母表升序的顺序来使用。尝试一下各种方法,看看哪种最适合你。

Vim寄存器非常强大,合理使用的话能够避免你输入数不清的重复文本。但是现在,是时候学习一下宏命令了。

宏命令

在编辑文件的时候,你会发现有时候你在反复地做一些相同的动作。如果你仅做一次,并在需要的时候调用这些动作岂不是会更好吗。通过 Vim 的宏命令,你可以将一些动作记录到 Vim 寄存器。

在本章中,你将会学习到如何通过宏命令自动完成一些普通的任务(另外,看你的文件在自动编辑是一件很酷的事情)。

基本宏命令

宏命令的基本语法如下:

1
2
qa                     开始记录动作到寄存器 a
q (while recording) 停止记录

你可以使用小写字母 (a-z)去存储宏命令。并通过如下的命令去调用:

1
2
@a    Execute macro from register a
@@ Execute the last executed macros

假设你有如下的文本,你打算将每一行中的所有字母都变为大写。

1
2
3
4
5
hello
vim
macros
are
awesome

将你的光标移动到 “hello” 栏的行首,并执行:

1
qa0gU$jq

上面命令的分解如下:

  • qa 开始记录一个宏定义并存储在 a 寄存器。
  • 0 移动到行首。
  • gU$ 将从光标到行尾的字母变为大写。
  • j 移动到下一行。
  • q 停止记录。

调用 @a 去执行该宏命令。就像其他的宏命令一样,你也可以为该命令加一个计数。例如,你可以通过 3@a 去执行 a 命令3次。你也可以执行 3@@ 去执行上一次执行过的宏命令3次。

安全保护

在执行遇到错误的时候,宏命令会自动停止。假如你有如下文本:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

你想将每一行的第一个词变为大写,你可以使用如下的宏命令:

1
qa0W~jq

上面命令的分解如下:

  • qa 开始记录一个宏定义并存储在 a 寄存器。
  • 0 移动到行首。
  • W 移动到下一个单词。
  • ~ 将光标选中的单词变为大写。
  • j 移动到下一行。
  • q 停止记录。

我喜欢对宏命令进行很多次的调用,所以我通常使用 99@a 命令去执行该宏命令99次。当 Vim 在最后一行执行 j 命令的时候,会发现已经没有下一行可以继续,遇到执行的错误,因此宏命令会停止。

实际上,遇到错误自动停止运行是一个很好的特性。否则,Vim 会继续执行该命令99次,尽管它已经执行到最后一行了。

命令行执行宏

在正常模式执行 @a 并不是宏命令调用的唯一方式。你也可以在命令行执行 :normal @a:normal 会将任何用户添加的参数作为命令去执行。例如添加 @a,和在 normal mode 执行 @a 的效果是一样的。

:normal 命令也支持范围参数。你可以在选择的范围内去执行宏命令。如果你只想在第二行和第三行执行宏命令,你可以执行 :2,3 normal @a。我会在后续的章节中介绍更多关于在命令行中执行的命令。

在多个文件中执行宏命令

假如你有很多的 .txt 文件,每一个文件包含不同的内容。并且你只想将包含有 “donut” 单词的行的第一个单词变为大写。那么,该如何在很多文件中特定的行执行执行变该操作呢?

第一个文件:

1
2
3
4
# savory.txt
a. cheddar jalapeno donut
b. mac n cheese donut
c. fried dumpling

第二个文件:

1
2
3
4
# sweet.txt
a. chocolate donut
b. chocolate pancake
c. powdered sugar donut

第三个文件:

1
2
3
# plain.txt
a. wheat bread
b. plain donut

你可以这么做:

  • :args *.txt 查找当前目录下的所有 .txt 文件。
  • :argdo g/donut/normal @a 在所有 :args 中包含的文件里执行一个全局命令 g/donut/normal @a
  • :argdo update 在所有 :args 中包含的文件里执行 update 命令会将修改后的内容保存下来。

如果你对全局命令 :g/donut/normal @a 不是很了解的话,该命令会在包含有 /donut/ 中的所有行执行normal @a 命令。我会在后面的章节中介绍全局命令。

递归执行宏命令

你可以递归地执行宏命令,通过在记录宏命令时调用相同的宏来实现。假如你有如下文本,你希望改变第一个单词的大小写:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

如下命令会递归地执行:

1
qaqqa0W~j@aq

上面命令的分解如下:

  • qaq 记录一个空白的宏命令到 “a” 。把宏命令记录在一个空白的命令中是必须的,因为你不会想将该命令包含有任何其他的东西。
  • qa 开始录入宏命令到寄存器 “a”。
  • 0 移动到行首。
  • W 移动到下一个单词。
  • ~ 改变光标选中的单词的大小写。
  • j 移动到下一行。
  • @a 执行宏命令 “a”。当你记录该宏命令时,@a 应该是空白的,因为你刚刚调用了 qaq
  • q 停止记录。

现在,让我们调用 @a 来查看 Vim 如何递归的调用该宏命令。

宏命令是如何知道何时停止呢?当宏执行到最后一行并尝试 j 命令时,发现已经没有下一行了,就会停止执行。

增添一个已知宏

如果你想在一个已经录制好的宏定义中添加更多的操作,与其重新录入它,不如选择修改它。在寄存器一章中,你学习了如何使用一个已知寄存器的大写字母来添加一个新的寄存器。为了在寄存器“a”中添加更多的操作,你可以使用“A”。假设你不仅希望将第一个单词变为大写,也希望在每一行末尾添加一个句点。

假设当前寄存器“a”中有如下的命令:

1
0W~

你可以这样做:

1
qAA.<esc>q

分解如下:

  • qA 开始在寄存器 “A” 中记录宏命令。
  • A.<esc> 在行的末尾(A)假如一个句点,并且退出插入模式。
  • q 停止记录宏命令。

现在,当你执行 @a 时,它会跳到行的第一个字符(0),跳到下一个单词(W),改变光标选中的字母的大小写(~),移动到最后一行并且转到插入模式(A),写入一个句点(.),退出插入模式(<esc>)。

修改一个已知宏

在已存在的宏定义的末尾添加新的动作是一个很好的功能,但假如你希望在一个宏命令的中间添加动作该怎么做呢?本节,我会向你展示如何修改一个宏。

假设,在改变第一个单词的大小写和在末尾加入一个句点之间,你想要在单词 “donut” 之前加入 “deep fried”(因为唯一比甜甜圈好的东西就是炸甜甜圈)。

我会重新使用上一节使用过的文本:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

首先,让我们通过 :put a 调用一个已经录制好的宏命令(假设你已经有了上一节中使用过的宏命令):

1
0W~A.^[

^[ 是什么意思呢?不记得了吗,你之前执行过 0W~A.<esc>^[ 是 Vim 的内部指令,表示 <esc>。通过这些指定的键值组合,Vim 知道这些是内部代码的一些替代。一些常见的内部指令具有类似的替代,例如 <esc><backspace><enter>。还有一些其他的键值组合,但这不是本章的内容。

回到宏命令,在改变大小写之后的键后面(~),让我们添加($)来移动光标到行末,回退一个单词(b),进入插入模式(i),输入“deep fried ”(别忽略“fried ” 后面的这个空格),之后退出插入模式(<esc>)。

完整的命令如下:

1
0W~$bideep fried <esc>A.^[

这里有一个问题,Vim 不能理解 <esc>。所以你需要将其替换为内部代码的形式。在插入模式,在按下<esc>后按下 Ctrl-v,Vim 会打印 ^[Ctrl-v 是一个插入模式的操作符,可以逐字地插入一个非数字字符。你的宏命令应该如下:

1
0W~$bideep fried ^[A.^[

为了在寄存器“a”中添加修改后的指令,你可以通过在一个已知寄存器中添加一个新入口的方式来实现。在一行的行首,执行 "ay$。这将会告诉 Vim 你打算使用寄存器 “a” ("a) 来存储从当前位置到行末的文本(y$)。

现在,但你执行 @a 时,你的宏命令会自动改变第一个单词的大小写,在“donut”前面添加“deep fried”,之后在行末添加“.”。

另一个修改宏命令的方式是通过命令行解析。执行 :let @a=",之后执行 Ctrl-r Ctrl-r a,这会将寄存器“a”的命令逐字打印出来。最后,别忘记在闭合的引号(")。如果你希望在编辑命令行表达式时插入内部码来使用特定的字符,你可以使用 Ctrl-v

拷贝宏

你可以很轻松的将一个寄存器的内容拷贝到另一个寄存器。例如,你可以使用 :let @z = @a 将寄存器“a” 中的命令拷贝到寄存器“z”。 @a 表示寄存器“a”中存储的内容,你现在执行 @z,将会执行和 @a 一样的指令。

我发现对常用的宏命令创建冗余是很有用的。在我的工作流程中,我通常在前7个字母(a-g)上创建宏命令,并且我经常不加思索地把它们替换了。因此,如果我将很有用的宏命令移动到了字母表的末尾,就不用担心我在无意间把他们替换了。

连续执行宏命令

Vim 可以连续和同时运行宏命令,假设你有如下的文本:

1
2
3
4
5
import { FUNC1 } from "library1";
import { FUNC2 } from "library2";
import { FUNC3 } from "library3";
import { FUNC4 } from "library4";
import { FUNC5 } from "library5";

假如你希望把所有的 “FUNC” 字符变为小写,那么宏命令为如下:

1
qa0f{gui{jq

分解如下:

  • qa 开始记录宏命令到 “a” 寄存器。
  • 0移动到第一行。
  • f{ 查找第一个 “{” 字符。
  • gui{ 把括号内的文本(i{)变为小写(gu)。
  • j 移动到下一行。
  • q 停止记录宏命令。

现在,执行 99@a 在剩余的行修改。然而,假如在你的文本里有如下 import 语句会怎么样呢?

1
2
3
4
5
6
import { FUNC1 } from "library1";
import { FUNC2 } from "library2";
import { FUNC3 } from "library3";
import foo from "bar";
import { FUNC4 } from "library4";
import { FUNC5 } from "library5";

执行 99@a,会只在前三行执行。而最后两行不会被执行,因为在执行第四行(包含“foo”)时会遇到错误而停止。然而这种情况你希望继续向下执行。你可以移动到包含(“FUNC4”)的一行,并重新调用该命令。但是假如你希望仅调用一次命令就完成所有操作呢?你可以并行地执行宏命令。

如本章前面所说,可以使用 :normal 去执行宏命令,(例如: :3,5 normal @a 会在 3-5行执行 a 寄存器中的宏命令)。如果执行 :1,$ normal @a,会在所有除了包含有 “foo” 的行执行,而且它不会出错。

尽管本质上来说,Vim 并不是在并行地执行宏命令,但表面上看,它是并行运行的。 Vim 会独立地在从第一行开始(1,$)每一行执行 @a 。由于 Vim 独立地在每一行执行命令,每一行都不会知道有一行(包含“foo”)会遇到执行错误。

聪明地学习宏命令

你在编辑器里做的很多事都是重复的。为了更好地编辑文件,请乐于发现这些重复性的行为。执行宏命令或者点命令,而不是做相同的动作两次。几乎所有你在 Vim 所作的事情都可以变为宏命令。

刚开始的时候,我发现宏命令时很棘手的,但是请不要放弃。有了足够的练习,你可以找到这种文本自动编辑的快乐。

使用某种助记符去帮助你记住宏命令是很有帮助的。如果你有一个创建函数(function)的宏命令,你可以使用 “f” 寄存器去录制它。如果你有一个宏命令去操作数字,那么使用寄存器 “n” 去记住它是很好的。用你想执行的操作时想起的第一个字符给你的宏命令命名。另外,我发现 “q” 是一个很好的宏命令的寄存器,因为执行 “qq” 去调用宏命令是很快速而简单的。最后,我喜欢按照字母表的顺序去添加我的宏命令,例如从 qaqb 再到 qc。去寻找最适合你的方法吧。

撤销

对于任何一个现代的软件来说,撤销都是一个很基本的特性。 Vim 的撤销系统不仅支持撤销和取消撤销任何修改,而且支持改动时间线功能。 在本章中,你将会学会如何执行撤销和 取消撤销文本,浏览撤销分支,反复撤销, 以及浏览改动时间线。

Undo,Redo,和 UNDO

对于一个基本的 undo 操作,你可以执行 u 或者 :undo
假设你有如下文本:

1
one

以及另一个文本:

1
2
one
two

如果你执行 u,Vim 会删除 “two”。
Vim 是如何知道应该恢复多少修改呢? 答案是,Vim每次仅恢复一次修改,这有点类似于点命令的操作(和 点命令不同之处在于,命令行命令也会被算作一次修改)。

为了取消上一次的撤销,执行 Ctrl-R 或者 :redo。例如上面的例子中,当你执行撤销来删除 “two” 以后,你可以执行 Ctrl-R 来恢复被删除掉的文本。

Vim 也有另一个命令 U 可以实现 UNDO 的功能,执行这个命令会撤销所有最新的修改。
那么,Uu 的区别是什么呢?首先,U 会删除 所有的 最新的修改,而 u 一次仅删除一次修改。 其次,执行u 不会被算作一次修改,而执行 U 则会被算作一次修改。

让我们会的之前的例子:

1
2
one
two

修改第二行的内容为 “three” (ciwthree<esc>):

1
2
one
three

再次修改第二行的例子为 “four” (ciwfour<esc>):

1
2
one
four

此时,如果你按下 u,你会看到 “three”。如果你再次按下 u,你会看到 “two”。然而,在第二行任为 “four” 的时候,如果你按下 U,你会看到

1
one

执行 U 会跳过中间所有修改,直接恢复到文件最初的状态(第二行为空)。另外,由于 UNO 实际上是执行了一个新的修改,因此你可以 UNDO 执行过的 UNDO。 执行 U 后 再次执行 U 会撤销 自己。假如你连续执行 U,那么你将看到第二行的文本不停地出现和消失。

就我个人而言,我几乎不会使用 U,因为很难记住文本最初的样子。如果非说在什么场景下使用 U,那就是在我意外地执行了 Shift-u

Vim 可以通过变量 undolevels 来选择最多可执行 undo 的次数。你可以通过 :echo &undolevels 来查看当前的配置。我一般设置为 1000。如果你也想设置为 1000 的话,你可以执行 :set undolevels=1000。不用担心,你可以设置它为任何一个你想设置的值。

断点插入操作

在上文中我提到,u 每次恢复一个修改,类似于点命令。在每次进入 插入模式和退出插入模式之间的任何修改都被定义为一次修改。

如果你执行 ione two three<esc> 之后,按下 u,Vim 会同时删除 “one two three”,因为这是一笔修改。如果你每次只输入较短的文本,那这是可接受的;可假设你在一次插入模式中输入了大量的文本,而后退出了插入模式,可很快你意识到这中间有部分错误。此时,如果你按下 u,你会丢失上一次输入的所有内容。 因此,假设你按下 u 只删除你上一次输入的一部分文本岂不是会更好。

幸运的是,你可以拆分它。当你在插入模式时,按下 Ctrl-G u 会生成一个断点。例如,如果你执行 ione <Ctrl-G u>two <Ctrl-G u>three<esc>,之后你按下u,你仅会失去文本 “three”,再次执行 u,会删除 “two”。当你想要输入一长段内容时,应该有选择性地执行断点插入操作。在每一句话的末尾,两个段落的中间,或者每一行代码结束时插入断点是一个很好的选择,这可以帮助你快速从错误中恢复出来。

在执行删除操作后插入断点也非常有用,例如通过 Ctrl-W 删除光标前的单词,以及 Ctrl-U删除光标前的所有文本。一个朋友建议我使用如下的映射:

1
2
inoremap <c-u> <c-g>u<c-u>
inoremap <c-w> <c-g>u<c-w>

通过上述命令,你可以很轻松地恢复被删除的文本。

撤销树

Vim 将每一次修改存储在一个撤销树中。如果你打开一个空白文件:

插入一段话:

1
one

插入一段话:

1
2
one
two

undo:

1
one

插入一段不同的话:

1
2
one
three

再次 undo

1
one

插入另一段话:

1
2
one
four

现在如果你执行 undo:

1
one

如果你再次执行 undo 操作:

文本 “one” 也会丢失。对于大部分编辑器来说,找回文本 “two” 和 “three” 都是不可能的事情,但是对于 Vim 来说却不是这样。执行 g+,你会得到:

1
one

再次执行 g+ ,你将会看到一位老朋友:

1
2
one
two

让我们继续执行 g+:

1
2
one
three

再一次执行 g+ :

1
2
one
four

在 Vim 中,你每一次执行 u 去做一次修改时,Vim都会通过创建一个“撤销分支”来保存下之前的文本内容。在本例中,你先输入“two”, 执行 u,输入“three”,执行 u,然后输入“three”,此时,撤销树已经包含了至少两个叶子节点,主节点包含文本“three”(最新),分支节点包含文本“two”。假如你执行了另一次撤销操作并且输入了“four”,那么此时会生成三个节点,一个主节点包含文本“four”, 以及另外两个节点分别存储了“three”和“two”。

为了在几个不同的节点状态间进行切换,你可以执行 g+ 去获取一个较新的状态,以及执行 g- 去获取一个教旧的状态。 uCtrl-Rg+, 和 g- 之间的区别是,u and Ctrl-R 只可以在 main 节点之间进行切换,而g+g- 可以在 所有 节点之间进行切换。

Undo 树并不可以很轻松地可视化。我发现一个插件 vim-mundo 对于理解 undo 树很有帮助。花点时间去与它玩耍吧。

保持撤销状态

当你通过 Vim 打开一个文件,并且立即按下 u,Vim 很可能会显示 “_Already at oldest change_” 的警告。 Vim 可以通过 :wundo 保持一份你的 undo 历史记录。

创建一个文件 mynumbers.txt. 输入:

1
one

插入另一行文件 (确保你要么退出并重新进入插入模式,要么创建了断点):

1
2
one
two

插入新的一行:

1
2
3
one
two
three

现在,创建你的撤销记录文件。 语法为 :wundo myundofile。 如果你需要覆盖一个已存在的文件,在 wundo 之后添加 !.

1
:wundo! mynumbers.undo

退出 Vim。

此时,在目录下,应该有mynumbers.txtmynumbers.undo 两个文件。再次打开 mynumbers.txt 文件并且按下 u,这是没有响应的。因为自打开文件后,你没有执行任何的修改。现在,通过执行 :rundo 来加载 undo 历史。

1
:rundo mynumbers.undo

此时,如果你按下 u,Vim 会删除 “three”。再次按下 u可以删除 “two”。这就好像你从来没有关闭过 Vim 一样。

如果你想要自动加载 undo 历史文件,你可以通过在你的 .vimrc 文件中添加如下代码:

1
2
set undodir=~/.vim/undo_dir
set undofile

我认为将所有的 undo 文件集中保存在一个文件夹中最好,例如在 ~/.vim 目录下。 undo_dir 是随意的。 set undofile 告诉 Vim 打开 undofile 这个特性,因为该特性默认是关闭的。现在,无论你何时保存,Vim 都会自动创建和保存撤销的历史记录(在使用undo_dir目录前,请确保你已经创建了它)。

时间旅行

是谁说时间旅行不存在。 Vim 可以通过 :earlier 命令将文本恢复为之前的状态。

假如有如下文本:

1
one

之后你输入了另一行:

1
2
one
two

如果你输入 “two” 的时间少于10秒,那么你可以通过如下命令恢复到 “two” 还没被输入前的状态:

1
:earlier 10s

你可以使用 :undolist 去查看之前所做的修改。 :earlier 可以加上分钟 (m), 小时 (h), and 天 (d) 作为参数。

1
2
3
4
:earlier 10s    恢复到10秒前的状态
:earlier 10m 恢复到10分钟前的状态
:earlier 10h 恢复到10小时前的状态
:earlier 10d 恢复到10天前的状态

聪明地学习撤销操作

uCtrl-R 是两个不可缺少的 Vim 参数。请先学会它们。在我的工作流中,我并不使用 UNDO,然而我认为承认它存在是很好的。下一步,学会如何使用:earlier:later,以及时间参数。在这之后,请花些时间理解 undo 树。 插件 vim-mundo 对我的帮助很大。单独输入本章中展示的文本,并且查看撤销树的每一次改变。一旦你掌握它,你看待撤销系统的眼光一定不同。

在本章之前,你学习了如何在项目内查找任何文本,配合撤销,你可以在时间维度上查找任何一个文本。你现在可以通过位置和写入时间找到任何一个你想找的文本。你已经对 Vim 无所不能了。

可视模式

您可能知道您可以高亮显示文本块并对其进行更改。 Vim也可以使用可视模式。 Vim有三种不同的可视模式可供使用。在本章中,您将学习如何使用每种可视模式来有效地处理文本块。

三种可视模式

这三种模式是:

1
2
3
v         角色可视模式
V 逐行可视模式
Ctrl-v 逐块可视模式

如果您有文字:

1
2
3
one
two
three

逐字符可视模式用于选择单个字符。在第一行的第一个字符上按v。然后使用j跳转至下一行。它高亮显示从”one”到光标位置的所有文本。现在,如果您按gU,Vim将高亮显示的字符转为大写。

逐行可视模式适用于整行。按V并观看Vim选择光标的所在行。就像逐字符可视模式一样,如果您运行gU,Vim将高亮显示的字符转为大写。

逐块可视模式适用于行和列。与其他两种模式相比,它为您提供了更大的移动自由度。按Ctrl-V。 Vim像逐个字符可视模式一样高亮显示光标下的字符,除了可以在下一行之前不高亮显示每个字符直到行的末尾,它可以转到下一行而不高亮显示当前行的整个字符。尝试用h/j/k/l移动,并观察光标的移动。

在Vim窗口的左下方,您会看到显示-- VISUAL ---- VISUAL LINE ---- VISUAL BLOCK --以提示您所处的可视模式。

当您处于可视模式时,可以通过按vVCtrl-V键切换到另一种可视模式。例如,如果您处于逐行可视模式,并且想要切换为逐块可视模式,请运行Ctrl-V。试试吧!

有三种退出可视模式的方法:escCtrl-C和与当前可视模式相同的键。

后者的意思是,如果您当前处于逐行可视模式(V),则可以通过再次按V退出它。如果您处于字符可视模式,则可以通过按v退出它。如果您处于逐块可视模式,请按Ctrl-V

实际上,还有另一种进入可视模式的方式:

1
gv    转到上一个可视模式

它将在与上次相同的高亮显示的文本块上启动相同的可视模式。

可视模式导航

在可视模式下,您可以使用Vim动作扩展高亮显示的文本块。

让我们使用之前使用的相同文本:

1
2
3
one
two
three

这次让我们从”two”行开始。按v进入字符可视模式:

1
2
3
one
[t]wo
three

j,Vim将高亮显示从”two”行到”three”行的第一个字符的所有文本。

1
2
3
one
[two
t]hree

假设您刚刚意识到还需要高亮显示”one”行,因此按k。令您沮丧的是,它现在排除了”three”。按k实际上会还原高亮,而不是使其高亮。

1
2
3
one
[t]wo
three

有没有一种方法可以自由地扩展视觉选择范围,以向您想要的任何方向发展?

答案是肯定的。让我们稍微备份一下高亮显示”two”和”three”行的位置。

1
2
3
one
[two
t]hree <-- 光标

视觉高光跟随光标移动。如果要将其向上扩展到行”one”,则需要在光标位于字母”two”而不是”three”上时向上移动光标。现在,您的光标在”three”行上。要移动它,用oO切换光标位置。

1
2
3
one
[two <-- 光标
t]hree

现在,当您按k时,它不再缩小选择,而是向上扩展。

1
2
3
[one
two
t]hree

可视模式语法

可视模式是Vim的一种模式。成为一种模式意味着同一键的工作方式可能不同于另一种模式。幸运的是,可视模式与普通模式共享许多常用键。

例如,如果您有以下文字:

1
2
3
one
two
three

用逐行可视模式(V)高亮显示”one”和”two”行:

1
2
3
[one
two]
three

按下d键将删除选择,类似于普通模式。请注意,普通模式的语法规则动词+名词不适用。仍然存在相同的动词(d),但在可视模式下没有名词。可视模式下的语法规则是名词+动词,其中名词是高亮显示的文本。首先选择文本块,然后进行操作。

在普通模式下,有一些命令不需要移动,例如x删除光标下方的单个字符,rx替换光标下方的字符为x。在可视模式下,这些命令现在将应用于整个高亮显示的文本,而不是单个字符。返回高亮显示的文本:

1
2
3
[one
two]
three

运行x会删除所有高亮显示的文本。

您可以使用此行为在markdown文本中快速创建标题。假设您在markdown文件中有一个文本:

1
Chapter One

您需要快速将此标题转换为标题。首先,您使用yy复制文本,然后使用p粘贴文本:

1
2
Chapter One
Chapter One

现在转到第二行,以逐行可视模式选择它:

1
2
Chapter One
[Chapter One]

在markdown中,您可以通过在文本下方添加一系列=来创建标题,因此您可以通过运行r=来替换整个高亮显示的文本:

1
2
Chapter One
===========

要了解有关可视模式下的运算符的更多信息,请查看:h visual-operators

可视模式和Ex命令

您可以有选择地在高亮显示的文本块上应用Ex命令。如果您具有以下表达式:

1
2
3
const one = "one";
const two = "two";
const three = "three";

您只需要用”let”替换”const”的前两行。用_任何_可视模式高亮显示前两行,然后运行替代命令:s/const/let/g

1
2
3
let one = "one";
let two = "two";
const three = "three";

请注意,我说过您可以使用_任何_可视模式执行此操作。您不必高亮显示整个行即可在该行上运行Ex命令。只要您在每行上至少选择一个字符,就会应用Ex命令。

跨多行编辑

您可以使用逐块可视模式在Vim中跨多行编辑文本。如果需要在每行末尾添加分号:

1
2
3
const one = "one"
const two = "two"
const three = "three"

将光标放在第一行上:
-运行逐块可视模式,并向下两行(Ctrl-V jj)。
-高亮显示到行尾($)。
-附加(A) ,然后键入”;”。
-退出可视模式(esc)。

您应该看到附加的 “;” 在每一行上。顺便说一下,在逐块可视模式下,要进入插入模式,可以使用A在光标后输入文本,也可以使用I在光标前输入文本。请勿将它们与正常模式下的AI混淆。

另外,您也可以使用:normal命令:

-高亮显示所有3行(vjj)。
-输入:normal! A;

记住,:normal命令执行普通模式命令。您可以指示它运行A;来添加文本”;”在该行的末尾。

递增数字

Vim有Ctrl-XCtrl-A命令来减少和增加数字。与可视模式一起使用时,可以跨多行递增数字。

如果您具有以下HTML元素:

1
2
3
4
5
<div id="app-1"></div>
<div id="app-1"></div>
<div id="app-1"></div>
<div id="app-1"></div>
<div id="app-1"></div>

有多个具有相同名称的id是一个不好的做法,因此让我们对其进行递增以使其唯一:
-将光标移动到_第二个_”1”。
-启动逐块可视模式,并向下移动3行(Ctrl-V 3j)。这高亮显示剩余的”1”s。
-运行g Ctrl-A

您应该看到以下结果:

1
2
3
4
5
<div id="app-1"></div>
<div id="app-2"></div>
<div id="app-3"></div>
<div id="app-4"></div>
<div id="app-5"></div>

g Ctrl-A在多行上递增数字。 Ctrl-X/Ctrl-A也可以增加字母。如果您运行:

1
:set nrformats+=alpha

nrformats选项指示Vim将哪个基数视为Ctrl-ACtrl-X递增和递减的“数字”。通过添加alpha,现在将字母字符视为数字。如果您具有以下HTML元素:

1
2
3
4
5
<div id="app-a"></div>
<div id="app-a"></div>
<div id="app-a"></div>
<div id="app-a"></div>
<div id="app-a"></div>

将光标放在第二个”app-a”上。使用与上述相同的技术(Ctrl-V 3j 然后 g Ctrl-A)增加ID。

1
2
3
4
5
<div id="app-a"></div>
<div id="app-b"></div>
<div id="app-c"></div>
<div id="app-d"></div>
<div id="app-e"></div>

选择最后一个可视模式区域

您了解到gv可以快速高亮显示上一个可视模式。您还可以使用以下两个特殊标记转到最后一个可视模式的开始和结束位置:

1
2
`<    转到上一个可视模式高亮显示的最后一个位置
`> 转到上一个可视模式高亮显示的第一位

我要你观察一些东西。之前,我提到过您可以在高亮显示的文本上有选择地执行Ex命令,例如::s/const/let/g。当您这样做时,您应该看到以下内容:

1
:`<,`>s/const/let/g

您实际上是在使用标记作为范围来执行s/const/let/g命令。您随时可以随时编辑这些标记。相反,如果您需要从高亮显示的文本的开头到文件的末尾进行替换,则只需将命令行更改为:

1
:`<,$s/const/let/g

从插入模式进入可视模式

您也可以从插入模式进入可视模式。在插入模式下进入字符可视模式:

1
Ctrl-O v

回想一下,在插入模式下运行Ctrl-O可以使您执行普通模式命令。在正常模式命令挂起模式下,运行v进入逐字可视模式。请注意,在屏幕的左下方,它显示为--(insert) VISUAL--。该技巧适用于任何可视模式运算符:vV,和Ctrl-V

选择模式

Vim具有类似于可视模式的模式,称为_选择模式_。与可视模式一样,它也具有三种不同的模式:

1
2
3
gh         逐字符选择模式
gH 逐行选择模式
gCtrl-h 逐块选择模式

选择模式比Vim的可视模式更接近常规编辑器的文本高亮显示行为。

在常规编辑器中,高亮显示文本块并键入字母(例如字母”y”)后,它将删除高亮显示的文本并插入字母”y”。

如果您使用逐行选择模式(gH)高亮显示一行文本并键入”y”,它将删除高亮显示的文本并插入字母”y”,这与常规文本编辑器非常相似。

将此行为与可视模式进行对比:如果您使用逐行可视模式(V)高亮显示一行文本并键入”y”,则高亮显示的文本不会被删除,而是由文字字母”y”代替。仅将其拉动并存储在拉动寄存器"0中。

我个人从未使用过选择模式,但是很高兴知道它的存在。

以聪明的方式学习可视模式

可视模式是Vim高亮显示文本的过程。

如果发现使用可视模式操作的频率比正常模式操作的频率高得多,请当心。我认为这是一种反模式。运行可视模式操作所需的击键次数要多于普通模式下的击键次数。如果您需要删除一个内部单词,那么为什么只用三个按键(diw)就可以使用四个按键viwd(在视觉上高亮显示一个内部单词然后删除)呢?后者更为直接和简洁。当然,有时会使用适当的可视模式,但总的来说,更倾向于直接的方法。

搜索和替换

本章涵盖两个独立但相关的概念:搜索和替代。很多时候,您要搜索的文本并不简单,你必须寻找一个共同的模式。通过学习如何在搜索和替换中使用有意义的模式而不是文字,您将能够快速定位任何文本。

附带说明一下,在本章中,当谈论搜索时,我将主要使用/。您可以使用/进行的所有操作也可以使用?完成。

智能区分大小写

尝试匹配搜索词的大小写可能会很棘手。如果要搜索文本”Learn Vim”,则很容易错误键入一个字母的大小写,并得到错误的搜索结果。如果可以匹配任何情况,会不会更轻松,更安全?这是选项ignorecase闪亮的地方。只需在 vimrc 中添加setignorecase,所有搜索词就不区分大小写。现在,您不必再执行/Learn Vim了。 /learn vim将起作用。

但是,有时您需要搜索特定于案例的短语。一种方法是用 set ignorecase 关闭ignorecase选项,但是每次需要搜索区分大小写的短语时,都要打开和关闭很多工作。

是否有一种设置可以让您在大多数时间进行不区分大小写的搜索,但又知道在需要时进行区分大小写的搜索?原来有办法。

如果搜索模式_至少包含一个大写字符_,Vim 有一个smartcase选项来覆盖ignorecase。当您输入所有小写字符时,您可以将”ignorecase”和”smartcase”结合使用以执行不区分大小写的搜索,而输入一个或多个大写字符时则执行区分大小写的搜索。

在您的 vimrc 中,添加:

1
set ignorecase smartcase

如果您有这些文字:

1
2
3
hello
HELLO
Hello

您可以使用搜索词的大小写来控制不区分大小写:

  • /hello 匹配”hello”,”HELLO”和”Hello”。
  • /HELLO 仅匹配”HELLO”。
  • /Hello 仅匹配”Hello”。

有一个缺点。 如果只需要搜索小写字符串怎么办?
当您执行/hello时,Vim 将始终匹配其大写变体。
如果您不想匹配它们怎么办?您可以在搜索词前使用\C模式来告诉 Vim,
后续搜索词将区分大小写。如果执行/\Chello,它将严格匹配”hello”,而不是”HELLO”或”Hello”。

一行中的第一个和最后一个字符

您可以使用^匹配行中的第一个字符,并使用$匹配行中的最后一个字符。

如果您有以下文字:

1
hello hello

您可以使用/^hello来定位第一个”hello”。 ^后面的字符必须是一行中的第一个字符。 要定位最后一个”hello”,请运行/hello$。 $之前的字符必须是一行中的最后一个字符。

如果您有以下文字:

1
hello hello friend

运行/hello$将匹配不到任何内容,因为”friend”是该行的最后一项,而不是”hello”。

重复搜索

您可以使用”//“重复上一个搜索。如果您只是搜索/hello,则运行//等同于运行/hello。此快捷键可以为您节省一些按键操作,尤其是在您搜索了很长时间的情况下。还记得您还可以使用”n”和”N”分别以相同方向和相反方向重复上一次搜索。

如果您想快速回忆起_n_个最后一个搜索字词怎么办?您可以先遍历/,然后按“向上”/“向下”箭头键(或Ctrl-N/Ctrl-P),快速遍历搜索历史,直到找到所需的搜索词。要查看所有搜索历史,可以运行:history/

在搜索过程中到达文件末尾时,Vim 会引发错误:"搜索未找到匹配项的底部:<your-search>"("Search hit the BOTTOM without match for: <your-search>")。有时,这可以防止过度搜索,但是有时您又想将搜索重新循环到顶部。您可以使用set wrapscan选项使 Vim 在到达文件末尾时在文件顶部进行搜索。要关闭此功能,请执行set nowrapscan

搜索替代词

通常一次搜索多个单词。 如果您需要搜索”hello vim”或”hola vim”,而不是”salve vim”或”bonjour vim”,则可以使用||管道替代语法。

给予这样一段文本:

1
2
3
4
hello vim
hola vim
salve vim
bonjour vim

要同时匹配”hello”和”hola”,可以执行/hello\|hola。 您必须转义(\)管道(|)运算符,否则 Vim 将按字面意义搜索字符串”|”。

如果您不想每次都输入\|,则可以在搜索开始时使用魔术语法(\v):/\vhello|hola。 我不会在本章中介绍“魔术”,但是有了”\v”,您就不必再转义特殊字符了。 要了解有关\v的更多信息,请随时查看:h \v

设置比赛的开始和结束

也许您需要搜索作为复合词一部分的文本。 如果您有这些文字:

1
2
3
4
11vim22
vim22
11vim
vim

如果您需要选择”vim”,但仅当它以”11”开头并以”22”结束时,可以使用\zs(开始匹配)和\ze(结束匹配)运算符。 执行:

1
/11\zsvim\ze22

Vim 仍然必须匹配整个模式”11vim22”,但是仅突出显示介于\zs\ze之间的模式。 另一个例子:

1
2
foobar
foobaz

如果需要在”foobaz”中搜索”foo”,而不是在”foobar”中搜索,请运行:

1
/foo\zebaz

搜索范围字符

到目前为止,您所有的搜索字词都是文字搜索。在现实生活中,您可能必须使用通用模式来查找文本。最基本的模式是字符范围”[]”。

如果您需要搜索任何数字,则可能不想输入/0\|1\|2\|3\|4\|5\|6\|7\|8\|9\|0每一次。相反,请使用/[0-9]来匹配一位数字。 “0-9”表达式表示 Vim 尝试匹配的数字范围 0-9,因此,如果要查找 1 到 5 之间的数字,请使用”/[1-5]”。

数字不是 Vim 可以查找的唯一数据类型。您也可以执行/[a-z]来搜索小写字母,而/[A-Z]来搜索大写字母。

您可以将这些范围组合在一起。如果您需要搜索数字 0-9 以及从 a 到 f(十六进制)的小写字母和大写字母,可以执行/[0-9a-fA-F]

要进行否定搜索,可以在字符范围括号内添加”^”。要搜索非数字,请运行/[^0-9]。 Vim 可以匹配任何字符,只要它不是数字即可。请注意,范围括号内的插入符号(^)与行首插入符号(例如:/^hello)不同。如果插入号在一对方括号之外,并且是搜索词中的第一个字符,则表示“一行中的第一个字符”。如果插入符号在一对方括号内,并且是方括号内的第一个字符,则表示否定搜索运算符。 /^abc匹配行中的第一个”abc”,而/[^abc]匹配除”a”,”b”或”c”以外的任何字符。

搜索重复字符

如果需要在此文本中搜索两位数:

1
2
3
1aa
11a
111

您可以使用/[0-9][0-9]来匹配两位数字字符,但是该方法不可缩放。 如果您需要匹配二十个数字怎么办? 打字 20 次[[0-9]]并不是一种有趣的体验。 这就是为什么您需要一个count参数。

您可以将count传递给您的搜索。 它具有以下语法:

1
{n,m}

顺便说一句,当在 Vim 中使用它们时,这些“计数”花括号需要被转义。 count 运算符放在您要递增的单个字符之后。

这是count语法的四种不同变体: -{n}是完全匹配。 /[0-9]\{2\}匹配两个数字:”11”和”111”中的”11”。 -{n,m}是范围匹配。 /[0-9]\{2,3\}匹配 2 到 3 位数字:”11”和”111”。 -{,m}是符合条件的。 /[0-9]\{,3\}最多匹配 3 个数字:”1”,”11”和”111”。 -{n,}是至少匹配项。 /[0-9]\{2,\}至少匹配 2 个或多个数字:”11”和”111”。

计数参数\{0,\}(零或多个)和\{1,\}(一个或多个)是常见的搜索模式,Vim 为它们提供了特殊的运算符:*++需要被转义,而* 可以正常运行而无需转义)。 如果执行/[0-9]*,则与/[0-9]\{0,\}相同。 它搜索零个或多个数字。 它将匹配“”,”1”,”123”。 顺便说一句,它也将匹配非数字,例如”a”,因为在技术上,字母”a”中的数字为零。 在使用”*”之前,请仔细考虑。 如果执行/[0-9]\+,则与/[0-9]\{1,\}相同。 它搜索一个或多个数字。 它将匹配”1”和”12”。

预定义范围

Vim 为常见字符(例如数字和字母)提供了预定义范围。 我不会在这里逐一介绍,但可以在:h /character-classes中找到完整列表。 这是有用的:

1
2
3
4
5
6
7
\d    数字[0-9]
\D 非数字[^ 0-9]
\s 空格字符(空格和制表符)
\S 非空白字符(除空格和制表符外的所有字符)
\w 文字字符[0-9A-Za-z_]
\l 小写字母[a-z]
\u 大写字符[A-Z]

您可以像使用范围字符一样使用它们。 要搜索任何一位数字,可以使用/\d以获得更简洁的语法,而不使用/[0-9]

更多搜索示例

在一对相似字符之间捕获文本

如果要搜索由双引号引起来的短语:

1
"Vim is awesome!"

运行这个:

/"[^"]\+"

让我们分解一下:

  • " 是字面双引号。它匹配第一个双引号。
  • [^"] 表示除双引号外的任何字符,只要不是双引号,它就与任何字母数字和空格字符匹配。
  • \+表示一个或多个。由于 Vim 的前面是[^"],因此 Vim 查找一个或多个不是双引号的字符。
  • " 是字面双引号。它与右双引号匹配。

当看到第一个时,它开始模式捕获。Vim 在一行中看到第二个双引号时,它匹配第二个"模式并停止模式捕获。同时,两个“”之间的所有非“”字符都被[^"]\+ 模式捕获,在这种情况下,短语”Vim is awesome!”。由一对类似的定界符包围的词组:要捕获由单引号引起来的词组,可以使用/'[^']\+'

捕获电话号码

如果要匹配以连字符(-)分隔的美国电话号码,例如123-456-7890,则可以使用:

1
/\v\d\{3\}-\d\{3\}-\d\{4\}

美国电话号码由一组三位数字组成,其后是另外三位数字,最后是四位数字。 让我们分解一下:

  • \d\{3\}与精确重复三次的数字匹配
  • -是字面的连字符

此模式还可用于捕获任何重复的数字,例如 IP 地址和邮政编码。

这涵盖了本章的搜索部分。 现在开始替代。

基本替代

Vim 的替代命令是一个有用的命令,用于快速查找和替换任何模式。 替换语法为:

1
:s/old-pattern/new-pattern/

让我们从一个基本用法开始。 如果您有以下文字:

1
vim is good

让我们用”awesome”代替”good”,因为 Vim 很棒。 运行:s/good/awesome/.您应该看到:

1
vim is awesome

重复最后一次替换

您可以使用普通命令&或运行:s来重复最后一个替代命令。 如果您刚刚运行:s/good/awesome/,则运行&:s都会重复执行。

另外,在本章前面,我提到您可以使用”//“来重复先前的搜索模式。 此技巧可用于替代命令。 如果/good是最近完成的,并且将第一个替换模式参数留为空白,例如在:s//awesome/中,则与运行:s/good/awesome/相同。

替代范围

就像许多 Ex 命令一样,您可以将 range 参数传递给替代命令。 语法为:

1
:[range]s/old/new/

如果您具有以下表达式:

1
2
3
4
5
let one = 1;
let two = 2;
let three = 3;
let four = 4;
let five = 5;

要将第三行到第五行中的”let”替换为”const”,您可以执行以下操作:

1
:3,5s/let/const/

替代命令的范围语法与搜索({n,m})中的计数语法相似,但有细微差别。 这是通过范围的一些变化:

  • :,3/let/const/ - 如果逗号前没有给出任何内容,则表示当前行。 从当前行替换为第 3 行。
  • :1,s/let/const/ - 如果逗号后没有给出任何内容,则它也代表当前行。 从第 1 行替换为当前行。
  • :3s/let/const/ - 如果仅给出一个值作为范围(不带逗号),则仅在该行进行替换。

在 Vim 中,%通常表示整个文件。 如果运行:%s/let/const/,它将在所有行上进行替换。

模式匹配

接下来的几节将介绍基本的正则表达式。 强大的模式知识对于掌握替代命令至关重要。

如果您具有以下表达式:

1
2
3
4
5
let one = 1;
let two = 2;
let three = 3;
let four = 4;
let five = 5;

要在数字周围添加一对双引号:

1
:%s/\d/"\0"/

结果:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

让我们分解一下命令:

  • :%s 定位整个文件以执行替换。
  • \d 是 Vim 的数字预定义范围([0-9])。
  • "\0" 双引号是文字双引号。 \0是一个特殊字符,代表“整个匹配模式”。 此处匹配的模式是单个数字\d。 在第一行,”\0”的值为”1”。 在第二行,值为”2”。 在第三行,值为”3”,依此类推。

另外,&也代表\0之类的“整个匹配模式”。 :s/\d/"&"/也可以。

让我们考虑另一个例子。 给出以下表达式:

1
2
3
4
5
one let = "1";
two let = "2";
three let = "3";
four let = "4";
five let = "5";

您需要用变量名交换所有的”let”。 为此,请运行:

1
:%s/\(\w\+\) \(\w\+\)/\2 \1/

上面的命令包含太多的反斜杠,很难阅读。 使用\v运算符更方便:

1
:%s/\v(\w+) (\w+)/\2 \1/

结果:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

太好了! 让我们分解该命令:

  • :%s 定位文件中的所有行
  • (\w+) (\w+)对模式进行分组。\w是 Vim 预定义的单词字符范围([0-9A-Za-z_])之一。 包围在其中的()捕获一个单词字符匹配。 请注意两个分组之间的空间。 (\w+) (\w+) 分为两组。 在第一行上,第一组捕获“one”,第二组捕获“two”。
  • \2 \1 以相反的顺序返回捕获的组。 \2包含捕获的字符串”let”,而\1包含字符串”one”。 使\2 \1返回字符串”let one”。

回想一下\0代表整个匹配的模式。 您可以使用( )将匹配的字符串分成较小的组。 每个组都由\1, \2, \3等表示。

让我们再举一个例子来巩固这一匹配组的概念。 如果您有以下数字:

1
2
3
123
456
789

要颠倒顺序,请运行:

1
:%s/\v(\d)(\d)(\d)/\3\2\1/

结果是:

1
2
3
321
654
987

每个(\d)匹配并分组每个数字。 在第一行上,第一个(\d)的值为”1”,第二个(\d)的值为”2”,第三个(\d)的值为”3”。 它们存储在变量\1\2\3中。 在替换的后半部分,新模式\3\2\1在第一行上产生”321”值。

如果您运行了它,则:

1
:%s/\v(\d\d)(\d)/\2\1/

您将获得不同的结果:

1
2
3
312
645
978

这是因为您现在只有两个组。 被(\d\d)捕获的第一组存储在\1内,其值为”12”。 由(\d)捕获的第二组存储在\2内部,其值为”3”。 然后,\2\1返回”312”。

替代标志

如果您有以下句子:

1
chocolate pancake, strawberry pancake, blueberry pancake

要将所有 pancakes 替换为 donut,您不能只运行:

1
:s/pancake/donut

上面的命令将仅替换第一个匹配项,从而为您提供:

1
chocolate donut, strawberry pancake, blueberry pancake

有两种解决方法。 首先,您可以运行两次替代命令。 其次,您可以向其传递全局(g)标志来替换一行中的所有匹配项。

让我们谈谈全局标志。 运行:

1
:s/pancake/donut/g

Vim 迅速执行命令,将所有煎饼替换为甜甜圈。 全局命令是替代命令接受的几个标志之一。 您在替代命令的末尾传递标志。 这是有用的标志的列表:

1
2
3
4
5
6
&    重用上一个替代命令中的标志。 必须作为第一个标志传递。
g 替换行中的所有匹配项。
c 要求替代确认。
e 防止替换失败时显示错误消息。
i 执行不区分大小写的替换
I 执行区分大小写的替换

我上面没有列出更多标志。 要了解所有标志,请查看:h s_flags

顺便说一句,重复替换命令(&:s)不保留标志。 运行&只会重复:s/pancake/donut/而没有g。 要使用所有标志快速重复最后一个替代命令,请运行:&&

更改定界符

如果您需要用长路径替换 URL:

1
https://mysite.com/a/b/c/d/e

要用单词”hello”代替它,请运行:

1
:s/https:\/\/mysite.com\/a\/b\/c\/d\/e/hello/

但是,很难说出哪些正斜杠(/)是替换模式的一部分,哪些是分隔符。 您可以使用任何单字节字符(除字母,数字或|\之外的字符)来更改定界符。让我们将它们替换为+。上面的替换命令可以重写为 :

1
:s+https:\/\/mysite.com\/a\/b\/c\/d\/e+hello+

现在,更容易看到分隔符在哪里。

特殊替换

您还可以修改要替换的文本的大小写。 给出以下表达式:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

要大写变量“one”,“two”,“three”等,请运行:

1
%s/\v(\w+) (\w+)/\1 \U\2/

你会得到:

1
2
3
4
5
let ONE = "1";
let TWO = "2";
let THREE = "3";
let FOUR = "4";
let FIVE = "5";

这是该命令的细分:

  • (\w+) (\w+)捕获前两个匹配的组,例如”let”和”one”。
  • \1返回第一个组的值”let”
  • \U\2大写(\U)第二组(\2)。

该命令的窍门是表达式\U\2\U指示以下字符大写。

让我们再举一个例子。 假设您正在编写 Vim 书籍,并且需要将一行中每个单词的首字母大写。

1
vim is the greatest text editor in the whole galaxy

您可以运行:

1
:s/\<./\u&/g

结果:

1
Vim Is The Greatest Text Editor In The Whole Galaxy

细目如下:

  • :s 替换当前行
  • \<. 由两部分组成:\<匹配单词的开头和.匹配任何字符。 “<”运算符使以下字符成为单词的第一个字符。 由于.是下一个字符,因此它将匹配任何单词的第一个字符。
  • \u& 将后续符号&大写。 回想一下,&(或\0)代表整个比赛。 它与拒绝单词的第一个字符匹配。
  • g全局标志。 没有它,此命令将仅替换第一个匹配项。 您需要替换此行上的每个匹配项。

要了解替换的特殊替换符号(如\u\U)的更多信息,请查看:h sub-replace-special

替代模式

有时您需要同时匹配多个模式。 如果您有以下问候:

1
2
3
4
hello vim
hola vim
salve vim
bonjour vim

您仅需在包含单词”hello”或”hola”的行上用”friend”代替”vim”。 执行:

1
:%s/\v(hello|hola) vim)/\1 friend/g

结果:

1
2
3
4
hello friend
hola friend
salve vim
bonjour vim

这是细分:

  • %s 在文件的每一行上运行替代命令。
  • (hello|hola) 匹配*”hello”或”hola”并将其视为一个组。
  • vim 是字面意思”vim”。
  • \1 是第一个匹配组,它是文本”hello”或”hola”。
  • friend 是字面的“朋友”。

跨多个文件替换

最后,让我们学习如何在多个文件中替换短语。对于本节,假设您有两个文件: food.txtanimal.txt.

food.txt内:

1
2
3
corn dog
hot dog
chili dog

animal.txt内:

1
2
3
large dog
medium dog
small dog

假设您的目录结构如下所示:

1
2
├── food.txt
├── animal.txt

首先,在:args内同时捕获”food.txt”和”animal.txt”。回顾前面的章节,:args可用于创建文件名列表。在 Vim 内部有几种方法可以做到这一点:

1
2
3
4
:args *.txt                  捕获当前位置的所有txt文件
:args food.txt animal.txt 仅捕获索引和服务器js文件
:args **/*.txt 捕获每个txt文件
:args ** 捕获一切

您也可以在 Vim 外部运行上述命令,将文件作为 Vim 的_ arguments _传递(因此称为”args”命令)。 从终端运行

1
vim food.txt animal.txt

当 Vim 启动时,您将在:args中找到food.txtanimal.txt

无论哪种方式,当您运行:args时,您都应该看到:

1
[food.txt] animal.txt

要转到列表中的下一个或上一个参数,请输入:next:previous。 现在所有相关文件都存储在参数列表中,您可以使用:argdo命令执行多文件替换。 执行:

1
:argdo %s/dog/chicken/

这将对:args列表中的所有文件进行替换。 最后,使用以下命令保存更改的文件:

1
:argdo update

:args:argdo是在多个文件之间应用命令行命令的有用工具。 与其他命令一起尝试!

用宏替换多个文件

另外,您也可以跨多个带有宏的文件运行替代命令。 让我们从将相关文件放入 args 列表开始。 执行:

1
2
3
4
5
6
:args animal.txt food.txt
qq
:%s/dog/chicken/g
:wnext
q
99@q

以下是步骤的细分:

  • :args animal.txt food.txt 会将相关文件列出到:args列表中。
  • qq 启动”q”寄存器中的宏。
  • :%s/dog/chicken/g在当前文件的所有行上用”chicken”替换”dog”。
  • :wnext 写入(保存)文件,然后转到args列表中的下一个文件。就像同时运行:w:next一样。
  • q 停止宏录制。
  • 99@q 执行宏九十九次。 Vim 遇到第一个错误后,它将停止执行宏,因此 Vim 实际上不会执行该宏九十九次。

以聪明的方式学习搜索和替换

做好搜索的能力是编辑的必要技能。掌握搜索功能使您可以利用正则表达式的灵活性来搜索文件中的任何模式。花些时间学习这些。实际上,您可以自己完成本章中的搜索和替换。我曾经读过一本关于正则表达式的书,却没有真正去做,后来我几乎忘了读的所有东西。主动编码是掌握任何技能的最佳方法。

一种提高模式匹配技能的好方法是,每当您需要搜索模式时(例如”hello 123”),而不是查询文字搜索字词(/hello 123),都可以尝试为它(/\v(\l+) (\d+))。这些正则表达式概念中的许多不仅在使用 Vim 时,也适用于常规编程。

既然您已经了解了 Vim 中的高级搜索和替换,现在让我们学习功能最丰富的命令之一,即全局命令。

许可和版权

这些材料全部归 ©2020 Igor Irianto 所有。

这项作品已获得<<知识共享署名-非商业性-相同方式共享 4.0 版>>的许可。