tpope.vim-repeat/autoload/repeat.vim
Ingo Karkat eb1b48d94c Rework error return to prevent off-by-one in Ex mode
My automated tests run Vim in silent batch mode (:help -s-ex), these also cover repeat of mappings via repeat.vim. Unfortunately, the combination of feedkeys() and :execute used in the repeat.vim implementation to return a potential caught error cause the line where the repeat mapping is executed to be increased, so for me the tests fail in a really strange way. The same problem can occur if someone uses repeats in silent batch mode (e.g. through a recorded macro) - though the source of the problem would be even harder to track down there - in my test run it at least was easily reproducible.

I raised this as a potential Vim bug in https://github.com/vim/vim/issues/7153, but Bram explained that it's a side effect of Ex mode (where empty lines make the cursor go to the next line, and as repeat#run() returns the empty String on the happy path, that's an empty line being executed). Recommendation from Bram is to pass the "x" flag for immediate execution to feedkeys() (which avoids the problem). However, in the context of repeat.vim this would mean that any errors resulting from the repeat mapping invocation would have to be caught inside repeat#run(), because they would then execute within the function's context, and not after it. Also, the "x" flag would not be supported by old Vim 7.3 and earlier.

Instead, I chose to avoid the problem by replacing the :execute with a more straightforward :call (actually an :if that tests a returned Boolean success flag), so instead of the clever direct execution of the returned :echoerr command, the error message instead is retrieved from a script-local variable via a new repeat#errmsg() getter. I use this idiom in all of my plugins through a set of utility functions (5bb32fd27a/autoload/ingo/err.vim (L38-L41)), too.
So by adding a little (well encapsulated) variable and getter, this problem can be avoided without touching the sensitive feedkeys() and try...catch logic within the plugin.
2020-10-30 09:16:21 +01:00

171 lines
6 KiB
VimL

