什么是标签?标签就是一个标识符被定义的地方。一个例子就是 C 或者 C++ 程序中的函
数定义。标签列表可以保存在一个标签文件中。Vim 可以通过它来从任何地方跳转到该标
签,也就是一个标识符被定义的地方。
在当前目录下为所有的 C 文件生成标签文件,使用下面的这个命令:
ctags *.c
"ctags" 是一个独立的程序。大多数 Unix 系统上都已经安装了它。如果你还没有安装,
可以在这里找到 Universal/Exuberant ctags:
http://ctags.io
http://ctags.sf.net
推荐使用 Universal ctags,Exuberrant ctags 不再开发了。
现在你可以使用下面的命令跳转到一个函数定义的地方:
:tag startlist
这个命令会找到函数 "startlist",即使该函数是在另一个文件中。
CTRL-]
命令会跳转到当前光标下单词的标签。这样浏览毫无头绪的 C 代码会变得更
容易些。举个例子,假设你在函数 "write_block" 中。你可以看到它调用了函数
"write_line"。但 "write_line" 做了什么呢?将光标置于调用 "write_line" 的地方然
后按 CTRL-]
,你就跳转到了这个函数的定义的地方了。
"write_line" 函数调用了 "write_char"。你需要知道它做了什么。将光标定位到调
用 "write_char" 的地方然后按 CTRL-]
,你就到了定义 "write_char" 的地方。
+-------------------------------------+
|void write_block(char **s; int cnt) |
|{ |
| int i; |
| for (i = 0; i < cnt; ++i) |
| write_line(s[i]); |
|} | |
+-----------|-------------------------+
|
CTRL-]
|
| +----------------------------+
+--> |void write_line(char *s) |
|{ |
| while (*s != 0) |
| write_char(*s++); |
|} | |
+--------|-------------------+
|
CTRL-]
|
| +------------------------------------+
+--> |void write_char(char c) |
|{ |
| putchar((int)(unsigned char)c); |
|} |
+------------------------------------+
":tags" 命令显示你经过的标签列表:
:tags
# TO tag FROM line in file/text
1 1 write_line 8 write_block.c
2 1 write_char 7 write_line.c
>
现在介绍向回跳转。CTRL-T
命令跳转到上一个标签。在上例中,你会回到 "write_line"
函数调用 "write_char" 的地方。
这个命令接受一个计数参数,用来表示跳转回去的标签个数。你已经向前跳转,现在
又跳转了回去。现在我们再一次向前跳转。下面的命令跳转到标签列表中最上面的标
签:
:tag
你可以在前面加上要向前跳转的标签个数。比如: ":3tag"。CTRL-T
同样可以加上一个计
数参数。
通过这些命令,你可以用 CTRL-]
沿着调用树向前跳转,用 CTRL-T
向回跳转,用
":tags" 命令显示当前位置。
分 割 窗 口
":tag" 命令会将当前窗口的文件替换为包含新函数的文件。怎样才能同时查看两个文件
呢?你可以使用 ":split" 命令将窗口分开然后再用 ":tag" 命令。Vim 有个缩写命令可
以做到这些:
:stag tagname
使用下面的命令可以分割当前窗口并跳转到光标下的标签:
CTRL-W ]
如果指定了计数参数,新窗口将包含指定的那么多行。
多 个 标 签 文 件
如果在多个目录中都有文件,你可以在每一个目录下创建一个标签文件。Vim 只能跳转到
那个目录下的标签。
通过设定 'tags' 选项,你可以使用多个相关的标签文件。比如:
:set tags=./tags,./../tags,./*/tags
这会使 Vim 找到当前文件所在目录及其父目录和所有子目录下的标签文件。
这已经是不少的标签文件了,但也许仍不够。比如,当编辑 "~/proj/src" 目录下的
一个文件时,你无法找到 "~/proj/sub/tags" 目录下的标签文件。对这种情况,Vim 提
供了一个查找整个目录树下标签文件的方法,比如:
:set tags=~/proj/**/tags
单 个 标 签 文 件
当 Vim 在多个地方查找标签文件时,你会听到硬盘在格格作响。这样会有点慢。在这种
情况下,你最好将这些时间花在生成一个大的标签文件上。你可以要等一会儿。
这得借助上面提到的 Universal 或 Exuberant ctags 程序。它有一个选项可以搜索
整个目录树:
cd ~/proj
ctags -R .
这样做的好处是 Universal/Exuberant ctags 可以识别多种文件类型,它不仅适用于 C
和 C++ 程序,还适用于 Eiffel 甚至 Vim 脚本。请参考 ctags 文档进行调整所用参
数。
现在你只需要告诉 Vim 你的标签文件在何处:
:set tags=~/proj/tags
多 个 匹 配
当一个函数 (或类中的方法) 被定义多次,":tags" 命令会跳转到第一处。如果在当前文
件中存在匹配,那它将会被首先使用。
你现在可以跳转到同一个标签的其它匹配处:
:tnext
重复执行这个命令可以找到更多的匹配。如果存在很多匹配,你可以选择要跳转到哪一
个:
:tselect tagname
Vim 会为你展示一个选择列表:
# pri kind tag file
1 F f mch_init os_amiga.c
mch_init()
2 F f mch_init os_mac.c
mch_init()
3 F f mch_init os_msdos.c
mch_init(void)
4 F f mch_init os_riscos.c
mch_init()
Enter nr of choice (<CR>
to abort):
你现在可以输入要跳转到的匹配代号 (在第一列)。其它列的信息可以让你知道匹配在何
处被定义。
可以用这些命令在各匹配的标签间移动:
:tfirst 到第一个匹配
:[count]
tprevious 向前 [count]
个匹配
:[count]
tnext 向后 [count]
个匹配
:tlast 到最后一个匹配
如果没有指定,[count]
缺省为一。
猜 测 标 签 名
命令行补全是避免输入长标签名的好办法。只需输入开始的一部分然后按 <Tab>
:
:tag write_<Tab>
你会得到第一个匹配。如果这不是你想要的,重复输入 <Tab>
直到你找到正确的匹配。
有时你只知道一个函数名的一部分,或是你有很多以相同字符串开头而结尾不同的标
记。这时你可以告诉 Vim 使用一个模式来查找标签。
假设你要跳转到一个包含 "block" 的标签。首先输入:
:tag /block
现在再利用命令行补全功能: 输入 <Tab>
。Vim 会找到所有包含 "block" 的标签并使用
第一个匹配。
标签名前面的 "/" 告诉 Vim 这不是一个确定的标签名而是一个模式。你可以利用有
关查找模式的所有特性。举个列子,假设你要选择所有以 "write_" 开头的标签:
:tselect /^write_
"^" 指定标签以 "write_" 开头,否则在中间含有 "write_" 的标签名也会被找到。类似
地,"$" 指定标签名结尾处的匹配。
标 签 浏 览 器
CTRL-]
可以让你跳转到光标所在标识符的定义处,因此你可以利用标识符的列表来形成
一个目录。这里给出一个例子。
首先生成一个标识符列表 (需要 Universal 或 Exuberant ctags):
ctags --c-types=f -f functions *.c
现在打开 Vim 并在一个垂直分割窗口中编辑这个文件:
vim
:vsplit functions
窗口中包含一个所有函数的列表。其它的东西可以被忽略。用 ":setlocal ts=99" 命令
使其显示得更清晰些。
在这个窗口中,定义一个映射:
:nnoremap <buffer> <CR> 0ye<C-W>w:tag <C-R>"<CR>
移动光标至要跳转到函数的所在行,输入 <Enter>
。Vim 会在另一个窗口中跳转到所选择
的函数定义处。
相 关 杂 项
你可以在不改变 'tagcase' 为 "followic" 的情况下置位 'ignorecase' 选项,或者设
置 'tagcase' 为 "ignore" 来忽略标签名里的大小写。
'tagbsearch' 选项标明标签文件是否经过排序。缺省是假定为标签文件已排序,这样会
使查找更快,但如果文件没有被排序是无法工作的。
'taglength' 选项可用来告诉 Vim 标签的有效字符个数。
Cscope 是一个自由软件。它不仅可以找到一个标识符被声明的地方,还可以找到标识符
被使用的地方。请参考 cscope 。
当编辑含有函数调用的代码时,你需要使用正确的调用参数。要获知所要传递的值,你可
以查看这个函数是如何定义的。标签机制对此十分适用。如果定义可在另一个窗口内显示
那就更好了。对此我们可以利用预览窗口。
打开一个预览窗口来显示函数 "write_char":
:ptag write_char
Vim 会打开一个窗口,跳转到 "write_char" 标签。然后它会回到原来的位置。这样你可
以继续输入而不必使用 CTRL-W
命令。
如果函数名出现在文本中,你可以用下面的命令在预览窗口中得到其定义:
CTRL-W }
有一个脚本可以自动显示光标处的标签定义。请参考 CursorHold-example 。
用下面的命令关闭预览窗口:
:pclose
要在预览窗口中编辑一个指定的文件,用 ":pedit"。这在编辑头文件时很有用,比
如:
:pedit defs.h
最后,"psearch" 可用来查找当前文件和任何包含文件中的单词并在预览窗口中显示匹
配。这在使用没有标签文件的库函数时十分有用。例如:
:psearch popen
这会在预览窗口中显示含有 popen() 原型的 "stdio.h" 文件:
FILE *popen __P((const char *, const char *));
你可以用 'previewheight' 选项指定预览窗口打开时的高度。
因为程序代码是结构化的,Vim 可以识别其中的有关项目。一些特定的命令可用来完成相
关的移动。
C 程序中经常包含类似下面的代码:
#ifdef USE_POPEN
fd = popen("ls", "r")
#else
fd = fopen("tmp", "w")
#endif
有时会更长,也许还有嵌套。将光标置于 "#ifdef" 处按 %。Vim 会跳转到 "#else"。继
续按 % 会跳转到 "#endif"。再次按下 % 又回到原来的 "#ifdef"。
当代码嵌套时,Vim 会找到相匹配的项目。这是检查你是否忘记了一个 "#endif" 的
好办法。
当你在一个 "#ifdef" - "#endif" 块内的某个位置,你可以用下面的命令回到开始
处:
[#
如果你的位置不是在 "#if" 或 "#ifdef" 之后,Vim 会鸣音。用下面命令可以跳转到下
一个 "#else" 或 "#endif":
]#
这两个命令会跳过它所经过的 "#if" - "#endif" 块。
例如:
#if defined(HAS_INC_H)
a = a + inc();
# ifdef USE_THEME
a += 3;
# endif
set_width(a);
如果光标在最后一行,"[#" 会移动到第一行。中间的 "#ifdef" - "#endif" 块被跳过。
在 代 码 块 内 移 动
C 代码块包含在 {}
中,有时一个代码会很长。要跳转到外部代码块的开始处,用 "[["
命令。用 "][" 找到结尾处。(前提是 "{" 和 "}" 都在第一列。)
"[{" 命令跳转到当前代码块的开始处。它会跳过同一级别的 {}
对。"]}" 跳转到结
尾。
一点概述:
function(int a)
+-> {
| if (a)
| +-> {
[[ | | for (;;) --+
| | +-> { |
| [{ | | foo(32); | --+
| | [{ | if (bar(a)) --+ | ]} |
+-- | +-- break; | ]} | |
| } <-+ | | ][
+-- foobar(a) | |
} <-+ |
} <-+
当编写 C++ 或 Java 代码时,外部代码块是类,而下一级的 {}
是方法。在类内部用
"[m" 可以找到前一个方法的开始。"]m" 会找到下一个方法的开始。
另外,"[]" 反向移动到前一个函数的结尾,"]]" 正向移动到下一个函数的开始。函数的
结尾指的是处在第一列的 "}"。
int func1(void)
{
return 1;
+----------> }
|
[] | int func2(void)
| +-> {
| [[ | if (flag)
start +-- +-- return flag;
| ][ | return 2;
| +-> }
]] |
| int func3(void)
+----------> {
return 3;
}
不要忘了你还可以用 "%" 在匹配的 ()、{}
和 [] 间移动。这在它们相距很多行时仍然
适用。
在 括 号 内 移 动
"[(" 和 "])" 命令与 "[{" 和 "]}" 类似,只不过它们适用于 () 对而不是 {}
对。
[(
<--------------------------------
<-------
if (a == b && (c == d || (e > f)) && x > y)
-------------->
-------------------------------->
])
在 注 释 间 移 动
移动到一个注释的开始用 "[/";向前移动到注释的结尾用 "]/"。这只对 /* - */ 注释
有效。
+-> +-> /*
| [/ | * A comment about --+
[/ | +-- * wonderful life. | ]/
| */ <-+
|
+-- foo = bar * 3; --+
| ]/
/* a short comment */ <-+
你在编辑一个 C 程序,想要知道一个变量是被声明为 "int" 还是 "unsigned"。一个快
速的方法是使用 "[I" 命令来查找。
假设光标在单词 "column" 处。输入:
[I
Vim 会列出它所找出的匹配行,不仅在当前文件内查找,还会在所有的包含文件中查找。
结果如下所示:
structs.h
1: 29 unsigned column; /* column number */
相对使用标签文件或预览窗口的好处是包含文件也被搜索。大多数情况下都能找到正确
的声明。即使标签文件已经过期或者你没有为包含文件建立标签也不会影响结果。
但是一些准备工作是必要的,否则 "[I" 就没法工作。首先,'include' 选项必须指
定文件是如何被包含的。缺省值适用于 C 和 C++。对其它的语言,你需要自己设定。
定 位 包 含 文 件
Vim 会找到 'path' 选项指定路径中的包含文件。如果缺少某个目录,一些包含文件
将不会被找到。你可以用这个命令来查看:
:checkpath
它会列出不能找到的包含文件,以及被找到的包含文件。一个输出样例:
--- Included files not found in path ---
<io.h>
vim.h -->
<functions.h>
<clib/exec_protos.h>
文件 "io.h" 被当前文件包含但无法找到。"vim.h" 可以找到,这样 ":checkpath" 跟进
这个文件并检查其中的包含文件。结果显示无法找到 "vim.h" 包含的 "functions.h" 和
"clib/exec_protos.h" 文件。
备注
:
Vim 不是一个编译器。它无法识别 "#ifdef" 语句。这就是说所有的
"#include" 语句都会被使用,即使它在 "#if NEVER" 之后。
给 'path' 选项增加一个目录可以修正无法找到文件的错误。一个好的参考是
Makefile。注意
那些包括 "-I" 的条目,比如 "-I/usr/local/X11"。要增加这个目录,
用:
:set path+=/usr/local/X11
如果有很多的子目录,你可以用 "*" 通配符。例如:
:set path+=/usr/*/include
这会找到 "/usr/local/include" 以及 "/usr/X11/include" 目录下的文件。
如果你的工程项目的包含文件都在一个嵌套的目录树下,"**" 就非常有用。它会搜索所
有的子目录。例如:
:set path+=/projects/invent/**/include
这会找到这些目录下的文件:
/projects/invent/include
/projects/invent/main/include
/projects/invent/main/os/include
等等
还有其它的可能性。更多信息,请查看 'path' 选项。
如果你想查看找到的包含文件,用这个命令:
:checkpath!
你会得到一个 (很长) 的包含文件列表。为使它更短些,Vim 会对已经找到的文件显示
"(Already listed)" 而不再重新显示一遍。
跳 转 到 匹 配
"[I" 产生一个每项只有一行文本的列表。如果你想要进一步地查看第一项,你可以用这
个命令来跳转:
[<Tab>
你也可以使用 "[ CTRL-I
",因为 CTRL-I
和按 <Tab>
效果一样。
"[I" 产生的列表在每行的开头都有一个序号。如果你要跳转到第一项外的其它项,首先
输入序号:
3[<Tab>
会跳转到列表中的第三项。记住你可以用 CTRL-O
跳回到原来的地方。
相 关 命 令
[i 只列出第一项匹配
]I 只列出光标下面的项目
]i 只列出光标下面的第一项匹配
查 找 宏 定 义 标 识 符
"[I" 命令查找任何标识符。只查找 "#define" 定义的宏,用:
[D
同样,这会在所有的包含文件中查找。'define' 选项指定 "[D" 所查找的项目定义行的
模式。你需要改变它的值来适用于 C 或 C++ 以外的语言。
"[D" 相关命令:
[d 只列出第一项匹配
]D 只列出光标下面的项目
]d 只列出光标下面的第一项匹配
"[I" 命令查找所有的包含文件。要在当前文件中查找并跳转到光标处单词被首次使用的
地方,用:
gD
提示: Goto Definition。这个命令对查找局部 (C 语言中的 "static") 声明的变量或函
数很有用。例如 (光标在 "counter" 处):
+-> static int counter = 0;
|
| int get_counter(void)
gD | {
| ++counter;
+-- return counter;
}
要进一步的缩小查找范围,只在当前函数内查找,用这个命令:
gd
这会回到当前函数的开始处寻找光标处单词首次出现的地方。实际上,它是向后找到一个
在第一列为 '{' 的上方的空行,然后再从那里开始正向查找标识符。例如 (光标位于
idx 上):
int find_entry(char *name)
{
+-> int idx;
|
gd | for (idx = 0; idx < table_len; ++idx)
| if (strcmp(table[idx].name, name) == 0)
+-- return idx;
}