Vim-文本编辑器-摆脱鼠标提高开发效率

本文最后更新于:2021年3月10日 下午

信息

Vim是从vi发展出来的一个文本编辑器。其代码补完、编译及错误跳转等方便编程的功能特别丰富,是在开放源代码方式下发行的自由软件

对于大多数用户来说,Vim有着一个比较陡峭的学习曲线
这意味着开始学习的时候可能会进展缓慢,但是一旦掌握一些基本操作之后,能大幅度提高编辑效率

环境

vim的使用平台有很多,几乎每个Linux系统都会自带一个vim

此处以 VSCode + vim插件 进行学习
VSCode搜索插件vim安装
在插件页可以找到其 setting.json 默认配置

将其添加到 setting.json 即可弯沉给配置
在完成配置后,编辑 VSCode 内的文档会变成vim编辑的形式
可以在状态栏中查看到现在的模式

Vim 语法

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

语法规则

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

1
verb + noun # 动词 + 名词

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

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

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

词汇表

名词(动作 Motion)

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

名词/动作 意义
h
j
k
l
w 向前移动到下一个单词的开头
} 跳转到下一个段落
$ 跳转到当前行的末尾

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

动词(操作符 Operator)

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

操作符 作用
y yank(复制)
d delete(删除)
c change 删除文本,将删除的文本存到寄存器中,进入插入模式

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

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

动作也接受数字作为参数,如果你需要向上移动3行,你可以用3k代替按3次k

  • 向左拷贝2个字符:y2h
  • 删除后两个单词:d2w
  • 修改后面两行:c2j
    目前,也许需要想很久才能完成一个简单的命令,不过刚开始时都是这样,经历过类似的挣扎的阶段后速度就会快起来

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

目前为止还没有结束,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<
    下面列举的一些通常见到的文本对象:
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

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

假设你只知道:

  • 四个动作:w, $, },G
  • 删除操作符(d)

你可以做8件事:按四种方式移动(w, $, }, G)和删除4种文本对象(dw, d$, d}, dG)

如果有一天你学习了小写变大写的操作符(gU),你的 Vim 工具箱中多的不是1种工具,而是4种:gUw, gU$, gU}, gUG。现在你的 Vim工具箱中就有12种工具了

使用 Vim这种能够组合的工具,所有你知道的东西都可以被串起来完成更复杂的操作。你知道的越多,你就越强大

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

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

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

光标移动

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

上下左右步进移动

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

方向
键位 h j k l

为什么Vim使用hjkl进行移动?
实际上是历史原因。因为 Bill JoyVI(VIM前身)用的 Lear-Siegler ADM-3A终端没有方向键,而是把hjkl当做方向键

常用情景:

  • 从一个单词的一个部分移动到另一个部分,使用hl
  • 在可见的范围内上下移动几行,我会使用j和k

对于其它的场景,实际上会有更好的方法

对步进移动 计数

实际上,可以指定步进数量来达到指定目的
语法:

1
[计数] [步进移动]

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

单词导航

导航 作用
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下。 单词和词组都是短距离移动的很好的选择

当前行导航

导航 信息
0 跳到本行第一个字符
^ 跳到本行第一个非空字符
g_ 跳到本行最后一个非空字符
$ 跳到本行最后一个字符
n| 跳到本行第n列
f{chars} 在同一行向后搜索第一个{chars}匹配
F{chars} 在同一行向前搜索第一个{chars}匹配
t{chars} 在同一行向后搜索第一个{chars}匹配,并停在匹配前
T{chars} 在同一行向前搜索第一个{chars}匹配,并停在匹配前
; 在同一行重复最近一次搜索
, 在同一行向相反方向重复最近一次搜索
1
const hello = "world";

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

句子和段落导航

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

导航 信息
( 跳到前一个句子
) 跳到下一个句子

让我们来看一些例子,你觉得哪些字段是句子哪些不是? 可以尝试在 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
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了解更多

匹配导航

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

导航 信息
% 导航到匹配的 (), [], {}

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

