3

It is possible in vanilla vim to:

  1. visual select some number of characters, e.g. a variable name in code
  2. type c to begin replacing selection in insert mode
  3. type a replacement (variable name)
  4. exit insert mode
  5. navigate to another location (typically n to go to the next instance of original variable name)
  6. type . to repeat the rename.

Thus the last two steps can be repeated as much as necessary in this common refactoring activity, that of renaming a variable. The brevity of this unit of iteration is critical. I employ further machinery to bring it from 2 steps down to one step and which also accepts counts so I can fire these off expediently. It's all super composable, hence my fixation as you'll see on getting the repeat to work for this.

What I would like to do, which despite experimentation over a decade with a large number of plugins I have not been able to achieve, is the following small tweak to the above workflow: I would like to be able to start it off with a yank as step 0, and exchange step number 2 for a p (or any other suitable verb, I don't care).

Well it does not work. the . fails to replace.

When I employ some solution to prevent p from clobbering the default register like seen here: https://stackoverflow.com/a/290723/340947, what it achieves is the ability to yank the text that I want to use as replacement, go around places and select visually and hit p more than once, and this actually works. Although this is a step in the right direction, . remains broken so I have made very little progress as it does not help save time removing all of those visual operations. It would appear that only the erasure of the selection length got stored in ., not the whole action of replacement with the paste.

The fact that this works with c is tantalizing, but I am beginning to worry that this is some kind of a fundamental limitation.

I wonder if repeat.vim could be extended/hacked to do this.

I also wonder if https://github.com/vim-scripts/visualrepeat is exactly what I'm looking for or not. The description does not make it clear to me whether it is relevant. (Update: tested it and it does not help)

Further experimentation:

  • I can issue "0p from visual selection instead of p, unsurprisingly this commands vim to do precisely the same thing and does not make . behave any differently

I really want this so I'm willing to go out of the box. If . cannot be coerced then I'll just have to do without it; I could have a different bind to employ in these specific situations. To do this, all I would need is compute the number of characters that the original visual selection was, and delete that number of characters followed by performing a paste. I believe this is all possible (even if I have to actually :normal! gv to count it)... but it doesn't seem possible to combine this abomination and . into one single bind that knows the right thing to do all of the time.

0

2 Answers 2

4

Explanation of the problem

The visual put + repeat pattern is quite broken in vim. Internally, vim implements visual puts as a literal visual delete followed by a normal mode put. However, only the delete part gets stored in the repeat buffer. So, usage of . after visual put simply deletes text in the shape of the previous visual selection. This is not usually what is expected and this could likely be fixed in vim itself.

Existing solution

The workaround posted by Steve Lu and ZauzoftheCobble is very close, and is perhaps good enough for most purposes. It fails when control characters are present, for example (where ^H is a literal ctrl-h),

hello^Hworld    ; select this with _y11l
something       ; _vep

yields

hellworld

Plugin: a more advanced solution

I maintain a small plugin which addresses the visual put repeat problem.

https://github.com/andymass/vim-visput

Although the code is short, it uses some complex vim behaviors. I will break down the major points here.

First, replacements for visual put are created via maps.

xmap <silent> p <plug>(visput-p)
xmap <silent> P <plug>(visput-p)

In visual model, p and P are both identical so we continue this convention. We use xmap instead of vmap to target visual mode only (and not select mode).

Next, the core map of the plugin does three things: setup an opfunc, enter operator pending mode, then move according to previous visual mode.

xmap <silent> <plug>(visput-p) :<c-u>call <sid>setup()<cr>g@<sid>(visual)

g@ is a standard trick to make operations repeatable. The <sid>(visual) map is implemented as follows:

command! VisPutMotion normal! 1v
onoremap <silent> <sid>(visual) v:<c-u>VisPutMotion<cr>

The only important bit is 1v which is a lesser-known feature, meaning start visual mode with the same shape as the last visual operation. VisPutMotion is only there to produce a prettier print-out during usage.

Finally, the goal of this is to call our opfunc using the particular visual shape we want. The full details of this function are available here, but the important part of the opfunc is the following:

execute 'normal! gv"'.s:register.'p'

normal! gv is standard to re-enter visual mode inside an operator. We then do "{register}p where s:register is set to the register previously used.

The plugin also has some basic special handling for block mode, but the above is the core functionality to address the question.

2
  • Thanks for going in-depth here. Although I have not had a chance to dive into this yet I'll accept. Can always revoke it later ;)
    – Steven Lu
    Commented Dec 21, 2021 at 0:36
  • Hey, updating since the other solution has been found to have further deficiencies. The ctrl+H deficiency you point out isn't likely to be ever a factor for me, but it turns out that for one reason or another, indentation gets re-triggered (which would be fine if it weren't always wrong) with it. So I'm using your plugin now. So far so good. Thanks again.
    – Steven Lu
    Commented Oct 21, 2022 at 21:31
0

As far as I can tell (Will need a few days of testing to confirm) This dragon has been slayed with the help of a suggestion from Reddit. The solution is:

vmap p c<C-r>0<Esc>
5
  • 2
    I think you want to use xmap p c<c-r><c-r>0<esc> Commented Nov 29, 2021 at 7:47
  • You are correct, the version in the comment seems to be better. I had already disabled the implementation in my answer because I suspected it of messing up some other behavior, but hadn't fully investigated it yet. Came back here because the default behavior got on my nerves again. Thanks.
    – Steven Lu
    Commented Sep 26, 2022 at 21:35
  • @ChristianBrabandt I'm following up to report that this does not work as expected either. It does paste the content, but for multiline content it seems to re-flow indentation in a wrong way. I want the default behavior of pasting it without indentation getting manipulated.
    – Steven Lu
    Commented Oct 21, 2022 at 21:24
  • Not sure what you are trying to tell me. I merely commented to tell you there is some improved way than what you yourself post in your answer. Besides I have no idea what you mean with re-flow. Perhaps you have wrongly set indenting options? I'd suggest you open a new question stating exactly what kind of problem you are trying to solve Commented Oct 22, 2022 at 8:16
  • No worries! It was more of a note to self. @Mass's solution seems to be working pretty well so far.
    – Steven Lu
    Commented Oct 27, 2022 at 17:38

Not the answer you're looking for? Browse other questions tagged or ask your own question.