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:
-
alternate buffer is changed, so one has to use
:keepalt r
instead of:r
; -
:r
inserts contents after cursor line sometimes leaving unwanted empty line (read into an empty buffer for example); -
always have to navigate/complete full path to templates;
-
no filter for filetype specific templates.
Let’s enhance it a bit.
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:
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>)
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>)
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>)
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 withg&
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
-
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.
" 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>)
One can enhance it with other fuzzy plugins out there.
For vim-select
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>