4

I want to create a mapping in my .vimrc file that takes an argument.

I'm constantly defining latex commands which require arguments inside of curly brackets and writing them instead inside regular parens, e.g., writing \interior(x_y^z) when I meant to write interior{x_y_z}. If I know the name of the function, it's trivial, i.e.,

:g/interior(\(.*\))/s//interior{\1}/g

But I want to create a mapping to which I could feed the name of function, e.g., interior. It might look something like this, where myWord is the argument.

:map ;subst   :g/<myWord>(\(.*\))/s//<myWord>{\1}/g

and I would call it with something like ;subst <myWord>

I presume I have to use something like :noremap but the examples on this forum are always much more complicated than I can follow.

4
  • 1
    You probably want :h user-commands, not a mapping.
    – Matt
    Commented May 18, 2020 at 2:25
  • Nitpick: The name macro in Vim typically refers to recording into a register with the q command and replaying it with @. What you're describing here is typically referred to as a mapping. Would you consider to edit the question and use the term "mapping" rather than "macro"? I think that would improve the question. Thanks!
    – filbranden
    Commented May 18, 2020 at 4:13
  • 1
    @filbranden done ;)
    – D. Ben Knoble
    Commented May 18, 2020 at 14:43
  • @D.BenKnoble Thanks!
    – filbranden
    Commented May 18, 2020 at 14:46

1 Answer 1

6

Taking a word or otherwise long argument (longer than a single char) from a mapping usually involves prompting the user, using the input() function.

But that's often not very convenient, as you're somewhat slowed down by the prompting and it's also hard to invoke or reuse that mapping from a non-interactive context.

You'll probably find that using a user-defined command is typically a more handy way to expose features that take longer arguments.

In any case, let's create both.

Let's actually start by writing a function to perform the substitution, including the arbitrary word. Even if we didn't make the function public, it would make implementing either the mapping or the user-command easier.

function! SubstBrackets(word)
    let word_re = '\V'.escape(a:word, '/\')
    let pattern = '\v<('.word_re.'\v)\((.{-})\)'
    execute '%s/'.pattern.'/\1{\2}/eg'
endfunction

This function is a somewhat more sophisticated way to perform the substitution you described. It uses "very magic" and "very nomagic" mode together with escape() to ensure special characters in the word will not interfere.

It will also match the word at a word boundary (the < at the start of the match, you might have seen it as \< before, but we're in "very magic" mode here.) It matches the terms inside parens using non-greedy .{-} match to find the nearest closing paren. Finally, it uses two capturing groups, so the word doesn't need to be repeated on the replacement side.

You can test it with:

:call SubstBrackets('interior')

(We could go a step further in sophistication here by making SubstBrackets a "range" function, rather than implicitly acting on % for the whole buffer. See :help :func-range for more details on implementing a range function.)

At this point, we can implement our ;subst mapping using this function, using:

nnoremap <silent> ;subst :call SubstBrackets(
  \ input('Word to replace? ', 'interior'))<CR>

One downside of this mapping is that typing ;subst possibly without visual feedback can be awkward. If you type a single character incorrectly, you can't fix it with "backspace" either, you need to break execution of the macro altogether (with Ctrl+C or similar), then start again.

I briefly discussed how input() can be awkward in interrupting the flow with a prompt and also making it hard to reuse this mapping in a less interactive context. On the upside, input() lets you easily define a default text to use if user simply presses "enter", which can be nice too... (input() also allows you to specify how to do completion of the term you're typing, which can be useful too!)

Now I mentioned creating a user-defined command was a possibly better interface for this feature. This is how you would define one:

command! -bar -nargs=1 SubstBrackets
  \ call SubstBrackets(<q-args>)

And then you can use it with:

:SubstBrackets interior

Note that you can use tab completion here, so you can simply type :Su<Tab> and Vim will complete the command for you!

This interface tends to be better, since you can use this command from many other contexts, such as functions or if or for statements, etc.

Same as with input(), you can specify a completion type to use with the argument of this function. So even though you don't get the descriptive prompt, you do get one of the other benefits of input() here.

You could also implement a default value, by using -nargs=* and changing the function to take variable number of arguments, then checking the number of passed parameters and using a default if none was passed. (In fact, that also allows you to receive multiple arguments, which you could also handle by substituting bracket types in all passed words!)

Let's also explore allowing a range to be passed here. By turning the function into a range function and passing a -range=% to the command definition, we can take an optional range, while still acting on the whole buffer if no range is passed.

Here's a modified complete version of the command, that takes an optional range:

function! SubstBrackets(word) range
    let word_re = '\V'.escape(a:word, '/\')
    let pattern = '\v<('.word_re.'\v)\((.{-})\)'
    let range = a:firstline.','.a:lastline
    execute range.'s/'.pattern.'/\1{\2}/eg'
endfunction

command! -bar -nargs=1 -range=% SubstBrackets
  \ <line1>,<line2>call SubstBrackets(<q-args>)

Hopefully I've successfully demonstrated how and why the user-defined command version of this feature is the most flexible and usable one!

8
  • Fabulous, thanks much @filbranden Two questions: 1)I couldn't make range work: I tried :SubstBrackets -range=13,15 myWord where on lines 13 and 16 I had myWord(X), and hoped the mapping would just operate on line 13, but it didn't change anything. 2) There are many parens in the construction & I couldn't figure out which were the pair that matched those in my expr. I tried constructing a new command substParens that went in the reverse direction: on the let pattern line, I changed ((.{-})\) to ({.{-}}\) & on the execute line I changed \1{\2} to `\1(\2)' but it didnt work
    – Leo Simon
    Commented May 19, 2020 at 7:08
  • @LeoSimon 1) The range comes at the start of the command, so :13,15SubstBrackets myWord. The -range syntax is only for defining the command.
    – filbranden
    Commented May 19, 2020 at 12:23
  • @LeoSimon The literal parens are the ones escaped with `s. So you should.replace ((.{-}))` with \{(.{-})\} on the pattern side. (See :help /\v to see how "verymagic" works.)
    – filbranden
    Commented May 19, 2020 at 12:25
  • @filebranden Thanks very much for the clarifications! I've learned a great deal from this. And I absolutely promise this will be the last followup request. Playing around I realized it would be prudent to add a "confirm" option, as in g/myWord(X)/s//myWord{X}/gc Is there a way to add that option?
    – Leo Simon
    Commented May 19, 2020 at 18:03
  • @LeoSimon To always have it confirm, just change the end of the execute line from /eg' to /egc'. If you want that to be conditional (for example, :SubstBrackets -confirm interior) then it's a somewhat larger modification, but doable... Just make your command use -nargs=*, your function take multiple arguments and use a for loop in your function to check for a "-confirm" or similar argument.
    – filbranden
    Commented May 19, 2020 at 18:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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