VIM undo:撤消`undojoin`时为什么光标跳到错误的位置?

时间:2015-07-21 19:32:48

标签: vim undo cursor-position neovim

编辑:

为什么光标在以下两个示例中的位置不同:

  1. [正确的游标位置]替换的结果与缓冲区中的先前更改(第3行的添加)相连,光标位置正确恢复到第二行缓冲区。

    normal ggiline one is full of aaaa
    set undolevels=10 " splits the change into separate undo blocks
    
    normal Goline two is full of bbbb
    set undolevels=10
    
    normal Goline three is full of cccc
    set undolevels=10
    
    undojoin
    keepjumps %s/aaaa/zzzz/
    normal u
    
  2. [错误的游标位置]替换的结果与缓冲区中的先前更改(第4行的添加)相联系,光标位置错误地恢复到第一行缓冲区(应该是第3行)。

    normal ggiline one is bull of aaaa
    set undolevels=10 " splits the change into separate undo blocks
    
    normal Goline two is full of bbbb
    set undolevels=10 
    
    normal Goline three is full of cccc        
    set undolevels=10
    
    normal Goline four is full of aaaa's again
    set undolevels=10
    
    undojoin
    keepjumps %s/aaaa/zzzz/
    normal u
    
  3. 原始问题

    我的VIM设置方式,将缓冲区保存到文件会触发自定义的StripTrailingSpaces()函数(附在问题的末尾):

    autocmd BufWritePre,FileWritePre,FileAppendPre,FilterWritePre <buffer>
            \ :keepjumps call UmkaDK#StripTrailingSpaces(0)
    

    看到Restore the cursor position after undoing text change made by a script之后,我想通过将函数创建的撤销记录合并到缓冲区中上一次更改的末尾,从撤消历史记录中排除我的StripTrailingSpaces()函数所做的更改。

    这样,撤消更改时,该功能似乎根本无法创建自己的撤消记录。

    为验证我的想法,我使用了一个简单的测试用例:创建一个干净的缓冲区并手动输入以下命令,或将以下块保存为文件并通过以下方式获取:

    vim +"source <saved-filename-here>"

    normal ggiline one is full of aaaa
    set undolevels=10 " splits the change into separate undo blocks
    
    normal Goline two is full of bbbb
    set undolevels=10
    
    normal Goline three is full of cccc
    set undolevels=10
    
    undojoin
    keepjumps %s/aaaa/zzzz/
    normal u
    

    如您所见,在撤消缓冲区中的最后一次更改(即创建第三行)后,光标将正确返回到文件中的第二行。

    由于我的测试工作,我在StripTrailingSpaces()中实现了几乎相同的undojoin。但是,当我在函数运行后撤消上一次更改时,光标将返回到文件中的最顶端更改。这通常是一个剥离的空间,并且变更位置undojoin - 编辑。

    任何人都可以想到为什么会这样吗?更好的是,有人可以建议修复吗?

    function! UmkaDK#StripTrailingSpaces(number_of_allowed_spaces)
        " Match all trailing spaces in a file
        let l:regex = [
                    \ '\^\zs\s\{1,\}\$',
                    \ '\S\s\{' . a:number_of_allowed_spaces . '\}\zs\s\{1,\}\$',
                    \ ]
    
        " Join trailing spaces regex into a single, non-magic string
        let l:regex_str = '\V\(' . join(l:regex, '\|') . '\)'
    
        " Save current window state
        let l:last_search=@/
        let l:winview = winsaveview()
    
        try
            " Append the comming change onto the end of the previous change
            " NOTE: Fails if previous change doesn't exist
            undojoin
        catch
        endtry
    
        " Substitute all trailing spaces
        if v:version > 704 || v:version == 704 && has('patch155')
            execute 'keepjumps keeppatterns %s/' . l:regex_str . '//e'
        else
            execute 'keepjumps %s/' . l:regex_str . '//e'
            call histdel('search', -1)
        endif
    
        " Restore current window state
        call winrestview(l:winview)
        let @/=l:last_search
    endfunction
    

