Add auto-mode-alist functionality to .dir-locals.el

* doc/emacs/custom.texi (Directory Variables): Document
auto-mode-alist in .dir-locals.el (Bug#18721)
* doc/emacs/modes.texi (Choosing Modes): Update.
* lisp/files.el (set-auto-mode--apply-alist): New function,
from set-auto-mode.
(set-auto-mode): Check directory locals for auto-mode-alist.
(dir-locals-collect-variables): Add "predicate" parameter.
(hack-dir-local--get-variables): New function, from
hack-dir-local-variables.
(hack-dir-local-variables): Call hack-dir-local--get-variables.
* test/lisp/files-resources/.dir-locals.el: New file.
* test/lisp/files-resources/whatever.quux: New file.
* test/lisp/files-tests.el (files-tests-data-dir): New variable.
(files-test-dir-locals-auto-mode-alist): New test.
This commit is contained in:
Tom Tromey 2021-07-23 15:51:11 +02:00 committed by Lars Ingebrigtsen
parent 6a3b89f9df
commit ad5faa424a
7 changed files with 136 additions and 68 deletions

View file

@ -1415,6 +1415,16 @@ meanings as they would have in file local variables. @code{coding}
cannot be specified as a directory local variable. @xref{File
Variables}.
The special key @code{auto-mode-alist} in a @file{.dir-locals.el} lets
you set a file's major mode. It works much like the variable
@code{auto-mode-alist} (@pxref{Choosing Modes}). For example, here is
how you can tell Emacs that @file{.def} source files in this directory
should be in C mode:
@example
((auto-mode-alist . (("\\.def\\'" . c-mode))))
@end example
@findex add-dir-local-variable
@findex delete-dir-local-variable
@findex copy-file-locals-to-dir-locals

View file

@ -357,8 +357,12 @@ preferences. If you personally want to use a minor mode for a
particular file type, it is better to enable the minor mode via a
major mode hook (@pxref{Major Modes}).
Second, Emacs checks whether the file's extension matches an entry
in any directory-local @code{auto-mode-alist}. These are found using
the @file{.dir-locals.el} facility (@pxref{Directory Variables}).
@vindex interpreter-mode-alist
Second, if there is no file variable specifying a major mode, Emacs
Third, if there is no file variable specifying a major mode, Emacs
checks whether the file's contents begin with @samp{#!}. If so, that
indicates that the file can serve as an executable shell command,
which works by running an interpreter named on the file's first line
@ -376,7 +380,7 @@ same is true for man pages which start with the magic string
@samp{'\"} to specify a list of troff preprocessors.
@vindex magic-mode-alist
Third, Emacs tries to determine the major mode by looking at the
Fourth, Emacs tries to determine the major mode by looking at the
text at the start of the buffer, based on the variable
@code{magic-mode-alist}. By default, this variable is @code{nil} (an
empty list), so Emacs skips this step; however, you can customize it
@ -404,7 +408,7 @@ where @var{match-function} is a Lisp function that is called at the
beginning of the buffer; if the function returns non-@code{nil}, Emacs
set the major mode with @var{mode-function}.
Fourth---if Emacs still hasn't found a suitable major mode---it
Fifth---if Emacs still hasn't found a suitable major mode---it
looks at the file's name. The correspondence between file names and
major modes is controlled by the variable @code{auto-mode-alist}. Its
value is a list in which each element has this form,

View file

@ -2289,6 +2289,12 @@ This command, called interactively, toggles the local value of
** Miscellaneous
+++
*** .dir-locals.el now supports setting 'auto-mode-alist'.
The new 'auto-mode-alist' specification in .dir-local.el files can now
be used to override the global 'auto-mode-alist' in the current
directory tree.
---
*** New utility function 'make-separator-line'.

View file

@ -3195,11 +3195,62 @@ If FUNCTION is nil, then it is not called.")
"Upper limit on `magic-mode-alist' regexp matches.
Also applies to `magic-fallback-mode-alist'.")
(defun set-auto-mode--apply-alist (alist keep-mode-if-same dir-local)
"Helper function for `set-auto-mode'.
This function takes an alist of the same form as
`auto-mode-alist'. It then tries to find the appropriate match
in the alist for the current buffer; setting the mode if
possible. Returns non-`nil' if the mode was set, `nil'
otherwise. DIR-LOCAL is a boolean which, if true, says that this
call is via directory-locals and extra checks should be done."
(if buffer-file-name
(let (mode
(name buffer-file-name)
(remote-id (file-remote-p buffer-file-name))
(case-insensitive-p (file-name-case-insensitive-p
buffer-file-name)))
;; Remove backup-suffixes from file name.
(setq name (file-name-sans-versions name))
;; Remove remote file name identification.
(when (and (stringp remote-id)
(string-match (regexp-quote remote-id) name))
(setq name (substring name (match-end 0))))
(while name
;; Find first matching alist entry.
(setq mode
(if case-insensitive-p
;; Filesystem is case-insensitive.
(let ((case-fold-search t))
(assoc-default alist 'string-match))
;; Filesystem is case-sensitive.
(or
;; First match case-sensitively.
(let ((case-fold-search nil))
(assoc-default name alist 'string-match))
;; Fallback to case-insensitive match.
(and auto-mode-case-fold
(let ((case-fold-search t))
(assoc-default name alist 'string-match))))))
(if (and mode
(consp mode)
(cadr mode))
(setq mode (car mode)
name (substring name 0 (match-beginning 0)))
(setq name nil)))
(when (and dir-local mode)
(unless (string-suffix-p "-mode" (symbol-name mode))
(message "Ignoring invalid mode `%s'" (symbol-name mode))
(setq mode nil)))
(when mode
(set-auto-mode-0 mode keep-mode-if-same)
t))))
(defun set-auto-mode (&optional keep-mode-if-same)
"Select major mode appropriate for current buffer.
To find the right major mode, this function checks for a -*- mode tag
checks for a `mode:' entry in the Local Variables section of the file,
checks if there an `auto-mode-alist' entry in `.dir-locals.el',
checks if it uses an interpreter listed in `interpreter-mode-alist',
matches the buffer beginning against `magic-mode-alist',
compares the file name against the entries in `auto-mode-alist',
@ -3256,6 +3307,14 @@ we don't actually set it to the same mode the buffer already has."
(or (set-auto-mode-0 mode keep-mode-if-same)
;; continuing would call minor modes again, toggling them off
(throw 'nop nil))))))
;; Check for auto-mode-alist entry in dir-locals.
(unless done
(with-demoted-errors "Directory-local variables error: %s"
;; Note this is a no-op if enable-local-variables is nil.
(let* ((mode-alist (cdr (hack-dir-local--get-variables
(lambda (key) (eq key 'auto-mode-alist))))))
(setq done (set-auto-mode--apply-alist mode-alist
keep-mode-if-same t)))))
(and (not done)
(setq mode (hack-local-variables t (not try-locals)))
(not (memq mode modes)) ; already tried and failed
@ -3307,45 +3366,8 @@ we don't actually set it to the same mode the buffer already has."
(set-auto-mode-0 done keep-mode-if-same)))
;; Next compare the filename against the entries in auto-mode-alist.
(unless done
(if buffer-file-name
(let ((name buffer-file-name)
(remote-id (file-remote-p buffer-file-name))
(case-insensitive-p (file-name-case-insensitive-p
buffer-file-name)))
;; Remove backup-suffixes from file name.
(setq name (file-name-sans-versions name))
;; Remove remote file name identification.
(when (and (stringp remote-id)
(string-match (regexp-quote remote-id) name))
(setq name (substring name (match-end 0))))
(while name
;; Find first matching alist entry.
(setq mode
(if case-insensitive-p
;; Filesystem is case-insensitive.
(let ((case-fold-search t))
(assoc-default name auto-mode-alist
'string-match))
;; Filesystem is case-sensitive.
(or
;; First match case-sensitively.
(let ((case-fold-search nil))
(assoc-default name auto-mode-alist
'string-match))
;; Fallback to case-insensitive match.
(and auto-mode-case-fold
(let ((case-fold-search t))
(assoc-default name auto-mode-alist
'string-match))))))
(if (and mode
(consp mode)
(cadr mode))
(setq mode (car mode)
name (substring name 0 (match-beginning 0)))
(setq name nil))
(when mode
(set-auto-mode-0 mode keep-mode-if-same)
(setq done t))))))
(setq done (set-auto-mode--apply-alist auto-mode-alist
keep-mode-if-same nil)))
;; Next try matching the buffer beginning against magic-fallback-mode-alist.
(unless done
(if (setq done (save-excursion
@ -4166,10 +4188,13 @@ Returns the new list."
;; Need a new cons in case we setcdr later.
(push (cons variable value) variables)))))
(defun dir-locals-collect-variables (class-variables root variables)
(defun dir-locals-collect-variables (class-variables root variables
&optional predicate)
"Collect entries from CLASS-VARIABLES into VARIABLES.
ROOT is the root directory of the project.
Return the new variables list."
Return the new variables list.
If PREDICATE is given, it is used to test a symbol key in the alist
to see whether it should be considered."
(let* ((file-name (or (buffer-file-name)
;; Handle non-file buffers, too.
(expand-file-name default-directory)))
@ -4188,9 +4213,11 @@ Return the new variables list."
(>= (length sub-file-name) (length key))
(string-prefix-p key sub-file-name))
(setq variables (dir-locals-collect-variables
(cdr entry) root variables))))
((or (not key)
(derived-mode-p key))
(cdr entry) root variables predicate))))
((if predicate
(funcall predicate key)
(or (not key)
(derived-mode-p key)))
(let* ((alist (cdr entry))
(subdirs (assq 'subdirs alist)))
(if (or (not subdirs)
@ -4487,13 +4514,13 @@ Return the new class name, which is a symbol named DIR."
(defvar hack-dir-local-variables--warned-coding nil)
(defun hack-dir-local-variables ()
(defun hack-dir-local--get-variables (predicate)
"Read per-directory local variables for the current buffer.
Store the directory-local variables in `dir-local-variables-alist'
and `file-local-variables-alist', without applying them.
This does nothing if either `enable-local-variables' or
`enable-dir-local-variables' are nil."
Return a cons of the form (DIR . ALIST), where DIR is the
directory name (maybe nil) and ALIST is an alist of all variables
that might apply. These will be filtered according to the
buffer's directory, but not according to its mode.
PREDICATE is passed to `dir-locals-collect-variables'."
(when (and enable-local-variables
enable-dir-local-variables
(or enable-remote-dir-locals
@ -4512,21 +4539,33 @@ This does nothing if either `enable-local-variables' or
(setq dir-name (nth 0 dir-or-cache))
(setq class (nth 1 dir-or-cache))))
(when class
(let ((variables
(dir-locals-collect-variables
(dir-locals-get-class-variables class) dir-name nil)))
(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)))))))
(cons dir-name
(dir-locals-collect-variables
(dir-locals-get-class-variables class)
dir-name nil predicate))))))
(defun hack-dir-local-variables ()
"Read per-directory local variables for the current buffer.
Store the directory-local variables in `dir-local-variables-alist'
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))))
(defun hack-dir-local-variables-non-file-buffer ()
"Apply directory-local variables to a non-file buffer.

View file

@ -0,0 +1,2 @@
;; This is used by files-tests.el.
((auto-mode-alist . (("\\.quux\\'" . tcl-mode))))

View file

@ -0,0 +1,2 @@
# Used by files-test.el.
# Due to .dir-locals.el this should end up in Tcl mode.

View file

@ -1534,5 +1534,10 @@ The door of all subtleties!
(should-error (file-name-with-extension "Jack" "."))
(should-error (file-name-with-extension "/is/a/directory/" "css")))
(ert-deftest files-test-dir-locals-auto-mode-alist ()
"Test an `auto-mode-alist' entry in `.dir-locals.el'"
(find-file (ert-resource-file "whatever.quux"))
(should (eq major-mode 'tcl-mode)))
(provide 'files-tests)
;;; files-tests.el ends here