3

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!
dreftymac
  • 31,404
  • 26
  • 119
  • 182
Daniel Cooke
  • 1,426
  • 12
  • 22

2 Answers2

3

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.

filbranden
  • 8,522
  • 2
  • 16
  • 32
  • I like this, especially the `getchar()` part – Daniel Cooke Sep 04 '19 at 17:16
  • 1
    `getchar()` can be tricky... But if you're interested in that, take a look at the [first few lines of vim-surround](https://github.com/tpope/vim-surround/blob/master/plugin/surround.vim) for some ideas on that. The vim-surround plug-in does something similar to get surround characters and html tags, which is perhaps close to what you want. – filbranden Sep 04 '19 at 17:20
  • What is the exclamation mark after `function` for? – Martín Nieva Dec 29 '20 at 19:21
  • 1
    @MartínNieva See `:help :function`. (In general, Vim has a very rich help system, always use that first, as in most cases it can answer your questions directly.) – filbranden Dec 29 '20 at 20:24
2

Although q is often used with the term macro, its purpose is to simply record and repeat a sequence of characters. Repeating being key here — if your intention is not to repeat the same exact sequence, then q is probably not the ideal solution.

You should be able to achieve this easily with a map instead (note that :h macro will point you to :map). You can use :execute to build a command which substitutes the first occurrences of John in the current line and the line after with whatever input() returns. The latter is responsible for prompting the user.

Here's a small example that implements this mapped to ,n:

:nnoremap ,n :exe ',+s/John/' . input('Name: ')<CR>

If you don't know the name to be replaced, you can ask for that too:

:nnoremap ,n :exe ',+s/' . input('Old name: ') . '/' . input('New name: ')<CR>

Or use a regex which captures the first word after "name" ignoring ":" or "is":

:nnoremap ,n :exe ',+s/\vname(: \| is )+\zs\w+/' . input('New name: ')<CR>
sidyll
  • 57,726
  • 14
  • 108
  • 151
  • While this is certainly a solution, its not quite as general as I would like. It seems there no way to accept input from a macro unfortunately. With this solution (unless I'm mistaken) I would have to map a new command and replace `,+s/John/` with the name I want to replace – Daniel Cooke Sep 04 '19 at 17:09
  • @DanielCooke There is a way to accept input in a macro, which is demonstrated above. Note the distinction between macro and repeated keystrokes done by recorded sequenced with `q`. As for replacing John with the name you want, you mean manually? Of course not. Did you try executing the solution I posted? Type in the solution and then you can replace your names with `,nJoe`, `,nJane`, etc. – sidyll Sep 04 '19 at 17:20
  • Yes, and what im saying is - this command will not work for names that aren't John. What if I want to replace `Chris` with `Dan` - you have hardcoded in the string `,+s/John/` into the command. So as I said previously - I would have to change this hardcoded string everytime I want to change a new name – Daniel Cooke Sep 04 '19 at 17:24
  • @DanielCooke Well, you didn't mentioned that in your original question but I'll update my answer with a couple solutions. – sidyll Sep 04 '19 at 17:39
  • And just to further clarify, when you `:map` you are not creating a command, you're creating a macro. A command is created with `:command!`. A sequence of repeated keystrokes can be recorded with `q`. – sidyll Sep 04 '19 at 17:42
  • @siidyll thanks for the update! Is it possible to use `cw` in this macro? If so that would be perfect. – Daniel Cooke Sep 05 '19 at 09:27