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 cannot be specified as a directory local variable. @xref{File
Variables}. 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 add-dir-local-variable
@findex delete-dir-local-variable @findex delete-dir-local-variable
@findex copy-file-locals-to-dir-locals @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 particular file type, it is better to enable the minor mode via a
major mode hook (@pxref{Major Modes}). 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 @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 checks whether the file's contents begin with @samp{#!}. If so, that
indicates that the file can serve as an executable shell command, indicates that the file can serve as an executable shell command,
which works by running an interpreter named on the file's first line 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. @samp{'\"} to specify a list of troff preprocessors.
@vindex magic-mode-alist @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 text at the start of the buffer, based on the variable
@code{magic-mode-alist}. By default, this variable is @code{nil} (an @code{magic-mode-alist}. By default, this variable is @code{nil} (an
empty list), so Emacs skips this step; however, you can customize it 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 beginning of the buffer; if the function returns non-@code{nil}, Emacs
set the major mode with @var{mode-function}. 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 looks at the file's name. The correspondence between file names and
major modes is controlled by the variable @code{auto-mode-alist}. Its major modes is controlled by the variable @code{auto-mode-alist}. Its
value is a list in which each element has this form, 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 ** 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'. *** 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. "Upper limit on `magic-mode-alist' regexp matches.
Also applies to `magic-fallback-mode-alist'.") 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) (defun set-auto-mode (&optional keep-mode-if-same)
"Select major mode appropriate for current buffer. "Select major mode appropriate for current buffer.
To find the right major mode, this function checks for a -*- mode tag 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 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', checks if it uses an interpreter listed in `interpreter-mode-alist',
matches the buffer beginning against `magic-mode-alist', matches the buffer beginning against `magic-mode-alist',
compares the file name against the entries in `auto-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) (or (set-auto-mode-0 mode keep-mode-if-same)
;; continuing would call minor modes again, toggling them off ;; continuing would call minor modes again, toggling them off
(throw 'nop nil)))))) (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) (and (not done)
(setq mode (hack-local-variables t (not try-locals))) (setq mode (hack-local-variables t (not try-locals)))
(not (memq mode modes)) ; already tried and failed (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))) (set-auto-mode-0 done keep-mode-if-same)))
;; Next compare the filename against the entries in auto-mode-alist. ;; Next compare the filename against the entries in auto-mode-alist.
(unless done (unless done
(if buffer-file-name (setq done (set-auto-mode--apply-alist auto-mode-alist
(let ((name buffer-file-name) keep-mode-if-same nil)))
(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))))))
;; Next try matching the buffer beginning against magic-fallback-mode-alist. ;; Next try matching the buffer beginning against magic-fallback-mode-alist.
(unless done (unless done
(if (setq done (save-excursion (if (setq done (save-excursion
@ -4166,10 +4188,13 @@ Returns the new list."
;; Need a new cons in case we setcdr later. ;; Need a new cons in case we setcdr later.
(push (cons variable value) variables))))) (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. "Collect entries from CLASS-VARIABLES into VARIABLES.
ROOT is the root directory of the project. 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) (let* ((file-name (or (buffer-file-name)
;; Handle non-file buffers, too. ;; Handle non-file buffers, too.
(expand-file-name default-directory))) (expand-file-name default-directory)))
@ -4188,9 +4213,11 @@ Return the new variables list."
(>= (length sub-file-name) (length key)) (>= (length sub-file-name) (length key))
(string-prefix-p key sub-file-name)) (string-prefix-p key sub-file-name))
(setq variables (dir-locals-collect-variables (setq variables (dir-locals-collect-variables
(cdr entry) root variables)))) (cdr entry) root variables predicate))))
((or (not key) ((if predicate
(derived-mode-p key)) (funcall predicate key)
(or (not key)
(derived-mode-p key)))
(let* ((alist (cdr entry)) (let* ((alist (cdr entry))
(subdirs (assq 'subdirs alist))) (subdirs (assq 'subdirs alist)))
(if (or (not subdirs) (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) (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. "Read per-directory local variables for the current buffer.
Store the directory-local variables in `dir-local-variables-alist' Return a cons of the form (DIR . ALIST), where DIR is the
and `file-local-variables-alist', without applying them. directory name (maybe nil) and ALIST is an alist of all variables
that might apply. These will be filtered according to the
This does nothing if either `enable-local-variables' or buffer's directory, but not according to its mode.
`enable-dir-local-variables' are nil." PREDICATE is passed to `dir-locals-collect-variables'."
(when (and enable-local-variables (when (and enable-local-variables
enable-dir-local-variables enable-dir-local-variables
(or enable-remote-dir-locals (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 dir-name (nth 0 dir-or-cache))
(setq class (nth 1 dir-or-cache)))) (setq class (nth 1 dir-or-cache))))
(when class (when class
(let ((variables (cons dir-name
(dir-locals-collect-variables (dir-locals-collect-variables
(dir-locals-get-class-variables class) dir-name nil))) (dir-locals-get-class-variables class)
(when variables dir-name nil predicate))))))
(dolist (elt variables)
(if (eq (car elt) 'coding) (defun hack-dir-local-variables ()
(unless hack-dir-local-variables--warned-coding "Read per-directory local variables for the current buffer.
(setq hack-dir-local-variables--warned-coding t) Store the directory-local variables in `dir-local-variables-alist'
(display-warning 'files and `file-local-variables-alist', without applying them.
"Coding cannot be specified by dir-locals"))
(unless (memq (car elt) '(eval mode)) This does nothing if either `enable-local-variables' or
(setq dir-local-variables-alist `enable-dir-local-variables' are nil."
(assq-delete-all (car elt) dir-local-variables-alist))) (let* ((items (hack-dir-local--get-variables nil))
(push elt dir-local-variables-alist))) (dir-name (car items))
(hack-local-variables-filter variables dir-name))))))) (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 () (defun hack-dir-local-variables-non-file-buffer ()
"Apply directory-local variables to a 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 "Jack" "."))
(should-error (file-name-with-extension "/is/a/directory/" "css"))) (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) (provide 'files-tests)
;;; files-tests.el ends here ;;; files-tests.el ends here