Migrating from AutoCommands to ftplugins

Why?

If you're like me, you have a lot of autocommands for specific file types in your .vimrc. Specific setlocals, some nice mappings, etc. After a while those autocmds start adding up and you soon have hundreds of lines of


autocmd FileType file setlocal foo=bar

You might want to put those commands in a function and call it once, or maybe put them in a separate file that you source with a single autocmd. At this poing you'll start wondering if there's a better way. Of course there is. We're using Vim after all, there is almost always a better way. Enter ftplugins.

What are ftplugins?

Filetype plugins, or ftplugins for short, are .vim files that run specific commands depending on what file is opened. Vim has a lot (and I mean a lot) ftplugins built in and plugins can add additional ones.

How to use them

ftplugins are sourced automatically (assuming you have filetype plugin on in your .vimrc), as long as they're in a specific folder and have a specific file name. You can write either a complete replacement to an existing ftplugin, or just add extra stuff to whatever Vim does by default. You control that with the directory you put it in.

The file has to be named in a specific way: [file type].vim. You can also name it [file type]_whatever.vim, though I personally don't see any reason for that. The directory where it should live depends on what you want to do

Enhance defaults

This is what I recommend for most users. You can add key bindings, extra setlocals, override a single default setting, maybe a few new commands, or whatever else you have in your autocmds. In order to use your new settings, NeoVim users put the file in ~/.config/nvim/after/ftplugin, while for Vim users it's ~/.vim/after/ftplugin/

Completely override default settings

A word of warning before you do that: You'll have to set up everything Vim does by default for that specific file type. This includes omni function, comment string, format options etc. It's almost always overkill and you lose the ability to receive upstream updates for the file type. If you still want to go for that, put your file in ~/.local/share/nvim/site/ftplugin/ for NeoVim, or ~/.vim/ftplugin/ for Vim

ftplugin file structure

While the file structure isn't forced upon you, there are some things the plugin file should have: a header, a check whether it needs to load or not, and an undo

Header

A header is just a comment describing the file. It's useful when sharing it on the Internet, so the other person knows what it is, when was it last updated, or who made it. I usually use a header that looks like this:


" *.[file] filetype config
" Created: [date]
" Last Modified: [date]
" Creator: Przemek DragaƄczuk 
" Licence: WTFPL
Feel free to use it and modify as needed

Loading check

Loading a file multiple times can cause some problems, like running a command multiple times. You can fix it in a simple way: check if a specific variable is defined, if it is, then there is no need to load the file, otherwise set the variable and run the rest of the file. The code for that looks like this:


if exists("b:did_ftplugin")
	finish
endif
let b:did_ftplugin = 1
I prefer naming the variable depending on the filetype, for example b:did_html_ftplugin, that way I have a way to disable this specific file from loading by just putting b:did_[filetype]_ftplugin = 1 in my .vimrc

Undo

What would happen if Vim guessed the filetype wrong and then you set it manually with :setfiletype? Vim would source the ftplugin for what it guessed, then it would source the correct one. This means that you still have all the settings from the incorrect ftplugin. You can get around that by setting a variable b:undo_ftplugin to contain all the commands that will revert whatever the fftplugin did.

I personally put them all in a function, like this:


function! s:undo()
setl formatoptions<
setl tabstop<
setl textwidth<
endfunction
let b:undo_ftplugin = "call s:undo()"

Calling setlocal [setting]<(notice the "<" at the end) resets the setting to it's global value.

Resulting boilerplate

After doing everything above you should have a .vim file that contains the following:


" *.[file] filetype config
" Created: [date]
" Last Modified: [date]
" Creator: [creator]
" Licence: [licence]

if exists("b:did_[file]_ftplugin")
	finish
endif
let b:did_[file]_ftplugin=1

let b:undo_ftplugin = "call s:undo()"
function s:undo()
	
endfunction

Update

It was brought to my attention that b:undo_ftplugin overwrites the undo created by the builtin ftplugin. In order to fix it, we need to change one line:


let b:undo_ftplugin .= "call s:undo()"
Notice the red dot, which means Vim will append to, instead of overwriting, the variable

Writing the actual plugin

Now that we have the boilerplate out of the way, we can get to setting some stuff. Using ftplugins gives us some extra modifiers to make sure we only affect the files we want.

setlocal

setlocal works how it used to, just remember to revert it in s:undo()

Key mappings

You can create maps that don't affect other buffers by adding <buffer> to your map commands. For example:


nnoremap <buffer> <Leader>e :echo "Hello world"
will cause pressing <Leader>e to display "Hello world", but only for specific filetypes

Variables

If you want/need to use some variables, you can prepend them with s: or b: to make them visible only in this script (the ftplugin you're writing) or current buffer respectively

That's it

You can now move all the stuff you set in auto commands or auto groups to their own ftplugins, cleaning up your .vimrc of all those autocmd FileType... calls.

Reference