" repeat.vim - Let the repeat command repeat plugin maps
" Maintainer: Tim Pope
" Version: 1.2
" GetLatestVimScripts: 2136 1 :AutoInstall: repeat.vim
" Installation:
" Place in either ~/.vim/plugin/repeat.vim (to load at start up) or
" ~/.vim/autoload/repeat.vim (to load automatically as needed).
"
" License:
" Copyright (c) Tim Pope. Distributed under the same terms as Vim itself.
" See :help license
"
" Developers:
" Basic usage is as follows:
"
" silent! call repeat#set("\<Plug>MappingToRepeatCommand",3)
"
" The first argument is the mapping that will be invoked when the |.| key is
" pressed. Typically, it will be the same as the mapping the user invoked.
" This sequence will be stuffed into the input queue literally. Thus you must
" encode special keys by prefixing them with a backslash inside double quotes.
"
" The second argument is the default count. This is the number that will be
" prefixed to the mapping if no explicit numeric argument was given. The
" value of the v:count variable is usually correct and it will be used if the
" second parameter is omitted. If your mapping doesn't accept a numeric
" argument and you never want to receive one, pass a value of -1.
"
" Make sure to call the repeat#set function _after_ making changes to the
" file.
"
" For mappings that use a register and want the same register used on
" repetition, use:
"
" silent! call repeat#setreg("\<Plug>MappingToRepeatCommand", v:register)
"
" This function can (and probably needs to be) called before making changes to
" the file (as those typically clear v:register). Therefore, the call sequence
" in your mapping will look like this:
"
" nnoremap <silent> <Plug>MyMap
" \ :<C-U>execute 'silent! call repeat#setreg("\<lt>Plug>MyMap", v:register)'<Bar>
" \ call <SID>MyFunction(v:register, ...)<Bar>
" \ silent! call repeat#set("\<lt>Plug>MyMap")<CR>
if exists("g:loaded_repeat") || &cp || v:version < 700
finish
endif
let g:loaded_repeat = 1
let g:repeat_tick = -1
let g:repeat_reg = ['', '']
" Special function to avoid spurious repeats in a related, naturally repeating
" mapping when your repeatable mapping doesn't increase b:changedtick.
function! repeat#invalidate()
autocmd! repeat_custom_motion
let g:repeat_tick = -1
endfunction
function! repeat#set(sequence,...)
let g:repeat_sequence = a:sequence
let g:repeat_count = a:0 ? a:1 : v:count
let g:repeat_tick = b:changedtick
augroup repeat_custom_motion
autocmd!
autocmd CursorMoved <buffer> let g:repeat_tick = b:changedtick | autocmd! repeat_custom_motion
augroup END
endfunction
function! repeat#setreg(sequence,register)
let g:repeat_reg = [a:sequence, a:register]
endfunction
function! s:default_register()
let values = split(&clipboard, ',')
if index(values, 'unnamedplus') != -1
return '+'
elseif index(values, 'unnamed') != -1
return '*'
else
return '"'
endif
endfunction
function! repeat#run(count)
let s:errmsg = ''
try
if g:repeat_tick == b:changedtick
let r = ''
if g:repeat_reg[0] ==# g:repeat_sequence && !empty(g:repeat_reg[1])
" Take the original register, unless another (non-default, we
" unfortunately cannot detect no vs. a given default register)
" register has been supplied to the repeat command (as an
" explicit override).
let regname = v:register ==# s:default_register() ? g:repeat_reg[1] : v:register
if regname ==# '='
" This causes a re-evaluation of the expression on repeat, which
" is what we want.
let r = '"=' . getreg('=', 1) . "\<CR>"
else
let r = '"' . regname
endif
endif
let c = g:repeat_count
let s = g:repeat_sequence
let cnt = c == -1 ? "" : (a:count ? a:count : (c ? c : ''))
if ((v:version == 703 && has('patch100')) || (v:version == 704 && !has('patch601')))
exe 'norm ' . r . cnt . s
elseif v:version <= 703
call feedkeys(r . cnt, 'n')
call feedkeys(s, '')
else
call feedkeys(s, 'i')
call feedkeys(r . cnt, 'ni')
endif
else
if ((v:version == 703 && has('patch100')) || (v:version == 704 && !has('patch601')))
exe 'norm! '.(a:count ? a:count : '') . '.'
else
call feedkeys((a:count ? a:count : '') . '.', 'ni')
endif
endif
catch /^Vim(normal):/
let s:errmsg = v:errmsg
return 0
endtry
return 1
endfunction
function! repeat#errmsg()
return s:errmsg
endfunction
function! repeat#wrap(command,count)
let preserve = (g:repeat_tick == b:changedtick)
call feedkeys((a:count ? a:count : '').a:command, 'n')
exe (&foldopen =~# 'undo\|all' ? 'norm! zv' : '')
if preserve
let g:repeat_tick = b:changedtick
endif
endfunction
nnoremap <silent> <Plug>(RepeatDot) :<C-U>if !repeat#run(v:count)<Bar>echoerr repeat#errmsg()<Bar>endif<CR>
nnoremap <silent> <Plug>(RepeatUndo) :<C-U>call repeat#wrap('u',v:count)<CR>
nnoremap <silent> <Plug>(RepeatUndoLine) :<C-U>call repeat#wrap('U',v:count)<CR>
nnoremap <silent> <Plug>(RepeatRedo) :<C-U>call repeat#wrap("\<Lt>C-R>",v:count)<CR>
if !hasmapto('<Plug>(RepeatDot)', 'n')
nmap . <Plug>(RepeatDot)
endif
if !hasmapto('<Plug>(RepeatUndo)', 'n')
nmap u <Plug>(RepeatUndo)
endif
if maparg('U','n') ==# '' && !hasmapto('<Plug>(RepeatUndoLine)', 'n')
nmap U <Plug>(RepeatUndoLine)
endif
if !hasmapto('<Plug>(RepeatRedo)', 'n')
nmap <C-R> <Plug>(RepeatRedo)
endif
augroup repeatPlugin
autocmd!
autocmd BufLeave,BufWritePre,BufReadPre * let g:repeat_tick = (g:repeat_tick == b:changedtick || g:repeat_tick == 0) ? 0 : -1
autocmd BufEnter,BufWritePost * if g:repeat_tick == 0|let g:repeat_tick = b:changedtick|endif
augroup END
" vim:set ft=vim et sw=4 sts=4: