154. VIM 从放弃到偶尔

第13章 全局命令

到目前为止,您已经了解了如何使用点命令(.)重复上一次更改,如何使用宏(q)重复动作以及将文本存储在寄存器中(")。

在本章中,您将学习如何在全局命令中重复命令行命令。

全局命令概述

Vim的全局命令用于同时在多行上运行命令行命令。

顺便说一句,您之前可能已经听说过 “Ex命令” 一词。在本书中,我将它们称为==命令行命令==,但Ex命令和命令行命令是相同的。它们是以冒号(:)开头的命令。在上一章中,您了解了替代命令。这是一个Ex命令的示例。它们之所以称为Ex,是因为它们最初来自Ex文本编辑器。在本书中,我将继续将它们称为命令行命令。有关Ex命令的完整列表,请查看:h ex-cmd-index

全局命令具有以下语法:

1
:g/pattern/command

pattern匹配包含该模式串的所有行,类似于替代命令中的模式串。command可以是任何命令行命令。全局命令通过对与pattern匹配的每一行执行command来工作。

如果您具有以下表达式:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

要删除所有包含”console”的行,可以运行:

1
:g/console/d

结果:

1
2
3
4
5
const one = 1;

const two = 2;

const three = 3;

全局命令在与”console”模式串匹配的所有行上执行删除命令(d)。

运行g命令时,Vim对文件进行两次扫描。在第一次运行时,它将扫描每行并标记与/console/模式传教匹配的行。一旦所有匹配的行都被标记,它将进行第二次运行,并在标记的行上执行d命令。

如果要删除所有包含”const”的行,请运行:

1
:g/const/d

结果:

1
2
3
4
5
console.log("one: ", one);

console.log("two: ", two);

console.log("three: ", three);

逆向匹配

要在不匹配的行上运行全局命令,可以运行:

1
:g!/{pattern}/{command}

或者

1
:v/{pattern}/{command}

如果运行:v/console/d,它将删除 包含”console”的所有行。

模式串

全局命令使用与替代命令相同的模式串系统,因此本节将作为更新。随意跳到下一部分或继续阅读!

如果您具有以下表达式:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

要删除包含”one”或”two”的行,请运行:

1
:g/one\|two/d

要删除包含任何一位数字的行,请运行以下任一命令:

1
:g/[0-9]/d

或者

1
:g/\d/d

如果您有表达式:

1
2
3
const oneMillion = 1000000;
const oneThousand = 1000;
const one = 1;

要匹配包含三到六个零的行,请运行:

1
:g/0\{3,6\}/d

传递范围参数

您可以在g命令之前传递一个范围。您可以通过以下几种方法来做到这一点:

  • :1,5g/console/d 删除第1行和第5行之间匹配字符串”console”的行。
  • :,5g/console/d 如果逗号前没有地址,则从当前行开始。它在当前行和第5行之间寻找字符串”console”并将该行删除。
  • :3,g/console/d 如果逗号后没有地址,则在当前行结束。它在第3行和当前行之间寻找字符串”console”并将该行删除。
  • :3g/console/d 如果只传递一个地址而不带逗号,则仅在第3行执行命令。在第3行查找,如果包含字符串”console”,则将其删除。

除了数字,您还可以将这些符号用作范围:

  • . 表示当前行。范围.,3表示当前行和第3行之间。
  • $ 表示文件的最后一行。 3,$范围表示在第3行和最后一行之间。
  • +n 表示当前行之后的n行。您可以将其与.结合使用,也可以不结合使用。 3,+13,.+1表示在第3行和当前行之后的行之间。

如果您不给它任何范围,默认情况下它将影响整个文件。这实际上不是常态。如果您不传递任何范围,Vim的大多数命令行命令仅在当前行上运行(两个值得注意的例外是:这里介绍的全局命令(:g)和save(:w)命令)。

普通模式命令

您可以将全局命令和:normal命令行命令一起运行。

如果您有以下文字:

1
2
3
4
5
6
7
8
9
const one = 1
console.log("one: ", one)

const two = 2
console.log("two: ", two)

const three = 3
console.log("three: ", three)

要添加”;”运行到每一行的末尾:

1
:g/./normal A;

让我们分解一下:

  • :g 是全局命令。
  • /./ 是“非空行”的模式。它匹配至少包含1个字符的行。因此将与包含“const”和“console”的行匹配。它不匹配空行。
  • normal A; 运行:normal命令行命令。 A; 是普通模式命令,用于在该行的末尾插入”;”。

执行宏

您也可以使用全局命令执行宏。宏只是普通模式下的操作,因此可以使用:normal来执行宏。如果您有以下表达式:

1
2
3
4
5
6
7
8
const one = 1
console.log("one: ", one);

const two = 2
console.log("two: ", two);

const three = 3
console.log("three: ", three);

请注意,带有”const”的行没有分号。让我们创建一个宏,以在寄存器”a”的这些行的末尾添加逗号:

1
qa0A;<esc>q

如果您需要复习,请查看有关宏的章节。现在运行:

1
:g/const/normal @a

现在,所有带有”const”的行的末尾将带有”;”。

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

如果您一步一步按照示例做,您将会在第一行末尾看到两个分号。为避免这种情况,使用全局命令时,给一个范围参数,从第2行到最后一行, :2,$g/const/normal @a

递归全局命令

全局命令本身是命令行命令的一种,因此您可以从技术上在全局命令中运行全局命令。

给定表达式:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

如果您运行:

1
:g/console/g/two/d

首先,g将查找包含模式”console”的行,并找到3个匹配项。然后,第二个”g”将从那三个匹配项中查找包含模式”two”的行。最后,它将删除该匹配项。

您也可以将gv结合使用以找到正负模式。例如:

1
:g/console/v/two/d

与前面的命令不同,它将查找 包含”two”的行。

更改定界符

您可以像替代命令一样更改全局命令的定界符。规则是相同的:您可以使用任何单字节字符,但字母,数字,", |, 和 \除外。

要删除包含”console”的行:

1
:g@console@d

如果在全局命令中使用替代命令,则可以有两个不同的定界符:

1
g@one@s+const+let+g

此处,全局命令将查找包含”one”的所有行。 替换命令将从这些匹配项中将字符串”const”替换为”let”。

默认命令

如果在全局命令中未指定任何命令行命令,会发生什么?

全局命令将使用打印(:p)命令来打印当前行的文本。如果您运行:

1
:g/console

它将在屏幕底部打印所有包含”console”的行。

顺便说一下,这是一个有趣的事实。因为全局命令使用的默认命令是p,所以这使g语法为:

1
:g/re/p
  • g = 全局命令
  • re = 正则表达式模式
  • p = 打印命令

这三个元素连起来拼写为 **”grep”**,与命令行中的grep 相同。但这 是巧合。 g/re/p命令最初来自Ed编辑器(一个行文本编辑器)。 grep命令的名称来自Ed。

您的计算机可能仍具有Ed编辑器。从终端运行ed(提示:要退出,请键入q)。

反转整个缓冲区

要翻转整个文件,请运行:

1
:g/^/m 0 

^表示行的开始。使用^匹配所有行,包括空行。

如果只需要反转几行,请将其传递一个范围。要将第5行到第10行之间的行反转,请运行:

1
:5,10g/^/m 0

要了解有关move命令的更多信息,请查看:h :move

汇总所有待办事项

当我编码时,有时我会想到一个随机的绝妙主意。不想失去专注,我通常将它们写在我正在编辑的文件中,例如:

1
2
3
4
5
6
7
8
9
10
11
const one = 1;
console.log("one: ", one);
// TODO: 喂小狗

const two = 2;
// TODO:自动喂小狗
console.log("two: ", two);

const three = 3;
console.log("three: ", three);
// TODO:创建一家销售自动小狗喂食器的初创公司

跟踪所有已创建的TODO可能很困难。 Vim有一个:t(copy)方法来将所有匹配项复制到一个地址。要了解有关复制方法的更多信息,请查看:h :copy

要将所有TODO复制到文件末尾以便于自省,请运行:

1
:g/TODO/t $

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const one = 1;
console.log("one: ", one);
// TODO:喂小狗

const two = 2;
// TODO:自动喂小狗
console.log("two: ", two);

const three = 3;
console.log("three: ", three);
// TODO:创建一家销售自动小狗喂食器的初创公司

// TODO:喂小狗
// TODO:自动喂小狗
// TODO:创建一家销售自动小狗喂食器的初创公司

现在,我可以查看我创建的所有TODO,另外找个时间来完成它们,或将它们委托给其他人,然后继续执行下一个任务。

如果不想复制,而是将所有的 TODO 移动到末尾,可以使用移动命令 m

1
:g/TODO/m $

结果:

1
2
3
4
5
6
7
8
9
10
11
12
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

// TODO:喂小狗
// TODO:自动喂小狗
// TODO:创建一家销售自动小狗喂食器的初创公司

黑洞删除

回想一下寄存器那一章,已删除的文本存储在编号寄存器中(允许它们足够大)。每当运行:g/console/d时,Vim都会将删除的行存储在编号寄存器中。如果删除多行,所有编号的寄存器将很快被填满。为了避免这种情况,您可以使用黑洞寄存器("_ 将删除的行存储到寄存器中。

1
:g/console/d _

通过在d之后传递_,Vim不会将删除的行保存到任何寄存器中。

将多条空行减少为一条空行

如果您的文件带有多个空行,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
const one = 1;
console.log("one: ", one);


const two = 2;
console.log("two: ", two);





const three = 3;
console.log("three: ", three);

您可以快速将多个空行减少为一条空行。运行:

1
:g/^$/,/./-1j

结果:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

一般情况下全局命令遵循下列格式::g/pattern/command。但是,您也可以使用下面的格式::g/pattern1/,/pattern2/command。用这种格式,Vim将会使command作用在pattern1pattern2上。

记住上面说的格式,让我们根据:g/pattern1/,/pattern2/command这个格式分解一下命令:g/^$/,/./-1j

  • /pattern1/ 就是 /^$/ 。它表示一个空行(一个没有任何字符的行)。
  • /pattern2/ 就是 /./(用-1作为行修正)。/./表示一个非空行(一个含有至少1个字符的行)。这里的 -1 意思是向上偏移1行。
  • command 就是 j,一个联接命令(:j)。在这个示例中,该全局命令联接所有给定的行。

顺便说一句,如果您想要将多个空行全部删去,运行下面的命令:

1
:g/^$/,/./j

或者:

1
:g/^$/-j

您的文本将会减少为:

1
2
3
4
5
6
const one = 1;
console.log("one: ", one);
const two = 2;
console.log("two: ", two);
const three = 3;
console.log("three: ", three);

(译者补充:j连接命令的格式是::[range]j。比如::1,5j将连接第1至5行。在前面的命令中:g/pattern1/,/pattern2/-1j/pattern1//pattern2都是j命令的范围参数,表示连接空行至非空行上方一行,这样就会保留1个空行。在早前的英文版本中有关于j命令的介绍,不知为何在后面的更新中,原作者删除了关于j命令的介绍)

高级排序

Vim有一个:sort命令来对一个范围内的行进行排序。例如:

1
2
3
4
5
d
b
a
e
c

您可以通过运行:sort对它们进行排序。如果给它一个范围,它将只对该范围内的行进行排序。例如,:3,5sort仅在第三和第五行之间排序。

如果您具有以下表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const arrayB = [
"i",
"g",
"h",
"b",
"f",
"d",
"e",
"c",
"a",
]

const arrayA = [
"h",
"b",
"f",
"d",
"e",
"a",
"c",
]

如果需要排序数组中的元素,而不是数组本身,可以运行以下命令:

1
:g/\[/+1,/\]/-1sort

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const arrayB = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
]

const arrayA = [
"a"
"b",
"c",
"d",
"e",
"f",
"h",
]

这很棒!但是命令看起来很复杂。让我们分解一下。该命令依然遵循 :g/pattern1/,/pattern2/command这个格式。

  • :g 是全局命令
  • /\[/+1 是第一个模式串,它匹配左方括号”[“。+1表示匹配行的下面1行。
  • /\]/-1 是第二个模式串,它匹配右方括号”]”。-1表示匹配行的上面1行。
  • /\[/+1,/\]/-1 表示在”[“和”]”之间的行。
  • sort 是命令行命令:排序。

聪明地学习全局命令

全局命令针对所有匹配的行执行命令行命令。有了它,您只需要运行一次命令,Vim就会为您完成其余的工作。要精通全局命令,需要做两件事:良好的命令行命令词汇表和正则表达式知识。随着您花费更多的时间使用Vim,您自然会学到更多的命令行命令。正则表达式知识需要更多的实际操作。但是一旦您适应了使用正则表达式,您将领先于很多其他人。

这里的一些例子很复杂。不要被吓到。真正花时间了解它们。认真阅读每个模式串,不要放弃。

每当需要在多个位置应用命令时,请暂停并查看是否可以使用g命令。寻找最适合工作的命令,并编写一个模式串以同时定位多个目标。

既然您已经知道全局命令的功能强大,那么让我们学习如何使用外部命令来增加工具库。

第14章 外部命令

在Unix系统内部,您会发现许多小型的,超专业化命令,每个命令只做一件事(而且能很好地完成)。您可以将这些命令链接在一起以共同解决一个复杂的问题。如果可以从Vim内部使用这些命令,那不是很好吗?

答案是肯定的!在本章中,您将学习如何扩展Vim以使其与外部命令无缝协作。

Bang 命令

Vim有一个Bang(!)命令,可以执行三件事:

1.将外部命令的STDOUT读入当前缓冲区。
2.将缓冲区的内容作为STDIN写入外部命令。
3.从Vim内部执行外部命令。

让我们一个个认真看一下。

将外部命令的标准输出STDOUT读入Vim

将外部命令的STDOUT读入当前缓冲区的语法为:

1
:r !{cmd}

:r是Vim的读命令。如果不带!使用它,则可以使用它来获取文件的内容。如果当前目录中有文件file1.txt,运行:

1
:r file1.txt

Vim会将file1.txt的内容放入当前缓冲区。

如果您运行的:r命令后面跟一个!和外部命令,则该命令的输出将插入到当前缓冲区中。要获取ls命令的结果,请运行:

1
:r !ls

它返回类似下列的文本:

1
2
3
file1.txt
file2.txt
file3.txt

您可以从curl命令读取数据:

1
:r !curl -s 'https://jsonplaceholder.typicode.com/todos/1'

r命令也接受一个地址:

1
:10r !cat file1.txt

现在,将在第10行之后插入来自运行cat file.txt的STDOUT。

将缓冲区内容写入外部命令

:w命令除了保存文件,还可以用来将当前缓冲区中的文本作为作为STDIN传递给外部命令。语法为:

1
:w !cmd

如果您具有以下表达式:

1
2
console.log("Hello Vim");
console.log("Vim is awesome");

确保在计算机中安装了node,然后运行:

1
:w !node

Vim将使用node执行Javascript表达式来打印”Hello Vim”和”Vim is awesome”。

当使用:w命令时,Vim使用当前缓冲区中的所有文本,与global命令类似(大多数命令行命令,如果不给它传递范围,则仅对当前行执行该命令)。如果您通过:w来指定地址:

1
:2w !node

“Vim”只使用第二行中的文本到node解释器中。

:w !node:w! node形式上区别很小,但功能上相隔千里。使用:w !node,您是将当前缓冲区中的文本”写入”到外部命令node中。用:w! node,则您将强制保存文件并将其命名为”node”。

执行外部命令

您可以使用bang命令从Vim内部执行外部命令。语法为:

1
:!cmd

要以长格式查看当前目录的内容,请运行:

1
:!ls -ls

要终止在PID 3456上运行的进程,可以运行:

1
:!kill -9 3456

您可以在不离开Vim的情况下运行任何外部命令,因此您可以专注于自己的任务。

过滤文本

如果给!范围,则可用于过滤文本。假设您有:

1
2
hello vim
hello vim

让我们使用tr (translate)命令将当前行大写。运行:

1
:.!tr '[:lower:]' '[:upper:]'

结果:

1
2
HELLO VIM
hello vim

命令分解:

  • .! 在当前行执行filter命令。
  • !tr '[:lower:]' '[:upper:]' 调用tr外部命令将所有小写字符替换为大写字符。

==必须传递范围以运行外部命令作为过滤器。==如果您尝试在没有.的情况下运行上述命令(:!tr '[:lower:]' '[:upper:]'),则会看到错误。

假设您需要使用awk命令删除两行的第二列:

1
:%!awk "{print $1}"

结果:

1
2
hello
hello

命令分解:

  • :%! 在所有行(%)上执行filter命令。
  • awk "{print $1}" 仅打印匹配项的第一列。

您可以使用管道运算符(|)链接多个命令,就像在终端中一样。假设您有一个包含这些美味早餐的文件:

1
2
3
4
name price
chocolate pancake 10
buttermilk pancake 9
blueberry pancake 12

如果您需要根据价格对它们进行排序,并且仅以均匀的间距显示菜单,则可以运行:

1
:%!awk 'NR > 1' | sort -nk 3 | column -t

结果:

1
2
3
buttermilk pancake 9
chocolate pancake 10
blueberry pancake 12

命令分解:

  • :%! 将过滤器应用于所有行(%)。
  • awk 'NR > 1' 仅从第二行开始显示文本。
  • |链接下一个命令。
  • sort -nk 3使用列3(k 3)中的值对数字进行排序(n)。
  • column -t以均匀的间距组织文本。

普通模式命令

在普通模式下,Vim有一个过滤运算符(!)。如果您有以下问候:

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

要大写当前行和下面的行,可以运行:

1
!jtr '[a-z]' '[A-Z]'

命令分解:

  • !j 运行常规命令过滤器运算符(!),目标是当前行及其下方的行。回想一下,因为它是普通模式运算符,所以适用语法规则”动词+名词”。
  • tr '[a-z]' '[A-Z]'将小写字母替换为大写字母。

filter normal命令仅适用于至少一行以上的motion或至少一行以上的文本对象。如果您尝试运行!iwtr'[az]''[AZ]'(在内部单词上执行tr),您会发现它在整个行上都应用了tr命令,而不是光标所在的单词。

聪明地学习外部命令

Vim不是IDE。它是一种轻量级的模式编辑器,通过设计可以高度扩展。由于这种可扩展性,您可以轻松访问系统中的任何外部命令。这样,Vim离成为IDE仅一步之遥。有人说Unix系统是有史以来的第一个IDE。

Bang 命令的有用程度与您知道多少个外部命令相关。如果您的外部命令知识有限,请不要担心。我还有很多东西要学。以此作为持续学习的动力。每当您需要过滤文本时,请查看是否存在可以解决问题的外部命令。不必担心掌握所有的命令。只需学习完成当前任务所需的内容即可。

第15章 命令行模式

在前三章中,您已经学习了如何使用搜索命令(/, ?)、替换命令(:s)、全局命令(:g),以及外部命令(!)。这些都是命令行模式命令的一些例子。

在本章中,您将学习命令行模式的更多技巧。

进入和退出命令行模式

命令行模式本身也是一种模式,就像普通模式、输入模式、可视模式一样。在这种模式中,光标将转到屏幕底部,此时您可以输入不同的命令。

有 4 种进入命令行模式的方式:

  • 搜索命令 (/, ?)
  • 命令行指令 (:)
  • 外部命令 (!)

您可以从正常模式或可视模式进入命令行模式。

若要离开命令行模式,您可以使用 <esc>Ctrl-cCtrl-[

有时其他资料可能会将”命令行指令”称为”Ex 命令”,将”外部命令”称为”过滤命令”或者”叹号运算符”。

重复上一个命令

您可以用 @: 来重复上一个命令行指令或外部命令。

如果您刚运行 :s/foo/bar/g,执行 @: 将重复该替换。如果您刚运行 :.!tr '[a-z]' '[A-Z]',执行 @: 将重复上一次外部命令转换过滤。

命令行模式快捷键

在命令行模式中,您可以使用 LeftRight 方向键,来左右移动一个字符。

如果需要移动一个单词,使用 Shift-LeftShift-Right (在某些操作系统中,您需要使用 Ctrl 而不是 Shift)。

使用 Ctrl-b移动到该行的开始,使用 Ctrl-e移动到该行的结束。

和输入模式类似,在命令行模式中,有三种方法可以删除字符:

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

最后,如果您想像编辑文本文件一样来编辑命令,可以使用 Ctrl-f

这样还可以查看过往的命令,并在这种”命令行编辑的普通模式”中编辑它们,同时还能按下 Enter 来运行它们。

寄存器和自动补全

当处于命令行模式时,您可以像在插入模式中一样使用 Ctrl-r 从Vim寄存器中插入文本。如果您在寄存器 a 中存储了字符串 “foo” ,您可以执行 Ctrl-r a 从寄存器a中插入该文本。任何在插入模式中您可以从寄存器中获取的内容,在命令行模式中您也可以获取。

另外,您也可以按 Ctrl-r Ctrl-w 获取当前光标下的单词(按 Ctrl-r Ctrl-A 获取当前光标下的词组)。还可以按 Ctrl-r Ctlr-l 获取当前光标所在行。按 Ctrl-r Ctrl-f 获取光标下的文件名。

您也可以对已存在的命令使用自动补全。要自动补全 echo 命令,当处于命令行模式时,首先输入 “ec”,接着按下 <Tab>,此时您应该能在左下角看到一些 “ec” 开头的 Vim 命令(例如:echo echoerr echohl echomsg econ)。按下 <Tab>Ctrl-n 可以跳到下一个选项。按下 <Shift-Tab>Ctrl-p 可以回到上一个选项。

一些命令行指令接受文件名作为参数。edit 就是一个例子,这时候您也可以使用自动补全。当输入 :e 后(不要忘记空格了),按下 <Tab>,Vim 将列出所有相关的文件名,这样您就可以进行选择而不必完整的输入它们。

历史记录窗口

您可以查看命令行指令和搜索项的历史记录(要确保在运行 vim --version 时,Vim 的编译选项中含有+cmdline_hist)。

运行 :his : 来查看命令行指令的历史记录:

1
2
3
4
##  cmd History
2 e file1.txt
3 g/foo/d
4 s/foo/bar/g

Vim 列出了您运行的所有 : 命令。默认情况下,Vim 存储最后 50 个命令。运行 :set history=100 可以将 Vim 记住的条目总数更改为 100。

一个更有用的做法是使用命令行历史记录窗口,按q:将会打开一个可搜索、可编辑的历史记录窗口。假设按下q:后您有如下的表达式:

1
2
3
51  s/verylongsubstitutionpattern/pancake/g
52 his :
53 wq

如果您当前任务是执行 s/verylongsubstitutionpattern/donut/g(”pancake”换成了”donut”),为什么不复用 s/verylongsubstitutionpattern/pancake/g 呢?毕竟,两条命令唯一不同的是替换的单词,”donut” vs “pancake” ,所有其他的内容都是相同的。

当您运行 q:后,在历史记录中找到 s/verylongsubstitutionpattern/pancake/g(在这个环境中,您可以使用Vim导航),然后直接编辑它! 在历史记录窗口中将 “pancake” 改为 “donut” ,然后按 <Enter。Vim立刻执行 s/verylongsubstitutionpattern/donut/g 命令,超级方便!

类似地,运行 :his /:his ? 可以查看搜索记录。要想打开您可以直接搜索和编辑的搜索历史记录窗口,您可以运行 q/q?

要退出这个窗口,按 Ctrl-c, Ctrl-w c, 或输入 :quit

更多命令行指令

Vim有几百个内置指令,要查看Vim的所有指令,执行 :h ex-cmd-index:h :index

聪明地学习命令行模式

对比其他三种模式,命令行模式就像是文本编辑中的瑞士军刀。寥举几例,您可以编辑文本、修改文件和执行命令。本章是命令行模式的零碎知识的集合。同时,Vim 模式的介绍也走向尾声。现在,您已经知道如何使用普通、输入、可视以及命令行模式,您可以比以往更快地使用 Vim 来编辑文本了。

是时候离开 Vim 模式,来了解如何使用 Vim 标记进行更快的导航了。

第16章 标签

快速转到任意定义处,是文本编辑中一个非常有用的特性。在本章中,您将学习如何使用 Vim 标签来做到这一点。

标签概述

假设有人给了您一个新的代码库:

1
2
one = One.new
one.donut

Onedonut?呃,对于当时编写代码的开发者而言,这些代码的含义可能显而易见。问题是当时的开发者已经不在了,现在要由您来理解这些费解的代码。而跟随有Onedonut定义的源代码,是帮助您理解的一个有效方法。

您可以使用fzfgrep来搜索它们,但这种情况下,但使用标签将更快。

把标签想象成地址簿:

1
2
3
Name    Address
Iggy1 1234 Cool St, 11111
Iggy2 9876 Awesome Ave, 2222

当然,标签可不是存储着“姓名-地址”对,而是“定义-地址”对。

假设您在一个目录中有两个 Ruby 文件:

1
2
3
4
5
6
7
8
9
10
## one.rb
class One
def initialize
puts "Initialized"
end

def donut
puts "Bar"
end
end

以及

1
2
3
4
5
## two.rb
require './one'

one = One.new
one.donut

在普通模式下,您可以使用Ctrl-]跳转到定义。在two.rb中,转到one.donut所在行,将光标移到donut处,按下Ctrl-]

哦豁,Vim 找不到标签文件,您需要先生成它。

标签生成器

现代 Vim 不自带标签生成器,您需要额外下载它。有几个选项可供选择:

  • ctags = 仅用于 C,基本随处可见。
  • exuberant ctags = 最流行的标签生成器之一,支持许多语言。
  • universal ctags = 和 exuberant ctags 类似,但比它更新。
  • etags = 用于 Emacs,嗯……
  • JTags = Java
  • ptags.py = Python
  • ptags = Perl
  • gnatxref = Ada

如果您查看 Vim 在线教程,您会发现许多都会推荐 exuberant ctags,它支持 41 种编程语言,我用过它,挺不错的。但自2009年以来一直没有维护,因此 Universal ctags 更好些,它和 exuberant ctags 相似,并仍在维护。

我不打算详细介绍如何安装 Universal ctags,您可以在 universal ctags 仓库了解更多说明。

假设您已经安装好了ctags,接下来,生成一个基本的标签文件。运行:

1
ctags -R .

R 选项告诉 ctags 从当前位置 (.) 递归扫描文件。稍后,您应该在当前文件夹看到一个tags 文件,里面您将看到类似这样的内容:

1
2
3
4
5
6
7
8
9
10
11
12
!_TAG_FILE_FORMAT	2	/extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
!_TAG_OUTPUT_FILESEP slash /slash or backslash/
!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/
!_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/
!_TAG_PROGRAM_AUTHOR Universal Ctags Team //
!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/
!_TAG_PROGRAM_URL <https://ctags.io/> /official site/
!_TAG_PROGRAM_VERSION 0.0.0 /b43eb39/
One one.rb /^class One$/;" c
donut one.rb /^ def donut$/;" f class:One
initialize one.rb /^ def initialize$/;" f class:One

根据 Vim 设置和 ctag 生成器的不同,您的tags 文件可能会有些不同。一个标签文件由两部分组成:标签元数据和标签列表。那些标签元数据 (!TAG_FILE...) 通常由 ctags 生成器控制。这里我不打算介绍它们,您可以随意查阅文档。标签列表是一个由所有定义组成的列表,由ctags建立索引。

现在回到 two.rb,将光标移至 donut,再输入Ctrl-],Vim 将带您转到 one.rb 文件里def donut 所在的行上。成功啦!但 Vim 怎么做到的呢?

