Skip to content

Instantly share code, notes, and snippets.

@habamax
Last active August 4, 2022 07:59
Embed
What would you like to do?

Simple Templates In Vim

Vim has :h skeleton help topic for when you want to read a skeleton (template) file into a new file.

For me the idea of automatic templates is not that appealing, instead I would rather insert a template when I need it:

:r ~/.vim/asciidoctor/template_for_entity_relation_diagram

This kind of works, but:

  1. alternate buffer is changed, so one has to use :keepalt r instead of :r;

  2. :r inserts contents after cursor line sometimes leaving unwanted empty line (read into an empty buffer for example);

  3. always have to navigate/complete full path to templates;

  4. no filter for filetype specific templates.

Let’s enhance it a bit.

Template Files

We are going to have our templates in ~/.vim/templates/ directory:

" path to the templates
let s:template_path = expand('~/.vim/templates/')

Each template is a plain text file, for example ~/.vim/templates/loremipsum:

Lorem ipsum dolor sit amet, consectetur adipiscing elit.  Maecenas feugiat
fermentum pretium.  Cras eu dolor imperdiet justo mattis pulvinar.  Cras nec
lectus ligula.  Proin elementum luctus elit, a tincidunt quam facilisis non.
Nunc quis mauris non turpis finibus luctus.  Maecenas ante sapien, sagittis
quis accumsan in, feugiat quis sem.

Praesent et auctor libero.  Ut sed lacus id mauris mattis lobortis.  Aliquam ut
nisl eu neque viverra pretium.  Interdum et malesuada fames ac ante ipsum
primis in faucibus.  Etiam convallis, purus sit amet rutrum porttitor, enim
ante dapibus libero, finibus tempor tortor diam sed urna.

Aliquam ullamcorper ligula sit amet consequat faucibus.  Donec in nisl
tristique, porttitor eros eget, pellentesque orci.  Nunc porta dolor et
fringilla auctor.  Vestibulum ut dapibus eros.  Nunc ac cursus nisi, at
vulputate dolor.  Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas.

Filetype specific templates are in additional subdirectories, for example, contents of the ~/.vim/templates/asciidoctor/options (asciidoctor is a filetype, options is a template):

Maxim Kim
v0.1, 2021-05-21 : Draft
:pdf-theme: default
:doctype: article
:title-page:
:toc: left
:toclevels: 3
:sectnums:
:sectnumlevels: 4
:source-highlighter: rouge
:rouge-style: github
:!source-linenums-option:
:imagesdir: images
:chapter-signifier:
:icons: font
:autofit-option:
:experimental:
:compress:

"Better" :r

Instead of plain :r we will be using :Template command:

" path to the templates
let s:template_path = expand('~/.vim/templates/')

" insert template file content into current buffer after the cursor
func! TemplateInsert(tfile) abort
    exe "keepalt read " .. s:template_path .. a:tfile
endfunc

command! -nargs=1 Template call TemplateInsert(<q-args>)
119130192 8cedcc80 ba40 11eb 9bb1 f14a6a7feaa6

As you can see there is no completion, plus unwanted empty line.

Empty line fix is easy (save current line number before :read, delete if it is empty after :read):

" path to the templates
let s:template_path = expand('~/.vim/templates/')

" insert template file content into current buffer after the cursor
" if the cursor was on empty line -- delete it
func! TemplateInsert(tfile) abort
    let c_linenr = line('.')
    exe "keepalt read " .. s:template_path .. a:tfile
    if getline(c_linenr) =~ '^\s*$'
        exe c_linenr .. 'd_'
    endif
endfunc

command! -nargs=1 Template call TemplateInsert(<q-args>)

Completion

Vim user commands can have custom completion (:h :command-completion-custom) that should help us filter out templates we don’t need for the current filetype:

" path to the templates
let s:template_path = expand('~/.vim/templates/')

" insert template file content into current buffer after the cursor
" if the cursor was on empty line -- delete it
func! TemplateInsert(tfile) abort
    let c_linenr = line('.')
    exe "keepalt read " .. s:template_path .. a:tfile
    if getline(c_linenr) =~ '^\s*$'
        exe c_linenr .. 'd_'
    endif
endfunc

" command complete function
" returns list of template files that are matched against all general + all filetype specific templates
func! TemplateComplete(A, L, P)
    let ft_path = s:template_path .. &ft
    let tmpls = []

    " get filetype specific templates
    if !empty(&ft) && isdirectory(ft_path)
        " * readdirex returns list of dicts with files in `ft_path`
        " * map transforms each dict with file name into the list of filenames prepended with `filetype`
        let tmpls = map(readdirex(ft_path, {e -> e.type == 'file'}), {_, v -> &ft .. '/' .. v.name})
    endif

    " add general/common templates to the list
    if isdirectory(s:template_path)
        " * readdirex returns list of dicts with files in `s:template_path`
        " * map transforms each dict with file name into the list of filenames
        call extend(tmpls, map(readdirex(s:template_path, {e -> e.type == 'file'}), {_, v -> v.name}))
    endif

    " Nothing was entered, nothing to filter completion with, return whole list of templates
    if empty(a:A)
        return tmpls
    " return matched(with regexes) templates
    else
        return tmpls->filter({_, v -> v =~ '.*' .. a:A .. '.*'})
        " or fuzzy matched
        " return tmpls->matchfuzzy(a:A)
    endif
endfunc

command! -nargs=1 -complete=customlist,TemplateComplete Template call TemplateInsert(<q-args>)

