Published: June 17, 2019
by Tobias Pleyer
Tags: vim, neovim

Quick batch changes with Vim

Note: Everything mentioned in this post is valid for Vim and Neovim. Throughout this post I will refer to both of them as vim. If I refer to Vim’s own documentation I will do some using the the form (:h <some text>), where :h is a reference to Vim’s built-in help command.

Vim

Vim is a text editor and processor loaded with a very strong dual approach: on the one hand provide the user with a very comprehensive set of commands and key combinations to manipulate text and on the other hand to allow a fully programmatic access to the text. Vim’s power is that editing and scripting have a fluent transition and the borders are fuzzy.

Repetitive editing with Vim

Quite often we are faced with the task to generate text or source code that follows are pattern. Most of the text repeats and only a small portion of the text is dynamic, typically build from some input fed in.

Tasks of that nature beg to be automated to some degree. Depending on the complexity of the task vim offers a couple of alternatives:

  • Single repeats, aka the “dot formula” (:h single-repeat)
  • Line based repeats (:h multi-repeat)
  • Complex repeats, aka Macros (:h complex-repeats)
  • Write a script (:h using-scripts)

In this post I want to give an example for the latter.

The task

Let’s take this very basic C program as our starting point.

We can compile and run it:

In the code we have defined a enumeration for the days of the week and in the main function we provide a printf statement that will tell us which day of the week Sunday is (yes my week starts on a Monday).

Our task is to provide a similar printf statement for every other day in the enumeration.

Note: Obviously this task could be easily done with any of the aforementioned options. But it is quite easy to conceive a much more elaborate example.

Script based solution

Our solution is made up of 6 steps:

  1. Find the proper search pattern for the enumeration lines
  2. Mark the enum lines we are interested in via marks
  3. Mark the line where we want to put (insert) our generated code
  4. Write the function and execute it
  5. Apply the function

Find the proper search pattern for the enumeration lines

this task is much easier when you have incremental search (:h incsearch) activated. Vim will highlight the matches for you while you’re typing, giving instant feedback if the pattern does the job. Once we found a pattern we can save it for later. You can paste it in a line by pressing "/p.

This pattern does the job: ^\s\+\(\w\+\) = \(\d\+\),\?$

Mark the enum lines we are interested in via marks

This is just for convenience and reproducability. No need to write perfect code. We are about to write a function that gets the job done once.

Here is a set of vim commands that mark our enum lines between the marks a and b.

Mark the line where we want to put (insert) our generated code

I want to insert my code as the first line of the main function, so I mark the line right above (with the open curly brace) with the mark p for put.

Write the function and execute it

We have these options:

  • Write the function in the C file itself, yank it and execute the register containing it (:h @). This is ok for small functions but can get messy soon
  • Write the function in an unnamed buffer in Vim and load the contents of the buffer via the command sequence: let @s=joinlet @s = join(getbufline(<bufnr>, 1, '$'), "\n") | @s | call HelperFunc() You must know the buffer number for this. Use the ls command (:h ls)
  • Write the function to a file and source it (:h source)

I will go with the latter option and write the code in a file called script.vim.

Here is the code for the function:

function! HelperFunc()
    let lines = []
    call add(lines, "// Automatically generated variables")
    for l in getline("'a", "'b")
        let [all, day, nr, a, b, c, d, e, f, h] = matchlist(l, '^\s\+\(\w\+\) = \(\d\+\),\?$')
        let lower_weekday = tolower(day)
        let source_line = printf("    enum WeekDay %s = %s; // %d", lower_weekday, day, nr)
        call add(lines, source_line)
    endfor
    call add(lines, "// Automatically generated print statements")
    for l in getline("'a", "'b")
        let [all, day, nr, a, b, c, d, e, f, h] = matchlist(l, '^    \(\w\+\) = \(\d\+\),\?$')
        let lower_weekday = tolower(day)
        let source_line = printf("    printf(\"%s is the %%i%%s day of the week\\n\", (int)%s, get_quantifier(%s));", day, lower_weekday, lower_weekday)
        call add(lines, source_line)
    endfor
    call add(lines, "// Autogen End")
    let @a = join(lines, "\n")
    'p | put a
endfunction

Apply the function

All we have to do is execute this onelinerin vim’s command line: source script.vim | call HelperFunc()

Note: The pipe character is used to sequence commands in vim. It is not to confuse with the shell’s pipe command.

Note2: User defined functions that are globally available must start with an uppercase letter.

End result

Our main function should look like this now: