From 8249eb4ac28a5643c930acba9997e77b1b7e5ddb Mon Sep 17 00:00:00 2001 From: Stefan Monnier Date: Tue, 4 Jun 2024 11:00:32 -0400 Subject: [PATCH] (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. (cherry picked from commit 8253228d55b368ad7ea4d66d802059e8afff2b12) --- doc/lispref/variables.texi | 29 ++++++++++++++ etc/NEWS | 5 +++ lisp/files.el | 77 ++++++++++++++++++++++++++++++-------- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/doc/lispref/variables.texi b/doc/lispref/variables.texi index e05d3bb0f81..0ed1936cd84 100644 --- a/doc/lispref/variables.texi +++ b/doc/lispref/variables.texi @@ -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 diff --git a/etc/NEWS b/etc/NEWS index e5159b5d08f..51f59112efb 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2281,6 +2281,11 @@ unibyte string. * 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 diff --git a/lisp/files.el b/lisp/files.el index 65499c17865..fb8ef78f671 100644 --- a/lisp/files.el +++ b/lisp/files.el @@ -3515,6 +3515,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))) @@ -4790,7 +4792,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 @@ -4820,6 +4822,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' @@ -4827,21 +4839,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. -- 2.39.2