【问题标题】:VIM undo: Why does the cursor jump to the wrong position when undoing `undojoin`?VIM undo:为什么撤销`undojoin`时光标会跳错位置?
【发布时间】:2015-10-11 10:28:55
【问题描述】:

编辑:


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

  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. [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
    

原始问题

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

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

在看到Restore the cursor position after undoing text change made by a script 之后,我想到了将 StripTrailingSpaces() 函数所做的更改从撤消历史记录中排除,方法是将函数创建的撤消记录合并到缓冲区中先前更改的末尾。

这样,在撤消更改时,函数似乎根本没有创建自己的撤消记录。

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

vim +"source &lt;saved-filename-here&gt;"

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-ed 更改的位置。

谁能想到为什么会这样?更好的是,任何人都可以提出解决方案吗?

function! 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

【问题讨论】:

  • 对不起,这75行和:%s/\s*$/有什么区别?
  • @steffen:嗯……它有 74 行和 2866 个字符……它还具有描述性 cmets,保留您的搜索历史和上次搜索字符串,不会更改您的 '''. 和 @ 987654335@ 标记,不添加新的 jumplistchangelist 记录,保留您的视图和光标位置,并且应该创建更流畅的撤消体验。 (虽然最后一点是主观的,这也是这个问题在这里的原因。)
  • 在进行更改之前会记住光标位置,然后在撤消更改后恢复。

标签: vim undo cursor-position neovim


【解决方案1】:

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

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

在一些不同的文本上尝试它,其中包含不同数量的“一”并放置在不同的位置。您会注意到,之后有时 undo 会跳转到第一个替换发生的行,而其他时候它会跳转到第一个正常命令进行更改的位置。

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

undo
normal ma
redo

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

编辑: 在花一些时间挖掘源代码之后,实际上看起来您所追求的行为是错误。这是决定撤消后光标应该放在哪里的代码块:

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 目前不支持您想要的行为(进行更改但将其与先前的更改合并,除非确定在撤消更改时放置光标的位置)。

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

【讨论】:

  • 感谢@doliver,看来您已经投入了相当多的时间来调试它。非常感激!!但是,您提出的解决方案(即:将u 重新映射到自定义操作)需要修改 VIM 的核心功能之一,我宁愿避免这样做。此外,如果此行为是错误的结果,那么您提出的建议只会为我解决问题,而不是为整个社区解决问题。让我看看我是否可以让#vim(IRC)和vim_dev(邮件列表)参与其中。也许他们能帮助我们。
  • 这个问题现在交叉发布到 vim_dev 邮件列表:goo.gl/HpW4NX
  • 是的,我认为理想情况下这将被固定在核心内部。不过,我认为您不会将此类问题放在首位。如果您对 C 感到满意,如果可以在核心中相当容易地修复它,我不会感到惊讶。除了这样做或重新映射 u 键之外,最好的解决方法可能是不执行需要额外按键但保留该位置的 undojoin。我以前在 vim 核心中混过,所以我今天可能会看一下,看看它是否看起来很容易解决。
  • @UmkaDK 很高兴我能帮上忙!我已经用请求的信息更新了我的答案。
  • @ChristianBrabandt 我已经更新了 vim_dev 上的帖子,并在问题底部添加了一个指向该主题的链接,以便以后可以轻松找到它。
猜你喜欢
  • 2023-03-20
  • 2010-10-17
  • 2014-06-14
  • 2019-05-13
  • 2010-10-15
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多