Vim configuration is powerful and complex. I mean, this isn’t setting a few options in an INI file; we’re talking about a Turing complete programming language tailor made for scripting an editor. Thus, your config is a computer program in itself, and of course this means that there will be bugs…

There can be many plugins and script files sourced, each one can run code at basically any time, and each can set global variables and change global or buffer options. If one script doesn’t play well with another, or there is some obscure bug, it can be a nightmare to ferret out the problem. Some combinations of options can cause unexpected behaviour when editing.

This article is going to cover a range of techniques and ideas I’ve learnt over the years for maintaining a working config and quickly finding sources of issues. This isn’t going to cover debugging Vim itself or using debugging mode.

Understand the startup process

Vim’s startup procedure is complex and results in scripts being sourced from many locations and in a particular order. Understanding what files are sourced and when scripts are run is important when it comes to understanding potential issues and how to debug them. The best resource for this is :h startup.

For example, it is not possible to run a function defined in a plugin from ~/.vimrc, because Vim sources ~/.vimrc before sourcing any plugin. However, you could put the function in an after directory, because they are sourced after normal plugin scripts.

If you need to see what scripts Vim has sourced once it has started up, you can use the :scriptnames command to list them.

Clean code!

I know this isn’t a software project with code quality standards or required TDD, but it is still helpful to write clean vimscript and part of that is organizing everything neatly. Make the most of Vim’s runtime directories structure. See the post “From .vimrc to .vim” on Vimways for some ideas in this direction.

Put code specific to a certain plugin in separate files with an include guard. For example I store the following config for fzf.vim in ~/.vim/after/plugin/config/fzf.vim.

" include guard; quit if fzf isn't loaded
if ! exists(':FZF')
    finish
endif

map <silent> <c-space> :Files<cr>
map <silent> <leader>t :Tags<cr>
map <silent> <leader>m :Marks<cr>
let g:fzf_layout = { 'down': '~20%' }

Putting it in an after directory means that it will only be sourced after all other non-after plugins have loaded, so we can check if fzf has loaded before binding any mappings. Using a load guard means that I can temporarily remove the fzf.vim plugin and know that nothing will crash and there won’t be any zombie mappings.

If all plugin-specific config is behind load guards, it’s really easy to disable any set of plugins to help narrow down the cause of an issue.

Finally, some plugins might require some global options set before they are loaded for them to take effect. For example, for Deoplete, I have a script under ~/.vim/plugin/ that contains:

let g:deoplete#enable_at_startup = 1

And then further config under ~/.vim/after/plugin/ that will only run if Deoplete is loaded. See, it is useful to understand Vim’s startup order!

VCS

If you don’t already, please consider storing your config in a version control system! At the very least this will give you timestamped checkpoints and help narrow down the source of a recently introduced bug. Future you will be forever thankful.

Why is Vim’s startup slow?

OK, we’ve seen that Vim could end up sourcing many files on startup, especially if many plugins are installed. It may happen that Vim can start taking a long time to startup, and it can be useful to discover the culprit.

Start Vim with the --startuptime <fname> argument to profile the startup process. This will produce a file containing lines such as:

069.228  004.840  004.646: sourcing /home/samuel/.vim/pack/bundle/opt/vimwiki/ftplugin/vimwiki.vim
070.164  000.018  000.018: sourcing /usr/share/nvim/runtime/autoload/provider.vim
317.745  247.930  247.912: sourcing /home/samuel/.vim/pack/bundle/opt/taskwiki/ftplugin/vimwiki/taskwiki.vim
331.351  012.951  012.951: sourcing /home/samuel/.config/nvim/after/ftplugin/vimwiki.vim

Hmm, taskwiki is taking over 200ms to load…

Vim also has good support for profile individual scripts. See :h profile for more information.

Verbose

:verbose is your friend. This command executes its arguments as a command but with the ['verbose'] option set to 1. This helps with what comes next…

Where did that option/mapping/command come from??

Sometimes you may want to figure out just what script last set an option, mapping, command, etc.

Options

Vim has over 300 options (:h option-list) that can be set. A common issue is that a particular option may be unexpectedly set by a plugin and you want to find out which plugin and why. (Eg. Who turned off expandtab?) Here, you can use :verbose and the ? modifier to :set:

:verbose set shiftwidth?

This will display something like:

  shiftwidth=2
        Last set from ~/.vim/pack/bundle/start/vim-sleuth/plugin/sleuth.vim

This tells me that shiftwidth was set to 2 by the sleuth plugin.

Note that if the option was set manually instead of inside a function, command, or autocmd, it won’t display where it was last set.

Similarly many other things can be listed and traced to their source:

Mappings

Example command and output:

