vim跳转到函数开始([[)和转到声明(gd)的一些实现细节
intro
在vim的入门介绍中,明确说明了vim是一个"文本编辑器"(text editor)而不是一个程序编辑器,和C/C++的预处理(preprocessor)一样,vim本身并不理解程序的结构。
- Introduction intro
Vim stands for Vi IMproved. It used to be Vi IMitation, but there are so many
improvements that a name change was appropriate. Vim is a text editor which
includes almost all the commands from the Unix program "Vi" and a lot of new
ones. It is very useful for editing programs and other plain text.
程序员在使用vim的时候,又不可避免的需要知道程序的结构:复杂的包括基于语义的自动补全,简单的例如跳转到变量定义或者函数的开始。这些功能通常通过一些插件可以完成,但是有些功能在vim中也有内置的实现。尽管不是100%准确,但是大多数情况下亦可使用。这也就是通常所说的“穷人版"(poor man's)功能。
函数开始
常规缩进
在vim的motion.txt文件中,包含了一些函数界别的跳转功能:
]] [count] sections forward or to the next '{' in the
first column. When used after an operator, then also
stops below a '}' in the first column. exclusive
Note that exclusive-linewise often applies.][ [count] sections forward or to the next '}' in the
first column. exclusive
Note that exclusive-linewise often applies.[[ [count] sections backward or to the previous '{' in
the first column. exclusive
Note that exclusive-linewise often applies.[] [count] sections backward or to the previous '}' in
the first column. exclusive
Note that exclusive-linewise often applies.
尽管文档中明确说明了只会搜索位于第一列(first column)的大括弧,对于绝大部分的C语言缩进风格,这种方法都可以很好的跳转到函数的开始/结束:因为大括弧的确是在第一列。
golang
这种大括弧位于第一列在golang中并不满足,并且函数开始的大括弧必须和函数名在同一行,这样这种方法就不再适用。这一点vim文档也做了说明变通的映射方法,本质上就是执行尽可能多次数的(99)前向查找未匹配大括弧。
但是这种实现也有问题:例如C++中,可能这些函数是class的内联函数,这样就找到了类的开始。
The "]]" and "[[" commands stop at the '{' in the first column. This is
useful to find the start of a function in a C program. To search for a '}' in
the first column, the end of a C function, use "][" (forward) or "[]"
(backward). Note that the first character of the command determines the
search direction.If your '{' or '}' are not in the first column, and you would like to use "[["
and "]]" anyway, try these mappings:
:map [[ ?{w99[{
:map ][ /}b99]}
:map ]] j0[[%/{
:map [] k$][%?}
这个方案中也有一个有意思的地方:为什么执行99[{的时候要先执行字符搜索呢?经过测试可以看到一种情况:如果当前光标在字符串内部,那么此时如果直接执行“[{”会跳过当前行所有的括弧。先搜索下会跳出字符串,当然无法解决字符串有括弧问题的,而且本身意义也不大
int tsecer() {
{ { "cursor here" }}
}
java
在vim的说明中,特地强调了java类型的语言,可以通过("[m")[https://vimhelp.org/motion.txt.html#]m]之类的method跳转,那么为什么这种方法对于C++语言就不能适用了呢?
vim对于这个命令的处理位于nv_bracket_block函数。
/*
* "[{", "[(", "]}" or "])": go to Nth unclosed '{', '(', '}' or ')'
* "[#", "]#": go to start/end of Nth innermost #if..#endif construct.
* "[/", "[*", "]/", "]*": go to Nth comment start/end.
* "[m" or "]m" search for prev/next start of (Java) method.
* "[M" or "]M" search for prev/next end of (Java) method.
*/
static void
nv_bracket_block(cmdarg_T *cap, pos_T *old_pos)
{
pos_T new_pos = {0, 0, 0};
pos_T *pos = NULL; // init for GCC
pos_T prev_pos;
long n;
int findc;
int c;
if (cap->nchar == '*')
cap->nchar = '/';
prev_pos.lnum = 0;
if (cap->nchar == 'm' || cap->nchar == 'M')
{
if (cap->cmdchar == '[')
findc = '{';
else
findc = '}';
n = 9999;
}
else
{
findc = cap->nchar;
n = cap->count1;
}
for ( ; n > 0; --n)
{
if ((pos = findmatchlimit(cap->oap, findc,
(cap->cmdchar == '[') ? FM_BACKWARD : FM_FORWARD, 0)) == NULL)
{
if (new_pos.lnum == 0) // nothing found
{
if (cap->nchar != 'm' && cap->nchar != 'M')
clearopbeep(cap->oap);
}
else
pos = &new_pos; // use last one found
break;
}
prev_pos = new_pos;
curwin->w_cursor = *pos;
new_pos = *pos;
}
curwin->w_cursor = *old_pos;
// Handle "[m", "]m", "[M" and "[M". The findmatchlimit() only
// brought us to the match for "[m" and "]M" when inside a method.
// Try finding the '{' or '}' we want to be at.
// Also repeat for the given count.
if (cap->nchar == 'm' || cap->nchar == 'M')
{
// norm is TRUE for "]M" and "[m"
int norm = ((findc == '{') == (cap->nchar == 'm'));
n = cap->count1;
// found a match: we were inside a method
if (prev_pos.lnum != 0)
{
pos = &prev_pos;
curwin->w_cursor = prev_pos;
if (norm)
--n;
}
else
pos = NULL;
while (n > 0)
{
for (;;)
{
if ((findc == '{' ? dec_cursor() : inc_cursor()) < 0)
{
// if not found anything, that's an error
if (pos == NULL)
clearopbeep(cap->oap);
n = 0;
break;
}
c = gchar_cursor();
if (c == '{' || c == '}')
{
// Must have found end/start of class: use it.
// Or found the place to be at.
if ((c == findc && norm) || (n == 1 && !norm))
{
new_pos = curwin->w_cursor;
pos = &new_pos;
n = 0;
}
// if no match found at all, we started outside of the
// class and we're inside now. Just go on.
else if (new_pos.lnum == 0)
{
new_pos = curwin->w_cursor;
pos = &new_pos;
}
// found start/end of other method: go to match
else if ((pos = findmatchlimit(cap->oap, findc,
(cap->cmdchar == '[') ? FM_BACKWARD : FM_FORWARD,
0)) == NULL)
n = 0;
else
curwin->w_cursor = *pos;
break;
}
}
--n;
}
curwin->w_cursor = *old_pos;
if (pos == NULL && new_pos.lnum != 0)
clearopbeep(cap->oap);
}
if (pos != NULL)
{
setpcmark();
curwin->w_cursor = *pos;
curwin->w_set_curswant = TRUE;
#ifdef FEAT_FOLDING
if ((fdo_flags & FDO_BLOCK) && KeyTyped
&& cap->oap->op_type == OP_NOP)
foldOpenCursor();
#endif
}
}
在函数中可以看到一个类似的9999常量。这个数值是搜索到最外层的block结构的下一层(代码中的prev_pos保存整个文件级别最外层block的次一个层级)。也就是说:这种方法依赖于method之外有一个最外层的左括弧/右括弧作为定界,从光标位置找到最外层节点的上一层即认为是method的开头。这也意味着:在C++中,如果当前method外面有一个文件最顶层的大括弧,即使不是java这种方法依然有效。
总之,这个方法的实现就是找到“自光标向外的第二层括弧结构”。
以下面代码为例,当光标位于第7行时,[m向外的第二层左括弧为第3行;如果没有第一行(class tsecer {)中的左括号,在第7行执行[m则会跳转到第4行,因为它是自内向外的第2层。
1 class tsecer {
2
3 int harry(){
4 {
5 {
6 {
>> 7 "cursor here"
8 }
9 }
10 }
11 }
>> 12 }
13
变量声明
vim内置的跳转到声明的操作是gd/gD。这个文档准确/详细的描述了gd的执行流程:首先是通过'[["找到当前函数的开始(the start of the current function),如果找到则继续后向找到第一个空行,然后从该位置前向搜索第一个遇到的字符串。
这里有一个重要的细节:因为[[要求括弧在第一列,所以这种方法生效的前提是函数开始的左大括弧必须在第一列。
*gd*
gd Goto local Declaration. When the cursor is on a local
variable, this command will jump to its declaration.
First Vim searches for the start of the current
function, just like "[[". If it is not found the
search stops in line 1. If it is found, Vim goes back
until a blank line is found. From this position Vim
searches for the keyword under the cursor, like with
"*", but lines that look like a comment are ignored
(see 'comments' option).
Note that this is not guaranteed to work, Vim does not
really check the syntax, it only searches for a match
with the keyword. If included files also need to be
searched use the commands listed in |include-search|.
After this command |n| searches forward for the next
match (not backward).
文档也承认了vim并不会进行语法检查(Vim does not really check the syntax, , it only searches for a match with the keyword."。
以下面代码为例
class tsecer {
int sid;
int harry(int sid){
{
sid
}
}
}
当光标位于最后一个sid时,执行gd会找到第2行的sid。因为所有的括括弧都不在第一列,所以从文件第一行开始从后向前搜索。
引号的影响
当光标位于下面的引号内部时,执行[{会跳到函数后的左括号。这个从代码(findmatchlimit函数)看,是vim从光标位置向前搜索是,遇到第一个引号时,认为进入了字符串,所以并不会认为前面的两个左括弧匹配了"[{"的左括弧。但是到了第一行,重置了是否在引号的标志,所以认为匹配成功。当光标位于第二行引号外的时候,前向搜索引号数量是匹配的,第二个左括弧不在引号内,所以认为匹配成功。这个不知道是vim的feature还是bug,还是undefined behavior,不过问题不大。
int tsecer() {
{ { "cursor here" }}
}
outro
为了让vim内置命令处理起来更简单,最好把函数开始的大括号放在第一列。