vim-advanced

操作缓冲区文本的 Ex 命令

概述

用途 命令
删除指定范围内的行(到寄存器 x 中) :[range]delete [X]
复制指定范围的行(到寄存器 x 中) :[range]yank [x]
在指定行后粘贴寄存器 x 中的内容 :[line]put [x]
把指定范围内的行拷贝到 {address} 所指定的行之下 :[range] copy
把指定范围内的行移动到 {address} 所指定的行之下 :[range] move
连接指定范围内的行 :[range] join
对指定范围内的每一行执行普通模式命令 :[range] normal
把指定范围内出现 {pattern} 的地方替换为 :[range] substitute/{pattern}/{string}/[flags]
对指定范围内匹配 {pattern} 的所有行,在其上置顶 Ex 命令 :[range] global/{pattern}/[cmd]

普通模式命令一般操作当前字符或当前行

一般地说,Ex 命令操作范围更大,并且能够在一次执行中修改多行

**注意:范围是相对于文本的行数而言的,因此只要是涉及到 [range] 的命令,就一定是操作缓冲区文本的命令

指定范围

在一行或者多个连续行上执行命令

很多 Ex 命令可以用 [range] 指定要操作的范围。我们可以用行号、位置标记或是查找模式来指定范围的开始位置及结束位置。Ex 命令的优点之一是它们可以在某一范围内的所有行上执行。

此命令会把光标移到第 3 行,然后显示该行的内容。记住,这里用 :p 命令的目的只是为了进行讲解。如果你执行的是 :3d 命令,那么只需一条命令就可以跳到第 3 行并删除此行;而与之等效的普通模式命令,则要先执行 3G,再跟着执行 dd。因此,从这个例子就可以看出,Ex 命令执行得要比普通模式命令更快。

用地址指定一个范围

:2,5p

此例会打印从第 2 行到第 5 行之间的每一行的内容(含第 2 行及第 5 行)。注意,运行完这条命令后,光标将停留在第 5 行。通常,一个范围具有如下的形式::{start},{end} 需注意的是 {start}{end} 都是地址。到目前为止,我们已经看到过用行号作为地址,然而很快就会看到也能用查找模式或是位置标记作为地址。

符号 . 代表当前行的地址。因此,我们可以很容易地写出一个范围,用以代表从当前位置到文件末尾间的所有行::.,$p

符号 % 也有特殊含义,它代表当前文件中的所有行::%p

这种简写形式在和 :substitute 命令一起使用时非常普遍::%s/Practical/Pragmatic/

用高亮选区指定范围

我们也可以用高亮选区选定一个范围,而不是用数字指定。如果我们先执行 2G,再跟着执行 VG,就会选中如一个高亮选区

如果现在按下 : 键,命令行上就会预先填充一个范围 :'<,'>。这个范围看起来有点晦涩难懂,不过你可以简单地把它理解为一个代表高亮选区的范围。接下来我们就可以输入一条 Ex 命令,使它在每个被选中的行上执行

符号 '< 是代表高亮选区首行的位置标记,而 '> 则代表高亮选区的最后一行(更多关于位置标记的内容,请参见技巧 53),这些位置标记即使在退出可视模式后仍然存在。

如果你尝试在普通模式下直接运行 :'<,'>p,它会始终回显上一次高亮选区所选中的内容。

用模式指定范围

Vim 也接受以模式作为一条 Ex 命令的地址,如下所示::/<html>/,/<\/html>/p

用偏移对地址进行修正

假设我们想对位于 <html></html> 之间的每一行都运行一条 Ex 命令,但是不想包括 <html></html> 标签所在的行,那么可以为之加上偏移::/<html>/+1,/<\/html>/-1p

偏移的一般形式是这样的::{address}+n 如果 n 被省略,那么缺省偏移量为 1。{address} 可以是一个行号、一个位置标记,或是一个查找模式。

假设我们想对由当前行开始的特定几行执行一条命令,那么可以使用相对于当前行的偏移::2 然后执行 :.,.+3p,符号 . 代表当前行,所以上例中的 :.,.+3 相当于 :2,5

构建 Ex 命令的地址及范围的符号

使用 :t 命令复制行

:copy 命令(及其简写形式 :t)让我们可以把一行或多行从文档的一部分复制到另一部分,而 :move 命令则可以让我们把一行或多行移到文档的其他地方。

copy 命令的格式如下(参见 :h :copy)::[range]copy {address} 在此例中,[range] 是第 6 行,而 {address} 用的是符号 .,它代表当前行。因此,我们可以把 :6copy. 命令解读为“为第 6 行创建一份副本,并放到当前行下方”。

:copy 命令可以简写为两个字母 :co,或者也可以用更加简练的 :t 命令,它是 :copy 命令的同义词。为了更好地记忆,你可以把该命令想成“复制到(copy TO)”。下表展示了 :t 命令的一些应用实例:

用途 命令
把第 6 行复制到当前行下方 :6t.
把当前行复制到第 6 行下方 :t6
为当前行创建一个副本,类似于 yyp :t.
把当前行复制到文本结尾 :t$
把高亮选中的行复制到文件开发 :'<,'>t0

:t. 命令会创建一个当前行副本,而另外一种做法则是用普通模式的复制和粘贴命令(yyp)来达到同样的效果。这两种复制当前行的技术有个需要关注的差别:yyp 会使用寄存器,而 :t. 则不会。因此,当我不想覆盖默认寄存器中的当前内容时,有时我会使用 :t. 来复制行。

在上表中,也可以将 yyp 变化一下来复制我们想要的行,但不管怎样,这都需要一些额外的移动动作。我们得先跳到想复制的行上(6G),复制该行(yy),快速跳回原先的位置(<C-o>),然后再用粘贴命令(p)创建一个副本。由此可见,在复制距离较远的行时 :t 命令通常更加高效。

:m 命令移动行

:move 命令看上去和 :copy 命令很相似(参见:h :move)::[range]move {address} 我们可以把它简写为一个字母 :m

