File Templates with UltiSnips and Projectionist

by Noah Frederick |

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" }).

Further Reading