153. VIM 从入门到放弃

教程

  • 本人学习的总原则: 了解功能, 现用现学

    以熟练实用为目标, 以查漏补缺为方法, 达到究竟涅槃不可思议的程度

    以下的顺序即为学习顺序, 分为看过和感觉不错的

学习顺序推荐

  1. learn vim

    • zh

      不全(第25-29章是没有的)

    • en

  2. Vim实用技巧必知必会(极客时间吴咏炜)

  3. Vim参考手册 by Bram Molenaar, 译者: Willis

    一句话: 布莱姆·米勒(荷兰语:Bram Moolenaar,1961年—2023年8月3日)是一名荷兰程序员,在自由软件界素有盛名。他是Vim软件的最初开发者,软件版本管理者,被推崇为终身仁慈独裁者.

感觉不错的

  • 控制台运行 vimtutor 这是 Vim 官方实操教程

  • vim实操教程

    只能当作入门吧!

  • 简明 Vim 练级攻略

    左耳听风值得信赖, 很不错的入门教程

  • Vim Galore

    更新频繁,Vim 进阶必读. 确实东西很多!!!

  • 每日一Vim

    共 30 篇,说是比较全, 但是还达不到系统的程度, 可以用来拓展知识面, 而不应该当作系统知识学习

  • Open Vim

    交互式 Vim 教程

  • QuickRef.ME/vim

    Vim cheatsheet

计数

  • 移动技术
  • d
  • p P
  • 宏命令
  • :normal 命令也支持范围参数。你可以在选择的范围内去执行宏命令。如果你只想在第二行和第三行执行宏命令,你可以执行 :2,3 normal @a

插件

g

first

更多帮助

关于Vim帮助手册有一个额外的小技巧:假设您想了解关于Ctrl-p在插入模式时的功能。如果您仅仅查找:h CTRL-P,您将被导航到普通模式下关于Ctrl-P的介绍页面,这并不是您想要的信息。在这种情况下,改为查找:h i_CTRL-P。添加的i_表示插入模式。注意它属于哪个模式。

Vimrc

配置文件

激活这一配置(:source %)

您的vimrc文件中添加set nocompatible。如果启用compatible选项,许多Vim特有的功能将会被禁止

start

安装

从Vim的官方网站或官方仓库可以获得下载链接:

退出Vim

  • :q

在Vim的好几种模式下都可以通过按<Esc>键切回普通模式

保存文件

  • :w
  • :w filename
  • :wq
  • :q!

帮助

如果您需要查询一些信息,只需要输入:h后接关键词,然后按<Tab>

打开文件

  • 单个文件

    1
    vim hello1.txt
  • 多个文件

    1
    vim hello1.txt hello2.txt hello3.txt

参数

当前版本

终端

1
vim --version
  • ‘+’表示支持的特性
  • ‘-‘表示不支持的特性

内部实现

1
:version

替换

在Vim中,您可以使用:s命令(substitue的缩写)替换文本。如果您想打开hello.txt后立即将所有的”pancake”替换成”bagel”,在终端中:

1
vim +%s/pancake/bagel/g hello.txt

该命令可以被叠加,在终端中:

1
vim +%s/pancake/bagel/g +%s/bagel/egg/g +%s/egg/donut/g hello.txt

Vim会将所有”pancake” 实例替换为”bagel”,然后将所有”bagel”替换为”egg”,然后将所有”egg”替换为”donut”(在后面的章节中您将学到如何替换)。

您同样可以使用c标志来代替+语法,在终端中:

1
2
vim -c %s/pancake/bagel/g hello.txt
vim -c %s/pancake/bagel/g -c %s/bagel/egg/g -c %s/egg/donut/g hello.txt

打开多个窗口

使用oO选项使Vim打开后分别显示为水平或垂直分割的窗口

若想将Vim打开为2个水平分割的窗口,在终端中运行:

1
vim -o2

若想将Vim打开为5个水平分割的窗口,在终端中运行:

1
vim -o5

若想将Vim打开为5个水平分割的窗口,并使前两个窗口显示hello1.txthello2.txt的内容,在终端中运行:

1
vim -o5 hello1.txt hello2.txt

若想将Vim打开为2个垂直分割的窗口、5个垂直分割的窗口、5个垂直分割窗口并显示2个文件,在终端中分别运行以下命令:

1
2
3
vim -O2
vim -O5
vim -O5 hello1.txt hello2.txt

挂起

如果您编辑时想将Vim挂起,您可以按下Ctrl-z。同样,您也可以使用:stop:suspend命令达到相同的效果。若想从挂起状态返回,在终端中运行fg命令。

聪明的启动Vim

您可以向vim命令传递不同的选项(option)和标志(flag),就像其他终端命令一样。其中一个选项是命令行命令(+{cmd}-c cmd)。当您读完本教程学到更多命令后,看看您是否能将相应命令应用到Vim的启动中。同样,作为一个终端命令,您可以将vim命令和其他终端命令联合起来。比如,您可以将ls命令的输出重定向到Vim中编辑,命令是ls -l | vim -

缓冲区(Buffers),窗口(Windows)和选项卡(Tabs)

确保您的vimrc文件中开启了set hidden

Buffers

buffer就是内存中的一块空间,您可以在这里写入或编辑文本。当您在Vim中打开一个文件时,文件的数据就与一个buffer绑定。当您在Vim中打开3个文件,您就有3个buffers。

运行:buffers命令可以查看所有的buffers(另外,您也可以使用:ls:files命令)

遍历所有buffers

  • :bnext 切换至下一个buffer(:bprevious切换至前一个buffer)。
  • :buffer + 文件名。(按下<Tab>键Vim会自动补全文件名)。
  • :buffer + n, n是buffer的编号。比如,输入:buffer 2将使您切换到buffer #2。
  • 按下Ctrl-O将跳转至跳转列表中旧的位置,对应的,按下Ctrl-I将跳转至跳转列表中新的位置。这并不是属于buffer的特有方法,但它可以用来在不同的buffers中跳转。我将在第5章详细讲述关于跳转的知识。
  • 按下Ctrl-^跳转至先前编辑过的buffer。

