Nicolas Martyanoff — Brain dump About

Templating in Emacs with Tempo

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 set tempo-interative to t.

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.

Share the word!

Liked my article? Follow me on Twitter or on Mastodon to see what I'm up to.