tpope/vim-repeat

Repeating operators with custom motions

b4winckler opened this issue · 7 comments

I have a custom motion omap aa <Plug>AngryOuterPrefix that I'd like to be able to repeat when typing for example daa. Currently (not using repeat.vim) the result of pressing . is that Vim deletes the same number of characters as the first daa instead of deleting the actual text object defined by aa.

Is it possible to use repeat.vim to make daa, caa etc. work properly with .?

There's no built-in support. I would imagine you could individually map daa, caa, and any other particular dispatches up to call repeat#set('daa') and the like.

I doubt a general solution is possible. Vim doesn't provide any introspection for what the most recent operator was. Although perhaps there's a novel approach to the problem is missing.

Thanks for the reply. Perhaps the only real solution to this problem would be to patch the Vim source code. (I was a bit surprised that . didn't automatically work in this situation since a custom motion only performs a selection and not an actual edit.)

Anyway, I'll leave it up to you whether it is meaningful to leave this issue open or not.

tek commented

While implementing the identical text object as you, @b4winckler, I noticed that you can at least make the repetition of 'daa' work by incrementing repeat_tick by 1 after calling repeat#set().
As changing of text seems to increment b:changedtick in a way I couldn't determine, 'caa' cannot be implemented that way. Otherwise it would be possible to use a separate function to repeat, where register '@.' could be inserted, if v:operator was 'c'.

guns commented

I managed to implement repeat for my custom text motions¹. Here's the
problem and a solution:

vim-repeat uses b:changedtick to decide if the last change in the buffer
was the change registered by repeat#set(). However, in operator-pending
mappings b:changedtick is not incremented by vim until the mapping has
completed. A call to repeat#set() during the omap will record the value
of b:changedtick before it is incremented.

@tek noticed that manually incrementing g:repeat_tick by 1 seems to
work. It does work for delete operations that only change one line, but
multiline changes increment b:changedtick multiple times, so this is not
a general solution.

It turns out that while CursorMoved fires on normal mode movement
and changes to the cursor's line, it deliberately does not fire in
operator-pending mode. Thus you can register a one-time CursorMoved
handler that updates g:repeat_tick after the omap completes, and
repeat#run() will work just as intended:

function! RepeatSet(buf)
    call repeat#set(a:buf)

    augroup repeat_tick
        autocmd!
        autocmd CursorMoved <buffer>
            \ let g:repeat_tick = b:changedtick |
            \ autocmd! repeat_tick
    augroup END
endfunction

onoremap <Plug>my_motion :call move_cursor() \|
    \ call RepeatSet(v:operator . "\<Plug>my_motion")<CR>

Notice that we can use v:operator to get the most recent operator, so
both native and custom pending operations will be repeated.

This property of CursorMoved is documented, so I think this is a valid
solution. There is no need to patch vim-repeat, but it might be helpful
to add this to the documentation.

¹ https://github.com/guns/vim-sexp/blob/master/plugin/sexp.vim#L97

tek commented

That works, thanks a lot, @guns! Incredible.
With this, I was able to implement the repeatable change-command.
https://github.com/tek/vim-argh/blob/master/autoload/argh.vim#L54

No reason I can't roll this in, I suppose.

@tpope Thanks!