Make Vim follow XDG Base Directory specification

XDG Base Directory specification, $XDG_CONFIG_HOME etc. Great thing - configs separated from user data and cache, no clutter in home directory. Unfortunately, many programs still don't respect it, including Vim. But what would be our favourite text editor if we wouldn't be able to reconfigure it!

TL;DR #

Into shell config (e.g. in ~/.profile):

export VIMINIT="set nocp | source ${XDG_CONFIG_HOME:-$HOME/.config}/vim/vimrc"

At the top of vimrc:

" XDG support

if empty($MYVIMRC) | let $MYVIMRC = expand('<sfile>:p') | endif

if empty($XDG_CACHE_HOME)  | let $XDG_CACHE_HOME  = $HOME."/.cache"       | endif
if empty($XDG_CONFIG_HOME) | let $XDG_CONFIG_HOME = $HOME."/.config"      | endif
if empty($XDG_DATA_HOME)   | let $XDG_DATA_HOME   = $HOME."/.local/share" | endif
if empty($XDG_STATE_HOME)  | let $XDG_STATE_HOME  = $HOME."/.local/state" | endif

set runtimepath^=$XDG_CONFIG_HOME/vim
set runtimepath+=$XDG_DATA_HOME/vim
set runtimepath+=$XDG_CONFIG_HOME/vim/after

set packpath^=$XDG_DATA_HOME/vim,$XDG_CONFIG_HOME/vim
set packpath+=$XDG_CONFIG_HOME/vim/after,$XDG_DATA_HOME/vim/after

let g:netrw_home = $XDG_DATA_HOME."/vim"
call mkdir($XDG_DATA_HOME."/vim/spell", 'p', 0700)

set backupdir=$XDG_STATE_HOME/vim/backup | call mkdir(&backupdir, 'p', 0700)
set directory=$XDG_STATE_HOME/vim/swap   | call mkdir(&directory, 'p', 0700)
set undodir=$XDG_STATE_HOME/vim/undo     | call mkdir(&undodir,   'p', 0700)
set viewdir=$XDG_STATE_HOME/vim/view     | call mkdir(&viewdir,   'p', 0700)

if !has('nvim') " Neovim has its own special location
  set viminfofile=$XDG_STATE_HOME/vim/viminfo
endif

Step-by-step #

Relocating vimrc #

To begin with, since version 7.3.1178, Vim will search for ~/.vim/vimrc if ~/.vimrc is not found. So let's move the file there.

Let's move our ~/.vim to $XDG_CONFIG_HOME/vim. Now we need to command Vim to read config from this new location prior to ~/.vim. There are three ways to do it.

Shell alias #

Pretty straightforward method. Shell will just substitute command vim with the alias body

alias vim='vim -u ${XDG_CONFIG_HOME:-$HOME/.config}/vim/vimrc'

Downside? Works only in shell

VIMINIT environmental variable #

export VIMINIT="set nocp | source ${XDG_CONFIG_HOME:-$HOME/.config}/vim/vimrc"

Cons? If you wish for Neovim and Vim configurations to still be separated, then:

export VIMINIT="if has("nvim") | so ${XDG_CONFIG_HOME:-$HOME/.config}/nvim/init.vim | else | set nocp | so ${XDG_CONFIG_HOME:-$HOME/.config}/vim/vimrc | endif"

Wrapper script #

Save the following code as vim in $HOME/.local/bin * and make is executable with chmod +x vim

#!/usr/bin/env sh

IFS='
'
for dir in "$(echo "$PATH" | tr ":" "\n" | grep -Fxv "$(dirname $0)")"; do
    if [ -x "$dir/vim" ]; then
        exec "$dir/vim" -u "${XDG_CONFIG_HOME:-$HOME/.config}"/vim/vimrc "$@"
    fi
done

Doesn't affect Neovim and works outside shell, but you need to carry it together with your config

* Remember to add it to the beginning of PATH environment variable.
   It can be also other location of your choice instead of $HOME/.local/bin

Now the code in our vimrc #

First of all, although not mandatory, let's set $MYVIMRC variable:

if empty($MYVIMRC) | let $MYVIMRC = expand('<sfile>:p') | endif

Let's define fallback locations in case XDG_* variables are not set.

if empty($XDG_CACHE_HOME)  | let $XDG_CACHE_HOME  = $HOME."/.cache"       | endif
if empty($XDG_CONFIG_HOME) | let $XDG_CONFIG_HOME = $HOME."/.config"      | endif
if empty($XDG_DATA_HOME)   | let $XDG_DATA_HOME   = $HOME."/.local/share" | endif
if empty($XDG_STATE_HOME)  | let $XDG_STATE_HOME  = $HOME."/.local/state" | endif

Let's add entries to runtimepath:

set runtimepath^=$XDG_CONFIG_HOME/vim
set runtimepath+=$XDG_DATA_HOME/vim
set runtimepath+=$XDG_CONFIG_HOME/vim/after

$XDG_CONFIG_HOME/vim and $XDG_CONFIG_HOME/vim/after are just equivalents of ~/.vim and ~/.vim/after, but $XDG_DATA_HOME/vim is brand new - there we will keep downloadables (like plugins and spell files), Netrw bookmarks etc.

Let's set directory for Vim8 build-in packages:

set packpath^=$XDG_DATA_HOME/vim
set packpath+=$XDG_DATA_HOME/vim/after

Netrw is just as easy:

let g:netrw_home = $XDG_DATA_HOME."/vim"

What about spellings? Well, this one is more tricky, because it isn't controlled by any option. Instead it searches for spell directory in whole runtime path. If none is found then it falls back to ~/.vim/spell. So let's create one at desired location ourselves!

call mkdir($XDG_DATA_HOME."/vim/spell", 'p', 0700)

So far so good. We are left with state (backup, undo, swap, viminfo, view). Vim doesn't create directories for them (even for defaults), so we will need to do it ourselves - thankfully VimL has mkdir() function.

set backupdir=$XDG_STATE_HOME/vim/backup | call mkdir(&backupdir, 'p', 0700)
set directory=$XDG_STATE_HOME/vim/swap   | call mkdir(&directory, 'p', 0700)
set undodir=$XDG_STATE_HOME/vim/undo     | call mkdir(&undodir,   'p', 0700)
set viewdir=$XDG_STATE_HOME/vim/view     | call mkdir(&viewdir,   'p', 0700)

if !has('nvim') " Neovim has its own location which already complies with XDG specification
  set viminfofile=$XDG_STATE_HOME/vim/viminfo
endif

Congratulations! Now your Vim is configured with accordance to XDG Base Directory specification.

Sources #