15

Suppose I have a directory with these files.

/foo/bar/baz/.dir-locals.el
/foo/bar/.dir-locals.el
/foo/.dir-locals.el

When I go to create a file in /foo/bar/baz/, I'd like to daisy chain them together such that /foo/.dir-locals.el applies first, and then /foo/bar/.dir-locals.el, and then /foo/bar/baz/.dir-locals.el

Constantine
  • 9,072
  • 1
  • 34
  • 49
Eric Johnson
  • 371
  • 1
  • 9
  • Related thread: [How can I have a second `.dir-locals`?](http://emacs.stackexchange.com/questions/4267/how-can-i-have-a-second-dir-locals). – Dan Dec 18 '14 at 13:13
  • There is no option that would do that (I looked at the code pretty closely), but it *should be* (almost certainly is) possible with some extra code. I have a use for it too, so I might look into this... – Constantine Dec 18 '14 at 15:31
  • With elisp, all things are possible. :) – Eric Johnson Dec 18 '14 at 15:36

2 Answers2

7

Based on the answer here, we do this by advising hack-dir-local-variables to look one directory up and load check if that .dir-locals.el file is readable. It will keep going up until it finds a directory with no readable .dir-locals.el.

Depending on the value of walk-dir-locals-upward the files can be read from the current directory upward or from the last .dir-locals.el found downward. Downward is the default so that subdirectories can clobber the settings of their parents.

(defvar walk-dir-locals-upward nil
  "If non-nil, evaluate .dir-locals.el files starting in the
  current directory and going up. Otherwise they will be
  evaluated from the top down to the current directory.")

(defadvice hack-dir-local-variables (around walk-dir-locals-file activate)
  (let* ((dir-locals-list (list dir-locals-file))
         (walk-dir-locals-file (first dir-locals-list)))
    (while (file-readable-p (concat "../" walk-dir-locals-file))
      (progn
        (setq walk-dir-locals-file (concat "../" walk-dir-locals-file))
        (add-to-list 'dir-locals-list walk-dir-locals-file
                     walk-dir-locals-upward)
        ))
    (dolist (file dir-locals-list)
      (let ((dir-locals-file (expand-file-name file)))
        (message dir-locals-file)
        ad-do-it
        )))
  )
erikstokes
  • 12,686
  • 2
  • 34
  • 56
  • This seems to expect that every directory in the tree (up to some level up from the current path) has a `.dir-locals.el`. Will it work if I have a tree of directories `a/b/c` and there exist `a/.dir-locals.el` and `a/b/c/.dir-locals.el`, but no `a/b/.dir-locals.el` (assume that I'm visiting `a/b/c/foo.el` and I want settings from `a/.dir-locals.el` to be applied)? – Constantine Dec 18 '14 at 17:48
  • 1
    Yes, that's what I'm assuming. The missing dir-locals in `a/b/` breaks the chain. It has to stop somewhere and if you want it to keep going you can add an empty dir-locals files. – erikstokes Dec 18 '14 at 17:58
  • 3
    BTW, I'd welcome a patch for Emacs to support chaining dir-locals out of the box. – Stefan Apr 10 '15 at 04:54
7

Here's a different way of doing this.

I define a function that produces the list of all the directories in the current directory hierarchy.

(defun file-name-directory-nesting-helper (name previous-name accumulator)
  (if (string= name previous-name)
      accumulator                       ; stop when names stop changing (at the top)
      (file-name-directory-nesting-helper
       (directory-file-name (file-name-directory name))
       name
       (cons name accumulator))))

(defun file-name-directory-nesting (name)
  (file-name-directory-nesting-helper (expand-file-name name) "" ()))

An example is in order:

(file-name-directory-nesting "/foo/bar/baz/quux/foo.el")
;; => ("/" "/foo" "/foo/bar" "/foo/bar/baz" "/foo/bar/baz/quux" "/foo/bar/baz/quux/foo.el")

Now I can add advice to hack-dir-local-variables to make it "pretend" that we're visiting a file at the very top of the tree, apply directory-local settings, then step one level down, apply settings again, and so on.

(defun hack-dir-local-variables-chained-advice (orig)
  "Apply dir-local settings from the whole directory hierarchy,
from the top down."
  (let ((original-buffer-file-name (buffer-file-name))
        (nesting (file-name-directory-nesting (or (buffer-file-name)
                                                  default-directory))))
    (unwind-protect
        (dolist (name nesting)
          ;; make it look like we're in a directory higher up in the
          ;; hierarchy; note that the file we're "visiting" does not
          ;; have to exist
          (setq buffer-file-name (expand-file-name "ignored" name))
          (funcall orig))
      ;; cleanup
      (setq buffer-file-name original-buffer-file-name))))

(advice-add 'hack-dir-local-variables :around
            #'hack-dir-local-variables-chained-advice)
Constantine
  • 9,072
  • 1
  • 34
  • 49