2

I often do ver_w some times to change words into blanks. I am making quiz this way. What I noticed is that this is a bit of a pain. The amount of work is not so heavy as to make it a macro, but cannot be repeated by the . command.
Do you come up with an easier way to do this?

Example
I make it a rule never to mix business with pleasure
Change the italisized letters into _

Taro
  • 153
  • 3

2 Answers2

2

I pondered upon this question for quite a bit. Using macros would be the most straightforward way to set this up quickly, but you explicitly called out you find this “not so heavy as to make it a macro.”

You mentioned something that could be repeated by the . command, so I thought of a mapping using operator-pending mode (similar to the d command, which takes a motion, such as de to delete to the end of the word, or a text object, such as diw to delete a whole word.)

A simple approach

It is possible to set up a fairly simple mapping using the g@ operator to accomplish this goal. The simplest case would be:

function! ReplaceUnderscores(_)
  normal! `[v`]r_`]W
endfunction

nnoremap <silent> <leader>u :set operatorfunc=ReplaceUnderscores<CR>g@

The way you use it is by calling \ue to replace to the end of the word with underscores, or \uE to replace til the end of the WORD, or \uiW to replace the whole WORD under the cursor. At the end of the command, the cursor will be placed at the start of the next WORD, so you can repeat the same command using ..

For your specific example:

I make it a rule never to mix business with pleasure

If the cursor is at the beginning of the line, you could then use w to move to "make", then \uiW to turn that word into underscores, then ... to repeat that for the next three words, w to skip the "never" and finally . to replace the "to". End result:

I ____ __ _ ____ never __ mix business with pleasure

To understand what the opfunc is doing, what you need to know is it will set marks `[ and `] to the region that corresponds to the passed motion or text object. So `[v`] will turn that region into a Visual selection, then r_ will replace it with underscores, finally `] will move the cursor back to the end of the region and W will skip to the beginning of the next WORD (so that it's pretty convenient to repeat the command using repeated .s).

Improving on it

The simple approach is a clear improvement over using a macro, but since it takes quite a bit of work to set it up, it's only worth it if it's made permanent (i.e. adding it to your Vim configuration, such as your vimrc file.)

But if we're going to create a permanent mapping for it, can we make it better? I think we can! Thinking about your usage, I think one clear improvement would be to be able to act on multiple words at once, so that if you use \u4E on top of "make" then it will replace the four words "make it a rule" with underscores for you, in a single operation.

A naive implementation of this approach would be:

function! ReplaceUnderscores(_)
  execute "normal! `[v`]\<Esc>"
  '<,'>s/\%V\k/_/g
  normal! `>W
endfunction

This already supports the aforementioned \u4E above. You can also use something like \u$ to replace all words with underscores all the way to the end of the line.

Let's break down how it works. Here we're also making a Visual selection, but we're following it with <Esc> to go back to Normal mode right away. That's what the execute "normal! `[v`]<Esc>" is doing. We're using :execute and a double quoted string so we can insert an encoded \<Esc> keystroke there.

Then we're using a :substitute command, using a range of '<,'>, which covers the line in the Visual selection we've just created, and using the special \%V pattern to only match characters that were inside the last Visual selection. (The range restricts to the lines of the last Visual selection and the \%V ensures we ignore any characters in the first/last line that were outside the Visual selection.) Then we use \k to match keyword characters, which should be letters or numbers (excludes spaces and punctuation), so that we'll only replace those. Using [\k] is also flexible in that it uses the 'iskeyword' option for the list of supported characters, so you can customize that list if you'd like to include/exclude to the list of those you want to replace with underscore. Finally we replace with _ and we use g to do this to all matches in each line.

Finally, we use the same `>W to place the cursor at the beginning of the next WORD once the command has finished.

One way you can act on your sample sentence is w\u4ww\uw. While you're not using the . operator here, you're using a count, which can be more efficient if you're replacing many words. Using sentences for motions and paragraphs would also yield a big win here.

One further improvement of this approach is to add support for the \u command in Visual mode, so that you can prepare a Visual selection and then press \u to replace the words inside it with underscores. You can simply extend this approach to do that with:

xnoremap <silent> <leader>u :<C-U>set operatorfunc=ReplaceUnderscores<CR>gvg@

This calls g@ from Visual mode, which works the same way (setting the `[ and `] marks), it needs an extra <C-U> after the : because going to Command-line from Visual mode automatically adds the '<,'> range, and also a gv to restore back the Visual mode selection before invoking the actual g@.

Making it robust

Unfortunately, here is where things gets complicated in Vimscript. It's fairly quick to work out a proof-of-concept like the above, but polishing it and handling corner cases takes the one-liner that turned into a three-liner to now go to ~35 lines of code...

The extra code is to:

  • Ensure we're handling linewise selections, such as \u2j to act on 3 lines of text (same as d3j would delete 3 lines of text, whole lines, not just from the cursor down.) Since we're there, let's do blockwise too.
  • Save and restore Visual selection. Since we're using it to implement our feature, we should save and restore it, so that if you use the gv command after invoking \u, you'll get back the Visual selection you had before running the command (the one that was the last from your point of view), and not the one used in the \u implementation.
  • Ensure we're not depending on behavior of specific settings, in this case, the 'selection' setting, which defaults to inclusive (which is what we want), but would potentially break our behavior if the user changed it to something else... So let's set it explicitly to inclusive for the execution of our command, and later restore it to whatever it was set to.
  • Finally, some neatness around setting up 'operatorfunc' and using an <expr> mapping to issue the g@ itself.

The final resulting code is:

function! ReplaceUnderscores(type)
  if a:type ==# ''
    set operatorfunc=ReplaceUnderscores
    return 'g@'
  endif

let save_sel = &selection let save_vis_mode = visualmode() let save_vis_start = getpos("'<") let save_vis_end = getpos("'>")

try set selection=inclusive if a:type ==# 'char' execute "normal! [v]&lt;Esc>" elseif a:type ==# 'line' execute "normal! '[V']&lt;Esc>" elseif a:type ==# 'block' execute "normal! [\&lt;C-V&gt;]&lt;Esc>" endif

'&lt;,'&gt;s/\%V\k/_/g

if a:type ==# 'line'
  normal! '&gt;+
else
  normal! `&gt;W
endif

finally execute 'normal! '.save_vis_mode."&lt;Esc>" call setpos("'<", save_vis_start) call setpos("'>", save_vis_end) let &selection = save_sel endtry endfunction

nnoremap <expr> <leader>u ReplaceUnderscores('') xnoremap <expr> <leader>u ReplaceUnderscores('') nnoremap <expr> <leader>uu ReplaceUnderscores('').'_'

There's obviously quite a bit here, but a lot of it is pretty similar to what is done in the example in :help :map-operator, so for the most part I'll leave it as an exercise to the reader to understand exactly what each bit is doing...

Support for linewise makes it possible to use \uip and act on a paragraph (block of contiguous non-empty lines) and have it replace every word with underscores.

This also sets up a \uu mapping that you can use to replace the words in the current line with underscores, which even works with a count, so 4\uu would work too!

Hopefully this is useful enough for you that you'll consider adopting it? Either include it in your vimrc (or init.vim for NeoVim), or you can add it to a file with a *.vim extension inside ~/.vim/plugin/ (or ~/.config/nvim/plugin/ for NeoVim), in which case it will be loaded automatically during startup.

filbranden
  • 28,785
  • 3
  • 26
  • 71
0

Indeed . repeats last change that is bound to the visual selection you have used last time (4 chars for make and . would repeat 4 chars replacement with _ for it)

I usually do it with :norm command which is almost equal to a macro recording:

:norm! viwr_w and then @:, @@ ...

Or you could do a throwaway mapping like:

nnoremap gl ver_w

use it and then dismiss when finished:

unmap gl
Maxim Kim
  • 13,376
  • 2
  • 18
  • 46
  • My first thought was using the expression register with @='viwr_w' (followed by "Enter"), which allows for repeating with @@ starting from the first repetition (so slightly better than with the : register...) – filbranden Dec 23 '21 at 02:48