Vimscript Interpolation

We can spice our template "system" with some vimscript interpolation/evaluation to be able to, for example, insert current date.

Vim has eval and strftime("%Y-%m-%d") functions we could use for that.

Let’s agree our vimscript placeholder would be !!some vimscript!!:

Maxim Kim
v0.1, !!strftime("%Y-%m-%d")!! : Draft
:pdf-theme: default
:doctype: article
:title-page:
:toc: left
:toclevels: 3

So if our template would have !!strftime("%Y-%m-%d")!! it should be evaluated and substituted with evaluation result.

We going to do it in TemplateInsert function right after :read — we have proper '[ and '] marks set up we can use for :substitute command:

func! TemplateInsert(tfile) abort
    if empty(a:tfile)
        return
    endif
    let c_linenr = line('.')
    exe "keepalt read " .. s:template_path .. a:tfile

    " run :s command on freshly added lines
    " substitute everything inside !!.....!! with eval result
    :'[,']s/!!\(.\{-}\)!!/\=eval(submatch(1))/ge

    if getline(c_linenr) =~ '^\s*$'
        exe c_linenr .. 'd_'
    endif
endfunc

Instead of :read and :substitute commands we can and probably should use vim functions that don’t have side effects:

  • :read adds a file into buffer list plus changes alternate buffer

  • :substitute overrides last user substitute command accessible with g&

func! TemplateInsert(tfile) abort
    if empty(a:tfile)
        return
    endif

    " read template file into tlines and for each line
    " substitute everything inside !!.....!! with eval result
    let tlines = readfile(s:template_path .. a:tfile)
                \->map({_, v, -> substitute(v, '!!\(.\{-}\)!!', '\=eval(submatch(1))', 'g')})
    call append(line('.'), tlines)
    if getline('.') =~ '^\s*$'
        del _
    endif
endfunc

The Final

  • Read only general and filetype specific templates, use built-in fuzzymatching.

  • Use readfile(), append(), substitute() functions instead of :read and :s to avoid side effects.

  • No unwanted empty lines.

119132877 e4416c00 ba43 11eb 9c7a 12c61f02587c
" path to the templates
" use g:template_path if defined or ~/.vim/templates/ otherwise
let s:template_path = get(g:, "template_path", fnamemodify($MYVIMRC, ':p:h') .. '/templates/')

" insert template file content into current buffer after the cursor
" if the cursor was on empty line -- delete it
func! TemplateInsert(tfile) abort
    if empty(a:tfile)
        return
    endif

    let tlines = readfile(s:template_path .. a:tfile)
                \->map({_, v, -> substitute(v, '!!\(.\{-}\)!!', '\=eval(submatch(1))', 'g')})
    call append(line('.'), tlines)
    if getline('.') =~ '^\s*$'
        del _
    else
        normal! j^
    endif
endfunc


" command complete function
" returns list of template files that are fuzzymatched against all general + all filetype specific templates
func! TemplateComplete(A, L, P)
    let ft_path = s:template_path .. &ft
    let tmpls = []

    " get filetype specific templates
    if !empty(&ft) && isdirectory(ft_path)
        let tmpls = map(readdirex(ft_path, {e -> e.type == 'file'}), {_, v -> &ft .. '/' .. v.name})
    endif

    " add general/common templates to the list
    if isdirectory(s:template_path)
        call extend(tmpls, map(readdirex(s:template_path, {e -> e.type == 'file'}), {_, v -> v.name}))
    endif

    if empty(a:A)
        return tmpls
    else
        return tmpls->matchfuzzy(a:A)
    endif
endfunc

command! -nargs=1 -complete=customlist,TemplateComplete Template :call TemplateInsert(<q-args>)

Other Fuzzy-Shmuzzy Completion

One can enhance it with other fuzzy plugins out there.

119131527 4305e600 ba42 11eb 97b0 1f462122e8c4

I have the following setup:

let g:select_info.template = {}
let g:select_info.template.data = {_, buf -> s:template_data(buf)}
let g:select_info.template.sink = {
        \ "transform": {_, v -> fnameescape(fnamemodify($MYVIMRC, ':p:h') .. '/templates/' .. v)},
        \ "action": {v -> s:template_sink(v)}
        \ }
let g:select_info.template.highlight = {"DirectoryPrefix": ['\(\s*\d\+:\)\?\zs.*[/\\]\ze.*$', 'Comment']}

func! s:template_data(buf) abort
    let path = fnamemodify($MYVIMRC, ':p:h') .. '/templates/'
    let ft = getbufvar(a:buf.bufnr, '&filetype')
    let ft_path = path .. ft
    let tmpls = []

    if !empty(ft) && isdirectory(ft_path)
        let tmpls = map(readdirex(ft_path, {e -> e.type == 'file'}), {_, v -> ft .. '/' .. v.name})
    endif

    if isdirectory(path)
        call extend(tmpls, map(readdirex(path, {e -> e.type == 'file'}), {_, v -> v.name}))
    endif

    return tmpls
endfunc

func! s:template_sink(tfile) abort
    let tlines = readfile(a:tfile)
                \->map({_, v, -> substitute(v, '!!\(.\{-}\)!!', '\=eval(submatch(1))', 'g')})
    call append(line('.'), tlines)
    if getline('.') =~ '^\s*$'
        del _
    else
        normal! j^
    endif
endfunc

nnoremap <silent> <space>te :Select template<CR>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment