vim跳转到函数开始([[)和转到声明(gd)的一些实现细节

intro

在vim的入门介绍中,明确说明了vim是一个"文本编辑器"(text editor)而不是一个程序编辑器,和C/C++的预处理(preprocessor)一样,vim本身并不理解程序的结构。

  1. 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内置命令处理起来更简单,最好把函数开始的大括号放在第一列。

posted on 2024-09-23 21:03  tsecer  阅读(154)  评论(0编辑  收藏  举报

导航