From ad5faa424a5d2f0d67265906d21f7af98220df26 Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Fri, 23 Jul 2021 15:51:11 +0200 Subject: [PATCH] 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. --- doc/emacs/custom.texi | 10 ++ doc/emacs/modes.texi | 10 +- etc/NEWS | 6 + lisp/files.el | 169 ++++++++++++++--------- test/lisp/files-resources/.dir-locals.el | 2 + test/lisp/files-resources/whatever.quux | 2 + test/lisp/files-tests.el | 5 + 7 files changed, 136 insertions(+), 68 deletions(-) create mode 100644 test/lisp/files-resources/.dir-locals.el create mode 100644 test/lisp/files-resources/whatever.quux diff --git a/doc/emacs/custom.texi b/doc/emacs/custom.texi index ce6290c1171..999234e6d33 100644 --- a/doc/emacs/custom.texi +++ b/doc/emacs/custom.texi @@ -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 diff --git a/doc/emacs/modes.texi b/doc/emacs/modes.texi index cc25d3e1e33..9014221edff 100644 --- a/doc/emacs/modes.texi +++ b/doc/emacs/modes.texi @@ -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, diff --git a/etc/NEWS b/etc/NEWS index 29953a8fa26..c7249456ff6 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -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'. diff --git a/lisp/files.el b/lisp/files.el index 412562fc9a3..dc803d3a4cf 100644 --- a/lisp/files.el +++ b/lisp/files.el @@ -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. diff --git a/test/lisp/files-resources/.dir-locals.el b/test/lisp/files-resources/.dir-locals.el new file mode 100644 index 00000000000..84997b8a0c0 --- /dev/null +++ b/test/lisp/files-resources/.dir-locals.el @@ -0,0 +1,2 @@ +;; This is used by files-tests.el. +((auto-mode-alist . (("\\.quux\\'" . tcl-mode)))) diff --git a/test/lisp/files-resources/whatever.quux b/test/lisp/files-resources/whatever.quux new file mode 100644 index 00000000000..595583b911e --- /dev/null +++ b/test/lisp/files-resources/whatever.quux @@ -0,0 +1,2 @@ +# Used by files-test.el. +# Due to .dir-locals.el this should end up in Tcl mode. diff --git a/test/lisp/files-tests.el b/test/lisp/files-tests.el index a6b0c900bec..fce7e3fd719 100644 --- a/test/lisp/files-tests.el +++ b/test/lisp/files-tests.el @@ -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 -- 2.39.2