vim中重复及redo、undo实现
零、问题
在vim中有一个,英文句号(.)能够重复前一个执行的命令,在vim的帮助文档中可以看到下面的描述
. Repeat last change, with count replaced with [count].
Also repeat a yank command, when the 'y' flag is
included in 'cpoptions'. Does not repeat a
command-line command.
这个命令看起来非常好用,比方说在C语言中,给一个变量加上const或者将成员引用修改为指针引用等,简单重复可以使用该命令完成。并且它的位置也非常顺手,就在L键的下面,可以很快定位到。
那么,这个“单次重复”具体会重复多少/哪些内容呢?
一、dot命令执行的函数
为了更具体详细的说明该问题,可以看到在读取redo记录的时候,系统中只有两个redobuff实例,不存在链表记录所有的redo操作(那么这里就有一个很有意思的问题了:在vim中执行undo操作的时候,为什么可以恢复那么多步骤的操作?)。这个redobuff被重做的时候,会(几乎原封不动)的以字符串的形式重新传递给核心层进行执行。举个栗子:当在normal模式下执行ciw来修改一个单词,然后编辑输入hello wolrd字符串,之后按下“esc”键并执行dot命令,那么redo buffer中保存的就是"ciwhelloworld\033",下面是从gdb中看到的内容
(gdb) p/s (const char*)redobuff.bh_first.b_next->b_str
$15 = 0xb61f28 "ciwhello world\033"
/*
* "." command: redo last change.
*/
static void
nv_dot(cmdarg_T *cap)
{
……
if (start_redo(cap->count0, restart_edit != 0 && !arrow_used) == FAIL)
……
}
int
start_redo(long count, int old_redo)
{
int c;
/* init the pointers; return if nothing to redo */
if (read_redo(TRUE, old_redo) == FAIL)
return FAIL;
……
}
static int
read_redo(int init, int old_redo)
{
……
if (old_redo)
bp = old_redobuff.bh_first.b_next;
else
bp = redobuff.bh_first.b_next;
……
}
二、redobuffer的内容什么时候开始
从实现上来看,这个buffer的操作还是很简单的,只要通过调用ResetRedobuff接口即可完成redobuffer的重置。当业务需要开始新一轮的redo记录时,需要先调用这个接口。在prep_redo函数中会先清空redobuffer,然后将此次命令中所有的内容都保存到redobuffer中。对于前面的“diw”例子,这个prep_redo调用会将三个字符都保存到redobuffer中。
void
do_pending_operator(cmdarg_T *cap, int old_col, int gui_yank)
{
……
/* Only redo yank when 'y' flag is in 'cpoptions'. */
/* Never redo "zf" (define fold). */
if ((vim_strchr(p_cpo, CPO_YANK) != NULL || oap->op_type != OP_YANK)
&& ((!VIsual_active || oap->motion_force)
/* Also redo Operator-pending Visual mode mappings */
|| (VIsual_active && cap->cmdchar == ':'
&& oap->op_type != OP_COLON))
&& cap->cmdchar != 'D'
#ifdef FEAT_FOLDING
&& oap->op_type != OP_FOLD
&& oap->op_type != OP_FOLDOPEN
&& oap->op_type != OP_FOLDOPENREC
&& oap->op_type != OP_FOLDCLOSE
&& oap->op_type != OP_FOLDCLOSEREC
&& oap->op_type != OP_FOLDDEL
&& oap->op_type != OP_FOLDDELREC
#endif
)
{
prep_redo(oap->regname, cap->count0,
get_op_char(oap->op_type), get_extra_op_char(oap->op_type),
oap->motion_force, cap->cmdchar, cap->nchar);
if (cap->cmdchar == '/' || cap->cmdchar == '?') /* was a search */
{
/*
* If 'cpoptions' does not contain 'r', insert the search
* pattern to really repeat the same command.
*/
if (vim_strchr(p_cpo, CPO_REDO) == NULL)
AppendToRedobuffLit(cap->searchbuf, -1);
AppendToRedobuff(NL_STR);
}
else if (cap->cmdchar == ':')
{
/* do_cmdline() has stored the first typed line in
* "repeat_cmdline". When several lines are typed repeating
* won't be possible. */
if (repeat_cmdline == NULL)
ResetRedobuff();
else
{
AppendToRedobuffLit(repeat_cmdline, -1);
AppendToRedobuff(NL_STR);
VIM_CLEAR(repeat_cmdline);
}
}
}
……
}
三、编辑模式下字符添加到redobuffer
在编辑模式下输入的内容同样需要记录下来,例如前面例子中输入的“hello world”这个输入。这个从实现上看其实也比较简单,因为vim是以raw模式读取的,虽然是edit模式,但是用户输入的每个字符,vim同样会经过特殊处理的(否则也不能在edit模式下通过通过 ctrl-r ctrl-r reg来在插入模式下将reg寄存器中的值插入到当前位置。关于insert模式下的快捷键组合可以通过 h insert.txt 命令查看,或者通过h i_CTRL-R_CTRL-R 查看该命令的具体功能)。edit()===>>>insert_special()===>>>insertchar()===>>>AppendCharToRedobuff(),可以看到在编辑模式下,每个输入的按键都会被追加到redobuffer中,和前面的命令连接在一起组成redobuffer。
void
AppendCharToRedobuff(int c)
{
if (!block_redo)
add_char_buff(&redobuff, c);
}
四、redobuffer在edit模式下内容的重置
在执行A、I之类的命令时,它们虽然不是operator(因为它们不接受motion参数),但是它们也会被当做特殊命令会被添加到redobuffer中。
int
edit(
int cmdchar,
int startln, /* if set, insert at start of line */
long count)
{
……
if (cmdchar != NUL && restart_edit == 0)
{
ResetRedobuff();
AppendNumberToRedobuff(count);
#ifdef FEAT_VREPLACE
if (cmdchar == 'V' || cmdchar == 'v')
{
/* "gR" or "gr" command */
AppendCharToRedobuff('g');
AppendCharToRedobuff((cmdchar == 'v') ? 'r' : 'R');
}
else
#endif
{
if (cmdchar == K_PS)
AppendCharToRedobuff('a');
else
AppendCharToRedobuff(cmdchar);
if (cmdchar == 'g') /* "gI" command */
AppendCharToRedobuff('I');
else if (cmdchar == 'r') /* "r<CR>" command */
count = 1; /* insert only one <CR> */
}
}
……
}
五、undo的处理
前面提到一个问题:如果redobuffer只有一份,那么vim是如何实现多个undo列表的呢?在vim的源代码中可以看到有一个专门的undo.c文件夹,文件开始有关于undo列表的一些注释。但是,更关键的是undo使用的数据结构,每个undo是一个
/*
* structures used for undo
*/
typedef struct u_entry u_entry_T;
typedef struct u_header u_header_T;
struct u_entry
{
u_entry_T *ue_next; /* pointer to next entry in list */
linenr_T ue_top; /* number of line above undo block */
linenr_T ue_bot; /* number of line below undo block */
linenr_T ue_lcount; /* linecount when u_save called */
char_u **ue_array; /* array of lines in undo block */
long ue_size; /* number of lines in ue_array */
#ifdef U_DEBUG
int ue_magic; /* magic number to check allocation */
#endif
};
结构,这个结构中保存的是修改之前的文本内容(里面甚至没有精确到列,而是保存了整行的内容)。也就是当我们在“hello”文本上执行“diw”命令的时候,undo中记录的并不是这个命令,而是hello这个文本的内容。当执行undo的时候,vim把这个保存的文本内容拷贝回去即可。这样做可以避免为每个操作记录对应的undo。也就是说,这里并不是基于命令字的undo,而是基于内容的undo(这个跟《设计模式》中的概念并不相同)。
int
u_savecommon(
linenr_T top,
linenr_T bot,
linenr_T newbot,
int reload)
{
……
for (i = 0, lnum = top + 1; i < size; ++i)
{
fast_breakcheck();
if (got_int)
{
u_freeentry(uep, i);
return FAIL;
}
if ((uep->ue_array[i] = u_save_line(lnum++)) == NULL)
{
u_freeentry(uep, i);
goto nomem;
}
}
……
}
真正整行的保存代码为
/*
* u_save_line(): allocate memory and copy line 'lnum' into it.
* Returns NULL when out of memory.
*/
static char_u *
u_save_line(linenr_T lnum)
{
return vim_strsave(ml_get(lnum));
}
这里实现上的启示是
1、如果在大多数情况下,通过增加额外存储空间来减少逻辑复杂度,让实现更加简洁,这种实现是值得尝试。在这里,不精确到一行的某一列,可以极大的减少undo实现的复杂度,代码实现更加简洁。
2、undo并不一定是理想状态下的和每个操作对应的实现一个对应内容,而是可以全量保存原始内容。这种实现看起来并不是效率最高的,但是的确简单、健壮。MySQL中的某些undo可能也是这么实现的。
六、vim中“inner”"a"修饰符如何生效的
在vim中,通过i表示一个inner修饰,使用起来是非常方便的,但是在某些vim的版本中就没有这个功能。今天在看代码的时候可以看到,这个功能在vim中叫做textobject,主要实现代码在
/*
* "a" or "i" while an operator is pending or in Visual mode: object motion.
*/
static void
nv_object(
cmdarg_T *cap)
{
int flag;
int include;
char_u *mps_save;
if (cap->cmdchar == 'i')
include = FALSE; /* "ix" = inner object: exclude white space */
else
include = TRUE; /* "ax" = an object: include white space */
/* Make sure (), [], {} and <> are in 'matchpairs' */
mps_save = curbuf->b_p_mps;
curbuf->b_p_mps = (char_u *)"(:),{:},[:],<:>";
switch (cap->nchar)
{
case 'w': /* "aw" = a word */
flag = current_word(cap->oap, cap->count1, include, FALSE);
break;
case 'W': /* "aW" = a WORD */
flag = current_word(cap->oap, cap->count1, include, TRUE);
break;
case 'b': /* "ab" = a braces block */
case '(':
case ')':
flag = current_block(cap->oap, cap->count1, include, '(', ')');
break;
case 'B': /* "aB" = a Brackets block */
case '{':
case '}':
flag = current_block(cap->oap, cap->count1, include, '{', '}');
break;
case '[': /* "a[" = a [] block */
case ']':
flag = current_block(cap->oap, cap->count1, include, '[', ']');
break;
case '<': /* "a<" = a <> block */
case '>':
flag = current_block(cap->oap, cap->count1, include, '<', '>');
break;
case 't': /* "at" = a tag block (xml and html) */
/* Do not adjust oap->end in do_pending_operator()
* otherwise there are different results for 'dit'
* (note leading whitespace in last line):
* 1) <b> 2) <b>
* foobar foobar
* </b> </b>
*/
cap->retval |= CA_NO_ADJ_OP_END;
flag = current_tagblock(cap->oap, cap->count1, include);
break;
case 'p': /* "ap" = a paragraph */
flag = current_par(cap->oap, cap->count1, include, 'p');
break;
case 's': /* "as" = a sentence */
flag = current_sent(cap->oap, cap->count1, include);
break;
case '"': /* "a"" = a double quoted string */
case '\'': /* "a'" = a single quoted string */
case '`': /* "a`" = a backtick quoted string */
flag = current_quote(cap->oap, cap->count1, include,
cap->nchar);
break;
#if 0 /* TODO */
case 'S': /* "aS" = a section */
case 'f': /* "af" = a filename */
case 'u': /* "au" = a URL */
#endif
default:
flag = FAIL;
break;
}
curbuf->b_p_mps = mps_save;
if (flag == FAIL)
clearopbeep(cap->oap);
adjust_cursor_col();
curwin->w_set_curswant = TRUE;
}
七、edit模式下的单次命令模式
在edit模式下,有时候我们只是希望先暂时切换会normal模式,在其中执行一个简单命令(例如删除当前字符),此时通常是通过esc退回到normal模式,输入dw删除当前单词。但是在insert模式下,可以通过ctrl-O进入normal模式下单条命令模式,从而可以暂时回退到normal模式并执行一个normal下的命令,然后自动切回到当前的编辑模式(这种操作比esc+操作+insert少了一个步骤,并且ctrl-O也比较方便)。
这个和normal模式下的ctrl-o意义不同,在normal模式下,ctrl-o对应的是返回前一个位置。
可以通过h i_CTRL-O查看该模式的帮助,该命令本身描述比较简单:
CTRL-O execute one command, return to Insert mode i_CTRL-O