1 个答案:

答案 0 :(得分:3)

这绝对看起来像对我的替代命令的错误。据我所知,替换命令会偶尔接管更改位置,以便在包含它时跳转到撤消块。我无法隔离模式 - 有时它会在替换发生时执行此操作&gt;好几次。其他时候,替换的位置似乎会影响何时发生。这似乎非常不可靠。我不认为它实际上与undojoin命令有任何关系,因为我已经能够为不能使用它的其他功能重现这种效果。如果您有兴趣,请尝试以下方法:

 function! Test()
    normal ciwfoo
    normal ciwbar
    %s/one/two/
 endfunction

尝试使用不同数量的&#34;些&#34;包含并放置在不同的位置。您会注意到,之后有时撤消会跳转到第一次替换发生的行,有时会跳转到第一个正常命令发生变化的地方。

我认为这里的解决方案是做这样的事情:

undo
normal ma
redo

在你的函数的顶部然后将你绑定到你的函数中的某个类似的东西,以便在撤消之后它会跳回到实际发生第一次更改的地方而不是随机性:s force在你身上当然,它可以非常简单,因为一旦你完成跳跃等等,你将不得不取消映射,但这种模式通常会给你一种方法来保存正确的位置然后跳回去。当然,你可能想用一些全局变量而不是劫持标记来做所有这些,但你明白了。

修改: 在花了一些时间挖掘源代码之后,实际上看起来你所追求的行为就是这个bug。这是确定撤消后光标放置位置的代码块:

if (top < newlnum)
{
    /* If the saved cursor is somewhere in this undo block, move it to
     * the remembered position.  Makes "gwap" put the cursor back
     * where it was. */
    lnum = curhead->uh_cursor.lnum;
    if (lnum >= top && lnum <= top + newsize + 1)
    {
    MSG("Remembered Position.\n");
    curwin->w_cursor = curhead->uh_cursor;
    newlnum = curwin->w_cursor.lnum - 1;
    }
    else
    {
    char msg_buf[1000];
    MSG("First change\n");
    sprintf(msg_buf, "lnum: %d, top: %d, newsize: %d", lnum, top, newsize);
    MSG(msg_buf);
    /* Use the first line that actually changed.  Avoids that
     * undoing auto-formatting puts the cursor in the previous
     * line. */
    for (i = 0; i < newsize && i < oldsize; ++i)
        if (STRCMP(uep->ue_array[i], ml_get(top + 1 + i)) != 0)
        break;
    if (i == newsize && newlnum == MAXLNUM && uep->ue_next == NULL)
    {
        newlnum = top;
        curwin->w_cursor.lnum = newlnum + 1;
    }
    else if (i < newsize)
    {
        newlnum = top + i;
        curwin->w_cursor.lnum = newlnum + 1;
    }
    }
}

它相当复杂,但基本上它的作用是检查更改时光标所在的位置,然后如果它在撤消的更改块内,则将光标重置为该位置gw命令。否则,它会跳到最顶部的更改行并将您带到那里。替换所发生的是它为每个被替换的行激活这个逻辑,因此如果其中一个替换在撤消块中,那么它会在撤消之前跳转到光标的位置(你想要的行为)。其他时候,没有任何更改将在该块中,因此它将跳转到最顶部的更改行(可能它应该做什么)。因此,我认为您的问题的答案是,vim目前不支持您所需的行为(进行更改但将其与之前的更改合并,除非在确定撤消更改时放置光标的位置)。

修改 这个特殊的代码块位于undoredo函数内的第2711行的undo.c中。 u_savecommon内部是在实际调用undo之前设置整个事物的位置,并且保存了最终用于gw命令异常的光标位置(undo.c第385行并在调用时保存在第548行)在同步缓冲区上)。替换命令的逻辑位于第4268行的ex_cmds.c中,它在第5208行间接调用u_savecommon(调用调用u_savecommon的u_savesub)。