在选中高亮选区后,只需简单地执行命令 :'<,'>m$ 即可。另外还有种做法,我们也可以执行 dGp,此命令可以分解为 :d 删除高亮选区,G 跳转到文件结尾,而 p 则粘贴刚刚删除的文本。记住,'<,'> 代表了高亮选区。因此我们可以很容易地选中另外一个高亮选区,然后重复执行 :'<,'>m$ 命令把选中的文本移到文件结尾。

重复上次的 Ex 命令非常简单,只需按 `@: 即可(技巧31 给出了另一个例子),所以这里所采取的方式与使用普通模式命令相比,在重复执行时会更方便。

远距离文本操作用 Ex 命令更适合

普通模式命令适合在本地进行操作,而 Ex 命令则可以远距离操作。

在指定范围上执行普通模式命令

如果想在一系列连续行上执行一条普通模式命令,我们可以用 :normal 命令。此命令在与 . 命令或宏结合使用时,我们只需花费很少的努力就能完成大量重复性任务。

想一下在技巧2 中遇到过的例子,我们想在一系列行后添加一个分号。使用点范式让我们迅速完成了这项工作,但是在那个例子里,只需对连续的3 行做此修改。如果不得不做50 次同样的修改会怎么样呢?如果还用点范式的话,得按50 次 j.,总共得100 次按键动作!

这里有一种更好的方法。我们将在下面文件的每行后都添加一个分号,以此作为演示。为节省空间,此处只列出了5 行内容,然而你可以想象这里有50 行,那么这种方法看起来就颇具诱惑了。

我们像之前做的那样,首先修改第一行:

接下来,用不着一行一行地执行 . 命令,而是使用 Ex 命令 :normal 对整个范围内的所有行同时执行 . 命令:

:'<,'>normal. 命令可以解读为“对高亮选区中的每一行,对其执行普通模式下的 . 命令”。无论是操作5 行还是50 行文本,这种方法都能出色地完成任务,更棒的是我们甚至都不需要计算行数,在可视模式中选中这些行使我们摆脱了计数的负担。

这个例子使用 :normal 执行 . 命令,但是也可以用这种方式执行任意其他的普通模式命令。例如,可以用如下命令解决上面的问题::%normal A;

符号 % 代表整个文件范围,因此 :%normal A; 告诉 Vim 在文件每行的结尾都添加一个分号。在做此修改时会切换到插入模式,但是在修改完后Vim 会自动返回到普通模式。

在执行指定的普通模式命令之前,Vim 会先把光标移到该行的起始处。因此在执行时,用不着担心光标的位置。例如,下面这条命令可以把整个JavaScript 文件注释掉::%normal i//

虽然用 :normal 命令可以执行任意的普通模式命令,但是我发现当它和 Vim 的重复命令结合在一起时,最为强大,既可以用 :normal . 应对简单的重复性工作,也可以用 :normal @q 应对较复杂的任务。

具体的实例参见技巧67 和技巧69。在 Ex 命令影响范围广且距离远 中,我们说过 Ex 命令可以一次修改若干行。而 :normal 命令则让我们可以把具有强大表现力的 Vim 普通模式命令与具有大范围影响力的 Ex 命令结合在一起,这种结合真地是珠联璧合!对本节所涉及问题的另外一种解决方案,请参见技巧26。

命令行模式 Ex 命令

初时,先有 ed,ed 为 ex 之父,ex 为 vi 之父,而vi 为 Vim 之父。

The Old Testament of Unix

Vim 的先祖是vi,正是vi 开创了区分模式编辑的范例。相应的,vi 奉一个名为ex 的行编辑器为先祖,这就是为什么会有 Ex 命令。这些早期 Unix 文本编辑器的血脉依旧流淌在现代 Vim 中,对某些基于行的编辑任务来说,Ex 命令仍然是最佳工具。

在按下 : 键时,Vim 会切换到命令行模式。这个模式和shell 下的命令行有些类似,我们可以输入一条命令,然后按 <CR> 执行它。在任意时刻,我们都可以按 <Esc> 键从命令行模式切换回普通模式。在我们按 / 调出查找提示符或用 <C-r>= 访问表达式寄存器(参见技巧16)时,命令行模式也会被激活。

事实上,Vim 为几乎所有功能都提供了相应的Ex 命令(参见:hex-cmd-index 可获得完整列表)

常用 Ex 命令

:Set

显示行号 :set nu

取消行号 :set nonu

:Help 帮助窗口

:help 打开

输入 CTRL-W CTRL-W 可以使您在窗口之间跳转

输入 :q <回车> 可以关闭帮助窗口

提供一个正确的参数给":help" 命令,您可以找到关于该主题的帮助。请试验以下参数(可别忘了按回车键哦):

    :help w
    :help c_CTRL-D
    :help insert-index
    :help user-manual

提供一个正确的参数给":help" 命令,您可以找到关于该主题的帮助。请试验以下参数(可别忘了按回车键哦):

    :help w
    :help c_CTRL-D
    :help insert-index
    :help user-manual

命令行模式中的特殊按键

有些命令在插入模式和命令行模式中可以通用。例如,可以用 <C-w><C-u> 分别删除至上个单词的开头及行首,也可以用 <C-v><C-k> 来插入键盘上找不到的字符,还可以用 <C-r>{register} 命令把任意寄存器的内容插入到命令行,就像在技巧15 中见过的那样。

然而,有些命令行模式中的组合键在插入模式中不存在,我们将在技巧33 中结识几个这样的命令。

重复上次的 Ex 命令

. 命令可以重复上次的普通模式命令。然而,如果想重复上次的 Ex 命令的话,我们得使用 @: 才行。

在第1 章中,我们见识过如何用 . 命令重复上次的修改。但是,. 命令不会重复由Vim 命令行中做出的修改。作为替代,我们可以用 @: 来重复上次的 Ex 命令(参见 :h @:)。

: 寄存器总是保存着最后执行的命令行命令(参见:h quote_:)。在运行过一次 @: 后,后面就可以用 @@ 命令来重复它

自动补全ex 命令

如同在shell 中一样,在命令行上也可以用 <Tab> 键自动补全命令。Vim 在选取 Tab 补全的补全项时非常智能,它会检查命令行上已经输入的上下文,然后再构建合适的补全列表。例如,可以这样输入::col<C-d>

<C-d> 命令会让Vim 显示可用的补全列表(参见:h c_CTRL-D)。另外,如果我们多次按 <Tab> 键的话,命令行上会依次显示 colder、colorscheme,然后再回到最初的 col,如此循环往复。要想反向遍历补全列表,可以按 <S-Tab>

假设我们想改配色方案,但是不太记得要用的配色方案的名称,这时可以用 <C-d> 命令列出所有的可用选项::colorscheme <C-d>

在很多场景中,Vim 的 Tab 补全都能做出正确的选择。如果我们输入了一个以文件路径作为参数的命令(如 :edit:write),那么 <Tab> 会用当前工作目录中的目录或文件名进行补全。在 :tag 命令中,它会自动补全标签名;而在 :set:help 命令中,它可以对 Vim 的每一个设置选项进行补全。甚至在我们创建自定义 Ex 命令时,也能够定义该命令的 Tab 键补全行为。要想了解更多,请查阅 :h:command-complete

在多个补全项间选择

当 Vim 只找到一个Tab 补全项时,它会直接使用整个补全项。但是如果 Vim 找到了多个补全项,那么会有几种做法。缺省情况下,首次按下 Tab 键时,Vim 会用第一个补全项进行补全,以后每按一下 Tab 键,就会依次遍历剩余的补全项。调整‘wildmode’选项可以自定义补全行为(参见 :h'wildmode')。

如果你习惯用 bash shell 的方式工作,那么下面的设置会满足你的需要 :set wildmode=longest,list 如果你习惯于 zsh 提供的自动补全菜单,或许会想试试这个 :set wildmenuset wildmode=full,当 ‘wildmenu’ 选项被启用时,Vim 会提供一个补全导航列表。

我们可以按 <Tab><C-n><Right> 正向遍历其列表项,也可以用 <S-Tab><C-p><Left> 对其进行反向遍历。

回溯历史命令

先按 : 键切换到命令行模式,在保持提示符为空的情况下按 <Up> 键,此时最后执行的那条 Ex 命令就会被填充到命令行上。再接着按 <Up> 键的话,就可以回到更早的 Ex 历史命令;按 <Down> 键的话,则会沿相反方向滚动。

现在,我们尝试先输入 :help 然后再按 <Up> 键遍历之前的Ex 命令。这一次,Vim 不会显示所有的历史命令,而是会对列表进行过滤,只有以单词“help”开头的Ex 命令才会被包含在列表中。

Vim 缺省会记录最后 20 条命令,对内存越发便宜的现代计算机来说,保存更多历史命令只是小菜一碟,因此我们可以修改 ‘history’ 选项,以提高其保存的上限。你可以试着把下面这行内容加入 vimrc 文件 :set history=200

注意:命令历史不仅是为当前编辑会话记录的,这些历史即使在退出Vim 再重启之后仍然存在(参见 :h viminfo),因此提高历史记录的数目非常有价值。

Vim 不仅会记录 Ex 命令的历史,它也会为查找命令单独保存一份历史记录。在按/ 调出查找提示符后,用 <Up><Down> 键就可以正向或反向遍历之前的查找记录。从本质上讲,查找提示符只是命令行模式的另一种形式。

除了 <Up><Down> 键外,也可以用 <C-p><C-n> 组合键来反向或正向遍历命令历史。使用这些映射项的好处是不需要把手移到小键盘区,但 <C-p><C-n> 命令有个缺点,它们不会像 <Up><Down> 那样对历史命令进行过滤。通过创建下面的自定义映射项,我们可以把二者的优点结合到一起

cnoremap <C-p> <Up>
cnoremap <C-n> <Down>

结识命令行窗口

输入 q:,先结识一下命令行窗口

命令行窗口就像是一个常规的 Vim 缓冲区,只不过它的每行内容都对应着命令历史中的一个条目。我们可以用 k 及 j 键在历史中向前或向后移动,也可以用 Vim 的查找功能查找某一行。在按下 <CR> 键时,将会把当前行的内容当成Ex 命令加以执行。

命令行窗口的好处在于它允许我们使用 Vim 完整的、区分模式的编辑能力来修改历史命令。我们可以用任何习以为常的动作命令进行移动,也可以在高亮选区上操作,或是切换到插入模式中。我们甚至还能对命令行窗口中的内容执行 Ex 命令。

可惜了,vscodevim 并没有支持完整的功能

运行 Shell 命令

我们不用离开 Vim 就能方便地调用外部程序。更棒的是,我们还可以把缓冲区的内容作为标准输入发送给一个外部命令,或是把外部命令的标准输出导入到缓冲区里。

在Vim 的命令行中,符号 % 代表当前文件名(参见 :h cmdline-special)。在运行那些操作当前文件的外部命令时,我们可以使用它。例如,如果我们正在编辑某个Ruby 文件,那么可以用下面的方式执行此文件::!ruby %

!{cmd} 这种语法适用于执行一次性命令,但是如果想在 shell 中执行几条命令要怎么做?对这种情况,可以执行 Vim 的 :shell 命令来启动一个交互的 shell 会话

相当于是 vscode cmd + `

