Advent 2019 part 21, Project level Emacs config with .dir-locals.el

This post is part of Advent of Parens 2019, my attempt to publish one blog post a day during the 24 days of the advent.

An extremely useful Emacs feature which I learned about much too late is the .dir-locals.el. It allows you to define variables which will then be set whenever you open a file in the directory where .dir-locals.el is located (or any subdirectory thereof).

Here’s an example of a .dir-locals.el file of a project I was poking at today.

((nil . ((cider-clojure-cli-global-options    . "-A:fig:dev")
         (cider-preferred-build-tool          . clojure-cli)
         (cider-default-cljs-repl             . figwheel-main)
         (cider-figwheel-main-default-options . "dev")
         (cider-repl-display-help-banner      . nil))))

What this essentially does is set a bunch of CIDER variables for this specific project. Mainly it stops CIDER asking annoying questions. It really bugs me when it asks me five times a day what ClojureScript REPL I’m using. This is project level stuff, so with .dir-locals.el I configure it at the project level and CIDER never has to ask me again.

The syntax can throw people off though, so here’s a really quick primer on association lists.

Emacs Lisp doesn’t have the handy hash-map syntax that we Clojurists enjoy. It does have a kind of hash table but there’s no syntax literal for it, and in practice it seems it’s rarely used. Instead, like most traditional Lisps, Elisp primarily makes use of two-element tuples called cons cells.

You can create them with the cons function, or with a ‘dotted pair’ notation. So these two expressions are identical.

(cons "val1" "val2")
'("val1" . "val2")

Need to have a hash-map like associative mapping from key to value? Well then just stick a bunch of these cons cells in a list.

((:name . "Arne")
 (:favorite-lisp . "not Elisp"))

Back to .dir-locals.el. It contains an association list mapping major modes to variable mappings.

((clojure-mode . ((var-1 . "val 1")
                  (var-2 . "val 2")))
 (ruby-mode . ((var-3 . "val 3"))))

This will set var-1 and var-2 as buffer local variables in Clojure buffers, and it will set var-3 in Ruby buffers.

Or you can not really care about the major mode and instead just put nil, that way it will be used in all buffers regardless of their major mode. The per-major mode stuff is mainly useful for general Emacs variables that you want to change based on the language.

((js-mode . ((js-indent-level . 2)
             (indent-tabs-mode . nil))))

And once you set up these variables for your project by all means do check the .dir-locals.el into source control so others may benefit from it as well.

One caveat: when you update your .dir-locals.el the changes will only come into effect when you (re-)open a file in that directory. This gets me all the time. To quickly reload a file you already have open use C-x C-f RET in Emacs or SPC f A RET in Spacemacs.

A final trick to be aware of is that you can use the special eval variable to evaluate code when a file gets opened. This can be very useful for instance when you are using a type of ClojureScript REPL that CIDER does not (yet) know about.

((nil . ((cider-default-cljs-repl          . my-cljs-repl)
         (eval . (cider-register-cljs-repl-type 'my-cljs-repl "(code-that-switches-to-the-cljs-repl)")))))

For the full lowdown you naturally only need to check the info pages, (info "(emacs) Directory Variables").

Hi, my name is Arne (aka @plexus) and I consult companies and teams about application architecture, development process, tooling and testing. I collaborate with other talented people under the banner Gaiwan. If you like to have a chat about how we could help you with your project then please get in touch!