西洲渡

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

聪明的学习Vim(下)

前言

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

全局命令

到目前为止,您已经了解了如何使用点命令(.)重复上一次更改,如何使用宏(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);

要删除所有包含“控制台”的行,可以运行:

1
:g/console/d

结果:

1
2
3
4
5
const one = 1;

const two = 2;

const three = 3;

全局命令在与“控制台”模式匹配的所有行上执行删除命令(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,5/g/console/d  在第1行和第5行之间匹配字符串”console”并将其删除。
  • :,5/g/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
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 是全局命令。
  • /./ 是“非空行”的模式。回想一下正则表达式中的点(.)表示_任何字符_。它将行与至少一个字符匹配,因此将行与“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);

递归全局命令

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

给定表达式:

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”的行。最后,它将删除该匹配项。

您也可以将“g”与“v”结合使用以找到正负模式。例如:

1
:g/console/v/two/d

而不是查找包含模式”two”的行,它将查找_不_包含”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

^是“行的开始”的模式。使用^匹配所有行,包括空行。

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

1
:5,10g/^/m 0

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

汇总所有待办事项

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

1
2
3
4
5
6
7
8
9
10
const one = 1;
console.log("one: ", one);
// TODO: feed the puppy

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,找到时间来完成它们或将它们委托给其他人,然后继续执行下一个任务。

另一种选择是使用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 是全局命令
  • /^$/ 是空行的模式。回想一下,^表示行的开始,$表示行的结束。 ^ $匹配一个空行(一个零字符长的行)。
  • ,/./-1  是j命令的范围。由于您没有传递起始范围的值,因此它从当前行开始。您之前已经了解到/./是非空行的模式。 ,/./是从当前行到下一个非空行的范围。全局命令的范围/^$/会将您带到console.log("one: ", one);下面的第一行。这是当前行。 /./ 匹配第一条非空行,即const two = 2;行。最后,-1将其偏移一行。第一次匹配的有效范围是console.log("one: ", one);下方的空行和const two = 2;上方的空行。
  • j 是连接命令:j。您可以加入所有作为其范围的行。例如,:1,5j连接第一到第五行。
    请注意,您正在j命令之前传递范围(,/./-1)。仅仅因为您在全局命令中使用了命令行命令,并不意味着您不能给它一个范围。在这段代码中,您将传递给j命令自己的范围来执行。您可以在执行全局命令时将范围传递给任何命令。

顺便说一句,如果您想将多条空行减少为无行,而不是将,/./-1 用作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);

高级排序

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/\[/ 是全局命令模式。

  • :g 是全局命令。
  • /\[/ 是全局命令使用的模式。 \[查找文字”[“字符串。

+1,/\]/-1  是排序命令的范围。

  • 范围可以有开始和结束地址。在这种情况下,+1是起始地址,/\]/-1是结束地址。
  • +1 表示当前行之后的行,这是与全局命令中的模式“ [”匹配的行。 “ +1”将当前行偏移一行。因此,在第一个匹配项中,范围实际上是在const arrayB = [文本_之后_的一行。
  • /\]/-1 是结束地址。 \]代表一个文字的右方括号“]”。 -1将其偏移一行。结束地址是”]”上方的行。

sort是sort命令行命令。它对给定范围内的所有内容进行排序。 “[“到上方的行”]”之后的所有内容均已排序。

如果您仍然对该命令感到困惑,请不要担心。我花了很长时间才掌握它。稍事休息,离开屏幕,然后重新思考。

聪明地学习全局命令

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

这里的一些例子很复杂。不要被吓到。真是花时间了解它们。学习阅读模式。确保您知道每个命令中的每个字母代表什么。不要放弃。

每当需要在多个位置应用命令时,请暂停并查看是否可以使用g命令。寻找最适合工作的命令,并编写一个模式以同时定位多个目标。然后重复执行此操作,直到您无需考虑即可进行操作。下次,看看是否有更快,更有效的方法。

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

外部命令

在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命令仅适用于至少一行或更长的运动/文本对象。如果您尝试运行!iwtr'[az]''[AZ]'(在内部单词上执行tr),您会发现它在整个行上都应用了tr命令,而不是光标所在的单词开启。

聪明地学习外部命令

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

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

命令行模式

