Taming my Vim configuration

As a long-time Vim user, I've put a lot of time into my Vim configuration over the years. It is something that has organically grown as my editing habits and needs change over time.

Recently, when trying to figure out why certain things weren't working the way I expected, I realised that there was much in my config I either didn't need or just blatantly did not understand. I took this as an opportunity to remove all the old cruft and start my configuration file again from scratch, taking only what I absolutely needed.

My configuration file was also around 2000 lines in length. Organisation was a huge mess: related commands strewn around all over the place due to years of ad-hoc editing. During my refactor of my configuration, one key goal was also to clean this up.

When ranting to a colleague of mine about my configuration woes, he showed me his, and I was struck by inspiration πŸ‘Ό A means of micromanagingβ€”architectingβ€”Vim configuration to make it saner, compositional, and more optimal too.

A few key ideas I've used in my configuration are outlined below: hopefully, they'll help anyone else also looking to tame their Vim configuration.

Initial refactoring

The first thing I did when breaking apart my 2000 line configuration file was to untangle the organisational mess I've made.

I started this by moving all the configuration related to specific ideas (i.e. tabbing: tabstop, softtabstop, expandtab) into contiguous blocks like the following:

" Cache undos in a file, disable other backup settings
set noswapfile
set nobackup
set undofile

" Better searching functionality
set showmatch
set incsearch
set hlsearch
set ignorecase
set smartcase

" Search related settings
set showmatch
set incsearch
set hlsearch
set ignorecase
set smartcase

This let me tell at a glance what essential "modules" I was dealing with. These "modules" would be blocks of configuration concerning both related settings and configuration for certain plugins. After doing this, my Vim configuration was actually larger, but at least it was easier to read πŸ˜…

Dealing with Plugins

I personally use vim-plug for managing my Vim plugins. Some of the reasons I use vim-plug include features such as the minimal amount of boilerplate needed to get plugins working, as well as hooks for lazy loading: we'll get to this later.

The only requirement to use vim-plug is to install it as per its installation instructions, followed by adding the following block to the top of your Vim configuration:

call plug#begin('~/.vim/my_plugin_directory_of_choice')

Plug 'https://github.com/some_repo/example.git'
Plug 'scrooloose/nerdtree'

call plug#end()

Most Vim plugins will have installation instructions for if you're using vim-plug (and if you're not, I'm sure you know how to configure this bit already anyway!), but that's pretty much it. Running :plug-install will fetch all of the configured plugins and download them to the path you give into the call plug#begin(...) option. Once this is done, you're basically good to go.

Because vim-plug is just standard viml, I use the same "module" idea and segregate groups of similar/related plugins together, as follows.

call plug#begin('MY_CONFIG_PATH/bundle')

" General stuff
Plug 'scrooloose/nerdtree'
Plug 'jistr/vim-nerdtree-tabs'
Plug 'neoclide/coc.nvim', {'branch': 'release'}
Plug 'easymotion/vim-easymotion'
Plug 'majutsushi/tagbar'
Plug 'ludovicchabant/vim-gutentags'
Plug 'tpope/vim-surround'
Plug 'tpope/vim-repeat'
Plug 'tpope/vim-eunuch'
Plug 'machakann/vim-swap'

" Setup fzf
Plug '/usr/local/opt/fzf'
Plug '~/.fzf'
Plug 'junegunn/fzf.vim'

" Git plugins
Plug 'Xuyuanp/nerdtree-git-plugin'
Plug 'airblade/vim-gitgutter'
Plug 'tpope/vim-fugitive'

" Erlang plugins
Plug 'vim-erlang/vim-erlang-tags'         , { 'for': 'erlang' }
Plug 'vim-erlang/vim-erlang-omnicomplete' , { 'for': 'erlang' }
Plug 'vim-erlang/vim-erlang-compiler'     , { 'for': 'erlang' }
Plug 'vim-erlang/vim-erlang-runtime'      , { 'for': 'erlang' }

" Elixir plugins
Plug 'elixir-editors/vim-elixir' , { 'for': 'elixir' }
Plug 'mhinz/vim-mix-format'      , { 'for': 'elixir' }

" Handlebar Templates
Plug 'mustache/vim-mustache-handlebars' , { 'for': 'html' }

" Bunch of nice themes
Plug 'flazz/vim-colorschemes'

call plug#end()

Here we can see another killing feature of vim-plug: by specifying options such as { 'for': 'erlang' }, I'm instructing that that particular plugin should only be loaded if the filetype of the currently opened file is erlang. A 2000 line config file isn't necessarily huge by hardcore Vim standards, but even at 2000 lines, my Vim configuration took a second to load, which was distracting. You can use many different hooks to optimise your vim-plug loading time, so I suggest you consult their README for more information; for what it's worth, I get by simply with this { 'for': $filetype } directive.

Turning comments into real "modules"

