Vim auto-capitalisation

Aug 29, 2014

Continuing in the same theme as my last post, I am going to write about another awesome Vim tip I found recently. That is: automatic capitalisation of the first letter of each sentence.

Many word processors already include this. For some reason, Vim and other text editors don't usually have the option. I don't know why. I feel that a lot of unnecessary key-strokes are wasted on the shift keys. It also stresses the little fingers and twists the wrists.

The scripts

There is surprisingly little information available about this. The only decent thing I've ever found has been this superuser question/answer. There are two answers, both of which I like:

for char in split('abcdefghijklmnopqrstuvwxyz', '\zs')
    exe printf("inoremap <expr> %s search('[.!?]\\_s\\+\\%%#', 'bcnw') ? '%s' : '%s'", char, toupper(char), char)
endfor

The above script creates insert mode mappings for every lower-case letter. It uses the <expr> option to evaluate an expression instead of mapping directly to another key. That expression then searches backwards to check for certain punctuation. It then returns either the original character or the upper-case version of the character if a match was found.

I like it but I prefer the second answer:

augroup SENTENCES
    au!
    autocmd InsertCharPre * if search('\v(%^|[.!?]\_s)\_s*%#', 'bcnw') != 0 | let v:char = toupper(v:char) | endif
augroup END

Here, instead of creating mappings, an auto-command is used. It "listens" for characters to be inserted and intercepts them. It searches backwards for punctuation and converts the character to be inserted (v:char) to upper-case. The augroup and au! stuff is just for grouping and isn't terribly important.

If you want to bypass the script(s) and force a lower-case letter to be inserted, you can press Ctrl+V followed by the letter.

Customisation

I'll be the first to admit that my Vimscript isn't the best. Nevertheless, I still wanted to change a few things based on the way I type. I wanted to add a few more contexts in which letters get capitalised:

  • Markdown list items
  • YAML front matter titles
  • New paragraphs (which happen to come after something that doesn't end in a punctuation mark)

Here is my tweaked script:

augroup SENTENCES
    au!
    autocmd InsertCharPre * if search('\v(%^|[.!?]\_s+|\_^\-\s|\_^title\:\s|\n\n)%#', 'bcnw') != 0 | let v:char = toupper(v:char) | endif
augroup END

It can be tricky to figure out what's going on in a regular expression like that. I think the Vim documentation on patterns is pretty helpful. Here is a very brief summary of what's going on:

  • \v means that the following pattern is "very magic" so not as many characters need to be escaped
  • %^ is the start of the file
  • [.!?]\_s+ is a punctuation mark followed by some whitespace which may or may not include newlines
  • \_^\-\s is a dash at the start of a line followed by a whitespace character
  • \_^title\:\s is "title: " at the start of a line
  • \n\n matches two newlines
  • %# matches the cursor position
  • The 'b' option means search backwards

[Edit on 2014-09-29: I somehow forgot to add Markdown headers to the list of capitalisation contexts. I will leave that as an exercise for the reader.]

Putting it all together

Combining this with the rest of my configuration options gives the following section of my .vimrc file:

func! WordProcessorMode()
    " Load Markdown syntax highlighting but with custom hashtag support
    set filetype=mkd
    syn match htmlBoldItalic "#[-_a-zA-Z0-9]\+"

    " Other options
    set nonumber
    set wrap
    set linebreak
    set breakat=\ 
    set display=lastline
    set tabstop=4
    set softtabstop=4
    set shiftwidth=4
    set formatoptions=
    set spell spelllang=en_gb
    source ~/.vim/abbreviations.vim

    " Auto-capitalize script
    augroup SENTENCES
        au!
        autocmd InsertCharPre * if search('\v(%^|[.!?]\_s+|\_^\-\s|\_^title\:\s|\n\n)%#', 'bcnw') != 0 | let v:char = toupper(v:char) | endif
    augroup END
endfu

com! WP call WordProcessorMode()
au BufNewFile,BufRead *.mkd call WordProcessorMode()
au BufNewFile,BufRead *.md call WordProcessorMode()
au BufNewFile,BufRead diary-*.txt call WordProcessorMode()