master 8b8aa7ced6b: Delete empty local-variable block ad prop line

Eli Zaretskii <[email protected]> Thu, 30 Jul 2026 10:39:26 -0400 (EDT)
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: master
commit 8b8aa7ced6ba8b85e8aa057c592c85d5aa16ebf9
Author: Daniel Mendler <[email protected]>
Commit: Eli Zaretskii <[email protected]>

    Delete empty local-variable block ad prop line
    
    Delete local variable block or prop line when it became empty
    after deleting a variable.  (Bug#81516)
    
    * lisp/files-x.el (modify-file-local-variable)
    (modify-file-local-variable-prop-line): Delete empty block or
    prop-line.
    * test/lisp/files-x-tests.el (files-x-test-add-file-local-variable)
    (files-x-test-add-file-local-variable-prop-line)
    (files-x-test-delete-file-local-variable)
    (files-x-test-delete-file-local-variable-prop-line): New tests.
---
 lisp/files-x.el            |  43 ++++++++++-
 test/lisp/files-x-tests.el | 189 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 228 insertions(+), 4 deletions(-)

diff --git a/lisp/files-x.el b/lisp/files-x.el
index 0d8c1d96a9f..4aee38b856e 100644
--- a/lisp/files-x.el
+++ b/lisp/files-x.el
@@ -201,7 +201,8 @@ the user how to make the new value take effect."
 				       (match-beginning 0)))
 	     (suffix (buffer-substring (point) (line-end-position)))
 	     (prefix-re (concat "^" (regexp-quote prefix)))
-	     (suffix-re (concat (regexp-quote suffix) "$")))
+	     (suffix-re (concat (regexp-quote suffix) "$"))
+	     (deleted nil))
 
 	;; Find or add missing "End:".
 	(forward-line 1)
@@ -224,7 +225,17 @@ the user how to make the new value take effect."
 	    (while (re-search-forward
 		    (format "%s%S:.*%s" prefix-re variable suffix-re) end t)
 	      (delete-region (match-beginning 0) (1+ (match-end 0)))
-	      (setq replaced-pos (point)))))
+	      (setq replaced-pos (point)
+		    deleted t))
+	    ;; Delete empty local variables block.
+	    (when (and (eq op 'delete) deleted (= beg end))
+	      (save-excursion
+		(delete-region (progn (goto-char beg)
+				      (forward-line -1)
+				      (point))
+			       (progn (goto-char end)
+				      (forward-line 1)
+				      (point)))))))
 
 	;; Add a new variable/value pair.  Add `mode' to the start, add new
 	;; variable to the end, and add a replaced variable to its last location.
@@ -295,7 +306,7 @@ from the -*- line ignoring the input argument VALUE.
 If optional variable INTERACTIVE is non-nil, display a message telling
 the user how to make the new value take effect."
   (catch 'exit
-    (let ((beg (point)) end replaced-pos)
+    (let ((beg (point)) end replaced-pos deleted)
       (unless enable-local-variables
 	(throw 'exit (message "File-local variables are disabled")))
 
@@ -357,6 +368,7 @@ the user how to make the new value take effect."
 	    ;; Replace or delete MODENAME
 	    (progn
 	      (when (member op '(add-or-replace delete))
+		(setq deleted t)
 		(delete-region (match-beginning 1) (match-end 1)))
 	      (when (eq op 'add-or-replace)
 		(goto-char (match-beginning 1))
@@ -387,7 +399,8 @@ the user how to make the new value take effect."
 		(skip-chars-forward " \t;")
 		(when (eq key variable)
 		  (delete-region (match-beginning 0) (point))
-		  (setq replaced-pos (point)))))))
+		  (setq replaced-pos (point)
+                        deleted t))))))
 	;; Add a new variable/value pair.  Add `mode' to the start, add new
 	;; variable to the end, and add a replaced variable to its last location.
 	(when (eq op 'add-or-replace)
@@ -406,6 +419,28 @@ the user how to make the new value take effect."
 	  (insert (format "%S: %S;" variable value))
 	  (unless (eq (char-after) ?\s) (insert " ")))))
 
+      ;; Delete empty prop-line.
+      (when (and (eq op 'delete) deleted (= beg end))
+	(save-excursion
+          ;; First delete -*- -*-
+	  (delete-region
+	   (progn
+	     (goto-char beg)
+	     (search-backward "-*-" (line-beginning-position))
+	     (point))
+	   (progn
+	     (goto-char end)
+	     (search-forward "-*-" (line-end-position))
+	     (point)))
+          ;; And then delete line if comment is empty.
+	  (goto-char (line-beginning-position))
+	  (when (looking-at-p
+		 (concat
+                  "^[ \t]*\\(?:" (regexp-quote (or comment-start ";"))
+		  "\\)+[ \t]*" (regexp-quote (or comment-end ""))
+                  "[ \t]*$"))
+	    (delete-region (point) (progn (forward-line 1) (point))))))
+
       (when interactive
 	(modify-file-local-variable-message variable value op)))))
 