解剖标签文件

来看看donut 标签项:

1
donut	one.rb	/^  def donut$/;"	f	class:One

上面的标签项由四个部分组成:一个tagname、一个tagfile、一个tagaddress,以及标签选项。

  • donuttagname。当光标在 “donut” 时,Vim 搜索标签文件里含有 “donut” 字符串的一行。
  • one.rbtagfile。Vim 会搜寻 one.rb 文件。
  • /^ def donut$/tagaddress/.../ 是模式指示器。^ 代表一行中第一个元素,后面跟着两个空格,然后是def donut字符串,最后 $ 代表一行中最后一个元素。
  • f class:One 是标签选项,它告诉 Vim,donut 是一种函数 (f),并且是 One 类的一部分。

再看看另一个标签项:

1
One	one.rb	/^class One$/;"	c

这一行和 donut也是一样的:

  • Onetagname。注意,对于标签,第一次扫描区分大小写。如果列表中有 Oneone, Vim 会优先考虑 One 而不是 one
  • one.rbtagfile。Vim 会搜寻 one.rb 文件。
  • /^class One$/tagaddress 。Vim 会查找以 class 开头 (^) 、以 One 结尾 ($) 的行。
  • c 是可用标签选项之一。由于 One 是一个 ruby 类而不是过程,因此被标签为 c

