Search code examples
vimuser-inputinteractivevim-macros

Vim Macro which accepts user input


Lets say I have the following text file

name: John Doe
description: My name is John Doe and I'm really good at vim!
name: John Doe
description: My name is John Doe and I'm really good at vim!
name: John Doe
description: My name is John Doe and I'm really good at vim!
name: John Doe
description: My name is John Doe and I'm really good at vim!

Is there a way to record a macro that does the following:

Starting at the first John:

  1. cw but allow the user who runs the macro to give input here

  2. js<is>w sneak the word after is

  3. . replace John with the given input
  4. Repeat

Ideally I would like to enter the following keystrokes on the first line:

@qJane

and have:

name: Jane Doe
description: My name is Jane Doe and I'm really good at vim!

@qJim

name: Jim Doe
description: My name is Jim Doe and I'm really good at vim!

Solution

  • Simplest way to take user input is to use the input() function, which lets you prompt for a name and use it in expressions.

    Unfortunately, you can't really use it from a macro, since a macro would record your answer to the prompt as well (and replaying one you build would have a similar issue.)

    You can use it from a function and bind that function to a key map though:

    function! ChangeName()
      let newname = input('Name? ')
      return '0/\<John\>'."\<cr>cw".newname."\<esc>n."
    endfunction
    nnoremap <expr> <leader>cn ChangeName()
    

    You can then type \cn, which will prompt you for a new name. Once you type the new name and press ENTER, it will replace the first two occurrences of John with the new name.

    The function returns the expansion of the mapping, using <expr> in the map command to have it use the return value of the function. (Having the function issue a :normal! command with an :execute would also be a possible approach.)

    You mentioned pressing @qJim or @qJane, where there's not a prompt and the new name "feels" like part of the command. It's possible to get closer to something like that by using getchar() in a loop from the function behind your mapping. You still need to decide on how to terminate the name, will you take an ENTER at the end? Will you use timing to decide when the command is over (need to type it quickly?) You might also need to handle backspace and cancelling the command if you use getchar(). Using input() is certainly easier, and might be enough depending on your particular use case.