diff --git a/test/lisp/files-x-tests.el b/test/lisp/files-x-tests.el
index 31633df073e..e96b61a3944 100644
--- a/test/lisp/files-x-tests.el
+++ b/test/lisp/files-x-tests.el
@@ -638,5 +638,194 @@ or `executable-find', their default value must be used nonetheless."
        `(connection-local-profile-alist ',clpa now)
        `(connection-local-criteria-alist ',clca now)))))
 
+(ert-deftest files-x-test-add-file-local-variable ()
+  "Test adding and replacing file local variables."
+  ;; Simple adding
+  (with-temp-buffer
+    (add-file-local-variable 'foo 1)
+    (add-file-local-variable 'bar 2)
+    (should (equal (buffer-string)
+                   "\n;; Local Variables:\n;; foo: 1\n;; bar: 2\n;; End:\n")))
+  ;; Adding in c-mode
+  (with-temp-buffer
+    (c-mode)
+    (setq-local comment-start "/* " comment-end " */")
+    (add-file-local-variable 'foo 1)
+    (add-file-local-variable 'bar 2)
+    (should (equal (buffer-string)
+                   (concat "\n/* Local Variables: */\n"
+                           "/* foo: 1 */\n"
+                           "/* bar: 2 */\n"
+                           "/* End: */\n"))))
+  ;; Replacing
+  (with-temp-buffer
+    (add-file-local-variable 'foo 1)
+    (add-file-local-variable 'bar 2)
+    (add-file-local-variable 'foo t)
+    (add-file-local-variable 'bar nil)
+    (should (equal (buffer-string)
+                   "\n;; Local Variables:\n;; foo: t\n;; bar: nil\n;; End:\n")))
+  ;; Replacing with prefix/suffix
+  (with-temp-buffer
+    (insert ";; PRE Local Variables: SUF\n"
+            ";; PRE foo: t SUF\n"
+            ";; PRE bar: nil SUF\n"
+            ";; PRE End: SUF\n")
+    (add-file-local-variable 'foo t)
+    (add-file-local-variable 'bar nil)
+    (should (equal (buffer-string)
+                   (concat ";; PRE Local Variables: SUF\n"
+                           ";; PRE foo: t SUF\n"
+                           ";; PRE bar: nil SUF\n"
+                           ";; PRE End: SUF\n")))))
+
+(ert-deftest files-x-test-delete-file-local-variable ()
+  "Test deleting file local variables."
+  ;; Simple deleting
+  (with-temp-buffer
+    (setq-local comment-start ";;" comment-end "")
+    (insert "BEFORE\n"
+            ";; Local Variables:\n"
+            ";; foo: t\n"
+            ";; bar: nil\n"
+            ";; End:\n"
+            "AFTER")
+    (delete-file-local-variable 'foo)
+    (should (equal (buffer-string)
+                   (concat "BEFORE\n"
+                           ";; Local Variables:\n"
+                           ";; bar: nil\n"
+                           ";; End:\n"
+                           "AFTER")))
+    (delete-file-local-variable 'bar)
+    (should (equal (buffer-string) "BEFORE\nAFTER")))
+  ;; Deleting in c-mode
+  (with-temp-buffer
+    (c-mode)
+    (setq-local comment-start "/* " comment-end " */")
+    (insert "BEFORE\n"
+            "/* Local Variables: */\n"
+            "/* foo: t */\n"
+            "/* bar: nil */\n"
+            "/* End: */\n"
+            "AFTER")
+    (delete-file-local-variable 'foo)
+    (should (equal (buffer-string)
+                   (concat "BEFORE\n"
+                           "/* Local Variables: */\n"
+                           "/* bar: nil */\n"
+                           "/* End: */\n"
+                           "AFTER")))
+    (delete-file-local-variable 'bar)
+    (should (equal (buffer-string) "BEFORE\nAFTER")))
+  ;; Ensure that other blocks are untouched
+  (with-temp-buffer
+    (setq-local comment-start ";;" comment-end "")
+    (insert ";; Local Variables:\n"
+            ";; foo: t\n"
+            ";; End:\n"
+            ";; Local Variables:\n"
+            ";; PRESERVE\n"
+            ";; End:\n")
+    (delete-file-local-variable 'foo)
+    (should (equal (buffer-string)
+                   ";; Local Variables:\n;; PRESERVE\n;; End:\n")))
+  ;; Deleting with prefix/suffix
+  (with-temp-buffer
+    (setq-local comment-start ";;" comment-end "")
+    (insert "BEFORE\n"
+            ";; PRE Local Variables: SUF\n"
+            ";; PRE foo: t SUF\n"
+            ";; PRE bar: nil SUF\n"
+            ";; PRE End: SUF\n"
+            "AFTER")
+    (delete-file-local-variable 'foo)
+    (should (equal (buffer-string)
+                   (concat "BEFORE\n"
+                           ";; PRE Local Variables: SUF\n"
+                           ";; PRE bar: nil SUF\n"
+                           ";; PRE End: SUF\n"
+                           "AFTER")))
+    (delete-file-local-variable 'bar)
+    (should (equal (buffer-string) "BEFORE\nAFTER"))))
+
+(ert-deftest files-x-test-add-file-local-variable-prop-line ()
+  "Test adding and replacing file local variables in the prop-line."
+  ;; Simple adding
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (add-file-local-variable-prop-line 'foo 1)
+    (add-file-local-variable-prop-line 'bar 2)
+    (should (equal (buffer-string) ";; -*- foo: 1; bar: 2; -*-\n")))
+  ;; Adding in c-mode
+  (with-temp-buffer
+    (c-mode)
+    (setq-local comment-start "/* " comment-end " */")
+    (add-file-local-variable-prop-line 'foo 1)
+    (add-file-local-variable-prop-line 'bar 2)
+    (should (equal (buffer-string) "/* -*- foo: 1; bar: 2; -*- */\n")))
+  ;; Adding to mode variable
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert ";; -*- org-mode -*-\n")
+    (add-file-local-variable-prop-line 'foo 1)
+    (should (equal (buffer-string) ";; -*-  mode: org-mode; foo: 1;  -*-\n")))
+  ;; Simple replacing
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (add-file-local-variable-prop-line 'foo 1)
+    (add-file-local-variable-prop-line 'bar 2)
+    (add-file-local-variable-prop-line 'foo t)
+    (add-file-local-variable-prop-line 'bar nil)
+    (should (equal (buffer-string) ";; -*- foo: t; bar: nil; -*-\n")))
+  ;; Replacing with prefix/suffix
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert ";; PRE -*- foo: 1; bar: 2; -*- SUF\n")
+    (add-file-local-variable-prop-line 'foo t)
+    (add-file-local-variable-prop-line 'bar nil)
+    (should (equal (buffer-string)
+                   ";; PRE -*- foo: t; bar: nil; -*- SUF\n"))))
+
+(ert-deftest files-x-test-delete-file-local-variable-prop-line ()
+  "Test deleting file local variables in prop-line."
+  ;; Simple deleting
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert ";; -*- foo: t; bar: nil -*-\n")
+    (delete-file-local-variable-prop-line 'foo)
+    (should (equal (buffer-string) ";; -*- bar: nil -*-\n"))
+    (delete-file-local-variable-prop-line 'bar)
+    (should (equal (buffer-string) "")))
+  ;; Deleting mode variable
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert ";; -*- org-mode -*-\n")
+    (delete-file-local-variable-prop-line 'mode)
+    (should (equal (buffer-string) "")))
+  ;; Deleting in c-mode
+  (with-temp-buffer
+    (c-mode)
+    (setq-local comment-start "/* " comment-end " */")
+    (insert "/* -*- foo: t; bar: nil -*- */\n")
+    (delete-file-local-variable-prop-line 'foo)
+    (should (equal (buffer-string) "/* -*- bar: nil -*- */\n"))
+    (delete-file-local-variable-prop-line 'bar)
+    (should (equal (buffer-string) "")))
+  ;; Ensure that other prop lines are untouched
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert ";; -*- foo: t -*-\n;; -*- -*-")
+    (delete-file-local-variable-prop-line 'foo)
+    (should (equal (buffer-string) ";; -*- -*-")))
+  ;; Deleting with prefix/suffix
+  (with-temp-buffer
+    (emacs-lisp-mode)
+    (insert ";; PRE -*- foo: t; bar: nil -*- SUF\n")
+    (delete-file-local-variable-prop-line 'foo)
+    (should (equal (buffer-string) ";; PRE -*- bar: nil -*- SUF\n"))
+    (delete-file-local-variable-prop-line 'bar)
+    (should (equal (buffer-string) ";; PRE  SUF\n"))))
+
 (provide 'files-x-tests)
 ;;; files-x-tests.el ends here