Project-specific Configuration

A pretty common question about CIDER is how to handle project-specific configuration. There are many reasons for wanting to do something like this, but probably the first that comes to mind is running Leiningen with some specific profile or adding "-A:fig" to the jack-in command when using the Clojure CLI (a.k.a. tools.deps).

If you simply need to edit a jack-in command on the fly you’re probably better off prefixing the command with C-u (e.g. C-u C-c C-x j j), which will allow you to edit the entire command string in the minibuffer.

CIDER doesn’t have any special provisions for project-specific configuration, as this is something well supported in Emacs itself. Unfortunately the functionality in Emacs has the slightly weird name "dir-local variables", which is probably not the thing people would start googling for. On the bright side - the Emacs functionality is much more generic than dealing with project-specific configuration.

Very simply put, all you need to do is to create in the root of your project a file named .dir-locals.el which should look something like:

((clojurescript-mode
  (cider-clojure-cli-global-options . "-A:fig")
  (eval . (cider-register-cljs-repl-type 'super-cljs "(do (foo) (bar))"))
  (cider-default-cljs-repl . super-cljs)))

The structure of the file is a mapping of major modes and some variables that need to be set in them. As CIDER is not a major mode most of the time you’ll probably be setting variables in clojure-mode or clojurescript-mode. Note that clojurescript-mode derives from clojure-mode, so whatever applies to clojure-mode will apply to clojurescript-mode as well. You can also evaluate code by using eval as the variable name in the variable to value mapping, but that’s something you’ll rarely need in practice.

Normally, you’d simply create the .dir-locals.el manually and edit it like any other Emacs Lisp code. If you, however, feel overwhelmed by its syntax you can simply do M-x add-dir-local-variable and you’ll be able to select the major-mode, the variable and its value interactively. One small problem with this approach is that the resulting .dir-local.el will be created in the current directory, which may be a problem depending on what you’re trying to do. Users of Projectile may leverage the project-aware projectile-edit-dir-locals command instead.

Here’s one slightly more complex .dir-locals.el:

((emacs-lisp-mode
  (bug-reference-url-format . "https://github.com/clojure-emacs/cider/issues/%s")
  (bug-reference-bug-regexp . "#\\(?2:[[:digit:]]+\\)")
  (indent-tabs-mode . nil)
  (fill-column . 80)
  (sentence-end-double-space . t)
  (emacs-lisp-docstring-fill-column . 75)
  (checkdoc-symbol-words . ("top-level" "major-mode" "macroexpand-all" "print-level" "print-length"))
  (checkdoc-package-keywords-flag)
  (checkdoc-arguments-in-order-flag)
  (checkdoc-verb-check-experimental-flag)
  (elisp-lint-indent-specs . ((if-let* . 2)
                              (when-let* . 1)
                              (let* . defun)
                              (nrepl-dbind-response . 2)
                              (cider-save-marker . 1)
                              (cider-propertize-region . 1)
                              (cider-map-repls . 1)
                              (cider--jack-in . 1)
                              (cider--make-result-overlay . 1)
                              ;; need better solution for indenting cl-flet bindings
                              (insert-label . defun)              ;; cl-flet
                              (insert-align-label . defun)        ;; cl-flet
                              (insert-rect . defun)               ;; cl-flet
                              (cl-defun . 2)
                              (with-parsed-tramp-file-name . 2)
                              (thread-first . 1)
                              (thread-last . 1)))))

Did you manage to guess what it is? That’s CIDER’s own .dir-locals.el, which ensures that all people hacking on the Elisp codebase are going to be using some common code style settings. That’s why everything’s scoped to emacs-lisp-mode.

For a Clojure-centric example let’s take a look at cider-nrepl's .dir-locals.el:

((clojure-mode
  (clojure-indent-style . :always-align)
  (indent-tabs-mode . nil)
  (fill-column . 80)))

Here the point is to ensure everyone working on the Clojure codebase using Emacs would be sharing the same code style settings.

Often in the wild you’ll see dir-local entries with nil as the major mode there. This odd looking notation simply means that the configuration specified there will be applied to every buffer regardless of its major mode. Use this approach sparingly, as there’s rarely a good reason to do this.

Another thing to keep in mind is that you can have multiple .dir-locals.el files in your project. Their overall effect will be cumulative with the innermost file taking precedence for any files in the directories beneath it. I’ve never needed this in practice, but I can imagine it being useful for people who have multiple projects in a mono repo, or people who apply different conventions to "real" code and its tests.

You might be wondering when do changes to .dir-locals.el get reflected in the Emacs buffers affected by them. The answer is to this question is "when the buffers get created". If you change something in .dir-locals.el you’ll normally have to re-create the related buffers. Or you can do it in the hacker way and apply a bit of Elisp magic.

There are more aspects to dir-locals, but they are beyond the scope of this article. If you’re curious for all the gory details you should check out the official Emacs documentation on dir-locals.