tpope / vim-repeat

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

Repeating operators with custom motions #8

Closed b4winckler closed 10 years ago

b4winckler commented 12 years ago

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

tpope commented 12 years ago

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.

b4winckler commented 12 years ago

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 12 years ago

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 11 years ago

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 11 years ago

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

tpope commented 10 years ago

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

justinmk commented 10 years ago

@tpope Thanks!