标签文件的内容可能不尽相同,根据您使用的标签生成器而定。但至少,标签文件必须具有以下格式之一:

1
2
1.  {tagname} {TAB} {tagfile} {TAB} {tagaddress}
2. {tagname} {TAB} {tagfile} {TAB} {tagaddress} {term} {field} ..

标签文件

您知道,在运行 ctags -R . 后,一个新 tags 文件会被创建。但是,Vim 是如何知道在哪儿查找标签文件的呢?

如果运行 :set tags?,您可能会看见 tags=./tags,tags(根据您的 Vim 设置,内容可能有所不同)。对于 ./tags,Vim 会在当前文件所在路径查找所有标签;对于 tags,Vim 会在当前目录(您的项目根路径)中查找。

此外,对于 ./tags,Vim 会在当前文件所在路径内查找一个标签文件,无论它被嵌套得有多深。接下来,Vim 会在当前目录(项目根路径)查找。Vim 在找到第一个匹配项后会停止搜索。

如果您的 'tags' 文件是 tags=./tags,tags,/user/iggy/mytags/tags,那么 Vim 在搜索完 ./tagstags 目录后,还会在 /user/iggy/mytags 目录内查找。所以您可以分开存放标签文件,不必将它们置于项目文件夹中。

要添加标签文件位置,只需要运行:

1
:set tags+=path/to/my/tags/file

为大型项目生成标签:

如果您尝试在大型项目中运行 ctag,则可能需要很长时间,因为 Vim 也会查看每个嵌套目录。如果您是 Javascript 开发者,您会知道 node_modules 非常大。假设您有五个子项目,每个都包含自己的 node_modules 目录。一旦运行 ctags -R .,ctags 将尝试扫描这5个 node_modules。但您可能不需要为 node_modules 运行 ctag。

如果要排除 node_modules 后执行 ctags,可以运行:

1
ctags -R --exclude=node_modules .

这次应该只需要不到一秒钟的时间。另外,您还可以多次使用 exclude 选项:

1
ctags -R --exclude=.git --exclude=vendor --exclude=node_modules --exclude=db --exclude=log .

标签导航

仅使用 Ctrl-] 也挺好,但我们还可以多学几个技巧。其实,标签跳转键 Ctrl-] 还有命令行模式::tag my-tag。如果您运行:

1
:tag donut

Vim 就会跳转至 donut 方法,就像在 “donut” 字符串上按 Ctrl-] 一样。您还可以使用 <Tab> 来自动补全参数:

1
:tag d<Tab>

Vim 会列出所有以 “d” 开头的标签。对于上面的命令,结果则是 “donut”。

在实际项目中,您可能会遇到多个同名的方法。我们来更新下这两个文件。先是 one.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## one.rb
class One
def initialize
puts "Initialized"
end

def donut
puts "one donut"
end

def pancake
puts "one pancake"
end
end

然后 two.rb

1
2
3
4
5
6
7
8
9
10
## two.rb
require './one.rb'

def pancake
"Two pancakes"
end

one = One.new
one.donut
puts pancake

由于新添加了一些过程,因此编写完代码后,==不要忘记运行 ctags -R .==。现在,您有了两个 pancake 过程。如果您在 two.rb 内按下 Ctrl-],会发生什么呢?

Vim 会跳转到 two.rb 内的 def pancake,而不是 one.rbdef pancake。这是因为 Vim 认为 two.rb 内部的 pancake 过程比其他的pancake 过程具有更高优先级。

标签优先级

并非所有的标签都有着相同的地位。一些标签有着更高的优先级。如果有重复的标签项,Vim 会检查关键词的优先级。顺序是:

  1. 当前文件中完全匹配的静态标签。
  2. 当前文件中完全匹配的全局标签。
  3. 其他文件中完全匹配的全局标签。
  4. 其他文件中完全匹配的静态标签。
  5. 当前文件中不区分大小写匹配的静态标签。
  6. 当前文件中不区分大小写匹配的全局标签。
  7. 其他文件中区分大小写匹配的全局标签。
  8. 当前文件中不区分大小写匹配的静态标签。

根据优先级列表,Vim 会对在同一个文件上找到的精确匹配项进行优先级排序。这就是为什么 Vim 会选择 two.rb 里的 pancake 过程而不是 one.rb 里的。但是,上述优先级列表有些例外,取决于您的'tagcase''ignorecase''smartcase' 设置。我不打算介绍它们,您可以自行查阅 :h tag-priority

选择性跳转标签

如果可以选择要跳转到哪个标签,而不是始终转到优先级最高的,那就太好了。因为您可能想跳转到 one.rb 里的 pancake 方法,而不是 two.rb 里的。现在您可以使用 :tselect 做到它!运行:

1
:tselect pancake

您可以在屏幕底部看到:

1
2
3
4
5
6
## pri kind tag               file
1 F C f pancake two.rb
def pancake
2 F f pancake one.rb
class:One
def pancake

如果输入2 后再 <Return>,Vim 将跳转到 one.rb 里的pancake 过程。如果输入1 后再 <Return>,Vim 将跳转到 two.rb 里的。

注意pri 列,第一个匹配中该列是F C,第二个匹配中则是F。这就是 Vim 用来确定标签优先级的凭据。F C表示在当前 (C) 文件中完全匹配 (F) 的全局标签。F 表示仅完全匹配 (F) 的全局标签。F C 的优先级永远比 F 高。(译注:FFully-matchedCCurrent file

如果运行:tselect donut,即使只有一个标签可选,Vim 也会提示您选择跳转到哪一个。有没有什么方法可以让 Vim 仅在有多个匹配项时才提示标签列表,而只找到一个标签时就立即跳转呢?

当然!Vim 有一个 :tjump 方法。运行:

1
:tjump donut

Vim 将立即跳转到 one.rb 里的donut 过程,就像在运行 :tag donut 一样。现在试试:

1
:tjump pancake

Vim 将提示您从标签选项中选择一个,就像在运行:tselect pancaketjump 能两全其美。

tjump 在普通模式下有一个快捷键:g Ctrl-]。我个人喜欢g Ctrl-]胜过 Ctrl-]

标签的自动补全

标签能有助于自动补全。回想下第6章“插入模式”,您可以使用 Ctrl-x 子模式来进行各式自动补全。其中有一个我没有提到过的自动补全子模式便是 Ctrl-]。如果您在插入模式中输入Ctrl-x Ctrl-],Vim 将使用标签文件来自动补全。

