Subject: Re: bug#66663: 30.0.50; Allow dir locals file selection in *-dir-local-variable functions

From: Mauro Aranda <[email-masked]>
Date: Sat, 21 Oct 2023 09:16:28 -0300

tags 66663 patch
quit

Here's the patch.  Juri, could you take a look to see if this approach
is fine? Thank you.

From 1fa03835062bfd3519687eadc179d5647eb61f25 Mon Sep 17 00:00:00 2001
From: Mauro Aranda <[email-masked]>
Date: Sat, 21 Oct 2023 09:14:25 -0300
Subject: [PATCH] Allow specifying the dir locals file to edit (Bug#66663)

* lisp/files-x.el (modify-dir-local-variable): Take a 5th optional
argument, to manipulate which dir locals file gets selected.
(add-dir-local-variable, delete-dir-local-variable)
(copy-file-locals-to-dir-locals): Take a prefix argument and pass it
to modify-dir-local-variable.
* etc/NEWS: Announce the change.
* doc/emacs/custom.texi (Directory Variables): Document the new
functionality.
---
 doc/emacs/custom.texi | 14 ++++++-
 etc/NEWS              | 10 +++++
 lisp/files-x.el       | 91 +++++++++++++++++++++++++++++++++++--------
 3 files changed, 97 insertions(+), 18 deletions(-)

diff --git a/doc/emacs/custom.texi b/doc/emacs/custom.texi
index 8c30f26bbf7..df6bcad506a 100644
--- a/doc/emacs/custom.texi
+++ b/doc/emacs/custom.texi
@@ -1507,7 +1507,19 @@ Directory Variables
 entry defining the directory-local variable.  @kbd{M-x
 delete-dir-local-variable} deletes an entry.  @kbd{M-x
 copy-file-locals-to-dir-locals} copies the file-local variables in the
-current file into @file{.dir-locals.el}.
+current file into @file{.dir-locals.el}, or @file{.dir-locals-2.el} if
+that file is also present.
+
+Since both @file{.dir-locals.el} and @file{.dir-locals-2.el} file
+might exist in the same directory, there may be some clash about which
+file you want to modify when executing the above three commands.  To
+solve that, all three of them take a prefix argument, to indicate
+which file you want to modify.  When both files exist, a prefix
+argument means to prefer to modify @file{.dir-locals.el} instead of
+@file{.dir-locals-2.el}.  When one of the files doesn't exist, and
+you're adding a variable or copying the file-local variables, a prefix
+argument means to modify (i.e., create) the file that doesn't yet
+exist.
 
 @findex dir-locals-set-class-variables
 @findex dir-locals-set-directory-class
diff --git a/etc/NEWS b/etc/NEWS
index 3d4cdd876b3..5c5ae0581d2 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -187,6 +187,11 @@ right-aligned to is controlled by the new user option
 It can be used to add, remove and reorder functions that change
 the appearance of every tab on the tab bar.
 
++++
+** New optional argument for modifying directory local variables
+The commands 'add-dir-local-variable', 'delete-dir-local-variable' and
+'copy-file-locals-to-dir-locals' now take an optional prefix argument,
+to indicate which file you want to modify.
 ** Miscellaneous
 
 ---
@@ -1337,6 +1342,11 @@ Since circular alias chains now cannot occur, 'function-alias-p',
 'indirect-function' and 'indirect-variable' will never signal an error.
 Their 'noerror' arguments have no effect and are therefore obsolete.
 
+---
+** New optional argument to 'modify-dir-local-variable'
+A 5th argument, optional, has been added to
+'modify-dir-local-variable'.  It can be used to specify which
+dir-locals file to modify.
 
 * Changes in Emacs 30.1 on Non-Free Operating Systems
 
diff --git a/lisp/files-x.el b/lisp/files-x.el
index 3ba7632d253..c0379fe1084 100644
--- a/lisp/files-x.el
+++ b/lisp/files-x.el
@@ -31,6 +31,7 @@
 ;;; Code:
 
 (eval-when-compile (require 'subr-x)) ; for string-trim-right
+(declare-function dosified-file-name "dos-fns" (file-name))
 
 
 ;;; Commands to add/delete file-local/directory-local variables.
@@ -410,7 +411,7 @@ delete-file-local-variable-prop-line
 
 (defvar auto-insert) ; from autoinsert.el
 
-(defun modify-dir-local-variable (mode variable value op)
+(defun modify-dir-local-variable (mode variable value op &optional file)
   "Modify directory-local VARIABLE in .dir-locals.el depending on operation OP.
 
 If OP is `add-or-replace' then delete all existing settings of
@@ -422,7 +423,12 @@ modify-dir-local-variable
 this file in the current directory.
 
 If OP is `delete' then delete all existing settings of VARIABLE
-from the MODE alist ignoring the input argument VALUE."
+from the MODE alist ignoring the input argument VALUE.
+
+FILE specifies what file to modify.  It can be a string, the name of the
+dir-locals file to modify.  It can also be any other non-nil value, in which
+case the file to modify is .dir-locals.el, if .dir-locals-2.el exists, or
+.dir-locals-2.el if .dir-locals.el exists but the former doesn't."
   (catch 'exit
     (unless enable-local-variables
       (throw 'exit (message "Directory-local variables are disabled")))
@@ -432,7 +438,8 @@ modify-dir-local-variable
            (variables-file
             ;; If there are several .dir-locals, the user probably
             ;; wants to edit the last one (the highest priority).
-            (cond ((stringp dir-or-cache)
+            (cond ((stringp file) file)
+                  ((stringp dir-or-cache)
                    (car (last (dir-locals--all-files dir-or-cache))))
                   ((consp dir-or-cache)	; result from cache
                    ;; If cache element has an mtime, assume it came
@@ -441,9 +448,34 @@ modify-dir-local-variable
                    (if (nth 2 dir-or-cache)
                        (car (last (dir-locals--all-files (car dir-or-cache))))
                      (cadr dir-or-cache)))
-                  ;; Try to make a proper file-name.
-                  (t (expand-file-name dir-locals-file))))
+                  ;; Try to make a proper file-name, respecting FILE.
+                  (t (let* ((pri (expand-file-name
+                                  (if (eq system-type 'ms-dos)
+                                      (dosified-file-name dir-locals-file)
+                                    dir-locals-file)))
+                            (sec (replace-regexp-in-string ".el$" "-2.el" pri))
+                            (sec-exists-p (file-exists-p sec)))
+                       (if file
+                           ;; When alternating, prefer .dir-locals.el if
+                           ;; .dir-locals-2.el exists, or .dir-locals-2.el
+                           ;; if it doesn't exist (or neither exist).
+                           (if sec-exists-p pri sec)
+                         (if sec-exists-p sec pri))))))
            variables)
+      ;; When alternating, FILE is non-nil and not a string.
+      ;; We check for `dir-or-cache' non-nil, because we handled the nil
+      ;; case in the `cond' above.
+      (when (and file (not (stringp file))
+                 dir-or-cache
+                 (stringp variables-file))
+        ;; If we got the .dir-locals-2.el file, make it
+        ;; the .dir-locals.el file.
+        (if (string-match "[^2]\\(\\.el$\\)" variables-file)
+            (setq variables-file (replace-match "-2.el" t nil variables-file 1))
+          ;; And if we got the .dir-locals.el file, make it the
+          ;; .dir-locals-2.el file.
+          (setq variables-file
+                (replace-regexp-in-string "-2.el$" ".el" variables-file))))
       ;; I can't be bothered to handle this case right now.
       ;; Dir locals were set directly from a class.  You need to
       ;; directly modify the class in dir-locals-class-alist.
@@ -528,32 +560,57 @@ dir-locals-to-string
            variables "\n")))
 
 ;;;###autoload
-(defun add-dir-local-variable (mode variable value)
-  "Add directory-local VARIABLE with its VALUE and MODE to .dir-locals.el."
+(defun add-dir-local-variable (mode variable value &optional alt)
+  "Add directory-local VARIABLE with its VALUE and MODE to .dir-locals.el.
+
+A prefix argument changes the file to modify.  When given and .dir-locals.el
+exists but .dir-locals-2.el does not, it modifies .dir-locals-2.el, possibly
+creating it.  On the contrary, when both files exist, a prefix argument means
+to prefer to add VARIABLE to .dir-locals.el.
+
+When called from Lisp, ALT may also be the expanded name of the dir-locals file
+where to add VARIABLE."
   (interactive
    (let (variable)
      (list
       (read-file-local-variable-mode)
       (setq variable (read-file-local-variable "Add directory-local variable"))
-      (read-file-local-variable-value variable))))
-  (modify-dir-local-variable mode variable value 'add-or-replace))
+      (read-file-local-variable-value variable)
+      current-prefix-arg)))
+   (modify-dir-local-variable mode variable value 'add-or-replace alt))
 
 ;;;###autoload
-(defun delete-dir-local-variable (mode variable)
-  "Delete all MODE settings of file-local VARIABLE from .dir-locals.el."
+(defun delete-dir-local-variable (mode variable &optional alt)
+  "Delete all MODE settings of dir-local VARIABLE from .dir-locals.el.
+
+A prefix argument changes the file to modify.  If given and if both
+.dir-locals-2.el and .dir-locals.el exist, prefer to delete VARIABLE from
+.dir-locals.el.
+
+When called from Lisp, ALT may also be the expanded name of the dir-locals file
+from where to delete VARIABLE."
   (interactive
    (list
     (read-file-local-variable-mode)
-    (read-file-local-variable "Delete directory-local variable")))
-  (modify-dir-local-variable mode variable nil 'delete))
+    (read-file-local-variable "Delete directory-local variable")
+    current-prefix-arg))
+  (modify-dir-local-variable mode variable nil 'delete alt))
 
 ;;;###autoload
-(defun copy-file-locals-to-dir-locals ()
-  "Copy file-local variables to .dir-locals.el."
-  (interactive)
+(defun copy-file-locals-to-dir-locals (&optional alt)
+  "Copy file-local variables to .dir-locals.el.
+
+A prefix argument changes the file to modify.  When given and .dir-locals.el
+exists but .dir-locals-2.el does not, it modifies .dir-locals-2.el, possibly
+creating it.  On the contrary, if both files exist, a prefix argument means
+to prefer to copy the variables to .dir-locals.el.
+
+When called from Lisp, ALT may also be the expanded name of the dir-locals file
+where to copy the file-local variables."
+  (interactive "P")
   (dolist (elt file-local-variables-alist)
     (unless (assq (car elt) dir-local-variables-alist)
-      (add-dir-local-variable major-mode (car elt) (cdr elt)))))
+      (add-dir-local-variable major-mode (car elt) (cdr elt) alt))))
 
 ;;;###autoload
 (defun copy-dir-locals-to-file-locals ()
-- 
2.34.1