File Templates with UltiSnips and Projectionist
by |File templates (or “skeletons”) can save you some typing by automatically inserting boilerplate text into new buffers. Templates are like snippets, but are automatically triggered on buffer creation instead of through an insert-mode mapping or abbreviation. :help skeleton
recommends the following solution:
autocmd BufNewFile *.c 0read ~/skeleton.c
autocmd BufNewFile *.h 0read ~/skeleton.h
autocmd BufNewFile *.java 0read ~/skeleton.java
This method is very straightforward, but it has some limitations. Namely, the templates are not context-aware—you can’t include dynamic text or (easily) vary the template based on features of the file path. There are plenty of plug-ins out there that give you more control and flexibility. However, it always bothered me that I was using two separate mechanisms for file templates and regular snippets since there’s a lot of overlap. UltiSnips is the de facto standard for snippet plug-ins as of this writing, and the one I currently use, so I devised a system where I could use UltiSnips snippets to define my templates.
Automatic Templates with UltiSnips
The autocommand event we are interested in is BufNewFile
, which gets fired whenever a buffer is created referencing a file that does not yet exist on disk. Hence the first step is to create an autocommand. I’ve chosen to place it in a script that gets loaded after UltiSnips so that the functionality can be bypassed if UltiSnips is not available.
" after/plugin/ultisnips_custom.vim
if !exists('g:did_UltiSnips_plugin')
finish
endif
augroup ultisnips_custom
autocmd!
autocmd BufNewFile * silent! call snippet#InsertSkeleton()
augroup END
All the autocommand does is call an autoload function, which is defined below. Prefixing the function call with silent!
ensures that the message in the command line is not clobbered every time a new buffer is loaded.
" autoload/snippet.vim
function! s:try_insert(skel)
execute "normal! i_" . a:skel . "\<C-r>=UltiSnips#ExpandSnippet()\<CR>"
if g:ulti_expand_res == 0
silent! undo
endif
return g:ulti_expand_res
endfunction
function! snippet#InsertSkeleton() abort
let filename = expand('%')
" Abort on non-empty buffer or extant file
if !(line('$') == 1 && getline('$') == '') || filereadable(filename)
return
endif
call s:try_insert('skel')
endfunction
The snippet#InsertSkeleton()
function checks that the buffer is empty and that a corresponding file does not already exist on disk. It then defers to a separate script-local function to do the actual work (but we’ll be extending this function in the next section).
The s:try_insert()
function programmatically attempts to insert the snippet with the given name into the buffer. UltiSnips sets the g:ulti_expand_res
variable to the result of the expansion to indicate success or failure. The expansion will of course fail if the snippet is not defined (if there is no skeleton defined for the current file type). In that case, it is necessary to issue an :undo
in order to clear the snippet name that was inserted on the first line.
The convention for naming skeleton snippets enforced in s:try_insert()
is to use skel
prefixed with an underscore. Defining the following snippet for the sh
file type would insert a shebang for you to start off every new shell script:
snippet _skel "Shebang" b
#!/bin/sh
$0
endsnippet
Project-Specific Templates with Projectionist
Projectionist provides project configuration and serves as a great framework for defining and retrieving information about the structure of projects with a high level of specificity from Vim Script. Go ahead and read the help if you aren’t already familiar with how it works. What may not be obvious from reading the documentation the first time through is that its heuristics dictionary can be queried for arbitrary keys. We’ll take advantage of this fact to specify the name of the skeleton snippet to use on a project and file level.
As an aside, Projectionist provides its own file template mechanism with a few limitations. Namely, it does not allow for Vim Script interpolation in templates, and it of course only applies templates to buffers belonging to a recognized project. So for the sake of consistency, I prefer to use the UltiSnips method paired with Projectionist for file templates. It’s the best of both worlds. We’ll start by amending the after/plugin
script to call snippet#InsertSkeleton()
also on the ProjectionistActivate
event:
" after/plugin/ultisnips_custom.vim
if !exists('g:did_UltiSnips_plugin')
finish
endif
augroup ultisnips_custom
autocmd!
autocmd User ProjectionistActivate silent! call snippet#InsertSkeleton()
autocmd BufNewFile * silent! call snippet#InsertSkeleton()
augroup END
We’ll also amend the snippet#InsertSkeleton()
function with a call to projectionist#query
, which will fetch the skeleton
key from Projectionist’s dictionary for the current buffer:
" autoload/snippet.vim
function! s:try_insert(skel)
execute "normal! i_" . a:skel . "\<C-r>=UltiSnips#ExpandSnippet()\<CR>"
if g:ulti_expand_res == 0
silent! undo
endif
return g:ulti_expand_res
endfunction
function! snippet#InsertSkeleton() abort
let filename = expand('%')
" Abort on non-empty buffer or extant file
if !(line('$') == 1 && getline('$') == '') || filereadable(filename)
return
endif
if !empty('b:projectionist')
" Loop through projections with 'skeleton' key and try each one until the
" snippet expands
for [root, value] in projectionist#query('skeleton')
if s:try_insert(value)
return
endif
endfor
endif
" Try generic _skel template as last resort
call s:try_insert('skel')
endfunction
Each value of skeleton
is interpreted as a template name and is tried in succession for expansion by UltiSnips. Here’s an example .projections.json
manifest that defines custom skeletons for a generic MVC framework:
{
"app/models/*.generic": {
"skeleton": "model"
},
"app/views/*.generic": {
"skeleton": "view"
},
"app/controllers/*.generic": {
"skeleton": "controller"
}
}
And the corresponding snippet definitions in generic.snippets
might look something like this:
snippet _skel "Generic template" b
# A generic file
$0
endsnippet
snippet _model "Generic model template" b
# A generic model
class `!v expand("%:t:r")`_model {
$0
}
endsnippet
snippet _view "Generic view template" b
# A generic view
class `!v expand("%:t:r")`_view {
$0
}
endsnippet
snippet _controller "Generic controller template" b
# A generic controller
class `!v expand("%:t:r")`_controller {
$0
}
endsnippet
Editing a new file within your project should now insert the right template for the context. Note that the _skel
snippet is still used as a fallback. You can effectively override the fallback on a per-project basis by specifying it in the “root” projection (e.g., "*": { "skeleton": "foo" }
).