删除buffer

  • :bdelete
  • 接收一个buffer编号(:bdelete 3将删除buffer #3)一个文件名(:bdelete然后按<Tab>自动补全文件名)。

仅保留当前 buffer

%bd | e#

退出Vim

顺带说一句,如果您已经打开了多个buffers,您可以使用quit -all来关闭所有的buffers:

1
:qall

如果您想关闭所有buffers但不保存,仅需要在后面加!(叹号)就行了:

1
:qall!

若要保存所有buffers然后退出,请运行:

1
:wqall

Windows

一个window就是在buffer上的一个视口

在Vim中,您同样可以拥有多个窗口。

先前我说过,您看到的是file1.js的buffer。但这个说法并不完整,现在这句话得更正一下,您看到的是file1.js 的buffer通过 一个窗口 显示出来。窗口就是您查看的buffer所使用的视口。

如果您想在窗口之间导航,使用这些快捷键:

1
2
3
4
Ctrl-W H    移动光标到左边的窗口
Ctrl-W J 移动光标到下面的窗口
Ctrl-W K 移动光标到上面的窗口
Ctrl-W L 移动光标到右边的窗口

多窗口

  • split: 垂直排列

    1
    :split file2.js
  • vsplit: 水平排列

    1
    :split file2.js
  • 多个窗口显示同一个buffer

    1
    :buffer file2.js
1
2
3
:vsplit filename    垂直分割当前窗口,并在新窗口中打开名为filename的文件。
:split filename 水平分割当前窗口,并在新窗口中打开名为filename的文件。
:new filename 创建一个新窗口并打开名为filename的文件。

普通命令

1
2
3
4
Ctrl-W V    打开一个新的垂直分割的窗口
Ctrl-W S 打开一个新的水平分割的窗口
Ctrl-W C 关闭一个窗口
Ctrl-W O 除了当前窗口,关闭所有其他的窗口

关闭窗口

您可以按Ctrl-W C或输入:quit。当您关闭一个窗口后,buffers仍然会在列表中。

上下窗口最大化

1
ctrl+w+shift+_

上下窗口对等大小

1
ctrl+w+=

左右窗口最大化

1
ctrl+w+shift+|

左右窗口对等

1
ctrl+w+=

上下窗口大小微调

1
ctrl+w+-
1
ctrl+w+[num]+-
1
ctrl+w+shift+[+]
1
ctrl+w+[num]+shift+[+]

左右窗口大小微调

1
ctrl+w+shift+>
1
ctrl+w+[num]+shift+>
1
ctrl+w+shift+<
1
ctrl+w+[num]+shift+<

窗口对调

1
ctrl+w+x

窗口必须是对等的才行

窗口轮转

1
ctrl+w+r/R

窗口必须是对等的才行

移动所在窗口位置

1
ctrl+w+HJKL

不同窗口跳转

1
crtl+w+hjkl
1
crtl+w、ctrl+w

Tabs

Tabs就是windows的集合

功能

拥有多个tabs的好处是,您可以在不同的tab中使用不同的窗口布局。也许,您想让您的第1个tab包含3个垂直分割的窗口,然后让第2个tab为水平分割和垂直分割混合的窗口布局。tab是完成这件工作的完美工具!

tab启动

  • 导航

    1
    2
    3
    4
    5
    6
    :tabnew file.txt    在tab中打开一个文件
    :tabclose 关闭当前tab
    :tabnext 切换至下一个tab
    :tabprevious 切换至前一个tab
    :tablast 切换至最后一个tab
    :tabfirst 切换至第一个tab
  • gt切换到下一个标签页(对应的,可以用gT切换到前一个标签页)。您也可以传递一个数字作为参数给gt,这个数字是tab的编号。若想切换到第3个tab,输入3gt

  • 启动

    1
    vim -p file1.js file2.js file3.js

三维移动

窗口

就像在笛卡尔坐标系的二维平面上沿着X-Y轴移动

使用Ctrl-W H/J/K/L移动到上面、右侧、下面、以及左侧的窗口

buffer

在笛卡尔坐标系的Z轴上穿梭,想象您的buffer文件在Z轴上呈线性排列

使用:bnextbprevious在Z轴上一次一个buffer地遍历。您也可以使用:buffer 文件名/buffer编号在Z轴上跳转到任意坐标

总结

结合window和buffer的移动,您可以在 三维空间 中移动

可以使用window导航命令移动到上面、右侧、下面、或左侧的窗口(X-Y平面导航)

因为每个window都可能包含了多个buffers,您可以使用buffer移动命令向前、向后移动(Z轴导航)

使用

  • 首先,对于==某个特定任务==,我先使用buffers存储所有需要的文件。Vim就算打开很多buffer,速度一般也不会减慢。另外打开多个buffers并不会使我的屏幕变得拥挤。我始终只会看到1个buffer(假设我只有1个window),这可以让我注意力集中在1个屏幕上。当我需要使用其他文件时,可以快速切换至对应文件的buffer。
  • 当==比对文件、读文档、或追踪代码流==时,我使用==多窗口来一次查看多个buffers==。我尽量保持屏幕上的==窗口数不超过3个==,因为超过3个屏幕将变得拥挤(我使用的是小型笔记本)。当相应工作完成后,我就关掉多余的窗口。窗口越少可以使注意力更集中。
  • 我使用tmuxwindows来代替tabs。通常一次使用多个tmux窗口。比如,一个tmux窗口用来写客户端代码,一个用来写后台代码。

打开和搜索文件

打开和编辑文件

Vim中打开一个文件使用:edit

1
:edit file.txt
  • 如果file.txt已经存在,就会打开file.txtbuffer。
  • 如果file.txt不存在,会创建一个新buffer名为file.txt

注意

  • :edit命令支持使用<Tab>进行自动补全

  • :edit可以接收通配符参数。*匹配当前目录下的任意文件

    • :edit *.yml<Tab>: Vim将列出当前目录下所有.yml文件供您选择。

    • 可以使用**进行递归的搜索

      • 查找当前项目文件夹下所有*.md文件,但您不知道在哪个目录,您可以这样做

        1
        :edit **/*.md<Tab>
  • :edit可以用于运行netrw(Vim的内置文件浏览器)

    • 使用:给:edit一个目录参数而不是文件名就行了

    • 示例

      1
      2
      :edit .
      :edit test/unit/

使用find命令搜索文件

搜索文件

使用:find命令搜索文件

1
2
:find package.json
:find app/controllers/users_controller.rb

:find命令同样支持自动补全

1
2
:find p<Tab>                " to find package.json
:find a<Tab>c<Tab>u<Tab> " to find app/controllers/users_controller.rb

Find 和 Path

区别

  • :find命令根据path选项配置的路径查找文件
  • :edit不会

查看一下您的path是什么

1
:set path?

默认情况下,您的path内容很可能是这样的:

1
path=.,/usr/include,,
  • . 意思是在当前文件所在目录下搜索。(译者注:注意不是命令行输入pwd返回的当前目录,而是 当前所打开的文件 所在的目录)
  • , means to search in the current directory.(译者注:此处貌似作者有点小错误,逗号,应该是表示路径之间的分割符。连续的两个,,(两个逗号之间为空)才表示当前目录)
  • /usr/include 表示在C编译器头文件目录下搜索。

app/controllers/添加到当前path选项。以下是操作步骤

1
:set path+=app/controllers/

现在您的路径已经更新,当您输入:find u<Tab>时,Vim将会在app/controllers/目录内搜索所有以”u”开头的文件。

如果您有一个嵌套的目录controllers/,比如app/controllers/account/users_controller.rb,Vim就找不到users_controllers了。您必须改为添加:set path+=app/controllers/**

将整个项目文件夹添加到path中,这样当您按<Tab>,Vim将在所有文件夹内搜索您要找的文件,

1
:set path+=$PWD/**
  • 建议仅仅将您最常访问的文件/目录添加到path
  • 您可以将set path+={您需要添加的目录}添加到您的vimrc文件中。更新path仅花费几秒钟,但可以为您的工作节省很多时间。

使用Grep命令在文件中搜索

如果您想在文件内部查找(搜索文件中的词句),您可以使用grep。Vim有两个方法可以完成这个工作:

  • 内置grep (:vim。没错,就是:vim,它是:vimgrep的简写)。
  • 外部grep (:grep)。

内置grep

:vim有以下语法:

1
:vim /pattern/ file
  • /pattern/ 是您要搜索的内容的正则表达式。
  • file 是文件参数。您可以传入多个参数。Vim将在文件中搜索所有匹配正则表达式的内容。类似于:find,您可以传入*和**通配符。

示例

比如,要在app/controllers/目录下所有ruby文件(.rb)中,查找所有的”breakfast”字符串:

1
:vim /breakfast/ app/controllers/**/*.rb

输入上面的命令后,您将会被导航到第一个结果。Vim的vim搜索命令使用quickfix进行处理。要查看所有搜索结果,运行:copen会打开一个quickfix窗口。下面有一些有用的quickfix命令,可以让您提高效率:

1
2
3
4
5
6
:copen        打开quickfix窗口
:cclose 关闭quickfix窗口
:cnext 跳到下一个错误
:cprevious 跳到前一个错误
:colder 跳到旧的错误列表
:cnewer 跳到新的错误列表

缺点:慢,如果匹配结果数量较多时系统速度会变慢。这是因为Vim将每一个搜索结果读入内存。Vim加载每一个匹配的文件就像它们被编辑一样。如果Vim查到大量文件,它将消耗很多内存

外置grep

默认情况下,它使用终端命令grep。要想在app/controllers/目录中搜索字符串”lunch”,您可以这样做:

1
:grep -R "lunch" app/controllers/

注意这里不是使用/pattern/,它遵循的是终端grep的语法"pattern",它同样使用’quickfix’来显示所有的匹配结果。

Vim使用grepprg变量来决定运行:grep时,应该使用哪个外部程序。所以您并不是非得使用终端的grep命令。稍后我将为您演示如何改变外部grep程序的默认值。

用Netrw浏览文件

NERDTree 是一个很好的选择

Fzf插件

第4章 Vim 语法

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

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

如何学习一门语言

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

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

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

语法规则

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

1
verb + noun # 动词 + 名词

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

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

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

名词(动作 Motion)

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

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

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

动词(操作符 Operator)

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

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

顺带说一句,当你yank一段文本后,您可以使用p将它粘贴到光标后,或使用P粘贴到光标前。

动词(操作符 Operator)和名词(动作 motions)的结合

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

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

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

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

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

作为补充,行级的 操作符 operations (作用在整行中的操作符)在文本编辑中和其他的 操作符 一样,Vim允许你通过按两次 操作符使它执行行级的操作,例如ddyycc来执行删除,复制或修改整个行。您可以使用其他operations试一下(比如gUgU)。

666!从这可以看出Vim命令的一种执行模式。但是到目前为止还没有结束,Vim有另一种类型的名词:文本对象(text object)

更多名词(文本对象 Text Objects)

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

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

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

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

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

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

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

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

文本对象很强大因为你可以在同一个位置指向不同的内容,可以删除一对小括号中的文本,也可以是当前大括号中的函数体,也可以是当前单词。这一点也很好记忆,当你看到di(di{diw时,你也可以很好的意识到他们表示的是什么:小括号,大括号,单词。

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

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

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

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

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

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

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

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

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

结合性和语法

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

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

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

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

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

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

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

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

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

结果如下:

1
2
02  Kitten  Ok
03 Bunny Ok

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

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

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

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

甚至,动作操作符都是可拓展的,你可以自己创造动作操作符去丰富你的Vim工具箱,Vim-textobj-user插件允许你创建自己的文本对象,同时包含有一系列定义好的文本对象

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

聪明地学习语法

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

1
verb + noun

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

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

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

第5章 在文件中移动

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

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

字符导航

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

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

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

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

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

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

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

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

相对行号

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

1
set relativenumber number

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

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

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

对移动计数

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

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

1
[计数] + 移动

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

单词导航

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

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

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

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

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

1
const hello = "world";

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

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

当前行导航

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

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

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

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

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

回到上一个例子:

1
const hello = "world";

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

句子和段落导航

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

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

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

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

1
2
3
4
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的设置。

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

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

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

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

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

Hello again.

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

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

匹配导航

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

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

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

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

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

行号导航

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

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

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

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

窗格导航

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

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

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

滚动

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

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

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

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

搜索导航

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

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

假设你有一下文本:

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

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

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

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

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

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

位置标记

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

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

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

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

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

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

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

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

跳转

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

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

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

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

聪明地学习导航

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

我想,最好的开始的办法就是从一些少量的必要的移动开始记。 我推荐你从h,j,k,l,w,b,G,/,?,n开始,不断地重复这10个移动知道形成肌肉记忆,这花不了多少时间。

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

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

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

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

第6章 输入模式

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

尽管如此,这并不代表输入模式没什么好学的。Vim的输入模式包含许多有用功能。在这一章节中,你将能够学到如何利用Vim输入模式中的特性来提升你的输入效率。

进入输入模式的方法

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

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

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

退出输入模式的方法

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

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

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

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

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

重复输入模式

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

1
10i

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

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

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

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

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

用寄存器进行输入

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

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

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

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

1
Ctrl-r a

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

页面滚动

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

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

自动补全

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

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

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

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

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

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

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

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

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

执行普通模式下的命令

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

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

设置居中以及跳转

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

重复文本

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

执行终端命令

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

快速删除

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

聪明地学习输入模式

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

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

第7章 点命令

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

用法

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

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

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

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

什么才算是修改操作?

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

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

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

1
pancake, potatoes, fruit-juice,

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

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

1
pancake, potatoes, fruit-juice,

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

在Vim里,修改操作是不包括移动(motions)的,因为移动(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)开始到你退出输入模式()之间的所有输入都算作是一整个修改操作。

重复多行修改操作

假设你有如下的文本:

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

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

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

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

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";

其实还有更快的方法来完成整个操作。当你使用/let搜索后,执行cgnconst<Esc>,然后. . .

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

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

(译者在这里研究了一会,并做了不少实验,总结规律是:单独的motion(第4章中所说的名词)不算修改操作,而opeartor(动词)+motion(名词)时(请回顾第4章),motion被视为一个完整的修改操作中的一部分。再看一个例子,看看/命令是如何被包含在一个修改操作中的:

1
2
3
4
5
6
7
8
a
b
foo
c
d
foo
e
f

假设你的光标在第一行的a上,执行命令d/foo<Esc>,Vim会删除a,b。然后.,Vim会删除foo, c, d,再按.,Vim什么也不做,因为后面没有”foo”了。在这个例子中,/foo是一个motion(名词),是Vim语法(动词+名词:operator + motion)的一部分,前面的d则是动词。d/foo<Esc>这条命令的功能是:从当前光标所在位置开始删除,直到遇到”foo”为止。后面的点命令就重复这个功能,第二次按.之所以Vim什么也不做,是因为找不到下一个匹配了,所以这条命令就失效了。

聪明地学习点命令

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

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

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

第8章 寄存器

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

你可能已经在复制或删除文本并用pP粘贴它们到别处的时候使用过Vim的寄存器了。但是,你知道Vim总共有10种不同类型的寄存器吗?如果正确地使用Vim寄存器,将帮助您从重复的输入中解放出来。

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

寄存器的10种类型

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

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

寄存器命令

要使用寄存器,您必须先使用命令将内容存储到寄存器,以下是一些存值到寄存器中的操作:

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

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

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

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

pP都可以接受计数和一个寄存器标志作为参数。比如,想要把最近复制的文本粘贴10次的话可以用10p。想粘贴寄存器”a”中的文本,可以用"ap。想将寄存器“a”中的文本粘贴10次的话,可以使用10"ap。注意,从技术层面讲,命令p实际上表示的是”put”(放置),而不是”paste”(粘贴),使用粘贴只是因为它更符合传统习惯。

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

在输入模式中使用寄存器

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

1
Ctrl-r x

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

匿名寄存器("")

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

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

编号寄存器("0-9)

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

复制寄存器 ("0)

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

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

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

比如,如果你:

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

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

如果你:

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

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

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

编号寄存器 ("1-9)

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

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

1
2
3
line three
line two
line one

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

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

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

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

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

小删除寄存器("-)

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

比如:

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

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

另一个例子:

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

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

命名寄存器 ("a-z)

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

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

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

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

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

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

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

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

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

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

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

表达式寄存器 ("=)

Vim有一个表达式寄存器,"=,用于计算表达式的结果。

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

1
"=1+1<Enter>p

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

1
Ctrl-r =1+1

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

1
"=@a

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

1
Ctrl-r =@a

表达式是Vim中非常宏大的一个话题,所以我只会在这里介绍一些基础知识,我将会在之后的VimScript章节中进一步讲解更多关于表达式的细节。

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

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

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

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

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

1
set clipboard=unnamed

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

黑洞寄存器 ("_)

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

你可以使用黑洞寄存器("_)。想要删除一行并且不将其存储在任何寄存器中时,可以使用"_dd命令.

它是和 /dev/null 类似的寄存器。

搜索模式寄存器 ("/)

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

查看所有的寄存器

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

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

执行寄存器

命名寄存器不只可以用来存放文本,你还可以借助@来执行宏命令。我会在下一章节中介绍宏命令。

注意,因为宏命令时存储在Vim寄存器中的,使用宏时可能会覆盖存储的内容。如果你将文本”Hello Vim”存放在寄存器”a”中,并且之后你在同一个寄存器里记录了一个宏命令 (qa{macro-commands}q),那么这个宏命令将会覆盖之前存储的文本”Hello Vim”(你可以使用@a来执行寄存器中存储的宏命令)。

清除寄存器

从技术上来说,我们没有必要来清除任何寄存器,因为你下一个使用来存储文本的寄存器会自动覆盖该寄存器中之前的内容。然而,你可以通过记录一个空的宏命令来快速地清除任何命名寄存器。比如,如果你运行qaq,Vim就会在寄存器”a”中记录一个空的宏命令。

还有一种方法就是运行命令:call setreg('a','hello register a'),其中’a’代表的就是寄存器”a”。而”hello register a”就是你想存储的内容。

还有一种清除寄存器的方法就是使用表达式:let @a = ''来将寄存器”a 的值设为空的字符串。

获取寄存器中的内容

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

因为:put是一个命令行命令,您可以传一个地址给它。:10put a将会在当前光标下数10行,然后插入新行,内容为寄存器a中的内容。

一个很酷的技巧是将黑洞寄存器("_)传给:put命令。因为黑洞寄存器不保存任何值,:put _命令将插入一个新的空白行。您可将这个与全局命令联合起来,插入多个空行。比如,要在所有以文本”end”结尾的行下插入空行,使用:g/end/put _。在后面您将了解关于全局命令的知识。

聪明地学习寄存器

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

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

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

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

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

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

第9章 宏命令

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

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

基本宏命令

宏命令的基本语法如下:

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

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

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

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

1
2
3
4
5
hello
vim
macros
are
awesome

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

1
qa0gU$jq

上面命令的分解如下:

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

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

安全保护

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

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

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

1
qa0W~jq

上面命令的分解如下:

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

我喜欢对宏命令进行超过所需次数的调用,所以我通常使用 99@a 命令去执行该宏命令99次。使用该命令,Vim并不会真正执行这个宏99次,当 Vim 到达最后一行执行j时,它会发现无法再向下了,然后会抛出一个错误,并终止宏命令的执行。

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

命令行执行宏

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

:normal 命令也支持范围参数。你可以在选择的范围内去执行宏命令。如果你只想在第二行和第三行执行宏命令,你可以执行 :2,3 normal @a

在多个文件中执行宏命令

假如你有多个 .txt 文件,每一个文件包含不同的内容。并且你只想将包含有 “donut” 单词的行的第一个单词变为大写。假设,您的寄存器a中存储的内容是0W~j(就是前面例子中用到的宏命令),那么,您该如何快速完成这个操作呢?

第一个文件:

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

第二个文件:

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

第三个文件:

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

你可以这么做:

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

也许你对全局命令 :g/donut/normal @a 不是很了解,该命令会执行 /donut/搜索命令,然后在所有匹配的行中执行normal @a 命令。我会在后面的章节中介绍全局命令。

递归执行宏命令

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

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

如下命令会递归地执行:

1
qaqqa0W~j@aq

上面命令的分解如下:

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

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

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

增添一个已知宏

如果你想在一个已经录制好的宏定义中添加更多的操作,与其重新录入它,不如选择修改它。在寄存器一章中,你学习了如何使用一个已知寄存器的大写字母来想该寄存器中添加内容。同样的,为了在寄存器”a”中添加更多的操作,你也可以使用大写字母”A”。

假设寄存器a中已经存储了这个宏命令:qa0W~q(该宏命令将某行的第二个词组的头一个字母执行改变大小写操作),假设你想在这个基础上添加一些操作命令序列,使得每一行末尾添加一个句点,运行:

1
qAA.<esc>q

分解如下:

  • qA 开始在寄存器 “A” 中记录宏命令。
  • A.<esc> 在行的末尾加上一个句点(这里的A是进入插入模式,不要和宏A搞混淆),然后退出插入模式。
  • q 停止记录宏命令。

现在,当你执行@a时,它不仅将第二个词组的首字母转变大小写,同时还在行尾添加一个句点。

修改一个已知宏

如果想在一个宏的中间添加新的操作该怎么办呢?

假设您在寄存器a中已经存有一个宏命令0W~A.<Esc>,即改变首字母大小写,并在行尾添加句号。如果您想在改变首字母大小写和行尾添加句号之间,在单词”dount”前面加入”deep fried”。(因为唯一比甜甜圈好的东西就是炸甜甜圈)。

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

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

首先,让我们通过 :put a 调用一个已经录制好的宏命令(假设你上一节中保存在寄存器a中的宏命令还在):

1
0W~A.^[

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

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

完整的命令如下:

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

这里有一个问题,Vim 不能理解 <esc>。您不能依葫芦画瓢输入”“,所以你需要将<Esc>写成内部代码的形式。在插入模式,在按下<esc>后按下 Ctrl-v,Vim 会打印 ^[。 ==Ctrl-v 是一个插入模式的操作符,可以逐字地插入一个非数字字符==。你的宏命令应该如下:

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

为了在寄存器“a”中添加修改后的指令,你可以通过在一个已知命名寄存器中添加一个新条目的方式来实现。在一行的行首,执行 "ay$,使用寄存器 “a”来存储复制的文本。

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

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

拷贝宏

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

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

串行宏和并行宏

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

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

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

1
qa0f{gui{jq

分解如下:

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

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

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

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

你可以并行地执行宏命令。

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

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

聪明地学习宏命令

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

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

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

去寻找最适合你的方法吧。

第10章 撤销

所有人都会犯各种各样的输入错误。因此对于任何一个现代的软件来说,撤销都是一个很基本的功能。 Vim 的撤销系统不仅支持撤销和取消撤销任何修改,而且支持存取不同的文本形态,让你能控制你输入的所有文本。在本章中,你将会学会如何执行撤销和 取消撤销文本,浏览撤销分支,反复撤销, 以及浏览改动时间线。

撤销(undo),重做和行撤销(UNDO)

对于一个基本的 undo 操作,你可以执行 u 或者 :undo

假设你有如下文本(注意”one”下面有一个空行):

1
2
one

然后添加另一个文本:

1
2
one
two

如果你执行 u,Vim 会删除 “two”。

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

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

Vim 也有另一个命令 U 可以实现 行撤销 (UNDO) 的功能,执行这个命令会撤销所有最新的修改。

那么,Uu 的区别是什么呢?首先,U 会删除 最近修改的行中所有的 的修改,而 u 一次仅删除一次修改。 其次,执行u 不会被算作一次修改操作,而执行 U 则会被算作一次修改。

让我们会的之前的例子:

1
2
one
two

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

1
2
one
three

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

1
2
one
four

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

1
2
one

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

就我个人而言,我几乎不会使用 U,因为很难记住文本最初的样子。(我几乎不使用它)

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

断点插入操作

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

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

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

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

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

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

撤销树

Vim 将每一次修改存储在一个撤销树中。你打开一个空白文件,然后添加一段新文本:

1
2
one

再插入一段新文本:

1
2
one
two

undo一次:

1
2
one

插入一段不同的话:

1
2
one
three

再次 undo

1
2
one

再次插入另一段话:

1
2
one
four

现在如果你执行 undo,您将丢失刚刚添加的文本 “four” :

1
2
one

如果你再次执行 undo 操作:

1

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

1
2
one

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

1
2
one
two

让我们继续执行 g+:

1
2
one
three

再一次执行 g+ :

1
2
one
four

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

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

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

保存撤销记录

当你通过 Vim 打开一个文件,并且立即按下 u,Vim 很可能会显示 “Already at oldest change” 的警告。

要想从最近的一次编辑工作中(在vim关闭文件再打开,算做一次新的编辑工作),在撤销历史中回滚,可以通过 :wundo命令使Vim 保存一份你的 undo 历史记录。

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

1
one

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

1
2
one
two

插入新的一行:

1
2
3
one
two
three

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

1
:wundo! mynumbers.undo

退出 Vim。

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

1
:rundo mynumbers.undo

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

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

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

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

时间旅行

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

假如有如下文本:

1
2
one

之后你输入了另一行:

1
2
one
two

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

1
:earlier 10s

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

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

另外,它同样接受一个计数作为参数,告诉vim恢复到老状态的次数。比如,如果运行:earlier 2,Vim将恢复到2次修改前的状态。功能类似于执行g-两次。同样,你可以运行:earlier 10f命令告诉vim恢复到10次保存前的状态。

这些参数同样作用于:earlier命令的对应版本::later

1
2
3
4
5
6
:later 10s    恢复到10秒后的状态
:later 10m 恢复到10分钟后的状
:later 10h 恢复到10小时后的状
:later 10d 恢复到10天后的状态
:later 10 恢复到新状态10次
:later 10f 恢复到10次保存后的状态

聪明地学习撤销操作

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

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

第11章 可视模式

高亮显示文本块并对其进行更改,是很多文本编辑器中的常见功能。 Vim也可以使用可视模式实现这一功能。在本章中,您将学习如何使用可视模式来有效地处理文本块。

三种可视模式

Vim有三种可视模式,分别是:

1
2
3
v         逐字符可视模式
V 逐行可视模式
Ctrl-v 逐块可视模式

如果您有下列文字:

1
2
3
one
two
three

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

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

逐块可视模式适用于行和列。与其他两种模式相比,它为您提供了更大的移动自由度。按Ctrl-V,Vim像逐字符可视模式一样高亮显示光标下的字符,但向下移动时,除非光标已经在行尾,否则不会高亮显示光标上方的整行,它跳转至下一行时高亮显示尽可能少的字符。尝试用h/j/k/l移动,并观察光标的移动。

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

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

有三种退出可视模式的方法:escCtrl-C和与当前可视模式相同的键。后者的意思是,如果您当前处于逐行可视模式(V),则可以通过再次按V退出它。如果您处于字符可视模式,则可以通过按v退出它。如果您处于逐块可视模式,请按Ctrl-V

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

1
gv    转到上一个可视模式

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

可视模式导航

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

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

1
2
3
one
two
three

这次让我们从”two”行开始。按v进入字符可视模式(这里的方括号[]表示高亮显示的字符):

1
2
3
one
[t]wo
three

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

1
2
3
one
[two
t]hree

假设您刚刚意识到还需要高亮显示”one”行,因此按k。令您沮丧的是,它现在排除了”three”高亮。

1
2
3
one
[t]wo
three

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

答案是肯定的。让我们先恢复光标到高亮显示”two”和”three”行的位置。

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

高亮区域跟随光标移动。如果要将其向上扩展到行”one”,则需要将光标移动到”two”,现在您的光标在”three”行上。这时可以用oO切换光标位置。

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

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

1
2
3
[one
two
t]hree

在可视模式中使用oO,光标会在高亮选择区域的开头和结尾跳转,以便与您扩展高亮区域。

可视模式语法

可视模式与普通模式使用相同的操作符(operations)。

例如,如果您有以下文字,然后您想在可视模式中删除前两行:

1
2
3
one
two
three

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

1
2
3
[one
two]
three

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

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

1
2
3
[one
two]
three

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

您可以使用此行为在markdown文本中快速创建标题。假设您需要快速下面的文本转换为一级markdown标题(“===”):

1
Chapter One

首先,您使用yy复制文本,然后使用p粘贴文本:

1
2
Chapter One
Chapter One

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

1
2
Chapter One
[Chapter One]

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

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

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

可视模式和Ex命令

您可以有选择地在高亮显示的文本块上应用Ex命令。如果您具有以下表达式,并想将前两行的”const”替换为”let”:

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

任意 可视模式高亮显示前两行,然后运行替换命令:s/const/let/g

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

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

跨多行编辑

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

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

将光标放在第一行上:

  • 进入逐块可视模式,并向下两行(Ctrl-V jj)。
  • 高亮显示到行尾($)。
  • 按(A) ,然后键入”;”。
  • 退出可视模式(esc)。

您应该看到在每一行后面附加的 “;”。666! 有两种方法可以从逐块可视模式进入输入模式:可以使用A在光标后输入文本,也可以使用I在光标前输入文本。请勿将它们与普通模式下的AI混淆。(普通模式中,A表示在行尾添加内容,I表示在行首非空字符前插入内容)。

另外,您也可以使用:normal命令在多行添加内容:
-高亮显示所有3行(vjj)。
-输入:normal! A;

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

递增数字

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

如果您具有以下HTML元素:

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

有多个具有相同名称的id是一个不好的做法,因此让我们对其进行递增以使其唯一:

  • 将光标移动到 第二行的 “1”。
  • 启动逐块可视模式,并向下移动3行(Ctrl-V 3j)。这高亮显示剩余的”1”,现在除了第一行,所有的”1”应该已经高亮。
  • 运行g Ctrl-A

您应该看到以下结果:

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

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

1
:set nrformats+=alpha

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

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

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

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

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

前面章节中我提到了gv可以快速高亮显示上一个可视模式选择的内容。您还可以使用以下两个特殊标记转到最后一个可视模式的开始和结束位置:

1
2
'<    转到上一个可视模式高亮显示的第一个位置(行)(译者注,英文原版中'<'前面的符号是`,但这应该是一个错误,应该是单引号')
'> 转到上一个可视模式高亮显示的最后位置(行)

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

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

您实际上是在使用('<, '>) 标记作为范围来执行 s/const/let/g命令。这太有趣了!

您随时可以随时编辑这些标记。比如,如果您需要从高亮显示的文本的开头到文件的末尾进行替换,则只需将命令行更改为:

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

从插入模式进入可视模式

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

1
Ctrl-O v

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

选择模式

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

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

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

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

将此行为与可视模式进行对比:如果您使用逐行可视模式(V)高亮显示一行文本并键入”y”,则高亮显示的文本不会被删除且被字母”y”代替,而是仅将其复制(yank)。在选择模式中,你不能执行对高亮文本执行普通模式的命令。

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

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

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

如果发现使用可视模式操作的频率比正常模式操作的频率高得多,请当心。==我认为这是一种反模式。运行可视模式操作所需的击键次数要多于普通模式下的击键次数。==假设您需要删除一个内部单词(inner word,请回顾前面的文本对象),如果可以只用三个按键(diw),为什么要使用四个按键viwd(先v进入可视模式,然后iw高亮一个内部单词,最后d删除)呢?前者更为直接和简洁。当然,有时使用可视模式是合适的,但总的来说,更倾向于直接的方法。

第12章 搜索和替换

本章涵盖两个独立但相关的概念:搜索和替代。很多时候,您得基于文本的共同模式搜索大量的内容。通过学习如何在搜索和替换中使用正则表达式而不是字面字符串,您将能够快速定位任何文本。

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

智能区分大小写

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

但是,有时您需要搜索特定大小写的短语。一种方法是用 set noignorecase 关闭ignorecase选项,但是每次需要搜索区分大小写的短语时,都得反复地打开和关闭这个选项。

为避免反复开关ignorecase选项,Vim 有一个smartcase选项。您可以将ignorecasesmartcase选项结合起来,当您输入的搜索词全部是小写时,进行大小写不敏感搜索;而当搜索词 至少有1个大写字母时,进行大小写敏感搜索。

在您的 vimrc 中,添加:

1
set ignorecase smartcase

如果您有这些文字:

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

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

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

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

如果您有以下文字:

1
hello hello

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

如果您有以下文字:

1
hello hello friend

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

重复搜索

您可以使用//重复上一个搜索。如果您只是搜索/hello,则运行//等同于运行/hello。此快捷键可以为您节省一些按键操作,尤其是在您刚搜索了一个很长的字符串的情况下。另外,回想一下前面的章节,您还可以使用nN分别以相同方向和相反方向重复上一次搜索。

如果您想快速回忆起 第n个最近使用的搜索字怎么办?您可以先按/,然后按up/down方向键(或Ctrl-N/Ctrl-P),快速遍历搜索历史,直到找到所需的搜索词。要查看所有搜索历史,可以运行:history /

在搜索过程中到达文件末尾时,Vim 会抛出一个错误:"搜索到达底部,未找到匹配项:{your-search}"("Search hit the BOTTOM without match for: {your-search}")。有时这个特性能成为一个安全守卫,可以防止过度搜索,但是有时您又想将搜索重新循环到顶部。您可以使用set wrapscan选项使 Vim 在到达文件末尾时回到文件顶部进行搜索。要关闭此功能,请执行set nowrapscan

使用候选词搜索

一次搜索多个单词属于日常操作。 如果您需要搜索”hello vim”或”hola vim”,而不是”salve vim”或”bonjour vim”,则可以使用|或运算符。

给予这样一段文本:

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

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

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

设置模式匹配的开始位置和结束位置

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

1
2
3
4
11vim22
vim22
11vim
vim

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

1
/11\zsvim\ze22

Vim仍然会匹配整个模式”11vim22”,但是仅高亮显示介于\zs\ze之间的内容。 另一个例子:

1
2
foobar
foobaz

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

1
/foo\zebaz

搜索字符组

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

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

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

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

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

搜索重复字符

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

1
2
3
1aa
11a
111

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

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

1
{n,m}

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

这是count语法的四种不同变体:

  • {n}是精确匹配。 /[0-9]\{2\}匹配两个数字:”11”,以及”111”中的”11”。
  • {n,m}是范围匹配。 /[0-9]\{2,3\}匹配 2 到 3 位数字:”11”和”111”。
  • {,m}是上限匹配。 /[0-9]\{,3\}匹配最多 3 个数字:”1”,”11”和”111”。
  • {n,}是下限匹配。 /[0-9]\{2,\}匹配最少 2 个或多个数字:”11”和”111”。

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

预定义的字符组

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

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

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

搜索示例:在一对相似字符之间捕获文本

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

1
"Vim is awesome!"

运行这个:

1
`/"[^"]\+"`

让我们分解一下:

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

当看到第一个"时,它开始模式捕获。Vim 在一行中看到第二个双引号时,它匹配第二个"模式并停止模式捕获。同时,两个双引号之间的所有非双引号字符都被[^"]\+ 模式捕获,在这个例子中是短语”Vim is awesome!”。这是一个通用模式(其实就是正则表达式)用于捕获 由一对类似的定界符包围的短语

  • 要捕获被单引号包围的短语,你可以使用/'[^']\+'
  • 要捕获为0包围的短语,你可以使用/0[^0]\+0

搜索示例:捕获电话号码

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

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

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

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

为==避免转义,可使用\v==:

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

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

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

基本替换

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

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

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

1
vim is good

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

1
vim is awesome

重复最后一次替换

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

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

替换范围

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

1
:[range]s/old/new/

如果您有以下表达式:

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

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

1
:3,5s/let/const/

下面是一些你可以使用的范围参数的变体:

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

在 Vim 中,%通常表示整个文件。 如果运行:%s/let/const/,它将在所有行上进行替换。请记住这个范围参数语法,在后面章节中很多命令行命令都遵循这个语法。

模式匹配

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

如果您具有以下表达式:

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

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

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

结果:

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

让我们分解一下命令:

  • :%s 定位整个文件以执行替换。
  • \d 是 Vim 的预定义数字范围简写(类似使用[0-9])。
  • "\0" 双引号是双引号的字面值。 \0是一个特殊字符,代表”整个匹配的模式”。 此处匹配的模式是单个数字\d

另外,&也同样代表”整个匹配的模式”,就像\0一样。 :s/\d/"&"/也可以。

让我们考虑另一个例子。 给出以下表达式,您需要将所有的”let”和变量名交换位置:

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

为此,请运行:

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

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

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

结果:

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

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

  • :%s 定位文件中的所有行以执行替换操作
  • (\w+) (\w+)对模式进行分组。\w是 Vim 预定义的单词字符范围简写([0-9A-Za-z_])之一。 包围\w()将匹配的单词字符进行分组。 请注意两个分组之间的空间。 (\w+) (\w+) 捕获两个分组。 在第一行上,第一组捕获”let”,第二组捕获”one”。(英文版中,作者写成了:第一组捕获”one”,第二组捕获”two”,可能是作者不小心的错误)。
  • \2 \1 以相反的顺序返回捕获的组。 \2包含捕获的字符串”let”,而\1包含字符串”one”。 使\2 \1返回字符串”let one”。

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

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

1
2
3
123
456
789

要颠倒顺序,请运行:

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

结果是:

1
2
3
321
654
987

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

相反,如果您运行下面的命令:

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

您将获得不同的结果:

1
2
3
312
645
978

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

替换标志

如果您有以下句子:

1
chocolate pancake, strawberry pancake, blueberry pancake

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

1
:s/pancake/donut

上面的命令将仅替换第一个匹配项,返回的结果是:

1
chocolate donut, strawberry pancake, blueberry pancake

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

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

1
:s/pancake/donut/g

Vim 迅速将所有”pancake”替换为”donut”。 全局命令是替代命令接受的几个标志之一。 您在替代命令的末尾传递标志。 这是有用的标志的列表:

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

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

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

更改定界符

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

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

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

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

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

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

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

特殊替换

您还可以修改要替换的文本的大小写。 给出以下表达式,您的任务是将所有变量名比如 “one”, “two”, “three”等,改成大写:

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

请运行:

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

你会得到:

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

这是该命令的细分:

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

该命令的窍门是表达式\U\2\U将后面跟着的字符变为大写。

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

1
vim is the greatest text editor in the whole galaxy

您可以运行:

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

结果:

1
Vim Is The Greatest Text Editor In The Whole Galaxy

细目如下:

  • :s 替换当前行
  • \<. 由两部分组成:\<匹配单词的开头,.匹配任何字符。 \<运算符使后面跟着的字符表示单词的第一个字符。 由于.是下一个字符,因此它将匹配任意单词的第一个字符。
  • \U& 将后续符号子序列&大写。 回想一下,&(或\0)代表整个匹配。 这里它匹配单词的第一个字符。
  • g全局标志。 没有它,此命令将仅替换第一个匹配项。 您需要替换此行上的每个匹配项。

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

候选模式

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

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

您仅需在包含单词”hello”或”hola”的行上用”friend”代替”vim”。回想一想本章前面的知识点,你可以使用| 来分隔可选的模式:

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

结果:

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

这是细分:

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

指定替换模式的开始位置和结束位置

回想一下,您可以使用\zs\ze来指定一个匹配的开始位置和结束位置。这个技术在替换操作中同样有效,如果你有以下文本:

1
2
3
chocolate pancake
strawberry sweetcake
blueberry hotcake

要想将”hotcake”中的”cake”替换为”dog”,得到”hotdog”:

1
:%s/hot\zscake/dog/g

结果是:

1
2
3
chocolate pancake
strawberry sweetcake
blueberry hotdog

贪婪与非贪婪

您可以使用下面技巧,在某行中替换第n个匹配:

1
One Mississippi, two Mississippi, three Mississippi, four Mississippi, five Mississippi.

要想将第3个”Mississippi”替换为 “Arkansas”,运行:

1
:s/\v(.{-}\zsMississippi){3}/Arkansas/g

命令分解:

  • :s/ 替换命令。
  • \v 魔术关键字,使您不必转义特殊字符。
  • . 匹配任意单个字符。
  • {-} 表示使用非贪婪模式匹配前面的0个或多个字符。
  • \zsMississippi 使得从”Mississippi”开始捕获匹配。
  • (...){3} 查找第3个匹配

在本章前面的内容中,你已经看到过{3}这样的语法。在本例中,=={3}将精确匹配第3个匹配==。这里的新技巧是{-}。它表示进行非贪婪匹配。它会找到符合给定模式的==最短的匹配==。在本例中,(.{-}Mississippi)匹配以任意字符开始、数量最少的”Mississippi”。对比(.*Mississippi),后者会找到符合给定模式的最长匹配。

如果您使用(.{-}Mississippi),你会得到5个匹配:”One Mississippi”, “Two Mississippi”,等。如果您使用(.*Mississippi),您只会得到1个匹配:最后一个 “Mississippi”。*表示贪婪匹配,而{-}表示非贪婪匹配。要想了解更多,可以查看 :h /\{-:h non-greedy

让我们看一个简单的例子。如果您有以下字符串:

1
abc1de1

用贪婪模式匹配 “abc1de1” :

1
/a.*1

用非贪婪模式匹配 “abc1”:

1
/a.\{-}1

因此,如果您需要将最长的匹配转为大写(贪婪模式),运行:

1
:s/a.*1/\U&/g

会得到:

1
ABC1DE1

如果您需要将最短的匹配转为大写(非贪婪模式),运行:

1
:s/a.\{-}1/\U&/g

会得到:

1
ABC1de1

如果您是第一次接触贪婪模式与非贪婪模式这两个概念,可能会把你绕晕。围绕不同的组合去实验,知道您明白这两个概念。

跨多个文件替换

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

food.txt内:

1
2
3
corn dog
hot dog
chili dog

animal.txt内:

1
2
3
large dog
medium dog
small dog

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

1
2
├ food.txt
├ animal.txt

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

1
:args *.txt                  捕获当前位置的所有txt文件

测试一下,当您运行:args时,您应该会看到:

1
[food.txt] animal.txt

现在,所有的相关文件都已经存储在参数列表中,您可以用 :argdo 命令跨多文件替换,运行:

1
:argdo %s/dog/chicken/

这条命令对所有:args列表中的文件执行替换操作。最终,存储修改的文件:

1
:argdo update

:args:argdo 是两个有用的工具,用于跨多文件执行命令行命令。可以用其他命令结合尝试一下!

用宏跨多个文件替换

另外,您也可以用宏跨多个文件运行替代命令。执行:

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

以下是步骤的细分:

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

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

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

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

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

总结

转义

  • 使用候选词搜索
  • {n,m}
  • 特殊的操作符:*++需要被转义,而* 可以正常运行而无需转义)

% 和 g

  • 将文档每行==第一个匹配==上的 <old string> 替换为 <new string>

    1
    :%s/<old string>/<new string>/
  • 将文档中==所有==的 <old string> 替换为 <new string>

    1
    :%s/<old string>/<new string>/g

设置tab位4个空格

修改配置

如果要修改全局Vim的配置

1
vim /etc/vim/vimrc1

但是不建议这么做,可以只修改当前用户的Vim配置

1
vim ~/.vimrc1

在配置文件中添加以下参数

  1. 第一种配置参数

    1
    2
    set ts=4
    set sw=412
  2. 第二种配置参数

    1
    2
    3
    set ts=4
    set expandtab
    set autoindent123

建议使用第二种,具有兼容性

修改已保存文件

Tab替换为空格

1
2
3
:set ts=4
:set expandtab
:%retab!123

空格替换为Tab

1
2
3
:set ts=4
:set noexpandtab
:%retab!123

加感叹号!是用于处理非空白字符之后的Tab,即所有的Tab

若不加感叹号!,则只处理行首的Tab修改配置


如果要修改全局Vim的配置

1
vim /etc/vim/vimrc1

但是不建议这么做,可以只修改当前用户的Vim配置

1
vim ~/.vimrc1

在配置文件中添加以下参数

  1. 第一种配置参数

    1
    2
    set ts=4
    set sw=412
  2. 第二种配置参数

    1
    2
    3
    set ts=4
    set expandtab
    set autoindent123

建议使用第二种,具有兼容性

修改已保存文件

Tab替换为空格

1
2
3
:set ts=4
:set expandtab
:%retab!123

空格替换为Tab

1
2
3
:set ts=4
:set noexpandtab
:%retab!123

加感叹号!是用于处理非空白字符之后的Tab,即所有的Tab

若不加感叹号!,则只处理行首的Tab