在插入模式下输入Ctrl-x Ctrl-],您会看到:

1
2
3
4
One
donut
initialize
pancake

标签堆栈

Vim 维持着一个标签堆栈,上面记录着所有您从哪儿来、跳哪儿去的标签列表。使用 :tags 可以看到这个堆栈。如果您首先跳转到pancake,紧接着是donut,此时运行:tags,您将看到:

1
2
3
4
  # TO tag         FROM line  in file/text
1 1 pancake 10 ch16_tags/two.rb
2 1 donut 9 ch16_tags/two.rb
>

注意上面的 > 符号,它代表着您当前在堆栈中的位置。要“弹出”堆栈,从而回到上一次的状态,您可以运行:pop。试试它,再运行:tags看看:

1
2
3
4
  # TO tag         FROM line  in file/text
1 1 pancake 10 puts pancake
> 2 1 donut 9 one.donut

注意现在 > 符号位于 donut 所在的第二行了。再 pop 一次,然后运行:tags

1
2
3
  # TO tag         FROM line  in file/text
> 1 1 pancake 10 puts pancake
2 1 donut 9 one.donut

在普通模式下,您可以按下 Ctrl-t 来达到和 :pop 一样的效果。

自动生成标签

Vim 标签最大的缺点之一是,每当进行重大改变时,您需要重新生成标签文件。如果您将pancake 过程重命名为 waffle,标签文件不知道 pancake 被重命名了,标签列表仍旧存储着 pancake 过程。运行ctags -R . 可以创建更新的标签文件,但这可能会很缓慢。

幸运的是,有几种可以自动生成标签的方法。这一小节不打算介绍一个简单明了的过程,而是提出一些想法,以便您可以扩展它们。

在保存时生成标签

Vim 有一个自动命令 (autocmd) 方法,可以在触发事件时执行任意命令。您可以使用这个方法,以便在每次保存时生成标签。运行:

1
:autocmd BufWritePost *.rb silent !ctags -R .

上面命令的分解如下:

  • autocmd 是 Vim 的自动命令方法,它接受一个事件名称、文件和一个命令。
  • BufWritePost 是保存缓冲区时的一个事件。每次保存文件时将触发一次 BufWritePost 事件。
  • .rb 是 ruby (rb) 文件的一种文件模式。
  • ==silent 是您传递的命令的一部分。如果不输入它,每次触发自动命令时,Vim 都会提示 press ENTER or type command to continue。==
  • !ctags -R . 是要执行的命令。回想一下,!cmd 从 Vim 内部执行终端命令。

现在,每次您保存一个 ruby 文件时,Vim 都会运行ctags -R .

使用插件

有几种插件可以自动生成 ctags:

我使用 ==vim-gutentags==。它的使用方法很简单,而且装上就可以直接使用。

Ctags 以及 Git 钩子

Tim Pope 是一个写了很多非常棒的 Vim 插件的作者,他写了一篇博客,建议使用 git 钩子。可以看一看

聪明地学习标签

只要配置得当,标签是非常有用的。假设在一个新的代码库中,您想要搞清楚 functionFood 干了什么,您可以通过跳转到它的定义来搞懂它们。在那儿可以看到,它又调用了 functionBreakfast。继续跟踪,发现还调用了 functionPancake。现在您明白了,函数调用路径图长这样:

1
functionFood -> functionBreakfast -> functionPancake

进一步可以知道,这段代码和早餐吃煎饼有关。

现在您已经知道如何使用标签,通过 :h tags 可以学习更多有关标签的知识。接下来让我们一起来探索另一个功能:折叠。

第17章 折叠

在阅读文件时,经常会有一些不相关的文本会妨碍您理解。使用 Vim 折叠可以隐藏这些不必要的信息。

本章中,您将学习如何使用不同的折叠方法。

手动折叠

想象您正在折叠一张纸来覆盖一些文本,实际的文本不会消失,它仍在那儿。Vim 折叠的工作方式与此相同,它折叠一段文本,在显示时会隐藏起来,但实际上并不会真的删除它。

折叠操作符是z。(折叠纸张时,它看起来也像字母 “z”)。

假设有如下文本:

1
2
Fold me
Hold me

把光标放在第一行,输入 zfj。Vim 将这两行折叠成一行,同时会看到类似消息:

1
+-- 2 lines: Fold me -----

上面的命令分解如下:

  • zf 是折叠操作符。
  • j 是用于折叠操作符的动作。

您可以使用 zo 打开/展开已折叠文本,使用 zc 关闭/收缩文本。

折叠是一个操作符,所以它遵循语法规则(动词+名词)。您可以在折叠运算符后,加上一个动作(motion)或文本对象。例如,使用 zfip 可以折叠内部段落;使用 zfG 可以折叠至文件末尾;使用 zfa{ 可以折叠 {} 之间的文本。

您可以在可视模式下进行折叠。高亮您想要折叠的区域后 (v, V, 或 Ctrl-v),再输入 zf 即可。

您也可以在命令行模式下,使用 :fold 命令执行一次折叠。若要折叠当前行及紧随其后的第二行,可以运行:

1
:,+1fold

,+1 是要折叠的范围。如果不传递范围参数,默认当前行。+1 是代表下一行的范围指示器。运行 :5,10fold 可以折叠第5至10行。运行 :,$fold 可以折叠当前行至文件末尾。

还有许多其他折叠和展开的命令。我发现他们实在太多,以至于在刚起步时很难记住。最有用的一些命令是:

  • zR 展开所有折叠。
  • zM 收缩所有折叠。
  • za 切换折叠状态。

zRzM 可用于任意行上,但 za 仅能用于已折叠/未折叠的行上。输入 :h fold-commands 可查阅更多有关折叠的指令。

不同的折叠方法

以上部分涵盖了 Vim 手动折叠的内容。实际上,Vim 有六种不同的折叠方法:

  1. 手动折叠
  2. 缩进折叠
  3. 表达式折叠
  4. 语法折叠
  5. 差异折叠
  6. 标志折叠

运行 :set foldmethod? 可查看您当前正在使用哪一种折叠方式。默认情况下,Vim 使用手动方式。

在本章的剩余部分,您将学习其他五种折叠方法。让我们从缩进折叠开始。

缩进折叠

要使用缩进折叠,需要将 'foldmethod' 选项更改为缩进:

1
:set foldmethod=indent

假设有如下文本:

1
2
3
One
Two
Two again

运行 :set foldmethod=indent 后将看到:

1
2
One
+-- 2 lines: Two -----

使用缩进折叠后,Vim 将会查看每行的开头有多少空格,并将它与 'shiftwidth' 选项进行比较,以此来决定该行可折叠性。'shiftwidth' 返回每次缩进所需的空格数。如果运行:

1
:set shiftwidth?

Vim 的默认 'shiftwidth' 值为2。对于上面的文本而言,”Two” 和 “Two again” 的开头都有两个空格。当 Vim 看到了空格数 'shiftwidth'值都为2时,Vim 认为该行的缩进折叠级别为1。

假设这次文本开头只有一个空格:

1
2
3
One
Two
Two again

运行 :set foldmethod=indent 后,Vim 不再折叠已缩进的行了,因为这些行没有足够的空格。1个空格不会被视作一个缩进。然而,当您改变 'shiftwidth' 的值为1后:

1
:set shiftwidth=1

文本现在可以折叠了!现在一个空格将被视为一个缩进。

现在,我们将 'shiftwidth' 以及文本开头的空格数都重新恢复为2后,另外添加一些内容:

1
2
3
4
5
One
Two
Two again
Three
Three again

运行折叠命令 (zM) 后可以看到:

1
2
One
+-- 4 lines: Two -----

展开已折叠的行 (zR),接着移动光标至 “Three”,然后切换文本的折叠状态 (za):

1
2
3
4
One
Two
Two again
+-- 2 lines: Three -----

这是啥?叠中叠?

是的,您可以嵌套折叠。文本 “Two” 和 “Two again” 的折叠级别都为1,文本 “Three” 和 “Three again” 的折叠级别都为2。如果在一段可折叠文本中,具有另一段折叠级别更高的可折叠文本,则可以具有多个折叠层。

表达式折叠

表达式折叠允许您定义要匹配折叠的表达式。定义折叠表达式后,Vim 会计算每行的 'foldexpr' 值。这是必须配置的变量,它要返回适当的值。如果返回 0,则不折叠行。如果它返回 1,则该行的折叠级别为 1。如果它返回 2,则该线的折叠级别为 2。除了整数外还有其他的值,但我不打算介绍它们。如果你好奇,可以查阅:h fold-expr

首先,更改折叠方法:

1
:set foldmethod=expr

假设您有一份早餐食品列表,并且想要折叠所有以 “p” 开头的早餐项:

1
2
3
4
5
6
donut
pancake
pop-tarts
protein bar
salmon
scrambled eggs

其次,更改 foldexpr 为捕获以 “p” 开头的表达式:

1
:set foldexpr=getline(v:lnum)[0]==\\"p\\"

这表达式看起来有点吓人。我们来分解下:

  • :set foldexpr 设置 'foldexpr' 为自定义表达式。
  • getline() 是 Vim 脚本的一个函数,它返回指定行的内容。如运行 :echo getline(5) 可以获取第5行的内容。
  • v:lnum 是 Vim 'foldexpr' 表达式的特殊变量。Vim 在扫描每一行时,都会将行号存储至 v:lnum 变量。在第5行,v:lnum 值为5。在第10行,v:lnum值为10。
  • [0] 处于 getline(v:lnum)[0] 语境时,代表每一行的第一个字符。Vim 在扫描某一行时,getline(v:lnum) 返回该行的内容,而 getline(v:lnum)[0] 则返回这一行的第一个字符。例如,我们早餐食品列表的第一行是 “donut”,则 getline(v:lnum)[0] 返回 “d”;列表的第二行是 “pancake”,则 getline(v:lnum)[0] 返回 “p”。
  • ==\\"p\\" 是等式表达式的后半部分,它检查刚才表达式的计算结果是否等于 “p”。如果是,则返回1,否则返回0。在 Vim 的世界里,1代表真,0代表假。所以,那些以 “p” 开头的行,表达式都会返回1。回想一下本节的开始,如果 'foldexpr' 的值为1,则折叠级别为1。

在运行这个表达式后,您将看到:

1
2
3
4
donut
+-- 3 lines: pancake -----
salmon
scrambled eggs

语法折叠

语法折叠是由Vim的语法高亮决定的。如果您使用了语法高亮插件,比如vim-polyglot,那么装上插件就可以直接使用语法折叠。仅仅需要将foldmethod选项改为 syntax

1
:set foldmethod=syntax

假设您正在编辑一个JavaScript文件,而且您已经装好了 vim-polyglot 插件。如果您有以下文本:

1
2
3
4
5
6
const nums = [
one,
two,
three,
four
]

上述文本将会使用语法折叠折起来。当您为某个特定语言(位于 syntax/ 目录中的语言即是典型例子)定义了语法高亮,您可以添加一个 fold 属性,使它支持折叠。下面是 vim-polyglot 中JavaScript语法文件中的一个片段。注意最后的 fold 关键字。

1
syntax region  jsBracket                      matchgroup=jsBrackets            start=/\[/ end=/\]/ contains=@jsExpression,jsSpreadExpression extend fold

本书不会详细介绍 syntax 功能。如果您感兴趣,可以查阅 :h syntax.txt

差异折叠

Vim 可以对多个文件进行差异比较。

如果您有 file1.txt

1
2
3
4
5
6
7
8
9
10
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome

以及 file2.txt

1
2
3
4
5
6
7
8
9
10
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
emacs is ok

运行 vimdiff file1.txt file2.txt

1
2
3
4
5
6
7
8
+-- 3 lines: vim is awesome -----
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
[vim is awesome] / [emacs is ok]

Vim 会自动折叠一些相同的行。运行 vimdiff 命令时,Vim 会自动使用 foldmethod=diff。此时如果运行 :set foldmethod?,它将返回 diff

标志折叠

要使用标志折叠,请运行:

1
:set foldmethod=marker

假设有如下文本:

1
2
3
4
5
6
Hello

{{{
world
vim
}}}

输入 zM 后会看到:

1
2
3
hello

+-- 4 lines: -----

Vim 将 {{{` 和 `}}} 视为折叠指示器,并折叠其中的内容。使用标志折叠时,Vim 会寻找由 'foldmarker' 选项定义的特殊标志,并标记折叠区域。要查看 Vim 使用的标志,请运行:

1
:set foldmarker?

默认情况下,Vim 把 {{{` 和 `}}} 作为指示器。如果您想将指示器更改为其他诸如 “coffee1” 和 “coffee2” 的字符串,可以运行:

1
:set foldmarker=coffee1,coffee2

假设有如下文本:

1
2
3
4
5
6
hello

coffee1
world
vim
coffee2

现在,Vim 将使用 coffee1coffee2 作为新折叠标志。注意,指示器必须是文本字符串,不能是正则表达式。

持久化折叠

当关闭 Vim 会话后,您将失去所有的折叠信息。假设您有 count.txt 文件:

1
2
3
4
5
one
two
three
four
five

手动从第三行开始往下折叠 (:3,$fold):

1
2
3
one
two
+-- 3 lines: three ---

当您退出 Vim 再重新打开 count.txt 后,这些折叠都不见了!

要在折叠后保留它们,可以运行:

1
:mkview

当打开 count.txt 后,运行:

1
:loadview

您的折叠信息都被保留下来了。然而,您需要手动运行 mkviewloadview。我知道,终有一日,我会忘记运行 mkview 就关闭文件了,接着便会丢失所有折叠信息。能不能自动实现这个呢?

当然能!要在关闭 .txt 文件时自动运行 mkview,以及在打开 .txt 文件后自动运行 loadview,将下列内容添加至您的 vimrc:

1
2
autocmd BufWinLeave *.txt mkview
autocmd BufWinEnter *.txt silent loadview

在上一章您已经见过 autocmd 了,它用于在事件触发时执行一条命令。这里的两个事件是:

  • BufWinLeave 从窗口中删除缓冲时。
  • BufWinEnter 在窗口中加载缓冲时。

现在,即使您在 .txt 文件内折叠内容后直接退出 Vim,下次再打开该文件时,您的折叠信息都能自动恢复。

默认情况下,当运行 mkview 时,Vim将折叠信息保存在~/.vim/view (Unix 系统)。您可以查阅 :h 'viewdir' 来了解更多信息。

聪明地学习折叠

当我刚开始使用 Vim 时, 我会跳过学习 Vim 折叠,因为我觉得它不太实用。然而,随着我码龄的增长,我越发觉得折叠功能大有用处。得当地使用折叠功能,文本结构可以更加清晰,犹如一本书籍的目录。

当您学习折叠时,请从手动折叠开始,因为它可以随学随用。然后逐渐学习不同的技巧来使用缩进和标志折叠。最后,学习如何使用语法和表达式折叠。您甚至可以使用后两个来编写您自己的 Vim 插件。

第18章 Git

Vim 和 Git 是两种实现不同功能的伟大工具。Vim 用于文本编辑,Git 用于版本控制。

在本章中,您将学习如何将 Vim 和 Git 集成在一起。

差异比较

在上一章中,您看到了如何运行 vimdiff 命令以显示多个文件之间的差异。

假设您有两个文件,file1.txtfile2.txt

file1.txt 的内容如下:

1
2
3
4
5
6
7
8
pancakes
waffles
apples

milk
apple juice

yogurt

file2.txt 的内容如下:

1
2
3
4
5
6
7
8
pancakes
waffles
oranges

milk
orange juice

yogurt

若要查看两个文件之间的差异,请运行:

1
vimdiff file1.txt file2.txt

或者也可以运行:

1
vim -d file1.txt file2.txt

Basic diffing with Vim

vimdiff 并排显示两个缓冲区。左边是 file1.txt,右边是 file2.txt。不同的两行(apples 和 oranges)会被高亮显示。

假设您要使第二个缓冲区相应位置变成 apples,而不是 oranges。若想从 file1.txt 传输您当前位置(当前您在 file1.txt)的内容到 file2.txt,首先使用 ]c 跳转到下一处差异(使用 [c 可跳回上一处差异),现在光标应该在 apples 上了。接着运行 :diffput。此时,这两个文件都是 apples 了。

Finding files in FZF

如果您想从另一个缓冲区(orange juice,file2.txt)传输文本来替代当前缓冲区(apple juice,file1.txt),让您的光标仍然位于 file1.txt 的窗口中,首先使用 ]c 跳转至下一处差异,此时光标应该在 apple juice 上。接着运行 :diffget 获取另一个缓冲区的 orange juice 来替代当前缓冲区中的 apple juice。

