I have used text editors for more than 20 years; in all that time, I have never used a templating system to generate content because copy pasting and replacing always felt good enough. I recently decided to give it a try.
In Emacs it is hard to talk about templating without mentionning YASnippet. Developped by the prolific João Távora, who also developped Eglot, YASnippet lets you write templates as text documents.
But while I was researching the subject, I found out that Emacs already had two builtin templating modules: Skeleton and Tempo.
Tempo looked simpler, so I decided to give it a chance.
Using Tempo
Tempo templates are functions defined with tempo-define-template
. The
content to be inserted is a S-expression containing various kinds of elements
which control the insertion process.
As an example, let us define a template to insert a HTML figure.
First we define a variable to store a list of templates. When using
html-mode
, we instruct Tempo to use it.
(defvar g-html-tempo-tags nil)
(defun g-init-html-tempo-templates ()
(tempo-use-tag-list 'g-html-tempo-tags))
(add-hook 'html-mode-hook 'g-init-html-tempo-templates)
Then we define the template itself:
(tempo-define-template
"g-html-figure"
'("<figure>" n
> "<img src=\"" (p "URI: ") "\">" n
> "<figcaption>" (p "Caption: ") "</figcaption>" n
"</figure>")
"figure"
"a figure containing an image and caption"
'g-html-tempo-tags)
This form creates a function named tempo-template-g-html-figure
. When it is
called, Tempo processes elements of the template:
- Strings are inserted in the buffer.
- The
n
symbol causes the insertion of a new line character - The
>
symbol tells Emacs to indent the current line according to the rules of the major mode of the buffer. - The
p
forms cause Tempo to ask the user for values to insert. Note that you need to settempo-interative
tot
.
Note that Tempo supports more elements; refer to the documentation string of
tempo-define-template
for more information.
The template is associated to the figure
text tag which will be used for
completion. We also add a description, and finally add the template to the
g-html-tempo-tags
list. Note that these three last arguments are optional.
Tag matching
Of course we do not have to call the template manually. When we call
tempo-complete-tag
, Tempo uses the string before the cursor to decide which
template to insert. Open a HTML buffer, type figure
and execute
tempo-complete-tag
(I bind it to M-S-<tab>
): Tempo will automatically
insert our template.
Tempo will handle the case where there is partial match for multiple templates and will spawn a completion buffer.
Now it would make sense to use <figure>
as tag. To do so, update the
g-init-html-tempo-templates
function to set the local variable that Tempo
uses to detect a tag:
(setq tempo-match-finder "\\(<[a-z]+>\\)\\=")
Doing so will use HTML tags as Tempo tags. We can then alter the call to
tempo-define-template
to use <figure>
as tag, and from now on use it for
template insertion.
Of course this means we can customize it to allow different formats of Tempo tags depending on the major mode we are in. Handy.
Writing a template selector
Calling template functions manually is unpractical and tag completion requires remembering which tags have been defined. My interface of choice would be a simple key spawning an incremental completion buffer (Helm in my case) to let me select a template to insert.
As it turns out, it is not that hard:
(defun g-insert-tempo-template ()
(interactive)
(let* ((tags-data
(mapcar (lambda (entry)
(let ((function (cdr entry)))
(list function (documentation function))))
(tempo-build-collection)))
(completion-extra-properties
`(:annotation-function
(lambda (string)
(let* ((data (alist-get string minibuffer-completion-table
nil nil #'string=))
(description (car data)))
(format " %s" description)))))
(function-name (completing-read "Template: " tags-data))
(function (intern function-name)))
(funcall function)))
As we have already seen in a previous
post,
completing-read
is quite limited in terms of presentation. I will probably
spend some time switching from Helm to a mix of
Vertico and
Marginalia which apparently offers more
options.
This will do the job in the mean time.
Conclusion
Tempo is reasonably satisfying. While it is a very simple module, it lets me define templates as Emacs Lisp expressions, associate them to tags and store them in tag lists which can be used in the major modes of my choice.
I do not expect to start using dozens of tiny templates for the simplest constructions, but Tempo is going to help with recurrent complex constructions such as Emacs module skeletons or Common Lisp system definitions.