tpope / vim-repeat

repeat.vim: enable repeating supported plugin maps with "."
http://www.vim.org/scripts/script.php?script_id=2136
2.58k stars 81 forks source link

"." may fail to repeat a command after reloading a buffer #82

Open lacygoill opened 4 years ago

lacygoill commented 4 years ago

Describe the bug

. may fail to repeat a command after reloading a buffer.

To Reproduce

Run this shell command (you'll need to update the path to the plugin):

vim -Nu NONE -S <(cat <<'EOF'
    set rtp^=/path/to/vim-repeat
    nno <c-b> xp:call repeat#set("\<lt>c-b>")<cr>
    au CursorMoved,TextChanged * "
    %d
    pu!='abc'
    update
EOF
) /tmp/file

Result: the buffer contains baac.

Expected behavior

The buffer contains abc.

Screenshots

gif

Environment

Additional context

The issue is due to the fact that when we reload a buffer, BufEnter is fired but not BufLeave. We need an event to hook into and save the state of the ticks synchronization, so that we can restore it if necessary on BufEnter. IOW, we need a replacement for BufLeave; I think BufUnload is the only possible event which can play this role.

It could be added into the list of events in this line:

https://github.com/tpope/vim-repeat/blob/c947ad2b6a16983724a0153bdf7f66d7a80a32ca/autoload/repeat.vim#L161

Which would give:

autocmd BufLeave,BufWritePre,BufReadPre,BufUnload * let g:repeat_tick = (g:repeat_tick == b:changedtick || g:repeat_tick == 0) ? 0 : -1
                                       ^^^^^^^^^^

During my limited tests, it seemed to fix the issue. I can send you a PR if you think it's the right fix.


Note that BufReadPre can't help here, because b:changedtick is incremented by 1 on BufReadPre when reloading a buffer.

Btw, why does the autocmd listen to BufReadPre? AFAICT, its goal is to save the state of the ticks synchronization. But it seems we can't do that on BufReadPre; either b:changedtick is incremented by 1 when reloading a buffer:

$ touch /tmp/file{1..2} && vim -Nu NONE -S <(cat <<'EOF'
    let g:abuf = 'expand("<abuf>")'
    eval getcompletion('buf*', 'event')
        \->filter({_,v -> v !~# 'Cmd$'})
        \->map({_,v -> execute(printf(
        \ 'au %s * unsilent echom "%s in buf "..%s..": tick is "..getbufvar(%s, "changedtick")'
        \ , v, v, g:abuf, g:abuf))})
    call map(range(10), {_,v -> execute('pu='..v)})
    undo
EOF
) /tmp/file1
:mess clear
:e
:mess
BufUnload in buf 1: tick is 34
"/tmp/file1" 0L, 0C
BufRead in buf 1: tick is 35
BufReadPost in buf 1: tick is 35
BufEnter in buf 1: tick is 36
BufWinEnter in buf 1: tick is 36

" notice how the tick was 34 on BufUnload, and then 35 on BufRead

or it has already been initialized to 1 when loading a new file (it happens a little earlier on BufNew):

$ touch /tmp/file{1..2} && vim -Nu NONE -S <(cat <<'EOF'
    let g:abuf = 'expand("<abuf>")'
    eval getcompletion('buf*', 'event')
        \->filter({_,v -> v !~# 'Cmd$'})
        \->map({_,v -> execute(printf(
        \ 'au %s * unsilent echom "%s in buf "..%s..": tick is "..getbufvar(%s, "changedtick")'
        \ , v, v, g:abuf, g:abuf))})
    call map(range(10), {_,v -> execute('pu='..v)})
    undo
EOF
) /tmp/file1
:mess clear
:e /tmp/file2
:mess
BufNew in buf 2: tick is 1
BufAdd in buf 2: tick is 1
BufCreate in buf 2: tick is 1
BufLeave in buf 1: tick is 34
BufWinLeave in buf 1: tick is 34
BufUnload in buf 1: tick is 34
BufReadPre in buf 2: tick is 1
"/tmp/file2" 0L, 0C
BufRead in buf 2: tick is 1
BufReadPost in buf 2: tick is 1
BufEnter in buf 2: tick is 2
BufWinEnter in buf 2: tick is 2

" notice how the tick is initialized to 1 on BufNew, and unchanged on BufRead

In both cases, b:changedtick has been altered on BufReadPre, so there's no guarantee that the state of the ticks synchronization has been preserved.

Is there some case where BufReadPre is useful?

svermeulen commented 4 years ago

I encountered this issue as well, and have been using your suggested fix here. So far it has addressed the issue.