:diffput 将文本从当前缓冲区 输出 到另一个缓冲区。:diffget 从另一个缓冲区 获取 文本到当前缓冲区。

如果有多个缓冲区,可以运行 :diffput fileN.txt:diffget fileN.txt 来指定目标缓冲区 fileN。

使用 Vim 作为合并工具

“我非常喜欢解决合并冲突。” ——佚名

我不知道有谁喜欢解决合并冲突,但总之,合并冲突是无法避免的。在本节中,您将学习如何利用 Vim 作为解决合并冲突的工具。

首先,运行下列命令来将默认合并工具更改为 vimdiff

1
2
3
git config merge.tool vimdiff
git config merge.conflictstyle diff3
git config mergetool.prompt false

或者您也可以直接修改 ~/.gitconfig(默认情况下,它应该处于根目录中,但您的可能在不同的位置)。上面的命令应该会将您的 gitconfig 改成如下设置的样子,如果您还没有运行上面的命令,您也可以手动更改您的 gitconfig。

1
2
3
4
5
6
7
[core]
editor = vim
[merge]
tool = vimdiff
conflictstyle = diff3
[difftool]
prompt = false

让我们创建一个假的合并冲突来测试一下。首先创建一个目录 /food,并初始化 git 仓库:

1
git init

添加 breakfast.txt 文件,内容为:

1
2
3
pancakes
waffles
oranges

添加文件并提交它:

1
2
git add .
git commit -m "Initial breakfast commit"

接着,创建一个新分支 apples:

1
git checkout -b apples

更改 breakfast.txt 文件为:

1
2
3
pancakes
waffles
apples

保存文件,添加并提交更改:

1
2
git add .
git commit -m "Apples not oranges"

真棒!现在 master 分支有 oranges,而 apples 分支有 apples。接着回到 master 分支:

1
git checkout master

breakfast.txt 文件中,您应该能看到原来的文本 oranges。接着将它改成 grapes,因为它是现在的应季水果:

1
2
3
pancakes
waffles
grapes

保存、添加、提交:

1
2
git add .
git commit -m "Grapes not oranges"

嚯!这么多步骤!现在准备要将 apples 分支合并进 master 分支了:

1
git merge apples

您应该会看到如下错误:

1
2
3
Auto-merging breakfast.txt
CONFLICT (content): Merge conflict in breakfast.txt
Automatic merge failed; fix conflicts and then commit the result.

没错,一个冲突!现在一起来用一下新配置的 mergetool 来解决冲突吧!运行:

1
git mergetool

Three-way mergetool with Vim

Vim 显示了四个窗口。注意一下顶部三个:

  • LOCAL 包含了 grapes。这是“本地”中的变化,也是您要合并的内容。
  • BASE 包含了 oranges。这是 LOCALREMOTE 的共同祖先,用于比较它们之间的分歧。
  • REMOTE 包含了 apples。这是要被合并的内容。

底部窗口(也即第四个窗口),您能看到:

1
2
3
4
5
6
7
8
9
pancakes
waffles
<<<<<<< HEAD
grapes
||||||| db63958
oranges
=======
apples
>>>>>>> apples

第四个窗口包含了合并冲突文本。有了这步设置,就能更轻松看到哪个环境发生了什么变化。您可以同时查看 LOCALBASEREMOTE 的内容。

您的光标应该在第四个窗口的高亮区域。再运行 :diffget LOCAL,就可以获取来自 LOCAL 的改变(grapes)。同样,运行 :diffget BASE 可以获取来自 BASE 的改变(oranges),而运行 :diffget REMOTE 可以获取来自 REMOTE 的改变(apples)。

在这个例子中,我们试着获取来自 LOCAL 的改变。运行 :diffget LOLOCAL 的简写),第四个窗口变成了 grapes。完成后,就可以保存并退出所有文件(:wqall)了。还不错吧?

稍加留意您会发现,现在多了一个 breakfast.txt.orig 文件。这是 Git 防止事与愿违而创建的备份文件。如果您不希望 Git 在合并期间创建备份文件,可以运行:

1
git config --global mergetool.keepBackup false

在 Vim 中使用 Git

Vim 本身没有集成 Git,要在 Vim 中执行 Git 命令,一种方法是在命令行模式中使用 ! 叹号运算符。

使用 ! 可以运行任何 Git 命令:

1
2
3
4
:!git status
:!git commit
:!git diff
:!git push origin master

您还可以使用 Vim 的特殊字符 % (当前缓冲区) 或 # (其他缓冲区):

1
2
:!git add %         " git add current file
:!git checkout # " git checkout the other file

这里有一个Vim技巧,您可以用来添加不同Vim窗口中的多个文件,运行:

1
windo !git add %

然后提交:

1
:!git commit "添加了Vim窗口中的所有文件,酷"

windo命令是VIm的 “do” 命令其中之一,类似于您前面看到的 argdowindo 将命令执行在每一个窗口中。

插件

这里有很多提供git支持的Vim插件。以下是 Vim 中较流行的 Git 相关插件列表(您读到这篇文章时可能又有更多):

其中最流行的是 vim-fugitive。本章的剩余部分,我将使用此插件来介绍几个 git 工作流。

Vim-Fugitive

vim-fugitive 插件允许您在不离开 Vim 编辑器的情况下运行 git 命令行界面。您会发现,有些命令在 Vim 内部执行时会更好。

开始前,请先使用 Vim 插件管理器(vim-plugvundledein.vim 等)安装 vim-fugitive。

Git Status

当您不带参数地运行 :Git 命令时,vim-fugitive 将显示一个 git 概要窗口,它显示了未跟踪、未暂存和已暂存的文件。在此 “git status” 模式下,您可以做一些操作:

  • Ctrl-n / Ctrl-p 转到下一个 / 上一个文件。
  • - 暂存或取消暂存光标处的文件。
  • s 暂存光标处的文件。
  • u 取消暂存光标处的文件。
  • > / < 内联显示或隐藏光标处文件的差异变化。

Finding files in FZF

查阅 :h fugitive-staging-maps 可获得更多信息。

Git Blame

在当前文件运行 :Git blame 命令,vim-fugitive 可以显示一个拆分的问责窗口。这有助于追踪那些 BUG 是谁写的,接着就可以冲他/她怒吼(开个玩笑)。

"git blame" 模式下您可以做:

  • q 关闭问责窗口。
  • A 调整作者列大小。
  • C 调整提交列大小。
  • D 调整日期/时间列大小。

查阅 :h :Git_blame 可获得更多信息。

Finding files in FZF

Gdiffsplit

当您运行 :Gdiffsplit 命令后,vim-fugitive 会执行 vimdiff,比对索引或工作树中的版本与当前文件最新更改的区别。如果运行 :Gdiffsplit <commit>,vim-fugitive 则会根据 <commit> 中的版本来执行 vimdiff

Finding files in FZF

