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 keyword
s. 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
- Read through chapters 41-49 of Learn Vimscript the Hard way (you really should read the whole thing)
- Look at other syntax plugins on GitHub
- Look at local syntax plugins with
:edit $VIMRUNTIME/syntax/LANGUAGE.vim