跳到主要内容

第05章 在文件中移动

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

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

字符导航

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

h   左
j 下
k 上
l 右

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

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

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中设置:

set relativenumber number

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

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

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

对移动计数

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

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

[计数] + 移动

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

单词导航

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

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

w       移动到下一个单词的开头
W 移动到下一个词组的开头
e 移动到下一个单词的结尾
E 移动到下一个词组的结尾
b 移动到前一个单词的开头
B 移动到前一个词组的开头
ge 移动到前一个单词的结尾
gE 移动到前一个词组的结尾

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

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

const hello = "world";

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

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

当前行导航

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

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

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

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

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

回到上一个例子:

const hello = "world";

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

句子和段落导航

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

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

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

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

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)中"字符对"所指定的段落宏的每个集合开始。

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

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

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

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了解更多。

匹配导航

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

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

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

(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%跳到文件的中间。

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

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

窗格导航

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

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

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

滚动

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

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

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

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

搜索导航

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

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

假设你有一下文本:

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。 因为我经常使用这个功能,所以我会设置一个映射:

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

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

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

位置标记

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

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以外还有别的标签,其中有一些例如:

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

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

跳转

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

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

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

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

聪明地学习导航

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

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

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

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

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

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