由于您处于 vimdiff 模式中,因此您可以使用 :diffput:diffget获取输出 差异。

Gwrite 和 Gread

当您在更改文件后运行 :Gwrite 命令,vim-fugitive 将暂存更改,就像运行 git add <current-file> 一样。

当您在更改文件后运行 :Gread 命令,vim-fugitive 会将文件还原至更改前的状态,就像运行 git checkout <current-file> 一样。使用 :Gread 还有一个好处是操作可撤销。如果在运行 :Gread 后您改变主意,想要保留原来的更改,您只需要撤消(u),Vim 将撤回 :Gread 操作。要换作是在命令行中运行 git checkout <current-file>,就完成不了这种操作了。

Gclog

当您运行 :Gclog 命令时,vim-fugitive 将显示提交历史记录,就像运行 git log 命令一样。Vim-fugitive 使用 Vim 的 quickfix 来完成此任务,因此您可以使用 :cnext:cprevious 来遍历下一个或上一个日志信息。您还可以使用 :copen:cclose 打开或关闭日志列表。

Finding files in FZF

"git log" 模式中,您可以做两件事:

  • 查看树。
  • 访问父级(上一个提交)。

您可以像 git log 命令一样,传递参数给 :Gclog 命令。如果您项目的提交历史记录很长,只想看最后三个提交,则可以运行 :Gclog -3。如果需要根据提交日期来筛选记录,可以运行类似 :Gclog --after="January 1" --before="March 14" 的命令。

Vim-Fugitive 的更多功能

以上只是寥寥几个 vim-fugitive 功能的例子,您可以查阅 :h fugitive.txt 来了解更多有关 vim-fugitive 的信息。大多数流行的 git 命令可能都有 vim-fugitive 的优化版本,您只需在文档中查找它们。

如果您处于 vim-fugitive 的“特殊模式”(如 :Git:Git blame 模式)中,按下 g? 可以了解当前有哪些可用的快捷键,Vim-fugitive 将为您所处的模式显示相应的 :help 窗口。棒极了!

聪明地学习 Vim 和 Git

每个人都有不同的 git 工作流,可能 vim-fugitive 非常合适您的工作流(也可能不适合)。总之,我强烈建议您试试上面列出的所有插件。可能还有一些其他的我没有列出来,都可以去试一试。

要让Vim-git的集成工作得更好,一个显而易见的办法就是去深入了解git。Git 本身是一个很庞大的主题,我只向您展示了它其中很小的一部分。好了,接下来谈谈如何使用 Vim 编译您的代码。

第19章 编译

编译是许多编程语言的重要主题。在本章中,您将学习如何在 Vim 中编译。此外,您将看到如何利用好 Vim 的 :make 命令。

从命令行编译

您可以使用叹号运算符(!)进行编译。如果您需要使用 g++ 来编译 .cpp 文件,可以运行:

1
:!g++ hello.cpp -o hello

但要每次手动指定文件名和输出文件名会非常繁琐和容易出错。而 makefile 是条可行之路。

Make命令

Vim 有运行 makefile 的 :make 命令。当您运行它时,Vim 会在当前目录查找 makefile 并执行它。

在当前目录创建一个文件名为 makefile ,然后添加下列内容:

1
2
3
4
5
6
all:
echo "Hello all"
foo:
echo "Hello foo"
list_pls:
ls

在 Vim 中运行:

1
:make

Vim 执行它的方式与从终端运行它的方式相同。:make 命令也接受终端中 make 命令的参数。运行:

1
2
3
4
5
:make foo
" Outputs "Hello foo"

:make list_pls
" Outputs the ls command result

如果命令执行异常,:make 命令将使用 Vim 的 quickfix 来存储这些错误。现在试着运行一个不存在的目标:

1
:make dontexist

您应该会看到该命令执行错误。运行 quickfix 命令 :copen 可以打开 quickfix 窗口来查看该错误:

1
|| make: *** No rule to make target `dontexist'.  Stop.

使用 Make 编译

让我们使用 makefile 来编译一个基本的 .cpp 程序。首先创建一个 hello.cpp 文件:

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "Hello!\n";
return 0;
}

然后,更新 makefile 来编译和运行 .cpp 文件:

1
2
3
4
5
6
all:
echo "build, run"
build:
g++ hello.cpp -o hello
run:
./hello

现在运行:

1
:make build

g++ 将编译 ./hello.cpp 并且生成 ./hello。接着运行:

1
:make run

您应该会看到终端上打印出了 "Hello!"

不同的Make程序

当您运行 :make 时,Vim 实际上会执行 makeprg 选项所设置的任何命令,您可以运行 :set makeprg? 来查看它:

1
makeprg=make

:make 的默认命令是外部的 make 命令。若想修改 :make 命令,使每次运行它时执行 g++ <your-file-name>,请运行:

1
:set makeprg=g++\ %

\ 用于转义 g++ 后的空格。Vim 中 % 符号代表当前文件。因此,g++\ % 命令等于运行 g++ hello.cpp

转到 ./hello.cpp 然后运行 :make,Vim 将编译 hello.cpp 并输出 a.out(因为您没有指定输出)。让我们重构一下,使用去掉扩展名的原始文件名来命名编译后的输出。运行下面的命令(或将它们添加到vimrc):

1
:set makeprg=g++\ %\ -o\ %<

上面的命令分解如下:

  • g++\\ % 如上所述,等同于运行 g++ <your-file>
  • -o 输出选项。
  • %< 在 Vim 中代表了没有扩展名的当前文件名(如 hello.cpp 变成 hello)。

当您在 ./hello.cpp 中运行 :make 时,它将编译为 ./hello。要在 ./hello.cpp 中快速地执行 ./hello,可以运行 :!./%<。同样,它等同于运行 :!./<无后缀的当前文件名>

查阅 :h :compiler:h write-compiler-plugin 可以了解更多信息。

保存时自动编译

有了自动化编译,您可以让生活更加轻松。回想一下,您可以使用 Vim 的 autocmd 来根据某些事件自动执行操作。例如,要自动在每次保存后编译 .cpp 文件,您可以将下面内容添加到vimrc:

1
:autocmd BufWritePost *.cpp make

现在您每次保存 .cpp 文件后,Vim 都将自动执行 make 命令。

切换编译器

Vim 有一个 :compiler 命令可以快速切换编译器。您的 Vim 可能附带了一些预构建的编译配置。要检查您拥有哪些编译器,请运行:

1
:e $VIMRUNTIME/compilers/<tab>

您应该会看到一个不同编程语言的编译器列表。

若要使用 :compiler 命令,假设您有一个 ruby 文件 hello.rb,内容是:

1
puts "Hello ruby"

回想一下,如果运行 :make,Vim 将执行赋值给 makeprg 的任何命令(默认是 make)。如果您运行:

1
:compiler ruby

Vim 执行 $VIMRUNTIME/compiler/ruby.vim 脚本,并将 makeprg 更改为使用 ruby 命令。现在如果您运行 :set makeprg?,它会显示 makeprg=ruby(这取决于您 $VIMRUNTIME/compiler/ruby.vim 里的内容,如果您有其他自定义的 ruby 编译器,您的结果可能会有不同)。:compiler <your-lang> 命令允许您快速切换至其他编译器。如果您的项目使用多种语言,这会非常有用。

您不必使用 :compilermakeprg 来编译程序。您可以运行测试脚本、分析文件、发送信号或任何您想要的内容。

创建自定义编译器

让我们来创建一个简单的 Typescript 编译器。先在您的设备上安装 Typescript(npm install -g typescript),安装完后您将有 tsc 命令。如果您之前没有尝试过 typescript,tsc 将 Typescript 文件编译成 Javascript 文件。假设您有一个 hello.ts 文件:

1
2
const hello = "hello";
console.log(hello);

运行 tsc hello.ts 后,它将被编译成 hello.js。然而,如果您的 hello.ts 文件中有如下内容:

1
2
3
const hello = "hello";
hello = "hello again";
console.log(hello);

这会抛出错误,因为不能更改一个 const 变量。运行 tsc hello.ts 的错误如下:

1
2
3
4
5
6
7
hello.ts:2:1 - error TS2588: Cannot assign to 'person' because it is a constant.

2 person = "hello again";
~~~~~~


Found 1 error.

要创建一个简单的 Typescript 编译器,请在您的 ~/.vim/ 目录中新添加一个 compiler 目录(即 ~/.vim/compiler/),接着创建 typescript.vim 文件(即 ~/.vim/compiler/typescript.vim),并添加如下内容:

1
2
CompilerSet makeprg=tsc
CompilerSet errorformat=%f:\ %m

第一行将 makeprg 设置为运行 tsc 命令。第二行将错误格式设置为显示文件(%f),后跟冒号(:)和转义的空格(\ ),最后是错误消息(%m)。查阅 :h errorformat 可了解更多关于错误格式的信息。

您还可以阅读一些预制的编译器,看看它们是如何实现的。输入 :e $VIMRUNTIME/compiler/<some-language>.vim 查看。

有些插件可能会干扰 Typescript 文件,可以使用 --noplugin 标志以零插件的形式打开hello.ts 文件:

1
vim --noplugin hello.ts

检查 makeprg

1
:set makeprg?

它应该会显示默认的 make 程序。要使用新的 Typescript 编译器,请运行:

1
:compiler typescript

当您运行 :set makeprg? 时,它应该会显示 tsc 了。我们来测试一下:

1
:make %

回想一下,% 代表当前文件。看看您的 Typescript 编译器是否如预期一样工作。运行 :copen 可以查看错误列表。

异步编译器

有时编译可能需要很长时间。在等待编译时,您不会想眼睁睁盯着已冻结的 Vim 的。如果可以异步编译,就可以在编译期间继续使用 Vim 了,岂不美哉?

幸运的是,有插件来运行异步进程。有两个比较好的是:

在这一章中,我将介绍 vim-dispatch,但我强烈建议您尝试上述列表中所有插件。

Vim 和 NeoVim 实际上都支持异步作业,但它们超出了本章的范围。如果您好奇,可以查阅 :h job-channel-overview.txt

插件:Vim-dispatch

Vim-dispatch 有几个命令,最主要的两个是 :Make:Dispatch

异步Make

Vim-dispatch 的 :Make 命令与 Vim 的 :make 相似,但它以异步方式运行。如果您正处于 Javascript 项目中,并且需要运行 npm t,可以将 makeprg 设置为:

1
:set makeprg=npm\\ t

如果运行:

1
:make

Vim 将执行 npm t。但同时,您只能盯着冻结了的屏幕。有了 vim-dispatch,您只需要运行:

1
:Make

Vim 将启用后台进程异步运行 npm t,同时您还能在 Vim 中继续编辑您的文本。棒极了!

异步调度(Dispatch)

:Dispatch 命令的工作方式和 :compiler:! 类似,它可以在Vim中运行任意外部命令。

假设您在 ruby spec 文件中,需要执行测试,可以运行:

1
:Dispatch rspec %

Vim 将对当前文件异步运行 rspec 命令。

自动调度

Vim-dispatch 有一个缓冲区变量b:dispatch,您可以配置它来自动执行特定命令,您可以利用 autocmd和它一起工作。如果在您的 vimrc 中添加如下内容:

1
autocmd BufEnter *_spec.rb let b:dispatch = 'bundle exec rspec %'

现在每当您进入(BufEnter)一个以 _spec.rb 结尾的文件,运行:Dispatch 将自动执行 bundle exec rspec <your-current-ruby-spec-file>

聪明地学习编译

在本章中,您了解到可以使用 makecompiler 命令从Vim内部异步运行 任何 进程,以完善您的编程工作流程。Vim 拥有通过其他程序来扩展自身的能力,这使其变得强大。

第20章 视图、会话和 Viminfo

当您做了一段时间的项目后,您可能会发现这个项目逐渐形了成自己的设置、折叠、缓冲区、布局等,就像住了一段时间公寓后,精心装饰了它一样。问题是,关闭 Vim 后,所有的这些更改都会丢失。如果能保留这些更改,等到下次打开 Vim 时,一切恢复如初,岂不美哉?

本章中,您将学习如何使用 视图、会话 和 Viminfo 来保存项目的“快照”。

视图

视图是这三个部分(视图、会话、Viminfo)中的最小子集,它是单个窗口相关设置的集合。如果您长时间在一个窗口上工作,并且想要保留其映射和折叠,您可以使用视图。

我们来创建一个 foo.txt 文件:

1
2
3
4
5
6
7
8
9
10
foo1
foo2
foo3
foo4
foo5
foo6
foo7
foo8
foo9
foo10

在这个文件中,做三次修改:

  1. 在第 1 行,创建一个手动折叠 zf4j(折叠接下来 4 行)。
  2. 更改 number 设置:setlocal nonumber norelativenumber。这会移除窗口左侧的数字指示器。
  3. 创建本地映射,每当按一次 j 时,向下两行::nnoremap <buffer> j jj

您的文件看起来应该像:

1
2
3
4
5
6
+-- 5 lines: foo1 -----
foo6
foo7
foo8
foo9
foo10

配置视图属性

运行:

1
:set viewoptions?

默认情况下会显示(根据您的 vimrc 可能会有所不同):

1
viewoptions=folds,cursor,curdir

我们来配置 viewoptions。要保留的三个属性分别是折叠、映射和本地设置选项。如果您的设置和我的相似,那么您已经有了 folds 选项。运行下列命令使视图记住 localoptions

1
:set viewoptions+=localoptions

查阅 :h viewoptions 可了解 viewoptions 的其他可用选项。现在运行 :set viewoptions?,您将看到:

1
viewoptions=folds,cursor,curdir,localoptions

保存视图

foo.txt 窗口经过适当折叠并设置了 nonumber norelativenumber 选项后,现在我们来保存视图。运行:

1
:mkview

Vim 创建了一个视图文件。

视图文件

您可能会想“Vim 将这个视图文件保存到哪儿了呢?”,运行下列命令就可以看到答案了:

1
:set viewdir?

默认情况下会显示 ~/.vim/view(根据您的操作系统,可能会有不同的路径。查阅 :h viewdir 获得更多信息)。如果您运行的是基于Unix的操作系统,想修改该路径,可以在您的 vimrc 中添加下列内容:

1
set viewdir=$HOME/else/where

加载视图文件

关闭并重新打开 foo.txt,您会看到原来的文本,没有任何改变。这是预期行为。运行下列命令可以加载视图文件:

1
:loadview

现在您将看到:

1
2
3
4
5
6
+-- 5 lines: foo1 -----
foo6
foo7
foo8
foo9
foo10

那些折叠、本地设置以及映射都恢复了。如果您细心还可以发现,光标位于上一次您运行 :mkview 时所处的行上。只要您有 cursor 选项,视图将记住光标位置。

多个视图

Vim 允许您保存 9 个编号的视图(1-9)。

假设您想用 :9,10 fold 来额外折叠最后两行,我们把这存为视图 1。运行:

1
:mkview 1

如果您又想用 :6,7 fold 再折叠一次,并存为不同的视图,运行:

1
:mkview 2

关闭并重新打开 foo.txt 文件,运行下列命令可以加载视图 1:

1
:loadview 1

要加载视图 2,运行:

1
:loadview 2

要加载原始视图,运行:

1
:loadview

自动创建视图

有一件可能会发生的很倒霉的事情是,您花了很长时间在一个大文件中进行折叠,一不小心关闭了窗口,接着丢失了所有折叠信息。您可以在 vimrc 中添加下列内容,使得在关闭缓冲区后 Vim 能自动创建视图,防止此类灾难发生:

1
autocmd BufWinLeave *.txt mkview

另外也能在打开缓冲区后自动加载视图:

1
autocmd BufWinEnter *.txt silent loadview

现在,当您编辑 txt 文件时,不用再担心创建和加载视图了。但也注意,==随着时间的推移,视图文件会不断积累,记得每隔几个月清理一次。==

会话

如果说视图保存了某个窗口的设置,那么会话则保存了所有窗口(包括布局)的信息。

创建新会话

假设您在 foobarbaz 工程中编辑着 3 个文件:

foo.txt 的内容:

1
2
3
4
5
6
7
8
9
10
foo1
foo2
foo3
foo4
foo5
foo6
foo7
foo8
foo9
foo10

bar.txt 的内容:

1
2
3
4
5
6
7
8
9
10
bar1
bar2
bar3
bar4
bar5
bar6
bar7
bar8
bar9
bar10

baz.txt 的内容:

1
2
3
4
5
6
7
8
9
10
baz1
baz2
baz3
baz4
baz5
baz6
baz7
baz8
baz9
baz10

假设您的窗口布局如下所示(适当地使用 splitvsplit 来放置):

要保留这个外观,您需要保存会话。运行:

1
:mksession

与默认存储在 ~/.vim/viewmkview 不同,mksession 在当前目录存储会话文件(Session.vim)。如果好奇,您可以看看文件。

如果您想将会话文件另存他处,可以将参数传递给 mksession

1
:mksession ~/some/where/else.vim

使用 ! 来调用命令可以覆盖一个已存在的会话文件(:mksession! ~/some/where/else.vim)。

加载会话

运行下列命令可以加载会话:

1
:source Session.vim

现在 Vim 看起来就像您离开它时的样子!或者,您也可以从终端加载会话文件:

1
vim -S Session.vim

配置会话属性

您可以配置会话要保存的属性。若要查看当前哪些属性正被保存,请运行:

1
:set sessionoptions?

我的显示:

1
blank,buffers,curdir,folds,help,tabpages,winsize,terminal

如果在保存会话时不想存储 terminal,可以运行下列命令将其从会话选项中删除:

1
:set sessionoptions-=terminal

如果要在保存会话时存储 options,请运行:

1
:set sessionoptions+=options

下面是一些 sessionoptions 可以存储的属性:

  • blank 存储空窗口
  • buffers 存储缓冲区
  • folds 存储折叠
  • globals 存储全局变量(必须以大写字母开头,并且至少包含一个小写字母)
  • options 存储选项和映射
  • resize 存储窗口行列
  • winpos 存储窗口位置
  • winsize 存储窗口大小
  • tabpages 存储选项卡
  • unix 以 Unix 格式存储文件

查阅 :h 'sessionoptions' 来获取完整列表。

会话是保存项目外部属性的好工具。但是,一些内部属性不存储在会话中,如本地标记、寄存器、历史记录等。要保存它们,您需要使用 Viminfo!

Viminfo

如果您留意,在复制一个单词进寄存器 a,再退出并重新打开 Vim 后,您仍然可以看到存储在寄存器中的文本。这就是 Viminfo 的功劳。没有它,在您关闭 Vim 后,Vim 会忘记这些寄存器。

如果您使用 Vim 8 或更高版本,Vim 会默认启用 Viminfo。因此您可能一直在使用 Viminfo,而您对它毫不知情!

您可能会问:Viminfo 存储了什么?与会话有何不同?

要使用 Viminfo,您必须启用了 +viminfo 特性(:version)。Viminfo 存储着:

  • 命令行历史记录。
  • 字符串搜索历史记录。
  • 输入行历史记录。
  • 非空寄存器的内容。
  • 多个文件的标记。
  • 文件标记,它指向文件中的位置。
  • 上次搜索 / 替换模式(用于 “n” 和 “&”)。
  • 缓冲区列表。
  • 全局变量。

通常,会话存储“外部”属性,Viminfo 存储“内部”属性。

每个项目可以有一个会话文件,而 Viminfo 与会话不同,通常每台计算机只使用一个 Viminfo。Viminfo 是项目无关的。

对于 Unix,Viminfo 的默认位置是 $HOME/.viminfo~/.viminfo)。如果您用其他操作系统,Viminfo 位置可能会有所不同。可以查阅 :h viminfo-file-name。每一次您做出的“内部”更改,如将文本复制进一个寄存器,Vim 都会自动更新 Viminfo 文件。

请确保您设置了 nocompatible 选项(set nocompatible),否则您的 Viminfo 将不起作用。

读写 Viminfo

尽管只使用一个 Viminfo 文件,但您还是可以创建多个 Viminfo 文件。使用 :wviminfo 命令(缩写为 :wv)来创建多个 Viminfo 文件。

1
:wv ~/.viminfo_extra

要覆盖现有的 Viminfo 文件,向 wv 命令多添加一个叹号:

1
:wv! ~/.viminfo_extra

Vim 默认情况下会读取 ~/.viminfo 文件。运行 :rviminfo(缩写为 :rv)可以读取不同的 Vimfile 文件:

1
:rv ~/.viminfo_extra

要在终端使用不同的 Viminfo 文件来启动 Vim,请使用 “i” 标志:

1
vim -i viminfo_extra

如果您要将 Vim 用于不同的任务,比如写代码和写作,您可以创建两个 Viminfo,一个针对写作优化,另一个为写代码优化。

1
2
3
vim -i viminfo_writing

vim -i viminfo_coding

不使用 Viminfo 启动 Vim

要不使用 Viminfo 启动 Vim,可以在终端运行:

1
vim -i NONE

要永不使用 Viminfo,可以在您的 vimrc 文件添加:

1
set viminfo="NONE"

配置 Viminfo 属性

viewoptions 以及 sessionoptions 类似,您可以用 viminfo 选项指定要存储的属性。请运行:

1
:set viminfo?

您会得到:

1
!,'100,<50,s10,h

看起来有点晦涩难懂。命令分解如下:

  • ! 保存以大写字母开头、却不包含小写字母的全局变量。回想一下 g: 代表了一个全局变量。例如,假设您写了赋值语句 let g:FOO = "foo",Viminfo 将存储全局变量 FOO。然而如果您写了 let g:Foo = "foo",Viminfo 将不存储它,因为它包含了小写字母。没有 !,Vim 不会存储这些全局变量。
  • '100 代表标记。在这个例子中,Viminfo 将保存最近 100 个文件的本地标记(a-z)。注意,如果存储的文件过多,Vim 会变得很慢,1000 左右就可以了。
  • <50 告诉 Viminfo 每个寄存器最多保存多少行(这个例子中是 50 行)。如果我复制 100 行文本进寄存器 a("ay99j)后关闭 Vim,下次打开 Vim 并从寄存器 a("ap)粘贴时,Vim 最多只粘贴 50 行;如果不指定最大行号, 所有 行都将被保存;如果指定 0,什么都不保存了。
  • s10 为寄存器设置大小限制(kb)。在这个例子中,任何大于 10kb 的寄存器都会被排除。
  • h 禁用高亮显示(hlsearch 时)。

可以查阅 :h 'viminfo' 来了解其他更多选项。

聪明地使用视图、会话和 Viminfo

Vim 能使用视图、会话和 Viminfo 来保存不同级别的 Vim 环境快照。对于微型项目,可以使用视图;对于大型项目,可以使用会话。您应该花些时间来查阅视图、会话和 Viminfo 提供的所有选项。

为您的编辑风格创建属于您自己的视图、会话和 Viminfo。如果您要换台计算机使用 Vim,只需加载您的设置,立刻就会感到就像在家里的工作环境一样!

第21章 多文件操作

多文件编辑更新是一个值得掌握、非常有用的编辑工具。前面您已经学会了如何使用 cfdo 命令在多个文本中进行更新。本章,您将学到如何在Vim中进行多文件编辑的更多不同方法。

在多个文件中执行命令的几种方法

要在多个文件中执行命令,Vim有8种方法:

  • 参数列表 (argdo)
  • 缓冲区列表 (bufdo)
  • 窗口列表 (windo)
  • tab 列表(tabdo)
  • 快速修复列表 (cdo)
  • 文件方式的快速修复列表 (cfdo)
  • 位置列表 (ldo)
  • 文件方式的位置列表 (lfdo)

实际上,大部分时间您可能只会用到1种或2种(就我个人而言,我使用 cdoargdo比其他的多得多),但了解所有可行方法还是很有用的,这样您就可以选择一个最符合您个人编辑风格的方法。

学习所有8个命令可能听起来让人有点打退堂鼓。但实际上,这些命令工作方式很相似。学习了其中一个后,再学习剩余的将容易的多。它们的运行方式都大体相同:分别创建一个列表(列表中的元素根据命令有所不同),然后向列表传递一个您想执行的命令。

参数列表

参数列表是最基础的列表。它创建一个文件列表。要想为 file1, file2, file3创建文件列表,您可以执行:

1
:args file1 file2 file3

您也可以传递一个通配符(*),所以如果您想为当前目录下所有的 .js 文件创建一个列表,运行:

1
:args *.js

如果您想为当前目录下所有以 “a” 开头的Javascript文件创建列表,运行:

1
:args a*.js

*)通配符匹配当前目录下的1个或多个任意文件名中的字符。但如果您想在某个目录下进行递归搜索怎么办呢?您可以使用双通配符(**)。要得到您当前位置下所有子目录中的Javascript文件,运行:

1
:args **/*.js

您运行了 args 命令后,您的当前buffer将会切换到列表中的第一个文件。运行 :args可以查看您刚才创建的文件列表。当您创建好了您的列表后,您就可以遍历它们了。:first 将让您跳至列表中的第一个文件。:last 将跳到最后一个文件。运行:next可以在列表中一次向前移动一个文件。运行 :prev可以在列表中一次向后移动一个文件。运行:wnext:wprev命令,在向前/向后移动文件的同时还会保存修改。查阅 : arglist 了解更多导航命令。

参数列表在定位某个特定类型的文件或少量文件时特别有用。假如您需要将所有 yml 文件中的donut 更新为 pancake。运行:

1
2
3
:args **/*.yml
:argdo %s/donut/pancake/g | update

注意如果您再次执行 args 命令,它将覆盖先前的列表。比如,如果您先前运行了:

1
:args file1 file2 file3

假设这些文件都是存在的,那么现在您的列表为 file1, file2,以及 file3。然后再运行:

1
:args file4 file5

您的初始列表 file1, file2, file3将被覆盖为 file4, file5。如果您的参数列表中已经有了 file1, file2, file3 ,而您想将 file4, file5 添加到初始列表中,请使用 :arga命令。运行

1
:arga file4 file5

现在您的列表为file1, file2, file3, file4, file5

如果您运行 :arga 时没有给任何参数,Vim会添加当前buffer到参数列表中。例如,如果您的参数列表中已经有了 file1, file2, file3,而您当前buffer是 file5,运行 :arga 将添加 file5 到您的列表中。

在前面的命令(:argdo %s/donut/pancake/g)中您已经看到过了,当您创建好列表后就可以向它传递任意命令行命令。其他的一些示例:

  • 删除参数列表所有文件内包含 “dessert” 的行, 运行 :argdo g/dessert/d.
  • 在参数列表每个文件中执行宏a(假设您已经在a中录好了一个宏),运行 :argdo norm @a.
  • 向参数列表所有文件的第一行插入”hello “+文件名 ,运行 :argdo 0put='hello ' . @%(译者注:在英文版中,原作者给出的命令是 :argdo 0put='hello ' .. @:,貌似这个命令有问题)。

把所有工作完成后,别忘了使用 :update 命令保存(:update只会保存当前buffer,要保存列表所有文件的修改,请用 :argdo update)。

有时候您仅仅需要在参数列表的前n个文件执行某条命令。如果是这种情况,只需要向 argdo 命令传递一个地址就可以了。比如,要在列表的前3个文件执行替换命令,运行::1,3argdo %s/donut/pancake/g

缓冲区列表

因为每次您创建新文件或打开文件时,Vim将它保存在一个buffer中(除非您显式地删除它),所以当您编辑新文件时,缓冲区列表就有组织地被创建了。如果您已经打开了3个文件:file1.rb file2.rb file3.rb,您的缓冲区列表就已经有了3个元素。运行 :buffers(或者:ls、或:files)可以显示缓冲区列表。要想向前或向后遍历缓冲区列表,可以使用 :bnext :bprev。要想跳至列表中第一个或最后一个buffer,可使用 :bfirst:blast

另外,这里有一个和本章内容不相关,但是很酷的缓冲区技巧:如果您的缓冲区有大量的文件,您可以使用 :ball 显示所有缓冲区。:ball 命令默认使用水平分割窗口进行显示,如果想使用垂直分割的窗口显示,运行::vertical ball

回到本章主题。在缓冲区列表中执行某个操作的方法与参数列表操作非常相似。当您创建好缓冲区列表后,您只需要在您想执行的命令前用 :bufdo 代替 :argdo就可以了。例如,如果您想将缓冲区列表内每个文件中所有的 “donut” 替换为 “pancake”并保存修改,可以运行::bufdo %s/donut/pancake/g | update

窗口列表和选项卡(Tab)列表

窗口列表、选项卡列表的操作和参数列表、缓冲区列表同样非常相似。唯一的区别在于它们的内容和语法。

窗口操作作用在每一个打开的窗口上,使用的命令是 :windo。选项卡(Tab)操作作用在每一个打开的选项卡上,使用的命令是 :tabdo。可以查询 :h list-repeat, :h :windo:h :tabdo,了解更多信息。

比如,如果您打开了4个窗口(您可以使用 Ctrl-w v打开一个垂直分割的窗口,也可以使用 Ctrl-w s打开一个水平分割的窗口),然后您运行 :windo 0put = 'hello' . @%,Vim将在所有打开的窗口的第一行输出 “hello”+文件名。

快速修复列表

在前面的章节中(第3章和第19章),我曾提到过快速修复(quickfix)。快速修复有很多作用,很多流行的插件都在使用快速修复提供的功能,因此值得花时间去理解它。

如果您是Vim新手,快速修复对于您可能是个新概念。回想以前您执行代码编译的时候,编译期间您可能遇到过错误,而这些错误都显示在一个特殊的窗口。这就是快速修复(quickfix)的由来。当您编译您的代码的时候,Vim会在快速修复窗口显示错误信息,您可以稍后去解决。许多现代语言已经不再需要进行显式地编译,但快速修复并没有被淘汰。现在,人们使用快速修复来做各种各样的事,比如显示虚拟终端的输入、存储搜索结果等。我们重点研究后者,存储搜索结果。

除编译命令外,某些特定的Vim命令也依赖快速修复接口。其中一种就是搜索命令,其使用过程中大量的使用了快速修复窗口,:vimgrep:grep 都默认使用快速修复。

比如,如果您需要在所有的Javascript文件中递归地搜索 “donut”,您可以运行:

1
:vimgrep /donut/ **/*.js

“donut”的搜索结果存储在快速修复窗口中。要查看快速修复窗口的结果,运行:

1
:copen

要关闭快速修复窗口,运行:

1
:cclose

在快速修复列表中向前或向后遍历,运行:

1
2
:cnext
:cprev

跳至第一个或最后一个匹配的元素,运行:

1
2
:cfirst
:clast

在前面我提到过,有两种快速修复命令:cdocfdo 。它们有什么区别?cdo 在修复列表中的每一个元素上执行命令,而 cfdo 在修复列表中的每一个文件上执行命令。

让我讲清楚一点,假设运行完上面的 vimgrep 命令后,您找到以下结果:

  • 1 result in file1.js
  • 10 results in file2.js

如果您运行 :cfdo %s/donut/pancake/g, 这个命令将会在 file1.jsfile2.js 上分别有效地运行一次%s/donut/pancake/g. 它执行的次数与 匹配结果中文件的数量 相同。因为搜索结果中有2个文件,因此Vim在 file1.js 上运行一次替换命令,在 file2.js 上再运行一次替换命令。 尽管在第二个文件中有10个搜索结果,但 cfdo 只关注快速修复列表中有多少个文件。

而如果您运行 :cdo %s/donut/pancake/g ,这个命令将会在 file1.js 上有效运行一次,然后在 file2.js 上运行10次。它执行的次数与 快速修复列表中元素的数量 相同。因为在 file1.js 上找到1个匹配结果,在 file2.js 上找到10个匹配结果,因此它执行的总次数是11次。

由于您要在列表中运行的命令是 %s/donut/pancake/g ,所以使用 cfdo命令是比较合理的。而使用 cdo 是不合理的,因为它将在 file2.js 中运行10次 %s/donut/pancake/g命令(%s已经是一个针对整个文件的替换操作)。一个文件运行一次 %s 就足够了。如果您使用 cdo,则传给它的命令应当改为 s/donut/pancake/g 才是合理的。

那到底什么时候该用 cfdo?什么时候该用 cdo? 这应当想一想您要传递的命令的作用域,要看命令作用域是整个文件(比如 :%s:g)?还是某一行(比如 :s:!)?

位置列表

位置列表在某种意义上和快速修复列表很像。Vim也使用一个特殊的窗口来显示位置列表的信息。区别在于:您==任何时候都只能有1个快速修复列表,而位置列表则是,有多少个窗口就可以有多少个位置列表。==

假设您打开了两个窗口,其中一个窗口显示 food.txt ,而另一个显示 drinks.txt。在 food.txt里面,运行一个位置列表搜索命令 :lvimgrep:vimgrep命令关于位置列表的一个变体)。

1
:lvim /bagel/ **/*.md

Vim将为 food.txt所在 窗口创建一个位置列表,用于存储所有的bagel搜索结果。用 :lopen命令可以查看位置列表。现在转到另一个窗口 drinks.txt,运行:

1
:lvimgrep /milk/ **/*.md

Vim将为 drinks.txt所在 窗口再创建一个 单独的位置列表,用于存储所有关于milk的搜索结果。

对于每个不同的窗口中您运行的位置命令,Vim都会单独创建一个位置列表。如果您有10个不同的窗口,您就可以有最多10个不同的位置列表。对比前面介绍的快速修复列表,快速修复列表任何时候都只能有1个。就算您有10个不同的窗口,您也只能有1个快速修复列表。

大多数位置列表命令都和快速修复列表命令相似,唯一不同就是位置列表命令有一个 l-前缀,比如: :lvimgrep, :lgrep, 还有 :lmake。在快速修复列表命令中与之对应的是: :vimgrep, :grep, 以及 :make。操作位置列表窗口的方式和快速修复窗口也很相似::lopen, :lclose, :lfirst, :llast, :lnext, 还有:lprev,与之对应快速修复版本是::copen, :cclose, :cfirst, :clast, :cnext, and :cprev

两个位置列表参数的多文件操作命令也和快速修复列表的多文件操作命令也很类似::ldo:lfdo:ldo 对位置列表中每一个元素执行命令,而 :lfdo 对位置列表中每一个文件执行命令。可以查阅 :h location-list了解更多信息。

在Vim中运行多文件操作命令

在编辑工作中,知道如何进行多文件操作是一个非常有用的技能。当您需要在多个文件中改变一个变量名字的时候,您肯定想一个操作就全部搞定。Vim有8种不同的方法支持你完成这个事。

事实上,您可能并不会用到所有8种方法。您会慢慢倾向于其中1中或2种。当您刚开始时,选择其中1个(我个人建议从参数列表开始 :argdo)并掌握它。当您习惯了其中1个,然后再学下一个。您将会发现,学习第二个、第三个、第四个时要容易多了。记得要创造性的使用,即将它和其他各种不同命令组合起来使用。坚持练习直到您可以不经思考地高效的使用它。让它成为您的肌肉记忆。

就像前面已经说过的,您现在已经掌握了Vim的编辑功能。恭喜您!