16

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,122
  • 1
  • 35
  • 50
Eric Johnson
  • 381
  • 1
  • 9

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,927
  • 2
  • 36
  • 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,122
  • 1
  • 35
  • 50