行号导航

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

导航 信息
gg 跳转到第一行
G 跳转到最后一行
nG 跳转到第n行
n% 跳到文件的n%
另外,如果你想看文件总行数,可以用CTRL-g查看

窗格导航

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

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

导航 信息
H 跳转到屏幕的顶部
M 跳转到屏幕的中间
L 跳转到屏幕的底部
nH 跳转到距离顶部n行的位置
nL 跳转到距离底部n行的位置

滚动

导航 信息
Ctrl-e 向下滚动一行
Ctrl-d 向下滚动半屏
Ctrl-f 向下滚动一屏
Ctrl-y 向上滚动一行
Ctrl-u 向上滚动半屏
Ctrl-b 向上滚动一屏

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

导航 信息
zt 将当前行置于屏幕顶部附近
zz 将当前行置于屏幕中央
zt 将当前行置于屏幕底部

搜索导航

通常,你已经知道这个文件中有一个字段,你可以通过搜索导航非常快速的定位你的目标

导航 信息
/ 向后搜索一个匹配
? 向前搜素一个匹配
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上并且你想向后搜索完全或部分匹配的单词,例如oneonetwo,你可以用g*替代*

导航 信息
* 向后查找光标所在的完整单词
# 向前查找光标所在的完整单词
g* 向后搜索光标所在的单词
g# 向前搜索光标所在的单词

位置标记

可以通过标记保存当前位置并在之后回到这个位置,就像文本编辑中的书签

导航 信息
ma 用a标签标记一个位置
`a 精确回到a标签的位置(行和列)
‘a 跳转到a标签的行

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

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

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

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

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

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

跳转

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

导航 信息
跳转到标记的行
` 跳转到标记的位置(行和列)
G 跳转到行
/ 向后搜索
? 向前搜索
n 重复上一次搜索,相同方向
N 重复上一次搜索,相反方向
% 查找匹配
( 跳转上一个句子
) 跳转下一个句子
{ 跳转上一个段落
} 跳转下一个段落
L 跳转到当前屏幕的最后一行
M 跳转到当前屏幕的中间
H 跳转到当前屏幕的第一行
[[ 跳转到上一个小节
]] 跳转到下一个小节
:s 替换
:tag 跳转到tag定义

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

为什么跳转有呢? 因为你可以在跳转列表中通过 Ctrl-o 和 Ctrl-i 在记录之间向上或向下跳转到对应位置
实际上是可以在不同文件中进行跳转的

输入模式

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

进入输入模式

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

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

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

退出输入模式的方法

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

命令 信息
退出输入模式进入普通模式
Ctrl-[ 退出输入模式进入普通模式
Ctrl-c 与 Ctrl-[ 和 功能相同, 但是不检查缩写

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

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

重复输入模式

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

1
10i

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

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

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

快捷键 信息
Ctrl-h 删除一个字符
Ctrl-w 删除一个单词
Ctrl-u 删除一整行

此外,这些快捷键也支持在 命令行模式 和 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)一样好用,但是也算是一个锦上添花的内置功能了

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

命令 信息
Ctrl-x Ctrl-l 补全一整行
Ctrl-x Ctrl-n 从当前文件中补全文本
Ctrl-x Ctrl-i 从引用(include)的文件中补全文本
Ctrl-x Ctrl-f 补全一个文件名

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

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

快捷键 信息
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命令进行查看

执行普通模式下的命令

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

设置居中以及跳转

命令 信息
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";
  1. 使用/let来进行匹配
  2. 使用cwconst<esc>来将let替换成const
  3. 使用n来找到下一个匹配的位置。
  4. 最后,使用点命令(.)来重复之前的操作。持续地使用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那一行的其他所有行

  1. 首先,使用d2j删除前三行
  2. 之后跳过foo这一行j,在其下一行使用点命令.两次来删除剩下的六行。完整的命令是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更加有复用性。在编辑时应该养成“修改操作驱动”的观念

未完待续


参考:


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!