Now that everything is super well organised, I did a second refactoring pass over my configuration. Since all my related commands were already in logical blocks, I further broke them out into their own files so that I wouldn't need to have the informational overhead in my brain whenever I wanted to change a trivial setting.

The cool thing is that viml is a Turing complete programming language, able to do pretty much anything a normal programming language can do. One thing I'm abusing is the idea of automatically sourcing files from elsewhere, essentially breaking out my logically separated comments into real, logical modules.

I do this via a very naive for ... in ... loop as follows:

for config in split(glob('MY_CONFIG_PATH/config/*.vim'), '\n')
  exe 'source' config
endfor

This basically tells vim to look for all .vim files inside my configured config/ directory and source them. Therefore, I end up organising my configuration modules as follows:

MY_CONFIG_PATH/config
β”œβ”€β”€ coc_config.vim
β”œβ”€β”€ ctag_config.vim
β”œβ”€β”€ easymotion_config.vim
β”œβ”€β”€ editor_config.vim
β”œβ”€β”€ elixir_config.vim
β”œβ”€β”€ erlang_config.vim
β”œβ”€β”€ fzf_config.vim
β”œβ”€β”€ jsonc_config.vim
└── nerdtree_config.vim

In the future as this grows, I might even split this up to house modules into different domains (i.e. MY_CONFIG_PATH/plugins/*.vim versus MY_CONFIG_PATH/languages/*.vim versus plain old MY_CONFIG_PATH/tab_settings.vim). In the meantime, however, this level of encapsulation is more than enough: editor_config.vim is where I keep all my editor commands such as tab stop settings; everything else is either plugin configuration (all my settings related to, say, fzf). Otherwise, files such as elixit_config.vim are configuration files that get loaded when I'm editing elixir files specifically.

Plugin Configuration

My nerdtree.vim plugin configuration file is listed below, but essentially all of these files (except language configuration files) look the same:

" NERDTREE toggle (normal mode)
nnoremap <C-n> :NERDTreeToggle<CR>

" Close VIM if NERDTREE is only thing open
autocmd bufenter * if (winnr("$") == 1 && exists("b:NERDTree") && b:NERDTree.isTabTree()) | q | endif

" Show hidden files by default
let NERDTreeShowHidden=1

" If more than one window and previous buffer was NERDTree, go back to it.
autocmd BufEnter * if bufname('#') =~# "^NERD_tree_" && winnr('$') > 1 | b# | endif

" Selecting a file closes NERDTREE
let g:NERDTreeQuitOnOpen = 1

" Style
let NERDTreeMinimalUI = 1
let NERDTreeDirArrows = 1

Since the plugins can be set to be lazily loaded, I don't bother with any further tinkering with these files. They're literally just plain viml that is sourced by my top-level init.vim or .vimrc.

Language configuration

Language configuration is likewise simple but a little different. I actually end up writing custom viml functions for overriding configuration, which might be set elsewhere.

As an example, I've included a minimal copy of my elixir.vim file below:

function! LoadElixirSettings()
  set tabstop=2
  set softtabstop=2
  set shiftwidth=2
  set textwidth=80
  set expandtab
  set autoindent
  set fileformat=unix
endfunction

au BufNewFile,BufRead *.ex,*.exs,*.eex call LoadElixirSettings()

" Mix format on save
let g:mix_format_on_save = 1

As you can see, I basically do two different things:

  1. I set a few options specific to plugins that might load for filetypes marked elixir, i.e. let g:mix_formation_on_save is a configuration option for a plugin I load in my top-level init.vim or .vimrc via vim-plug

  2. Settings that are sourced in other files (such as tabstop or autoindent) can't be guaranteed to be sourced in any particular order, because again, that's done via a naive for ... in ... loop. To ensure that these settings override the default settings I've defined, I set up an autogroup: something that automatically executes based on some conditions. I'm telling vim that when the buffer is editing a file ending in .ex, .exs, .eex, then run the function I've calledβ€”this, in turn, overrides the existing settings with no issues πŸ’ͺ

Conclusion

I find that structuring my Vim configuration this way is super helpful in cutting down the amount of mental overhead I have when trying to edit specific settings. All of my configuration is separated and logically grouped, and when I decide I no longer use a given plugin, I can delete its associated configuration file.

I hope this helps, and if you're interested, you can find my personal vim configuration here. This repo changes often and grows over time, so what you find there might not be 100% the same approach as I've outlined here, but that means I've improved and streamlined it even more! πŸ’ͺ


edit: as an aside, I personally opt to use Neovim over Vim because of asynchronous plugin support.

I originally had a section covering how to convert an existing Neovim configuration to be loadable by Vim (such as to ignore any differences and to enable Vim users to make meaningful use of this guide), but this was removed simply because even if the raw configuration can be made to work for both editors, plugins cannot.

Following this realisation, I've edited this article to be completely editor agnostic. I apologise for any confusion early drafts of this article might have caused!


Return to Posts β†’