VIM撤消:为什么撤消'撤消'时光标跳转到错误的位置



edit:

  • 我已经简化了函数并澄清了问题。

  • 交叉发送到vim_dev邮件列表:https://groups.google.com/forum/!主题/vim_dev _Rz3uVXbwsQ

  • 作为bug报告给Neovim:
    https://github.com/neovim/neovim/issues/6276


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

  1. [CORRECT CURSOR POSITION]替换的结果与缓冲区中先前的更改(增加第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
    

<标题> 原始问题

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

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

在看到撤消脚本所做的文本更改后恢复光标位置后,我有了一个想法,通过将函数创建的撤消记录合并到缓冲区中先前更改的末尾,从撤消历史中排除我的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。但是,当我在函数运行后撤消最后一次更改时,光标将返回到文件中最上面的更改。这通常是一个剥离的空格,是而不是的位置变化I undojoin -ed到。

有人能想到为什么会这样吗?更好的是,有人能提出解决办法吗?

function! StripTrailingSpaces(number_of_allowed_spaces)
    " Match all trailing spaces in a file
    let l:regex = [
                 '^zss{1,}$',
                 'Ss{' . a:number_of_allowed_spaces . '}zss{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

在我看来,这绝对像是替换命令的错误。据我所知,当包含撤消块时,替换命令偶尔会接管要跳转到的更改位置。我不能把这个模式分离出来-有时当替换发生了很多次时它就会这样做。其他时候,替换的位置似乎会影响这种情况的发生。它似乎很不可靠。我不认为它实际上与撤消连接命令有任何关系,因为我已经能够为其他不使用它的函数重现这种效果。如果您感兴趣,请尝试以下操作:

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

在包含不同数量的"1"并放置在不同位置的一些不同文本上尝试它。您会注意到,有时undo会跳转到第一个替换发生的行,有时它会跳转到第一个普通命令进行更改的地方。

我认为你的解决方案是这样做:

undo
normal ma
redo

放在函数的顶部,然后将u绑定到函数中的u'a之类的东西,这样在撤消之后,它将跳回到实际第一次更改发生的地方,而不是任何随机性强加给你的地方。当然,它不可能那么简单,因为你必须在完成跳跃后取消u,等等,但这种模式通常会给你一种方法来保存正确的位置,然后跳转回它。当然,您可能希望使用一些全局变量来完成所有这些工作,而不是劫持标记,但是您明白了。

编辑:在花了一些时间深入研究源代码之后,实际上看起来您所追求的行为就是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 changen");
    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命令的位置。否则,它将跳转到更改最多的行并将您放在那里。使用substitute时,它会为被替换的每一行激活这个逻辑,因此,如果其中一个替换是在撤消块中,那么它会跳到撤消之前的光标位置(您期望的行为)。其他时候,该块中没有任何更改,因此它将跳转到最上面的更改行(这可能是它应该做的)。因此,我认为你的问题的答案是,你想要的行为(做出改变,但合并它与以前的变化,除了在确定何处放置光标时,更改撤消)目前不支持vim。

编辑:

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

最新更新