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

Urs*_*aDK 10 vim undo cursor-position neovim

编辑:

  • 我已经简化了这个功能并澄清了这个问题.
    原始问题仍在页面下方.

  • 已转载到vim_dev邮件列表:https://groups.google.com/forum/#!topic/vim_dev/_Rz3uVXbwsQ

  • 报告为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
    
    Run Code Online (Sandbox Code Playgroud)
  2. [INCORRECT CURSOR POSITION]替换的结果与缓冲区中的先前更改(第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
    
    Run Code Online (Sandbox Code Playgroud)

原始问题

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

autocmd BufWritePre,FileWritePre,FileAppendPre,FilterWritePre <buffer>
        \ :keepjumps call UmkaDK#StripTrailingSpaces(0)
Run Code Online (Sandbox Code Playgroud)

撤消脚本进行文本更改后看到恢复光标位置后,我想通过将函数创建的撤销记录合并到上一个更改的结尾,从撤消历史中排除我的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
Run Code Online (Sandbox Code Playgroud)

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

由于我的测试工作,我undojoin在StripTrailingSpaces()中实现了几乎相同的.但是,当我在函数运行后撤消上一次更改时,光标将返回到文件中的最顶端更改.这通常是一个剥离的空间,而不是改变的位置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
Run Code Online (Sandbox Code Playgroud)

dol*_*ver 4

对我来说,这绝对看起来像是替代命令的一个错误。据我所知,替换命令将偶尔接管更改位置,以便在包含撤消块时跳转到该位置。我无法隔离该模式 - 有时当替换发生>一定次数时它会这样做。其他时候,替换位置似乎会影响这种情况发生的时间。看来是非常不靠谱的。我认为它实际上与 undojoin 命令没有任何关系,因为我已经能够为不使用该命令的其他函数重现这种效果。如果您有兴趣,请尝试以下操作:

 function! Test()
    normal ciwfoo
    normal ciwbar
    %s/one/two/
 endfunction
Run Code Online (Sandbox Code Playgroud)

在一些不同的文本上尝试一下,其中包含不同数量的“1”并放置在不同的位置。您会注意到,有时撤消会跳转到第一次替换发生的行,有时它会跳转到第一个普通命令进行更改的位置。

我认为您的解决方案是执行以下操作:

undo
normal ma
redo
Run Code Online (Sandbox Code Playgroud)

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

编辑:花了一些时间挖掘源代码后,它实际上看起来像您所追求的行为是错误。这是确定撤消后光标应放置在何处的代码块:

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;
    }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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