用 exit 命令可以退出此 shell 并返回 Vim。

假设我们正在 bash shell 中运行 Vim,然后需要执行一些 shell 命令。我们可以先按 Ctrl-z 挂起 Vim 所属的进程,并把控制权交还给 bash。此时 Vim 进程在后台处于挂起状态,让我们可以像往常一样与 bash 会话进行交互。运行下面这条命令可以查看当前的作业列表:$ jobs

在 bash 中,我们可以用 fg 命令唤醒一个被挂起的作业,把它移到前台。这会让 Vim 恢复成挂起前的状态。

Ctrl-zfg 命令比 Vim 所提供的 :shellexit 命令更加方便快捷。要想了解更多信息,请运行 man bash,然后阅读作业控制(jobcontrol)一节。

把缓冲区内容作为标准输入或输出

在用 :!{cmd} 时,Vim 会回显 {cmd} 命令的输出。如果命令的输出很少或没有输出,这工作得很好

但如果命令会产生大量输出,这样回显用处不大。另外一种做法是我们可以用 :read !{cmd} 命令,把 {cmd} 命令的输出读入当前缓冲区中(参见 :h read!)。

:read !{cmd} 命令让我们把命令的标准输出重定向到缓冲区。正如你所期望的一样,:write !{cmd} 做相反的事。它把缓冲区内容作为指定 {cmd} 的标准输入(参见 :h:write_c)