在前三章中,您已经学习了如何使用搜索命令(/, ?)、替换命令(: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 就可以插入它。输入模式中的寄存器能做到的一切,同样能在命令行模式中做到。

命令也能使用自动补全。例如,要在命令行模式中自动补全 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。

在命令行模式中,您可以按下 UpDown 键来遍历此历史记录列表。假设您的命令行指令历史记录如下:

1
2
3
51  s/foo/bar/g
52 s/foo/baz/g
53 s/foo//g

: 后再按 Up,您可以看到 :s/foo//g。再按 Up 可以看到 :s/foo/baz/g。Vim 向上遍历了历史记录。

类似地,运行 :his / 可以查看搜索记录。运行后,按下UpDown可以遍历此历史记录栈。

Vim 非常聪明,可以区分不同的历史记录。按下:后再按UpDown,Vim 自动显示命令历史记录。按下/后再按UpDown,Vim 自动显示搜索记录。

命令行窗口

历史记录窗口只能显示过往命令行指令,但无法运行它们。但在命令行窗口中,可以边浏览边执行。有三种命令行窗口:

1
2
3
q:    命令行窗口
q/ 向前搜索窗口
q? 向后搜索窗口

运行 q: 来打开命令行窗口。Vim 将在屏幕底部启动一个新窗口。 您可以使用 UpCtrl-p 键向上遍历,使用 DownCtrl-n 键可以向下遍历。按下 <Return>,Vim 将执行该命令。按下 Ctrl-cCtrl-w c:quit 可以退出命令行窗口。

类似地,运行 q/ 可以启动向前搜索命令行窗口,运行 q? 可以启动向后搜索命令行窗口。

聪明地学习命令行模式

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

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

标签

快速转到任意定义处,是文本编辑中一个非常有用的特性。在本章中,您将学习如何使用 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 仓库了解更多说明。在您安装 universal ctags 后,运行 ctags --version,它会显示:

1
2
3
Universal Ctags 0.0.0(b43eb39), Copyright (C) 2015 Universal Ctags Team
Universal Ctags is derived from Exuberant Ctags.
Exuberant Ctags 5.8, Copyright (C) 1996-2009 Darren Hiebert

请确保您看到了 “Universal 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 生成器控制。这里我不打算介绍它们,您可以随意查阅文档。

现在回到 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

这一行也是一样的:

  • 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 高。(译注:是,是)

如果运行: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
  # 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 .

two.rb 中添加一个新过程:

1
2
3
def waffle
"Two waffles"
end

现在保存文件,接着检查一下标签文件,您会在里面看到 waffle 了。成功啦!

使用插件

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

我使用 vim-gutentags。如果您使用了 Vim 插件管理器 (vim-plug, vundle, dein.vim, 等),只需要直接安装就能工作。

Ctags 以及 Git 钩子

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

聪明地学习标签

只要配置得当,标签是非常有用的。如果您像我一样很容易地忘记事情,标签可以帮助您可视化一个项目。

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

1
functionFood -> functionBreakfast -> functionPancake

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

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

折叠

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

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

手动折叠

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

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

假设有如下文本:

1
2
Fold me
Hold me

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

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

上面的命令分解如下:

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

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

Vim 折叠遵循语法规则。您可以在折叠运算符后,加上一个动作或文本对象。例如,使用 zfap 可以折叠外部段落;使用 zfG 可以折叠至文件末尾;使用 zfa{ 可以折叠 {} 之间的文本。

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

一个没有命令行模式版本的 Vim 操作符是不完整的。在命令行模式下,使用 :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 不再折叠已缩进的行了,因为这些行没有足够的空格。然而,当您改变 '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。如果在一段可折叠文本中,具有另一段折叠级别更高的可折叠文本,则可以具有多个折叠层。

标志折叠

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

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 把 {{{` 和 `}}} 作为指示器。如果您想将指示器更改为其他诸如 “foo1” 或 “foo2” 的字符串,可以运行:

1
:set foldmarker=foo1,foo2

假设有如下文本:

1
2
3
4
5
6
hello

foo1
world
vim
foo2

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

语法折叠

Vim 有一个能够自定义文本语法(高亮、字体粗细、颜色等)的语法系统。本章不会讨论语法系统的工作原理,但您可以使用它来指示要折叠的文本。要使用语法折叠,请运行:

1
:set foldmethod=syntax

假设您有如下文本,并且想折叠方括号里的所有内容:

1
2
3
4
5
[
"one",
"two",
"three"
]

您需要设置正确的语法定义,来捕获方括号之间的字符:

1
:syn region testFold start="\\[" end="\\]" transparent fold

您应该能看到:

1
+-- 5 lines: [ -----

上面的命令分解如下:

  • :syn 是语法命令。
  • region 构造一个可以跨越几行的语法区域。查阅 :h syntax.txt 可以获得更多信息。
  • start="\\[" end="\\]" 定义区域的起始和结束。您需要转义 (\\) 方括号,因为它们是特殊字符。
  • transparent 防止高亮。
  • fold 当语法匹配到起始字符和结束字符时,增加折叠级别。

表达式折叠

表达式折叠允许您定义要匹配折叠的表达式。定义折叠表达式后,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 变量。
  • [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”。
  • ==\\"s\\" 是等式表达式的后半部分,它检查刚才表达式的计算结果是否等于 “s”。如果是,则返回1,否则返回0。在 Vim 的世界里,1代表真,0代表假。所以,那些以 “s” 开头的行,表达式都会返回1。回想一下本节的开始,如果 'foldexpr' 的值为1,则折叠级别为1。

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

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

差异折叠

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

持久化折叠

当关闭 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

在上一章您已经见过 autocommand 了,它用于在事件触发时执行一条命令。现在有两个事件可以用于实现操作:

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

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

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

聪明地学习折叠

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

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

现在,您已经知道如何进行折叠了。让我们来学习下一个:使用 git 进行版本控制。

Git

Vim 和 Git 是两种实现不同功能的伟大工具。Vim 用于文本编辑,Git 用于版本控制。在本章中,您将学习如何将 Vim 和 Git 集成在一起。

差异比较

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

假设您有两个文件,file1.txtfile2.txtfile1.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

diffing-basic.png

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

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

diffing-apples.png

如果您想从另一个缓冲区(orange juice)传输文本来替代当前缓冲区(apple juice),首先使用 ]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 成如下内容,就可以开始了:

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

mergetool-initial.png

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。完成后,就可以保存并退出所有文件(:qall)了。还不错吧?

稍加留意您会发现,现在多了一个 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 中集成 Git,您必须使用插件。以下是 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 取消暂存光标处的文件。
  • > / < 内联显示或隐藏光标处文件的差异变化。

fugitive-git.png

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

Git Blame

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

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

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

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

fugitive-git-blame.png

Gdiffsplit

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

fugitive-gdiffsplit.png

由于您处于 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 的快速修复来完成此任务,因此您可以使用 :cnext:cprevious 来遍历下一个或上一个日志信息。您还可以使用 :copen:cclose 打开或关闭日志列表。

fugitive-git-log.png

"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 非常合适您的工作流(也可能不适合)。总之,我强烈建议您试试上面列出的所有插件。可能还有一些其他的我没有列出来,但适合您工作的就是最好的。

更多地了解 git 可以使您与 Vim-git 集成插件工作得更好。Git 本身是一个很庞大的主题,我只向您展示了它其中很小的一部分。好了,接下来谈谈如何使用 Vim 编译您的代码。

编译

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

从命令行编译

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

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

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

Makefile

在本节中,我将简要介绍一些 makefile 的基础知识。如果您已经知道如何使用 makefile,可以直接跳转到下一部分。在当前目录中,创建一个 makefile 文件,内容是:

1
2
all:
echo "Hello all"

在终端中运行 make 命令:

1
make

您将看到:

1
2
echo "Hello all"
Hello all

终端输出了 echo 命令本身及其输出。您可以在 makefile 中编写多个“目标”。现在我们多添加几个:

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

接着您可以用不同目标运行 make  命令:

1
2
3
4
5
make foo
## returns "Hello foo"

make list_pls
## returns the ls command

除了输出之外,make 还输出了实际命令。要停止输出实际命令,可以在命令开头添加 @

1
2
all:
@echo "Hello all"

现在运行 make,您将仅看到 “Hello all” 而没有 echo "Hello all" 了。

: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
:make foo

:make list_pls

如果命令执行异常,: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!"

makeprg

当您运行 :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(因为您没有指定输出)。让我们重构一下,使用去掉扩展名的原始文件名来命名编译后的输出。运行:

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

上面的命令分解如下:

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

当您在 ./hello.cpp 中运行 :make 时,它将编译为 ./hello。要在 ./hello.cpp 中快速地执行 ./hello,可以运行 :!./%<。同样,它等同于运行 :!./<current-file-name-minus-the-extension>

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

保存时自动编译

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

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 实际上都支持异步作业,但它们超出了本章的范围。如果您好奇,可以查阅 。

插件: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:! 类似。

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

1
:Dispatch rspec %

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

自动调度

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

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

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

聪明地学习编译

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

视图、会话和 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 获得更多信息)。在您的 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 来放置):

session-layout.png

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

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 文件。

请确保您设置了 选项(),否则您的 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,只需加载您的设置,立刻就会感到宾至如归!

许可和版权

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

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