Skip to content

Build your specialised editor with vim

Intro

In this post we’ll see how easy it is to configure vim to get a specialised editor. You only have to remember that any command-mode instruction you have available inside vim (those commands you type preceded by : to enter the command mode), can also be passed on the command line by prefixing the command with a + rather than a : (or use the flag -c). Those are iso-called Ex commands. This means that all you can do with an Ex command, you can do from the command-line. The way then to build your specialised editor is to identify which commands to run and pass them on the command line. the best is to take a look at a real-world example.

I have an application localised with po files. Po files are the format used by GNU Gettext and comes with tools to help the management of your translations stored in po files. One such tool is msgcmp which (in short) compares two po files and reports the missing translations. This is exactly how I how it in my application. msgcmp outputs a list of missing translations of the form:

locale/en/LC_MESSAGES/myowndbhelp.po:22: this message is untranslated
locale/en/LC_MESSAGES/myowndbhelp.po:26: this message is untranslated
locale/en/LC_MESSAGES/myowndbhelp.po:33: this message is untranslated
locale/en/LC_MESSAGES/myowndbhelp.po:37: this message is untranslated
locale/en/LC_MESSAGES/myowndbhelp.po:41: this message is untranslated
locale/en/LC_MESSAGES/myowndbhelp.po:47: this message is untranslated

And this format can be used to populate a vim quickfix list. Here’s a good blog post about it.

Populating the quickfix list

Wouldn’t it be great if we had vim open with the quickfix list populated with these entries? Easy! In my case I have this list generated by the command make check-pos. And vim provides the command :cexpr to read a quickfix list from the output of a command. Inside vim you can call it as :cexpr system("make --silent check-help-pos")'.

To have a solid solution, I had to add an errorformat directive, also shortened as efm. For single line error messages like in this case, it is to hard to build the printf-like format string.The result is:

set efm=%f:%l:\ this\ message\ is\ untranslated,%-G%.%#

where we identify the file with %f, the line with %l, the literal string to match, with spaces escaped with \. The only culprit is adding ,%-G%.%#. The erroformat string is actually a comma-separated list of directives, and this second directive tells vim to ignore any other line it might find in the output. IMPORTANT NOTE: Be sure to set efm before setting cexpr! Failing to do so will result in efm not being applied!

Having done this, we can navigate the missing translations with :cnext and :cprev. So, starting vim with the command vim 'set efm=%f:%l:\ this\ message\ is\ untranslated,%-G%.%#' '+cexpr system("make check-help-pos")', our cursor is placed at the start of the line of the first missing translation.

Insert mode at the right location at startup

A missing translation in my case is a line like this:

msgstr ""

This means I still have to move the cursor to the empty string "", go in insert mode, get out of insert mode, type :cnext (or :cn), and repeat. Can’t we do better? Of course yes!

The first thing we can do is automatically go in insert mode at the right place when we start vim. We do it in two steps: go the the end of the line, and enter insert mode. the difficulty here is that we need to move to the end of the line with an Ex command. Just typing vim +'$' will not do what you want: the Ex command $ will move you to the last line of the file. What we want is execute the normal mode command $ from the command mode. This can be done with the command mode instruction aptly named normal. Thus we append the vim command flag '+normal $$'. Entering insert mode is done we the command :startinsert (take good note of this, it was not easy to find on the internet ;-). We can thus add the flag '+startinsert' to the vim command.

At this stage we have the vim command vim '+cexpr system("make check-help-pos")' '+normal $$' '+startinsert'.

As an aside, note that you can combine multiple command in one by using :exec and using | as a command separator. The previous command line can thus be written similarly as vim '+cexpr system("make check-help-pos")' '+exec "normal $"|startinsert'.

So now, at startup, we are immediately in insert mode at the right location for the first missing translation. But for the other missing translation, we still have cumbersome navigation. Let’s improve that!

Rapid navigation

In my translations, I will never type a <Tab> character. Wouldn’t it be great that by pressing <Tab> my cursor is moved to the next missing translation? And would it be possible to not have to quit insert mode? Of course it is!

In vim we can define (non-recursive) mappings active in insert mode with the command :inoremap. We just have to identify what this mapping has to do. That’s not that difficult:

  • first escape insert mode, by pressing <Esc>
  • then move to the next translation with :cnext
  • move the cursor to the right place (in this case I do a search for "" with /"".)
  • pass in insert mode with a.

This results in this additional flag ‘+inoremap :cnext/""a’. Moving to the previous missing translation with <Shift>-<Tab> is then easily added with '+inoremap <S-Tab> <Esc>:cprev<CR>/""<CR>a'

Conclusion

In the end, I have at my disposal an editor specialised in the missing translations of my project. The final command is below. It can be added as an alias of in a dedicated script, but I added it to my makefile under the target fix-pos. This means that with the command make fix-pos I’m dropped by vim in insert mode at the first missing translation, and I can navigate to other missing translations with <Tab> and <Shift-Tab>.

Here’s the command:

vim \
    'set efm=%f:%l:\ this\ message\ is\ untranslated,%-G%.%#'
    '+cexpr system("make --silent check-help-pos")' \
    '+inoremap <Tab> <Esc>:cnext<CR>/""<CR>a' \
    '+inoremap <S-Tab> <Esc>:cprev<CR>/""<CR>a' \
    '+normal $$' \
    '+startinsert'

RIP Bram

I was working on this when I learned that Bram Molenaar, the author of vim, had passed away. RIP Bram, and thanks for the work put in vim. It literally defined my way of working. As witnesses, take all the :wq written in documents not authored in vim.