根据叹号在命令行上的位置不同,它的含义也不大相同。比较一下这些命令:

:write !sh
:write ! sh
:write! sh

前两个命令都会把缓冲区的内容传给外部的 sh 命令作为标准输入,而最后一条命令则调用 :write! 命令把缓冲区内容写到一个名为 sh 的文件,这里的叹号会让 Vim 覆盖任何已存的 sh 文件。正如你看到的那样,叹号放得位置不同,命令的作用也大相径庭。因此,在构建这类命令时要多加小心。

使用外部命令过滤缓冲区内容

技巧 355

常用命令汇总

用途 命令
启动一个 shell (输入 exit 返回 vim) :shell
在 shell 中执行 :!{cmd}
在 shell 中执行 {cmd},并把其标准输出插入到光标下方 read :!{cmd}
在 shell 中执行 {cmd},以 [range] 作为其标准输入 :[range]write !{cmd}
使用外部程序 {filer} 过滤指定的 [range] :[range]!{filer}

工作区

基本概念

Vim 允许在一个编辑会话中编辑多个文件,我们既可以每次显示一个文件,也可以把工作区分成若干个分割窗口或标签页,每个窗口或标签页包含一个独立的缓冲区。另外,我们还会看到在 Vim 中打开文件的几种不同方式,并掌握一些方法来解决无法把缓冲区保存到文件的问题。

缓冲区

Vim 是一个文本编辑器。每次文本都是作为 缓冲区 的一部分显示的。每一份文件都是在他们自己独有的缓冲区打开的,插件显示的内容也在它们自己的缓冲区中。

缓冲区有很多属性,比如这个缓冲区的内容是否可以修改,或者这个缓冲区是否和文件相关联,是否需要同步保存到磁盘上。

窗口

窗口 是缓冲区上一层的视窗。如果你想同时查看几个文件或者查看同一文件的不同位置,那样你会需要窗口。

请别把他们叫做 分屏 。你可以把一个窗口分割成两个,但是这并没有让这两个窗口完全 分离

标签

窗口可以水平或者竖直分割并且现有窗口的高度和宽度都是可以被调节设置的,因此,如果你需要多种窗口布局,请考虑使用标签。

标签页 (标签)是窗口的集合。因此当你想使用多种窗口布局时候请使用标签。

简单的说,如果你启动 Vim 的时候没有附带任何参数,你会得到一个包含着一个呈现一个缓冲区的窗口的标签。

顺带提一下,缓冲区列表是全局可见的,你可以在任何标签中访问任何一个缓冲区。

缓冲区

概念

用类似 vim file1 的命令启动 Vim 。这个文件的内容将会被加载到缓冲区中,你现在有一个 已载入的缓冲区。如果你在 Vim 中保存这个文件,缓冲区内容将会被同步到磁盘上(写回文件中)。

由于这个缓冲区也在一个窗口上显示,所以他也是一个 已激活的缓冲区。如果你现在通过 :e file2 命令加载另一个文件,file1 将会变成一个 隐藏的缓冲区,并且 file2 变成已激活缓冲区。

使用 :ls 我们能够列出所有可以列出的缓冲区。插件缓冲区和帮助缓冲区通常被标记为不可以列出的缓冲区,因为那并不是你经常需要在编辑器中编辑的常规文件。通过 :ls! 命令可以显示被放入缓冲区列表的和未被放入列表的缓冲区。

未命名的缓冲区 是一种没有关联特定文件的缓冲区,这种缓冲区经常被插件使用。比如 :enew 将会创建一个无名临时缓冲区。添加一些文本然后使用 :w /tmp/foo 将他写入到磁盘,这样这个缓冲区就会变成一个 已命名的缓冲区

参数列表

全局缓冲区列表 是 Vim 的特性。在这之前的 vi 中,仅仅只有参数列表,参数列表在 Vim 中依旧可以使用。

每一个通过 shell 命令传递给 Vim 的文件名都被记录在一个参数列表中。可以有多个参数列表:默认情况下所有参数都被放在全局参数列表下,但是你可以使用 :arglocal 命令去创建一个新的本地窗口的参数列表。

使用 :args 命令可以列出当前参数。使用 :next:previous:first:last 命令可以在切换在参数列表中的文件。通过使用 :argadd:argdelete 或者 :args 等命令加上一个文件列表可以改变参数列表。

偏爱缓冲区列表还是参数列表完全是个人选择,我的印象中大多数人都是使用缓冲区列表的。

然而参数列表在有些情况下被大量使用:批处理 使用 :argdo! 一个简单的重构例子:

