Writing Vim Syntax Plugins

Keith Smiley

One of Vim’s most powerful features is the vast built-in and available language support. With the rise of new languages and the desire to write them in Vim with proper syntax highlighting, it’s useful to understand how to create our own syntax plugins for Vim in order to write, extend, or contribute to open source syntax plugins.

Syntax Plugin Structure

Syntax plugins, like all Vim plugins, follow a basic folder structure. This file structure became mainstream with the release of pathogen and works with all modern Vim plugin managers such as Vundle and vim-plug. I’m going to be using swift.vim as an example of how syntax plugins fit together. Here is the relevant file structure (using tree):

├── ftdetect
│   └── swift.vim
├── ftplugin
│   └── swift.vim
├── syntax
│   └── swift.vim
└── indent
    └── swift.vim

ftdetect

The first configuration file we encounter is in the ftdetect folder (:help ftdetect). In this folder we have one or more files named after the filetype they apply to. In this example we have a swift.vim file. In ftdetect files we decide whether or not we want to set (or override) the filetype of a file in different situations. For swift.vim this file looks like this:

" ftdetect/swift.vim
autocmd BufNewFile,BufRead *.swift setfiletype swift

Here we’re using setfiletype on any files named *.swift whenever we create a new file or finish reading an existing file. setfiletype is a friendlier way to do set filetype=swift. If the filetype has already been set setfiletype will not override it. Depending on what our syntax plugin does that might not always be what we want.

ftplugin

Next we have the ftplugin folder (:help ftplugin). This lets us execute arbitrary Vimscript or set other settings whenever the file is sourced. Files in Vim’s runtimepath (:help rtp) nested in ftplugin folders are sourced automatically when the filename (before .vim) matches the current filetype setting (we can read this setting with :set ft). So after our ftdetect file sets the file type as swift, Vim will source the swift.vim file nested in the ftplugin folder. In swift.vim that file looks like this:

" ftplugin/swift.vim
setlocal commentstring=//\ %s
" @-@ adds the literal @ to iskeyword for @IBAction and similar
setlocal iskeyword+=?,!,@-@,#
setlocal tabstop=2
setlocal softtabstop=2
setlocal shiftwidth=2
setlocal completefunc=syntaxcomplete#Complete

All of these settings are things that apply to how we want Vim to act when editing Swift files. The most interesting here is when we set iskeyword. Here we’re adding a few symbols so that when we execute commands such as diw those symbols will be treated as just another character in the word instead of treating them as delimiters breaking words.

syntax

Now we get to the meat of syntax plugins. Files nested under the syntax folder named after the current filetype setting are sourced in order to actually highlight the file. Syntax files contain rules in order to match patterns in our file. Here are a few examples:

" syntax/swift.vim
" Match TODO comments
syntax keyword swiftTodos TODO XXX FIXME NOTE

" Match language specific keywords
syntax keyword swiftKeywords
      \ if
      \ let
      \ nil
      \ var

Both of these examples define new syntax matches for keywords. Keyword matches should be used for the language’s reserved words that should always be highlighted as such. Matches can also get more complicated:

" syntax/swift.vim
" Match all Swift number types
syntax match swiftNumber "\v<\d+>"
syntax match swiftNumber "\v<\d+\.\d+>"
syntax match swiftNumber "\v<\d*\.?\d+([Ee]-?)?\d+>"
syntax match swiftNumber "\v<0x\x+([Pp]-?)?\x+>"
syntax match swiftNumber "\v<0b[01]+>"
syntax match swiftNumber "\v<0o\o+>"

Here we’ve defined syntax matches with the match argument allowing us to match regular expressions (see :help magic for what’s going on here). These specific regular expressions match all of Swift’s number types. Some examples of matched numbers with these:

5
5.5
5e-2
5.5E2
0b1011
0o567
0xA2EF

Syntax matching can get even more complicated when we start matching function declarations and other statements. For example:

" syntax/swift.vim
" Match strings
syntax region swiftString start=/"/ skip=/\\"/ end=/"/ oneline contains=swiftInterpolatedWrapper
syntax region swiftInterpolatedWrapper start="\v\\\(\s*" end="\v\s*\)" contained containedin=swiftString contains=swiftInterpolatedString
syntax match swiftInterpolatedString "\v\w+(\(\))?" contained containedin=swiftInterpolatedWrapper

Here we’re matching Swift strings that are wrapped in double quotes. In the first line we start out by matching the swiftString region which defines start, end, and optional skip patterns. Here the skip pattern allows us to have escaped double quotes in our strings. We also pass some other arguments here. oneline specifies that this pattern can only span a single line. contains specifies what other syntax groups we’ve defined can exist within our region. In this case a string can contain interpolated arguments similar to Ruby. In Swift interpolated strings look like this:

"2 + 2 = \(2 + 2)"

The second line shows how we define the region enclosed within the interpolated string. Our start region specifies the starting \( pattern along with any number of optional spaces while our end region matches spaces and the closing ). We specify that this is contained within our strings so it is not matched elsewhere and that it contains our third syntax group. In our final group we specify what can be contained within the interpolated portion of our string.

When writing syntax files for entire languages, we’ll have to create matches using each of these types of groups to match different things. The hardest part of this is dealing with collisions. We have to be very aware that what we’re changing won’t break another portion of our highlighting.

Once we have our matches we need to tell Vim how to highlight them in a way it understands. Here are the highlighting associations for swift.vim:

" syntax/swift.vim
" Set highlights
highlight default link swiftTodos Todo
highlight default link swiftShebang Comment
highlight default link swiftComment Comment
highlight default link swiftMarker Comment

highlight default link swiftString String
highlight default link swiftInterpolatedWrapper Delimiter
highlight default link swiftNumber Number
highlight default link swiftBoolean Boolean

highlight default link swiftOperator Operator
highlight default link swiftKeywords Keyword
highlight default link swiftAttributes PreProc
highlight default link swiftStructure Structure
highlight default link swiftType Type
highlight default link swiftImports Include
highlight default link swiftPreprocessor PreProc

Here we are linking one of our match group names to a Vim-specific keyword based on what our group matches. We can see all available matches under :help group-name where they are highlighted based off our current color scheme. While it may be tempting to link a group to a type based on its appearance in our color scheme, it’s more important to maintain the match’s semantic meaning so they will work as expected in other user’s color schemes.

indent

Finally we have the indent folder. Again, it contains a file named after the filetype for how indentation should work as we’re writing in Vim. Personally I think handling indentation is the hardest part of writing syntax plugins. The goal in these files is to do one specific thing. Set our indentexpr (:help inde) to a function that calculates and returns the level of indent for the current line. While that may sound simple, taking the context of that line into account can be extremely difficult. Also, just like with syntax files, we have to worry about collisions. Let’s look at some of this file for swift.vim.

" indent/swift.vim
setlocal indentexpr=SwiftIndent()

function! SwiftIndent()
  let line = getline(v:lnum)
  let previousNum = prevnonblank(v:lnum - 1)
  let previous = getline(previousNum)

  if previous =~ "{" && previous !~ "}" && line !~ "}" && line !~ ":$"
    return indent(previousNum) + &tabstop
  endif

  ...
endfunction

Here we’re setting the indentexpr to our function. Then inside our function we’re getting some context around the line we’re trying to indent. The first thing we notice is the v:lnum variable (:help lnum-variable). This is a predefined variable (:help vim-variable) that is set automatically inside indent functions. It gives us the line number for the current line to be indented. We can then use it along with some other Vimscript to get the text on that line along with the text on the last non-blank line above it. Using this information, along with some pattern matching, we can build out cases where our lines should be indented more, less, or the same as the surrounding lines.

Wrapping up

Combining these four Vim plugin file types you can contribute to syntax plugins you have found bugs in, create syntax plugins for additions to languages or entire languages. Doing so you can help Vim continue to stay awesome as new programming languages come and go.

What’s next