(hack-dir-local-get-variables-functions): New hook

Make it possible to provide more dir-local variables, such as
done by the Editorconfig package.

* lisp/files.el (hack-dir-local--get-variables): Make arg optional.
(hack-dir-local-get-variables-functions): New hook.
(hack-dir-local-variables): Run it instead of calling
`hack-dir-local--get-variables`.

* doc/lispref/variables.texi (Directory Local Variables):
Document the new hook.
This commit is contained in:
Stefan Monnier 2024-06-04 11:00:32 -04:00
parent 3ecc6b4f3c
commit 8253228d55
3 changed files with 95 additions and 16 deletions

View file

@ -2277,6 +2277,35 @@ modification times of the associated directory local variables file
updates this list.
@end defvar
@defvar hack-dir-local-get-variables-functions
This special hook holds the functions that gather the directory-local
variables to use for a given buffer. By default it contains just the
function that obeys the other settings described in the present section.
But it can be used to add support for more sources of directory-local
variables, such as those used by other text editors.
The functions on this hook are called with no argument, in the buffer to
which we intend to apply the directory-local variables, after the
buffer's major mode function has been run, so they can use sources of
information such as @code{major-mode} or @code{buffer-file-name} to find
the variables that should be applied.
It should return either a cons cell of the form @code{(@var{directory}
. @var{alist})} or a list of such cons-cells. A @code{nil} return value
means that it found no directory-local variables. @var{directory}
should be a string: the name of the directory to which the variables
apply. @var{alist} is a list of variables together with their values
that apply to the current buffer, where every element is of the form
@code{(@var{varname} . @var{value})}.
The various @var{alist} returned by these functions will be combined,
and in case of conflicts, the settings coming from deeper directories
will take precedence over those coming from higher directories in the
directory hierarchy. Finally, since this hook is run every time we visit
a file it is important to try and keep those functions efficient, which
will usually require some kind of caching.
@end defvar
@defvar enable-dir-local-variables
If @code{nil}, directory-local variables are ignored. This variable
may be useful for modes that want to ignore directory-locals while

View file

@ -2186,6 +2186,11 @@ completion candidate.
* Lisp Changes in Emacs 30.1
+++
** New hook 'hack-dir-local-get-variables-functions'.
This can be used to provide support for other directory-local settings
beside '.dir-locals.el'.
+++
** 'auto-coding-functions' can know the name of the file.
The functions on this hook can now find the name of the file to

View file

@ -3494,6 +3494,8 @@ we don't actually set it to the same mode the buffer already has."
;; Check for auto-mode-alist entry in dir-locals.
(with-demoted-errors "Directory-local variables error: %s"
;; Note this is a no-op if enable-local-variables is nil.
;; We don't use `hack-dir-local-get-variables-functions' here, because
;; modes are specific to Emacs.
(let* ((mode-alist (cdr (hack-dir-local--get-variables
(lambda (key) (eq key 'auto-mode-alist))))))
(set-auto-mode--apply-alist mode-alist keep-mode-if-same t)))
@ -4769,7 +4771,7 @@ Return the new class name, which is a symbol named DIR."
(defvar hack-dir-local-variables--warned-coding nil)
(defun hack-dir-local--get-variables (predicate)
(defun hack-dir-local--get-variables (&optional predicate)
"Read per-directory local variables for the current buffer.
Return a cons of the form (DIR . ALIST), where DIR is the
directory name (maybe nil) and ALIST is an alist of all variables
@ -4799,6 +4801,16 @@ PREDICATE is passed to `dir-locals-collect-variables'."
(dir-locals-get-class-variables class)
dir-name nil predicate))))))
(defvar hack-dir-local-get-variables-functions
(list #'hack-dir-local--get-variables)
"Special hook to compute the set of dir-local variables.
Every function is called without arguments and should return either
a cons of the form (DIR . ALIST) or a (possibly empty) list of such conses,
where ALIST is an alist of (VAR . VAL) settings.
DIR should be a string (a directory name) and is used to obey
`safe-local-variable-directories'.
This hook is run after the major mode has been setup.")
(defun hack-dir-local-variables ()
"Read per-directory local variables for the current buffer.
Store the directory-local variables in `dir-local-variables-alist'
@ -4806,21 +4818,54 @@ and `file-local-variables-alist', without applying them.
This does nothing if either `enable-local-variables' or
`enable-dir-local-variables' are nil."
(let* ((items (hack-dir-local--get-variables nil))
(dir-name (car items))
(variables (cdr items)))
(when variables
(dolist (elt variables)
(if (eq (car elt) 'coding)
(unless hack-dir-local-variables--warned-coding
(setq hack-dir-local-variables--warned-coding t)
(display-warning 'files
"Coding cannot be specified by dir-locals"))
(unless (memq (car elt) '(eval mode))
(setq dir-local-variables-alist
(assq-delete-all (car elt) dir-local-variables-alist)))
(push elt dir-local-variables-alist)))
(hack-local-variables-filter variables dir-name))))
(let (items)
(when (and enable-local-variables
enable-dir-local-variables
(or enable-remote-dir-locals
(not (file-remote-p (or (buffer-file-name)
default-directory)))))
(run-hook-wrapped 'hack-dir-local-get-variables-functions
(lambda (fun)
(let ((res (funcall fun)))
(cond
((null res))
((consp (car-safe res))
(setq items (append res items)))
(t (push res items))))
nil)))
;; Sort the entries from nearest dir to furthest dir.
(setq items (sort (nreverse items)
:key (lambda (x) (length (car-safe x))) :reverse t))
;; Filter out duplicates, preferring the settings from the nearest dir
;; and from the first hook function.
(let ((seen nil))
(dolist (item items)
(when seen ;; Special case seen=nil since it's the most common case.
(setcdr item (seq-filter (lambda (vv) (not (memq (car-safe vv) seen)))
(cdr item))))
(setq seen (nconc (seq-difference (mapcar #'car (cdr item))
'(eval mode))
seen))))
;; Rather than a loop, maybe we should handle all the dirs
;; "together", e.g. prompting the user only once. But if so, we'd
;; probably want to also merge the prompt for file-local vars,
;; which comes from the call to `hack-local-variables-filter' in
;; `hack-local-variables'.
(dolist (item items)
(let ((dir-name (car item))
(variables (cdr item)))
(when variables
(dolist (elt variables)
(if (eq (car elt) 'coding)
(unless hack-dir-local-variables--warned-coding
(setq hack-dir-local-variables--warned-coding t)
(display-warning 'files
"Coding cannot be specified by dir-locals"))
(unless (memq (car elt) '(eval mode))
(setq dir-local-variables-alist
(assq-delete-all (car elt) dir-local-variables-alist)))
(push elt dir-local-variables-alist)))
(hack-local-variables-filter variables dir-name))))))
(defun hack-dir-local-variables-non-file-buffer ()
"Apply directory-local variables to a non-file buffer.