:args **/*.[ch]
:argdo %s/foo/bar/ge | update

这条命令将替换掉当前目录下以及当前目录的子目录中所有的 C 源文件和头文件中的“foo”,并用“bar”代替。

相关帮助::h argument-list

遍历缓冲区

下面两条命令在遍历缓冲区列表的条目时非常有用,用 :bn[ext] 可以在列表中逐项正向移动,而 :bp[revious] 命令则进行反向移动(技巧 36 详细讨论了缓冲区列表)。假设缓冲区列表中有大约十几个条目,而我们打算逐个查看每个缓冲区,因此可以输入一次下面的命令::bnext,然后再用 @: 重复执行此命令。留意一下这和运行宏的相似之处(参见通过执行宏来回放命令序列)

另外也需注意,: 寄存器总是保存着最后执行的命令行命令(参见:h quote_:)。在运行过一次 @: 后,后面就可以用 @@ 命令来重复它。

假设我们按得忘乎所以,执行了太多次 @: 命令以致于错过了目标。那要怎样才能改变方向往回跳呢?当然,我们可以执行 :bprevious 命令,但是想想如果以后再次执行 @: 命令会发生什么?没错,它会反向遍历缓冲区列表

在这种情况下,更好的选择是使用 <C-o> 命令(参见技巧 55)。每次运行 :bnext 命令(或用 @: 命令重复执行它)时,它都会在跳转列表中添加一条记录,而 <C-o> 命令会回到跳转列表的上条记录。

我们可以执行一次 :bnext,然后用 @: 重复任意多次;如果想往回跳,就用 <C-o> 命令。这样一来,如果接下来还想继续正向遍历缓冲区列表,就可以继续用 @: 命令。请牢记技巧 4 中提到的口诀:执行、重复、回退。

Vim 为几乎所有功能都提供了相应的 Ex 命令。虽然用 @: 总是可以重复上一条 Ex 命令,但如果想回退其影响,却没有这种直截了当的方式。用本节提到的 <C-o> 命令,也能够回退 :next:tnext 等命令的执行结果;然而对于表 5-1 中列出的 Ex 命令,则要用 u 键才能撤销其影响。

更多的缓冲区命令

用途 命令
在当前文件和轮换文件间快速切换 <C-^>
:bfirst
:blast
查看当前的缓冲区列表 :ls
在 :ls 列出的所有缓冲区上执行 Ex 命令 :bufdo

创建快速遍历 Vim 列表的按键映射项

我采用了下面这些映射项,它们是在 Tim Pope 的 unimpaired.vim 插件中定义的:

nnoremap <silent> [b :bprevious<CR>
nnoremap <silent> ]b :bnext<CR>
nnoremap <silent> [B :bfirst<CR>
nnoremap <silent> ]B :blast<CR>

Vim 已经用 [] 键作为一系列相关命令的前缀了(参见:h [),因此上面这些映射项的风格与其一致。除上面这些之外,unimpaired.vim 插件还提供了其他一些类似的映射项,分别用来遍历参数列表([a]a)、quickfix 列表([q]q)、位置列表([l]l)以及标签列表([t]t)。你自己去看看吧。

Vim 内置的缓冲区管理功能缺乏灵活性。如果我们想对缓冲区进行组织,使其满足工作过程的需要,使用缓冲区列表并不是最佳选择。相反,我们最好是把工作区划分成多个分割窗口、标签页,或是使用参数列表。接下来的几个技巧将会介绍这些内容。

窗口

创建分割窗口

Vim 在启动时只会打开单个窗口。用 <C-w>s 命令可以水平切分此窗口,使之成为两个高度相同的窗口;或者可以用 <C-w>v 命令对其进行垂直切分,这样会产生两个宽度相同的窗口。这两条命令可以重复任意多次,结果就会把工作区一次次地切分为更小的窗口,就像细胞分裂那样。

每次执行完 <C-w>s<C-w>v 命令后,新生成的两个窗口都会显示与原窗口相同的缓冲区。把同一缓冲区显示在不同窗口里会很有用,特别是在编辑长文件时。

感觉不如 vscode <C-p> 通过 enter 和 <S-CR>

在窗口间切换

<C-w>w
<C-w>h
<C-w>j
<C-w>k
<C-w>l

可以配合 vscode 的 <C-number> 使用

关闭窗口

文件

操作 快捷键
打开一个文件 :e <path/to/file>
将当前 VIM 中正在编辑的文件保存到名为 FILENAME 的文件中 :w FILENAM
将当前编辑文件中可视模式下选中的内容保存到文件 FILENAME 中 v motion :w FILENAM
可提取磁盘文件 FILENAME 并将其插入到当前文件的光标位置后面 :r FILENAME
读取 dir 命令的输出并将其放置到当前文件的光标位置后面 :r !dir
保存另一个文件 :saveas <path/to/file
保存并退出 :xZZ:wq → (:x 表示仅在需要时保存,ZZ 不需要输入冒号并回车)
退出不保存 :q!
强行退出所有的正在编辑的文件,就算别的文件有更改 :qa!
使用这两个命令来切换下一个或上一个文件。(陈皓注:我喜欢使用:n 到下一个文件) :bn :bp

用:edit 命令打开文件

:edit 命令允许通过文件的绝对路径或相对路径来打开文件。

我们也可以用 Tab 键自动补全文件路径(更多细节参见技巧 32)。因此,如果想打开 Navigation.js 文件的话,实际上只需输入 :edit a<Tab>c<Tab>N<Tab>

使用:find 打开文件

:find 命令允许我们通过文件名打开一个文件,但无需输入该文件的完整路径。要想利用此功能,我们首先要配置 path 选项。

切换工作区

在 vscode 中可以通过 <C-> 和 <C-S-> 切换

寄存器

用无名寄存器实现删除、复制与粘贴操作

调换字符

xp,可被用于“调换光标之后的两个字符”。

调换文本行

ddp,可被用于“调换当前行和它的下一行”。

创建文本行的副本

yyp

寄存器的工作原理

Vim 不使用单一的剪贴板进行剪切、复制与粘贴操作,而是为这些操作提供了多组寄存器。当使用删除、复制与粘贴命令时,我们可以明确指定它们中的某一个进行操作。

Vim 术语对照表

剪切(cut)、复制(copy)与粘贴(paste),这些都是众所周知的术语,而且大多数桌面软件和操作系统都支持这 3 类操作。Vim 当然也提供这些功能,只不过使用的是另外的术语 delete、yank 与 put

Vim 的 put 命令与粘贴操作完全相同。幸运的是,两词均以字母 p 开头,因此即使术语不同,也不会影响记忆。

Vim 的 yank 命令也等同于复制操作。但由于历史原因,当时 c 命令已经被用于修改(change)操作了,因此 Vi 的作者们被迫选择了另一个名字 yank。由于那时 y 键还可用,因此它就成了复制操作的命令。

Vim 的 delete 命令也与 标准剪切操作 的作用一致。也就是说,该命令会先把指定文本复制到寄存器后再从文档中删掉。能够理解这一点,是避开类似糟糕!我弄丢了复制内容所遇到的常见陷阱的关键。

你也许好奇,Vim 中真正删除文本的操作是什么。也就是说,我们怎样才能删除文本而不把其内容复制到任何寄存器?答案是使用名为“黑洞”的特殊寄存器,顾名思义,放到这里的文本真地是有去无回了。用下划线符号(参见 :h quote_)可以引用黑洞寄存器。因此,"_d{motion} 会执行真正的删除操作。

引用一个寄存器

Vim 的删除、复制与粘贴命令都会用到众多寄存器中的某一个。我们可以通过给命令加 "{register} 前缀的方式指定要用的寄存器。若不指明,Vim 将缺省使用无名寄存器。

让我们看一些引用寄存器的例子,如果我们想把当前单词复制到寄存器 a 中,可执行 "ayiw 或者可以用 "bdd 把当前整行文本剪切至寄存器 b 中。在此之后,我们既可以输入 "ap 粘贴来自寄存器 a 的单词,也可使用 "bp 命令粘贴来自寄存器 b 的一整行文本,两者互不干扰。

除了普通模式的命令外,Vim 也提供用于删除、复制与粘贴操作的 Ex 命令。例如,我们可以执行 :delete c,把当前行剪切到寄存器 c,然后再执行 :put c 命令将其粘贴至当前光标所在行之下。相比普通模式的命令而言,这些操作看似繁琐,但如果将它们与其他 Ex 命令结合起来使用,或者用于 Vim 脚本编程,将会更方便。例如,技巧 99 就为我们展示了 :yank 命令怎样和 :global 命令一起使用的场景。

:reg "0 命令可以检查寄存器中的内容

无名寄存器("")

倘若我们没有指定要使用的寄存器,Vim 将缺省使用无名寄存器,它可以用双引号表示(参见:h quote_quote)。为了显式地引用该寄存器,我们得使用两个双引号。例如,""p,它完全等同于 p 命令。

x、s、d{motion}、c{motion} 与 y{motion} 命令(以及它们对应的大写命令)都会覆盖无名寄存器中的内容。无论哪一种情况,都可以通过加 "{register} 前缀来指定另外一个寄存器,但无名寄存器总是缺省的。事实上,无名寄存器的内容很容易被覆盖,如果我们不小心的话,会导致问题发生。

复制专用寄存器("0)

当我们使用 y{motion} 命令时,要复制的文本不仅会被拷贝到无名寄存器中,而且也被拷贝到了复制专用寄存器中,后者可用数字 0(参见:h quote0)加以引用

复制专用寄存器,顾名思义,仅当使用 y{motion} 命令时才会被赋值。换句话讲,使用 x、s、c{motion} 以及 d{motion} 命令均不会覆盖该寄存器。如果我们复制了一些文本,可以确信该文本会一直保存于寄存器 0 中,直到我们复制其他文本时才会被覆盖。复制专用寄存器是稳定的,而无名寄存器是易变的。

向寄存器附加内容

用小写字母引用有名寄存器,会覆盖该寄存器的原有内容,而换用大写字母的话,则会将新内容添加到该寄存器的原有内容之后。请跳到技巧 99,那里展示了一个如何向寄存器附加内容的实例。

系统剪贴板("+)与选择专用寄存器("*)

到目前为止,我们所讨论的寄存器都是 Vim 内部的。如果想从 Vim 复制文本到外部程序(反之亦然),则必须使用系统剪贴板。Vim 的加号寄存器与系统剪贴板等效,可用 + 号(参见:h quote+)引用。

如果我们在外部程序中用剪切或复制命令获取了文本,就可以通过 "+p 命令(或在插入模式下用 <C-r>+)将其粘贴到 Vim 内部。相反地,如果在 Vim 的复制或删除命令之前加入 "+,相应的文本将被捕获至系统剪贴板。这意味着我们能够轻松地把文本粘贴到其他应用程序中了。

Windows 与 Mac OS X 操作系统并没有主剪贴板的概念,因此 "+ 寄存器与 "* 寄存器可以混用,它们都代表系统剪贴板。

(99+ 封私信 / 37 条消息) 如何将 Vim 剪贴板里面的东西粘贴到 Vim 之外的地方? - 知乎 (zhihu.com)

表达式寄存器("=)

其他寄存器

我们可以显式地使用删除与复制命令,来设置有名、无名以及复制专用寄存器的内容。另外,Vim 还提供了几组可被隐式赋值的寄存器。它们被称作只读寄存器(参见:h quote.[插图]),如下表所示:

用寄存器中的内容替换高亮选区的文本

在可视模式下使用 p 命令时,Vim 将用我们指定的寄存器内容来替换高亮选区中的文本(参见 :h v_p)。我们可以利用该功能解决糟糕!我弄丢了复制内容中的问题。

交换两个词

针对 Vim 在可视化粘贴时的这一特点,我们可以加以利用。假设我们想交换以下句中两个单词的次序

首先,我们使用 de 把单词“chips”剪切掉,实际上是把它复制到了无名寄存器;

然后,再选中要替换的单词“fish”。当我们执行 p 命令时,单词“chips”将重新出现在文档中,而 单词“fish”则会被复制到无名寄存器

最后,我们把光标重新移到因删除“chips”而留下的空白处,再将单词“fish”从无名寄存器粘贴回文档即可。

把寄存器的内容粘贴出来

普通模式下的粘贴命令,根据要插入文本的性质不同,执行结果也不同。确定要粘贴的文本区域是面向行的还是面向字符的,将有助于我们制定不同的策略。

在技巧 59 中,我们见识了用 xp 命令调换两字符的次序以及用 ddp 命令调换两行的顺序。尽管这两种情况都用到了 p 命令,但结果却略有差异。

p 命令旨在将寄存器中的文本粘贴到光标之后(参见:h p)。作为补充,Vim 也提供了(大写的)P 命令用于将文本插入到光标之前。至于当前光标前后的位置具体在哪,得根据将要插入的寄存器内容而定。

怎样才能知道 p 命令是把寄存器的文本粘贴到当前字符之后还是当前行之后呢?这取决于这个指定的寄存器是怎样被赋值的。

面向行的复制或者删除操作(例如:dd、yy 或者 dap),将创建面向行的寄存器;而面向字符的复制或者删除操作(例如:x、diw 或者 das),则创建面向字符的寄存器。

一般而言,使用 p 命令的结果会一目了然。(更多细节,请参见 :h linewise-register。)

粘贴面向字符的区域

我不喜欢被迫去判断面向字符的文本区域到底是放在光标之前还是之后。因此,较之使用普通模式的 p 和 P 命令,我有时更喜欢在插入模式中使用 <C-r>{register} 的映射项,来粘贴面向字符的文本区域。通过这种方式,寄存器的文本总会被插入至光标之前,就像我们在插入模式下手动输入它们一样。

即使是在命令行模式下,Vim 也始终知道光标位于何处以及哪个分割窗口处于活动状态。为节省时间,我们可以把活动窗口中的当前单词(或字串)插入到命令行中。

在 Vim 的命令行下,<C-r><C-w> 映射项会复制光标下的单词并把它插入到命令行中。我们可以利用这一功能减少击键的次数。

如果想插入光标下的字串的话(参见技巧 48 的说明),我们可以用 <C-r><C-a>,更多细节请参见 :h c_CTRL-R_CTRL-W

这里介绍另一种应用场景。试着打开你的 vimrc 文件,把光标移到其中的一项设置上,然后输入 :help<C-r><C-w>,你就可以查阅该设置的文档了。

虽然所有的 Ex 命令都可以使用这个操作,但是更常用的肯定是 substitute 这种吧

<C-r><C-w> map to <C-;><C-'>

<C-r>0

<C-r>+

粘贴面向行的区域

当要粘贴的内容来自于面向行的寄存器时,p 和 P 命令会把它们粘贴至当前行的上一行或下一行。这一点比面向字符的行为更直观。

Vim 提供的 gp 和 gP 命令也值得关注,因为它们同样可以将文本粘贴至在当前行之前或之后。不同的是,它们会把光标的位置移到被粘贴出来的文本结尾而不是开头。当复制多行文本时,gP 命令尤为管用,例如:

与系统剪贴板进行交互

除了 Vim 内置的粘贴命令,我们有时也要用到系统粘贴命令。但当 Vim 在终端内部运行时,使用该命令经常会产生意外的结果。为了避免这些问题,可在执行系统粘贴命令之前激活 ‘paste’ 选项。

如果你运行的 Vim 是已集成系统剪贴板的版本,就可以完全避免与 'paste' 选项打交道了。普通模式下的 "+p 命令用来粘贴加号寄存器中的内容,即系统剪贴板的镜像。更多细节,请参见系统剪贴板("+)与选择专用寄存器("*)。无论 'paste' 与 'autocommand' 选项激活与否,该命令都能保证位于剪贴板中的文本缩进不会乱套。

Vim 提供了不止一种方式用于重复之前所做的修改。我们已经学过 . 命令,用它来重复小的修改确实有效,但当我们想重复更大规模的改动时,Vim 的宏就派上用场了。我们可以用宏把任意数目的按键操作录制到寄存器,用于之后的回放。

宏的读取与执行

宏允许我们把一段修改序列录制下来,用于之后的回放。本节将对其细节进行深度剖析。

把命令序列录制成宏

q 键既是“录制”按钮,也是“停止”按钮。为了录制我们的按键操作,一开始需要按 q{register},从而指定一个用于保存宏的寄存器。当状态栏中出现“记录中”时,表示录制已经开始。此后,我们执行的每一条命令都将被宏捕获,直到我们再次按下 q 键停下为止。

首先,我们输入 qa 开始录制宏并将其内容保存至寄存器 a 中,然后,在第一行上做两处修改,在行尾添加一个分号,再在行首添加一个单词 var。在完成这些修改后,按 q 键停止宏的录制。

我们可以通过以下命令查看寄存器 a 中的内容 :reg a

通过执行宏来回放命令序列

我们可以用 @{register} 命令执行指定寄存器的内容(参见:h @),也可以用 @@ 来重复最近调用过的宏。

通过执行这个刚刚录制好的宏, Vim 对随后的每一行也重复了这两处相同的修改。

注意:我们在第一行用 @a 回放宏,而在下一行用 @@ 来回放同样的宏。

规范光标位置、直达目标以及中止宏

当我们执行一个宏时,Vim 会机械地重复这个打包在一起的按键操作序列。如果我们不小心的话,在回放宏时的结果会偏离我们的预期。但我们也可以录制更灵活的宏,针对每一种情况,它都能应对自如。

黄金法则:在录制一个宏时,要确保每条命令都可被重复执行。

规范光标的位置

这也许意味着应该把光标移到下一处查找匹配项(n),或者当前行的行首(0),又或是当前文件的首行(gg)。如果每次总是从确定的位置开始执行的话,那么命中正确的目标会变得更容易。

用可重复的动作命令直达目标

Vim 有一组丰富的动作命令集,通过它们,可以直达文本文件的各个角落。因此,我们要善用这些命令。

我推荐你用查找命令定位,或者用文本对象。总之,请用好 Vim 提供的所有动作命令,尽量使你的宏兼具灵活性与可重复性。还有一点别忘了,在录制宏的过程中,禁止使用鼠标。

当动作命令失败时,宏将中止执行

Vim 的动作命令可能会执行失败。举例来说,如果光标位于文件的首行,运行 k 命令将什么也不会发生。若光标位于文件的末行,按下 j 也会出现同样的情况。当发生上述情况时,Vim 缺省会发出“哔”的一声,提示我们动作命令失败了。

如果宏执行动作命令失败了,Vim 将中止执行宏的其余命令。这是一项功能,而不是漏洞。我们可以用动作命令进行简单测试,来判断该宏是否应该在当前上下文中继续执行。

考虑这样一个例子:我们要查找一个模式(pattern),假设文档中有 10 处匹配。我们开始录制宏,先用 n 重复上一次的查找操作。一旦光标移到匹配处,我们会做一些小的修改。然后,停止宏的录制。改完这处后,这个地方就再没有可匹配的模式了。至此,整篇文档只剩 9 处匹配了。

当我们执行这个宏时,光标将移到下一处匹配并做相同的修改。至此,整篇文档只剩下 8 处匹配了。就这样,我们周而复始地执行该宏,直到再也没有一处匹配为止。若此时再执行该宏,由于已没有匹配项,n 命令会失败,宏将中止退出。

假设宏保存在寄存器 a 中。这一次,我们不再执行 10 次 @a,而是改用次数作为前缀执行宏 10@a。这种技术的过人之处,就在于执行宏的时候,可以不必顾忌执行的次数。真地不用去管执行次数了么?当然!我们可以执行 100@a,甚至是 1000@a,反正结果都是一样的。

谁愿意坐在那里精确计算一个宏需要被执行多少次呢?反正我不愿意。为了完成该任务,我宁愿估算一个足够大的次数。出于懒惰的缘故,我通常用 22 这个数字,因为在我的键盘上,字符 @ 与 2 在同一个键上,容易输入。

宏的串行与并行

在连续的文本行上重复修改

录制工作单元

注意动作命令在该宏中的用法。首先,我们输入 0 命令,将光标置于行首,从而规范了光标的位置。这意味着下一条动作命令总是从相同的位置开始执行,重复性更强。

使用 f. 这条动作命令会为我们增加一种安全捕获机制。如果在当前行没找到字符 .f. 命令会提示一个错误,宏将中止执行。稍后我们将会用到这一特点,所以请大家牢记这种用法。

以串行方式执行宏

我们可以用 @a 执行刚刚录制好的宏,它将执行以下步骤:首先,把光标移到该行首个 . 字符上,把 . 改成 ),然后,将下一个单词的首字母变为大写,最后,将光标移至下一行。

我们可以调用 3 次 @a 命令完成这次任务,但运行 3@a 会更快。

让我们来看一个新的难题。假设即将要处理的文本中,会时不时地被注释行所隔断:

宏在执行到第 3 行时停了下来,没错,就是那行注释。f. 命令没有在这行发现字符 .,于是宏被中止执行了。安全捕获机制截住了我们。

以并行方式执行宏

技巧 30 展示过一种方法,在一组连续的文本行上运行 . 命令。在这里,我们也可以使用相同的技术:

我们又重新录制了宏。这一次,除了省略向下移动的 j 命令(最后一条)外,宏命令没变,因为这一次,我们不再需要它移动光标至下一行了。

:normal @a 命令指示 Vim 在高亮选区中的每一行上执行这个宏。像上次一样,宏在前两行执行成功了,但在第 3 行被中止了,但这次它并没有停在那儿,而是继续完成了任务。

决策:串行还是并行?

以并行的方式在多处执行宏更为健壮。在本例中,采用这种方法会更好。但如果宏在执行时遇到一处错误,而我们正想利用这些警告更正错误时,以串行、多次的方式执行宏可以更容易定位出问题所在。

如果宏命令的高亮选区不是那么容易框选,可以先执行串行,到了容易框选的区域执行并行,之后用串行处理剩余的行

给宏追加命令

有时候,我们在录制宏的过程中会漏掉某个至关重要的步骤。在这种情况下,我们没必要从头开始重录所有的步骤,而是可以在现有宏的结尾附加额外的命令。

我们刚一按下 q 键,停止了宏的录制,才发现应该在结束之前按一下 j 键,将光标移至下一行。

在解决此问题之前,先检查一下寄存器 a 中的内容

:reg a《

"a  0f.r)w~

在我们输入 qa 时,Vim 将开始录制接下来的按键操作,并将它们保存到寄存器 a 中,这会覆盖该寄存器原有的内容。如果我们输入的是 qA 的话,Vim 也会录制按键操作,但会把它们附加到寄存器 a 原有的内容之后。我们可以用这种方式更正该错误:

编辑宏的内容

我们已经在技巧 68 中看到,在宏的结尾添加命令非常容易。但我们如果想删除宏的最后一条命令或者在宏的开头改点什么东西,该怎么办呢?在本节中,我们将学习如何像编辑普通文本一样编辑宏的内容。

在一组文件中执行宏

用迭代求值的方式给列表编号

如果宏在每次执行时都能插入一个可变的数值,这将会很有用处。在本节中,我们将学习一种技术,它会在录制宏的时候使某个数字递增,这样一来,就可以在连续的文本上插入数字 1 到 5。

在技巧 16 中,我们已经看到可以用表达式寄存器进行简单的求和运算,并将结果插入至文档。在这里,我们只需在插入模式下运行 <C-r>=i<CR>,即可插入变量 i 的值

:normal @a 命令将指示 Vim 在高亮选中的每一行上执行这个宏(参见以并行方式执行宏)。i 的初始值是 2,但它在每次宏执行完后都会递增。最终,每行都以连续的数字开头了。

我们也可以通过复制、粘贴以及 <C-a> 命令完成同样的工作。作为练习,你不妨自己试一试。