:verbose map <c-a>
  x  <C-A>         <Plug>SpeedDatingUp
          Last set from ~/.vim/pack/bundle/opt/vim-speeddating/plugin/speeddating.vim
  n  <C-A>         <Plug>SpeedDatingUp
          Last set from ~/.vim/pack/bundle/opt/vim-speeddating/plugin/speeddating.vim

Also note that :map lists all mappings that begin with the key sequence given. This is helpful for things like listing all insert mode mappings that begin with the leader key:

:verbose imap <leader>

Abbreviations

:verbose ab teh
  i  teh           the
          Last set from ~/.vim/autoload/functions.vim

Highlight groups

:verbose highlight Visual
  Visual         xxx cterm=reverse ctermfg=10 ctermbg=8 gui=reverse guifg=#586e75 guibg=#002b36
          Last set from ~/.vim/pack/bundle/opt/flattened/colors/flattened_dark.vim

scriptease.vim

Let’s just digress a moment to introduce a useful plugin!

scriptease.vim is a helpful plugin by Tim Pope that provides some commands and mappings to help debug scripts. Some commands are related to debugging vimscript which is out of scope for this article, but others are more relevant. Commands like :Scriptnames and :Messages are wrappers around the native Vim counterparts that saves the output into the quickfix list for later use.

Anyway, zS is a mapping provided by scriptease.vim that echos syntax highlighting groups under the cursor. You can’t show details on a highlight group if you don’t know what highlight group you want!

Commands

:verbose command Sedit
      Name        Args       Address   Complete  Definition
      Sedit       *                                  call local#scratch#edit('edit', <q-args>)
          Last set from ~/.vim/init.vim

Functions

:verbose function local#scratch#edit
     function local#scratch#edit(cmd, options)
          Last set from ~/.vim/autoload/local/scratch.vim
  1    " use a system provided temporary file
  <snip> (the whole function body is listed)

It’s more complicated than a single option/mapping/something

Sometimes it is helpful to run Vim with the bare minimum of config to see if certain behaviour is present in vanilla Vim or in a particular plugin within a clean environment. A combination of command line arguments can be used as shown below (table taken from :h --noplugin):

argument load vimrc files load plugins
(nothing) yes yes
-u NONE no no
-u NORC no yes
--noplugin yes no

-u can also be used to specify an alternative vimrc file to load. Be careful though! vim -u my-vimrc will still load other scripts from ~/.vim/ as usual. vim -u my-vimrc --noplugin will disable this behaviour and load only from the my-vimrc file. Something to note is that Vim (not Neovim) also has a -U argument which controls the gvimrc file sourced, which is relevant if you use GVim.

An example minimal config for testing a particular plugin might look like:

" my-vimrc

" any required settings
set nocompatible
syntax on
filetype plugin indent on

" add the plugin to test here
" eg. (assuming available in an opt dir under packpath)
" this will work neatly with --noplugin
packadd nuake

Alternately, don’t use --noplugin and manually set the runtimepath or packpath to make sure the correct plugins are loaded. You should also decide whether or not you want the system shipped runtime files loaded or not too.

Something is still wrong?!?

Sometimes there can still be unexpected behaviour that can be difficult to debug, especially if caused by plugins conflicting. If your config is neatly structured this is going to be easy! Otherwise, I would recommend pausing and spending some time organizing config. Anyway, we can use binary search to locate the source of trouble!

The idea is to start by commenting out approximately half of your Vim configuration. Start Vim and try to reproduce the unexpected behaviour. If the behaviour is still there, then you know it’s in the uncommented half of the config. Otherwise, it’s in the commented half. Then, with the remaining half, comment out half of that and repeat. This can quickly hone in on a line in your config causing the issue.

The :finish command can be helpful during this process; it stops sourcing the script at that point so no code below :finish will be executed.

I find this technique helpful to find out which plugin is causing the issue. If you use a plugin package manager or store plugins under pack/*/opt/, this is as simple as commenting out the package manager commands or :packadd lines.

There is a plugin developed to automate this binary search process: Bisectly. I have never used it so I can’t vouch for it. It is however the only plugin I could find that advertises this functionality and may be worth a look.

The issue is in a plugin!

If you narrow the issue down to a plugin developed by someone else, then it’s a good idea (as with all open source projects) to open an issue on the plugin’s project page to alert maintainers and other users. If you can fix the issue locally, consider opening a pull request to share the fix! Finally, if you can’t wait for a fix to be added upstream, open source means you can freely fork the project, commit your fixes, and track your fork in your Vim config.

Conclusion

With these techniques you should now be better equipped to track down and squash those config bugs!


This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Permissions beyond the scope of this license may be available by contacting the author.