从yank put看vim寄存器

寄存器

在软件开发过程中,Ctrl-C和Ctrl-V是程序员的核心技能,这就不可避免的涉及到复制,粘贴,删除。在windows环境下,大家习惯了只有一个系统剪切板,复制之后直接粘贴还是比较方便。在vim环境下,有更多的寄存器可以选择:26个字母(大小写分别对应不同用途),还有0——9共10个数字对应的寄存器。

尽管有这么多寄存器,通常还是更习惯于使用一个寄存器的模式,就是直接拷贝了之后粘贴。一方面是由于习惯windows下的编辑模式,另一方面一直使用默认寄存器是最方便的,使用其他寄存器需要使用的双引号需要使用右手的小拇指操作,而刚好人类的右手小拇指又是最弱的。

特别是最为典型的场景中,通常需要先yank,然后删除掉待替换的内容。但是在这个过程中,由于删除也会覆盖缺省寄存器(")的内容,所以当执行paste的时候,惊喜的发现替换了个寂寞。

这个时候可以使用专门用来解决这个问题的black hole寄存器(),但是操作也比较麻烦,无论是寄存器的引导符(“),还是black hole寄存器的标识符()都是小手指的责任,还有复杂的shift按键。这些对于心灵手巧的键盘侠来说可能没有问题,但是像我这样的使用者还是觉得很麻烦。

另外,由于yank的内容始终在0寄存器中,所以使用0寄存器也可以。

vim有简洁、完整、详细的内置帮助文档,通过文档可以知道,通过命令行的registers(或者简写reg或者通过display命令)可以列出所有寄存器的内容

2. Numbered registers "0 to "9		quote_number quote0 quote1
					quote2 quote3 quote4 quote9
Vim fills these registers with text from yank and delete commands.
   Numbered register 0 contains the text from the most recent yank command,
unless the command specified another register with ["x].
   Numbered register 1 contains the text deleted by the most recent delete or
change command, unless the command specified another register or the text is
less than one line (the small delete register is used then).  An exception is
made for the delete operator with these movement commands: %, (, ), `,
/, ?, n, N, { and }.  Register "1 is always used then (this is Vi
compatible).  The "- register is used as well if the delete is within a line.
Note that these characters may be mapped.  E.g. % is mapped by the matchit
plugin.
   With each successive deletion or change, Vim shifts the previous contents
of register 1 into register 2, 2 into 3, and so forth, losing the previous
contents of register 9.
{Vi: numbered register contents are lost when changing files; register 0 does
not exist}

3. Small delete register "-				quote_- quote-
This register contains text from commands that delete less than one line,
except when the command specifies a register with ["x].

便捷方法

下面是一个更简洁的解决方案,因为使用的都是常规操作。

after you yw on NAME and moved to name you can do:

viwp

效果

使用可视化模式下put之后会发现其效果并没有想象的那么好:主要是以为当执行了put之后,无名寄存器(unnamed register)的内容依然会被修改。也就是同样的操作只能使用一次:当使用yiw拷贝了一个文件的内容,然后执行viwp将拷贝的寄存器粘贴到选中内容,下次到另一个单词执行viwp的时候发现put的内容已经不是第一次yank的内容了。

好在同样是vim的在线帮助文档非常完善,通过v_p的帮助文档可以直接找到原因:

                                                put-Visual-mode v_p v_P         
When using a put command like p or P in Visual mode, Vim will try to            
replace the selected text with the contents of the register.  Whether this      
works well depends on the type of selection and the type of the text in the     
register.  With blockwise selection it also depends on the size of the block    
and whether the corners are on an existing character.  (Implementation detail:  
it actually works by first putting the register after the selection and then    
deleting the selection.)                                                        
The previously selected text is put in the unnamed register.  If you want to    
put the same text into a Visual selection several times you need to use         
another register.  E.g., yank the text to copy, Visually select the text to                                                                                                                
replace and use "0p .  You can repeat this as many times as you like, the       
unnamed register will be changed each time.                                     
                                                                                
When you use a blockwise Visual mode command and yank only a single line into   
a register, a paste on a visual selected area will paste that single line on    
each of the selected lines (thus replacing the blockwise selected region by a   
block of the pasted line).          

帮助文档明确说明:当执行visual put的时候,选中的内容会被覆盖到无名寄存器中(The previously selected text is put in the unnamed register);同时,由于yank的内容始终是放在寄存器0中,并且不会被v_p修改,所以可以通过明明确指定put的源寄存器为0(yank the text to copy, Visually select the text to replace and use "0p)。

也就是为操作的“可持续性”,最好还是明确指定寄存器0,但是如果这样的话,就没有必要使用visual模式先选择了,直接diw删除掉内容,然后"0p即可。

为什么visual模式下put会修改unnamed register

  • 什么是unnamed register

unnamed register包含某些操作(d/c/s/x/y等)命令操作的文本内容。在没有指定操作寄存器的时候,删除类的内容放在small deletion(-)寄存器中;yank的内容放在0寄存器中。其中的0和-都是实实在在的数据结构,而unnamed寄存器可以认为是一个虚拟的寄存器,从代码上看它是一个“指针”,指向上次操作(x/d/y/..)等一类操作的内容。它的意义在于:当执行put操作时如果没有指定寄存器则从unnamed寄存器中取。由于这个指针指向的是多个操作的结果,所以多个操作(从逻辑上讲)都可以修改这个结果。

1. Unnamed register ""                          quote_quote quotequote          
Vim fills this register with text deleted with the "d", "c", "s", "x" commands  
or copied with the yank "y" command, regardless of whether or not a specific    
register was used (e.g.  "xdd).  This is like the unnamed register is pointing  
to the last used register.  Thus when appending using an uppercase register     
name, the unnamed register contains the same text as the named register.        
An exception is the '_' register: "_dd does not store the deleted text in any   
register.                                                                       
Vim uses the contents of the unnamed register for any put command (p or P)      
which does not specify a register.  Additionally you can access it with the     
name '"'.  This means you have to type two double quotes.  Writing to the ""    
register writes to register "0.                                                 
{Vi: register contents are lost when changing files, no '"'} 

vim代码中的unnamed寄存器是y_previous指针指向的内容,它本身不对应一个“实体的”yankreg_T结构。

/*
 * Number of registers.
 *	0 = unnamed register, for normal yanks and puts
 *   1..9 = registers '1' to '9', for deletes
 * 10..35 = registers 'a' to 'z'
 *     36 = delete register '-'
 *     37 = Selection register '*'. Only if FEAT_CLIPBOARD defined
 *     38 = Clipboard register '+'. Only if FEAT_CLIPBOARD and FEAT_X11 defined
 */
/*
 * Symbolic names for some registers.
 */
#define DELETION_REGISTER	36
#ifdef FEAT_CLIPBOARD
# define STAR_REGISTER		37
#  ifdef FEAT_X11
#   define PLUS_REGISTER	38
#  else
#   define PLUS_REGISTER	STAR_REGISTER	    /* there is only one */
#  endif
#endif
#ifdef FEAT_DND
# define TILDE_REGISTER		(PLUS_REGISTER + 1)
#endif

#ifdef FEAT_CLIPBOARD
# ifdef FEAT_DND
#  define NUM_REGISTERS		(TILDE_REGISTER + 1)
# else
#  define NUM_REGISTERS		(PLUS_REGISTER + 1)
# endif
#else
# define NUM_REGISTERS		37
#endif

/*
 * Each yank register has an array of pointers to lines.
 */
typedef struct
{
    char_u	**y_array;	/* pointer to array of line pointers */
    linenr_T	y_size;		/* number of lines in y_array */
    char_u	y_type;		/* MLINE, MCHAR or MBLOCK */
    colnr_T	y_width;	/* only set if y_type == MBLOCK */
#ifdef FEAT_VIMINFO
    time_t	y_time_set;
#endif
} yankreg_T;

static yankreg_T	y_regs[NUM_REGISTERS];

static yankreg_T	*y_current;	    /* ptr to current yankreg */
static int		y_append;	    /* TRUE when appending */
static yankreg_T	*y_previous = NULL; /* ptr to last written yankreg */
/*
 * Set y_current and y_append, according to the value of "regname".
 * Cannot handle the '_' register.
 * Must only be called with a valid register name!
 *
 * If regname is 0 and writing, use register 0
 * If regname is 0 and reading, use previous register
 *
 * Return TRUE when the register should be inserted literally (selection or
 * clipboard).
 */
    int
get_yank_register(int regname, int writing)
{
    int	    i;
    int	    ret = FALSE;

    y_append = FALSE;
    if ((regname == 0 || regname == '"') && !writing && y_previous != NULL)
    {
	y_current = y_previous;
	return ret;
    }
    i = regname;
    if (VIM_ISDIGIT(i))
	i -= '0';
  • delete和yank

阅读vim的代码可以发现,所谓的delete其实是先执行yank,然后再执行删除;或者说,delete操作是在yank之后额外增加了内容的删除操作。这个说起来平平无奇,但是关键的一点是在执行delete之前会现在还行相同的yank操作,从而delete和yank可以执行相同的寄存器操作逻辑

/*
 * Handle a delete operation.
 *
 * Return FAIL if undo failed, OK otherwise.
 */
    int
op_delete(oparg_T *oap)
{
///...
    /*
     * Do a yank of whatever we're about to delete.
     * If a yank register was specified, put the deleted text into that
     * register.  For the black hole register '_' don't yank anything.
     */
    if (oap->regname != '_')
    {
	if (oap->regname != 0)
	{
	    /* check for read-only register */
	    if (!valid_yank_reg(oap->regname, TRUE))
	    {
		beep_flush();
		return OK;
	    }
	    get_yank_register(oap->regname, TRUE); /* yank into specif'd reg. */
	    if (op_yank(oap, TRUE, FALSE) == OK)   /* yank without message */
		did_yank = TRUE;
	}

	/*
	 * Put deleted text into register 1 and shift number registers if the
	 * delete contains a line break, or when a regname has been specified.
	 * Use the register name from before adjust_clip_reg() may have
	 * changed it.
	 */
	if (orig_regname != 0 || oap->motion_type == MLINE
				   || oap->line_count > 1 || oap->use_reg_one)
	{
	    shift_delete_registers();
	    if (op_yank(oap, TRUE, FALSE) == OK)
		did_yank = TRUE;
	}

	/* Yank into small delete register when no named register specified
	 * and the delete is within one line. */
	if ((
#ifdef FEAT_CLIPBOARD
	    ((clip_unnamed & CLIP_UNNAMED) && oap->regname == '*') ||
	    ((clip_unnamed & CLIP_UNNAMED_PLUS) && oap->regname == '+') ||
#endif
	    oap->regname == 0) && oap->motion_type != MLINE
						      && oap->line_count == 1)
	{
	    oap->regname = '-';
	    get_yank_register(oap->regname, TRUE);
	    if (op_yank(oap, TRUE, FALSE) == OK)
		did_yank = TRUE;
	    oap->regname = 0;
	}
...
}

在更底层的、delete/yank都要调用的get_yank_register函数中,更新了y_previous这个虚拟寄存器的内容。


/*
 * Set y_current and y_append, according to the value of "regname".
 * Cannot handle the '_' register.
 * Must only be called with a valid register name!
 *
 * If regname is 0 and writing, use register 0
 * If regname is 0 and reading, use previous register
 *
 * Return TRUE when the register should be inserted literally (selection or
 * clipboard).
 */
    int
get_yank_register(int regname, int writing)
{
    int	    i;
    int	    ret = FALSE;

    y_append = FALSE;
    if ((regname == 0 || regname == '"') && !writing && y_previous != NULL)
    {
	y_current = y_previous;
	return ret;
    }
    i = regname;
    if (VIM_ISDIGIT(i))
	i -= '0';
    else if (ASCII_ISLOWER(i))
	i = CharOrdLow(i) + 10;
    else if (ASCII_ISUPPER(i))
    {
	i = CharOrdUp(i) + 10;
	y_append = TRUE;
    }
    else if (regname == '-')
	i = DELETION_REGISTER;
#ifdef FEAT_CLIPBOARD
    /* When selection is not available, use register 0 instead of '*' */
    else if (clip_star.available && regname == '*')
    {
	i = STAR_REGISTER;
	ret = TRUE;
    }
    /* When clipboard is not available, use register 0 instead of '+' */
    else if (clip_plus.available && regname == '+')
    {
	i = PLUS_REGISTER;
	ret = TRUE;
    }
#endif
#ifdef FEAT_DND
    else if (!writing && regname == '~')
	i = TILDE_REGISTER;
#endif
    else		/* not 0-9, a-z, A-Z or '-': use register 0 */
	i = 0;
    y_current = &(y_regs[i]);
    if (writing)	/* remember the register we write into for do_put() */
	y_previous = y_current;
    return ret;
}
  • 为什么viwp会修改unnamed register

其实visual模式下的put文档明确说明了这个现象以及问题的原因,甚至补充了实现的细节是先把寄存器内容放到选择区之后之后再删除选择内容(it actually works by first putting the register after the selection and then deleting the selection)。

                                                put-Visual-mode v_p v_P                                                                                                                    
When using a put command like p or P in Visual mode, Vim will try to            
replace the selected text with the contents of the register.  Whether this      
works well depends on the type of selection and the type of the text in the     
register.  With blockwise selection it also depends on the size of the block    
and whether the corners are on an existing character.  (Implementation detail:  
it actually works by first putting the register after the selection and then    
deleting the selection.)                                                        
The previously selected text is put in the unnamed register.  If you want to    
put the same text into a Visual selection several times you need to use         
another register.  E.g., yank the text to copy, Visually select the text to     
replace and use "0p .  You can repeat this as many times as you like, the       
unnamed register will be changed each time.                         

但是从代码上看并非如此:在visual模式下是先删除选择内容,然后再将寄存器内容put。只是在执行d操作之前先保存可能修改的寄存器内容,在执行d之后再恢复,在put之后再次恢复,从效果上看好像是执行d操作修改的内容被yank到了unnamed寄存器。

/*
 * "P", "gP", "p" and "gp" commands.
 */
    static void
nv_put(cmdarg_T *cap)
{
///...
	if (VIsual_active)
	{
	    /* Putting in Visual mode: The put text replaces the selected
	     * text.  First delete the selected text, then put the new text.
	     * Need to save and restore the registers that the delete
	     * overwrites if the old contents is being put.
	     */
	    was_visual = TRUE;
	    regname = cap->oap->regname;
#ifdef FEAT_CLIPBOARD
	    adjust_clip_reg(&regname);
#endif
	   if (regname == 0 || regname == '"'
				     || VIM_ISDIGIT(regname) || regname == '-'
#ifdef FEAT_CLIPBOARD
		    || (clip_unnamed && (regname == '*' || regname == '+'))
#endif

		    )
	    {
		/* The delete is going to overwrite the register we want to
		 * put, save it first. */
		reg1 = get_register(regname, TRUE);
	    }

	    /* Now delete the selected text. */
	    cap->cmdchar = 'd';
	    cap->nchar = NUL;
	    cap->oap->regname = NUL;
	    nv_operator(cap);
	    do_pending_operator(cap, 0, FALSE);
	    empty = (curbuf->b_ml.ml_flags & ML_EMPTY);

	    /* delete PUT_LINE_BACKWARD; */
	    cap->oap->regname = regname;

	    if (reg1 != NULL)
	    {
		/* Delete probably changed the register we want to put, save
		 * it first. Then put back what was there before the delete. */
		reg2 = get_register(regname, FALSE);
		put_register(regname, reg1);
	    }

	    /* When deleted a linewise Visual area, put the register as
	     * lines to avoid it joined with the next line.  When deletion was
	     * characterwise, split a line when putting lines. */
	    if (VIsual_mode == 'V')
		flags |= PUT_LINE;
	    else if (VIsual_mode == 'v')
		flags |= PUT_LINE_SPLIT;
	    if (VIsual_mode == Ctrl_V && dir == FORWARD)
		flags |= PUT_LINE_FORWARD;
	    dir = BACKWARD;
	    if ((VIsual_mode != 'V'
			&& curwin->w_cursor.col < curbuf->b_op_start.col)
		    || (VIsual_mode == 'V'
			&& curwin->w_cursor.lnum < curbuf->b_op_start.lnum))
		/* cursor is at the end of the line or end of file, put
		 * forward. */
		dir = FORWARD;
	    /* May have been reset in do_put(). */
	    VIsual_active = TRUE;
	}
	do_put(cap->oap->regname, dir, cap->count1, flags);

	/* If a register was saved, put it back now. */
	if (reg2 != NULL)
	    put_register(regname, reg2);
///...
}

为什么delete时数字寄存器没有shift

使用reg命令可以看到所有的寄存器列表,但是可以看到删除的内容并没有按照数字依次替换。不过有这个困惑的也不止我一个人,这个答复就解释了这个问题

Vim fills these registers with text from yank and delete commands. Numbered register 0 contains the text from the most recent yank command, unless the command specified another register with ["x]. Numbered register 1 contains the text deleted by the most recent delete or change command, unless the command specified another register or the text is less than one line (the small delete register is used then). An exception is made for the delete operator with these movement commands: %, (, ), `, /, ?, n, N, { and }. Register "1 is always used then (this is Vi compatible). The "- register is used as well if the delete is within a line. Note that these characters may be mapped. E.g. % is mapped by the matchit plugin. With each successive deletion or change, Vim shifts the previous contents of register 1 into register 2, 2 into 3, and so forth, losing the previous contents of register 9.

简单来说就是如果删除内容不够一行是不会放在数字寄存器中的,而是放在了small delete register(-)中,作为一个助记符,这个横线也是常规语法中的删除线。

插入模式下显示寄存器内容

理想的情况下vim可以提供一个类似于自动完成的(auto complete)功能,就是可以把所有的寄存器内容都列出来,搜了一下好像vim的内置自动完成并没有这个功能。这个插件提供了该功能,但是没有试用。

posted on 2024-02-01 17:49  tsecer  阅读(30)  评论(0编辑  收藏  举报

导航