You can’t have everything. Where would you put it?
– Steven Wright
Attack of the 5,000-line vimrc
Vim is an endlessly configurable and extensible editor, with a culture of users
sharing configuration for their ~/.vimrc
or ~/.vim/vimrc
startup
files. These files tend to expand over time. New users start by setting only a
few global defaults for options like 'expandtab'
and 'wrap'
,
and then add custom mappings, functions, filetype-specific logic, and
third-party plugins, often under an ever-shifting mantle of plugin managers.
Their vimrc files grow not only larger, but more intricate and complex.
If you have one of these longer files, you’re in good company. Damian Conway, a Vim guru specializing in Perl, published a vimrc with 1,855 lines, and at the time of writing, Steve Losh’s is a whopping 3,160 lines. I’m sure you can find vimrc files that are even longer—perhaps yours already is.
The issue with very long vimrc files isn’t the sheer amount of
configuration—after all, all of that power is there for a reason. However, if
you’ve been programming for a while, you’ll know from experience that it’s best
to avoid very large files with code that does many disparate things, because it
makes code hard to find, manage, and understand. Vim configuration is no
exception. Instead of a single large configuration file, there’s a case to be
made for having a set of smaller, well-organized files. Those files go in
~/.vim
.
Creating a directory hierarchy in ~/.vim
to replace your large vimrc keeps
your configuration manageable. It improves efficiency by loading code only when
needed. It becomes clearer from a file’s position in the hierarchy what its
purpose is. It also makes it easier to package configuration for others to
use.
Your own personal $VIMRUNTIME
Most of the benefit of an organized ~/.vim
directory comes from leaning on
Vim’s built-in behavior, which gives you a lot of control over how
configuration files are loaded.
Let’s start by looking at the structure of the runtime files that come with Vim
itself. You can find the path for this directory in the $VIMRUNTIME
variable:
:echo $VIMRUNTIME
If you’re using the version of Vim that was packaged with your operating
system, it will very likely be something like /usr/share/vim/vim81
.
Let’s take a look at the contents of that directory:
$ ls /usr/share/vim/vim81
autoload/ bugreport.vim colors/ compiler/
defaults.vim delmenu.vim doc/ evim.vim
filetype.vim ftoff.vim ftplugin/ ftplugin.vim
ftplugof.vim gvimrc_example.vim indent/ indent.vim
indoff.vim keymap/ lang/ macros/
menu.vim mswin.vim optwin.vim pack/
plugin/ print/ rgb.txt scripts.vim
spell/ synmenu.vim syntax/ tools/
tutor/ vimrc_example.vim
A quick-and-dirty count in the shell shows us there are 1,674 files in this directory tree:
$ find /usr/share/vim/vim81 -type f | wc -l
1674
Of those, 1,335 are .vim
files:
$ find /usr/share/vim/vim81 -type f -name \*.vim | wc -l
1335
All of these are just plain Vim script files, like your vimrc. Their location within this directory determines when they are loaded. Only a few of them are loaded on Vim startup. That’s well over a thousand files ready to be loaded only when relevant. We should take a hint from Bram on that!
If we look at the value of the 'runtimepath'
option in Vim, we can see
a few other paths:
:set runtimepath?
runtimepath=~/.vim,/usr/share/vim/vim81,…,~/.vim/after
The very first entry of 'runtimepath'
is ~/.vim
, and that’s where you can
build a structure mimicking that of $VIMRUNTIME
. This is your personal Vim
runtime directory.
Lose the :source
, Luke
If you’ve worked with Vim script for a while, you probably know how to use the
:source
command to read it from a file. To load a separate file with
something like mapping definitions in it, you might have a line like this in
your vimrc:
source ~/.vim/mappings.vim
Vim has another command named :runtime
for loading files that works
with the file layout of the 'runtimepath'
directories we just inspected. Used
without an exclamation mark, it reads Vim script commands from the first path
it finds in any of its 'runtimepath'
directories. With an exclamation mark
added, it reads all of them. In both cases, we can include filename pattern
matching with globs: *
and ?
characters.
runtime syntax/c.vim
runtime! syntax/c.vim
runtime! */maps.vim
runtime! **/maps.vim
Note that we don’t include the leading ~/.vim
path in these patterns.
The double asterisk in the last example here represents a set of
directory path elements, which can be up to 100 levels deep. This means that a
file in ~/.vim/foo/bar/baz/quux/maps.vim
would still be found and loaded.
Unlike :source
, :runtime
doesn’t raise errors if it can’t find any matching
files. This avoids boilerplate checks for file existence if a file’s absence is
not an error condition.
Much of Vim’s startup process is just thin wrappers around :runtime
commands.
So are some of its other commands, including :filetype
. We can leverage
this to run our own code before, instead of, or after Vim’s bundled runtime
code, in order to disable, replace, modify, or extend it.
Turn on, plugin
, drop out
You can start the process of breaking up your vimrc file by looking for blocks
of code that have expanded beyond simple configuration, and can be grouped
together meaningfully. We can extract these into self-contained files in the
plugin
subdirectory.
As an example, if you read others’ vimrc files, you will often see approaches to solving the problem of conveniently removing trailing whitespace at line endings. Here’s one approach, in a function from the Vim Tips wiki:
function StripTrailingWhitespace()
if !&binary && &filetype != 'diff'
normal mz
normal Hmy
%s/\s\+$//e
normal 'yz<CR>
normal `z
endif
endfunction
This kind of function is usually followed by a mapping to call it:
nnoremap <Leader>x :<C-U>call StripTrailingWhitespace()<CR>
This function doesn’t need to be loaded every time vimrc is sourced. Once
defined, it can just sit there, ready for calling when appropriate. In fact,
Vim throws an error if a vimrc with this function is reloaded. We could fix
that by declaring the function with function!
, but there’s another way:
instead of putting the function definition in ~/.vimrc
, we can drop it into a
.vim
file in ~/.vim/plugin
. We’ll use
~/.vim/plugin/strip_trailing_whitespace.vim
.
Once this file is created, we can restart Vim, and then confirm our plugin has
been loaded by checking its path is in the output of :scriptnames
:
:scriptnames
...
10: ~/.vim/plugin/strip_trailing_whitespace.vim
...
Note that the <Leader>x
mapping left in the vimrc still works, despite being
set before the function it calls was defined.
What’s a plugin, anyway?
Why should we put the script in ~/.vim/plugin
? You might object that our
example isn’t a real plugin; it’s just a single function. However, that’s not
a meaningful distinction to Vim. At startup, it sources any and all .vim
files in the plugin
subdirectory of each of the directories in
'runtimepath'
. It makes no difference what those files actually contain. A
set of related abbreviations? Custom commands? Code dependent on one particular
machine or operating system? Sure, why not?
Plugin subdirectories
Similarly, because *.vim
files are loaded from the plugin
directory
recursively, you can organize them in subdirectories if you want to:
~/.vim/plugin/insert/cancel.vim
~/.vim/plugin/insert/suspend.vim
~/.vim/plugin/visual/region.vim
~/.vim/plugin/whitespace/squeeze.vim
~/.vim/plugin/whitespace/trim.vim
The names of the subdirectories aren’t significant; Vim will search them all.
Remember how we mentioned Vim’s thinly-veiled :runtime
wrappers? This is one
of them. A clue here is in the command that :help load-plugins
suggests
as an analogue to what Vim does internally at this step:
:runtime! plugin/**/*.vim
Local script scope
Putting blocks of code like this in distinct files in ~/.vim/plugin
has some
other advantages. One of these is Vim’s script-variable scoping for
functions and variables that are only needed within the script:
let s:myvar = 'value'
function s:Myfunc()
...
endfunction
This applies a unique prefix to all of your function names and variable names at the time the file is sourced. That means you don’t have to worry about trampling on any other variables defined elsewhere in your configuration.
There are some caveats here for defining mappings; make sure you read :help
script-variable
carefully to make sure you understand how to use <SID>
prefixes.
Short-circuiting and load guards
Another advantage of separate script files is the ability to short-circuit
a script, to prevent it from loading if it’s not appropriate to do so. This is
done by checking at the start of the script whether the rest of it should be
loaded, and skipping it with :finish
if it shouldn’t.
We can use this to check options like 'compatible'
, the Vim version
number, the availability of a feature, or whether the plugin has already been
loaded:
if &compatible
\ || v:version < 700
\ || !has('folding')
\ || exists('g:loaded_myplugin')
finish
endif
let g:loaded_myplugin = 1
This way, you don’t have to wrap all your feature-dependent code in clumsy
:if
blocks.
The question of mappings
Should you include a mapping that uses a defined function in the plugin itself? It’s up to you, but the author likes to think of vimrc files as where user-level preferences go, and plugins where the code they call should go. Mapping choices are personal, and fall into the former category.
If you want to keep some abstraction between what the plugin does and how it’s
called, you can use <Plug>
prefix mappings to expose an interface from
the plugin file:
function s:StripTrailingWhitespace()
...
endfunction
nnoremap <Plug>StripTrailingWhitespace
\ :<C-U>call <SID>StripTrailingWhitespace()<CR>
You can then put your choice of mapping for that target in your vimrc:
nmap <Leader>x <Plug>StripTrailingWhitespace
If someone else wants to use your plugin, this makes choosing their own
mappings for it more straightforward. There’s more general advice about good
mapping practices in writing fully-fledged plugin files in :help
write-plugin
.
Not really my :filetype
Another pattern in big vimrc files is setting options only for buffers of a
certain filetype. For example, this line of code is intended to set the
'spell'
option, to highlight possible spelling errors in the text, but
only for mail
filetype buffers:
autocmd FileType mail setlocal spell
The first thing to note here is that this should be surrounded in a
self-clearing augroup
, so that reloading it doesn’t make multiple
definitions for the same hook:
augroup ftmail
autocmd!
autocmd FileType mail setlocal spell
augroup END
This is annoying, but there’s a way to avoid this boilerplate.
The second thing to note about our autocmd
is that it’s set every time vimrc
is loaded, regardless of whether a mail file is actually edited in that
session. It therefore makes more sense to put this into a filetype plugin
or ftplugin, so that it’s only loaded when relevant.
The autocmd
hooks that set a buffer’s filetype are defined in
$VIMRUNTIME/filetype.vim
. They apply heuristics to guess and then set the
type of a buffer, and then Vim runs any appropriate filetype plugins
afterwards: files in 'runtimepath'
directories named ftplugin/FILETYPE.vim
will be sourced.
This means there’s no need for the autocmd
hooks around our 'spell'
setting. We already have hooks for changes of filetype available to us, and we
can just put this single line in ~/.vim/ftplugin/mail.vim
to use them:
setlocal spell
With this done, upon editing a new mail
buffer, we can confirm that our
filetype plugin was loaded when the filetype was chosen using :scriptnames
:
:set filetype=mail
:scriptnames
...
20: ~/.vim/ftplugin/mail.vim
...
:set spell?
spell
This is better, but we can improve it further.
Loading filetype configuration afterwards
Rather than putting our 'spell'
setting in ~/.vim/ftplugin/mail.vim
, we can
put it in ~/.vim/after/ftplugin/mail.vim
—note the extra directory named
after
in the path.
Files in the after
runtime directory are loaded after the analogous
runtime files included in Vim. Using this path, we can ensure that our option
is set after the mail
filetype plugin in $VIMRUNTIME/ftplugin/mail.vim
has been sourced. This is how to override something a filetype plugin does if
you don’t like it.
Breaking up filetype plugins
If you need to make this even more granular, you can also put files in subdirectories named after the filetype:
~/.vim/after/ftplugin/mail/spell.vim
~/.vim/after/ftplugin/mail/quote.vim
The filetype followed by an underscore and then a script name works, too:
~/.vim/after/ftplugin/mail_spell.vim
~/.vim/after/ftplugin/mail_quote.vim
You may have guessed by now that filetype switching is yet another :runtime
wrapper. Switching to a filetype of mail
effectively runs this command:
:runtime! ftplugin/mail.vim ftplugin/mail_*.vim ftplugin/mail/*.vim
Undoing filetype settings
If the filetype of a buffer changes, we should reverse any local
configuration we applied. We can do this with the b:undo_ftplugin
variable, which contains a list of pipe-separated (|
) commands. When a
buffer’s filetype changes, the commands undo the buffer-specific settings for
the previous filetype, ready for the new filetype’s plugins to be loaded.
After each filetype plugin setting we make, we should append corresponding
commands to reverse that change to b:undo_ftplugin
. For our 'spell'
example, we’d do this:
setlocal spell
let b:undo_ftplugin .= '|setlocal spell<'
The spell<
syntax used here, with a trailing left angle bracket, specifies
that the local value of 'spell'
should be restored to match the global value
of 'spell'
when the mail
filetype is unloaded.
After setting a filetype, we can check the b:undo_ftplugin
variable’s value
with :let
:
:set filetype=mail
:let b:undo_ftplugin
b:undo_ftplugin setl modeline< tw< fo< comments<|setlocal spell<
The difference with indent
Filetype-specific code related to indentation goes in a different location
again: ~/.vim/indent/FILETYPE.vim
or ~/.vim/after/indent/FILETYPE.vim
.
Those files are sourced if you include the word indent
in your vimrc
’s
:filetype
call. You should use this layout for files that change
'autoindent'
or 'indentexpr'
settings, for example.
You can put indent settings in your filetype plugin if you want to, but
remember that we’re trying to find the right place for things. Doing it this
way keeps your indentation settings separate from all other filetype-specific
settings. This gives users an easy way to load only what they want, using
appropriate arguments to their vimrc’s :filetype
call.
Detecting filetypes
As a final note for filetype-dependent logic, if you have any hooks to set a
buffer’s filetype in the first place, based on its filename or contents, those
go in the ftdetect
directory. You might put this in
~/.vim/ftdetect/irssilog
, for example:
autocmd BufNewFile,BufRead */irc/*.log setfiletype irssilog
Putting the hooks in ~/.vim/ftdetect
means they are sourced as part of the
filetypedetect
augroup
defined in filetype.vim
. This is another context
in which you don’t need to surround autocmd
definitions in a self-clearing
augroup
, because it’s already been done for you.
Be water, my friend
All of the above is just the beginning. We haven’t even touched on
lazy-loading functions for speed with definitions in ~/.vim/autoload
,
or custom :compiler
definitions for setting 'makeprg'
and
'errorformat'
in ~/.vim/compiler
. These are yet more examples of Vim
functionality that wraps around :runtime
loading.
While Vim gives you a lot of flexibility in configuring and customizing, there is definitely a Way of Vim for the timely loading of relevant configuration, and if you learn a little about how it works, you’ll fight with your editor that much less. If this seems stringent to you, think back to when you first learned Vim. Do you remember how strange using the HJKL keys for movement seemed, before it made sense? Do you remember how you wanted to stay in insert mode all the time, before normal mode made sense?
Working within the Vim runtime file structure instead of ignoring it or
fighting with it makes your ~/.vim
directory into a refined toolbox, with a
place for everything, and everything in its place. It’s well worth the effort!
If you’d like to see an example of how this layout can end up looking when you
make it work for you, the author’s personal ~/.vim
directory is available
for download.