bug#81516: 31.0.91; delete-file-local-variable(-prop-line) should delete "Local Variables" block (prop-line)

Daniel Mendler via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]> Thu, 30 Jul 2026 16:18:54 +0200
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
Eli Zaretskii <[email protected]> writes:

>> From: Daniel Mendler <[email protected]>
>> Cc: [email protected],  [email protected],  [email protected]
>> Date: Thu, 30 Jul 2026 09:31:29 +0200
>> 
>> Eli Zaretskii <[email protected]> writes:
>> 
>> > Thanks.  Could we please have tests for this, including in cases where
>> > the prop line doesn't exist, or looks similar, but isn't a prop line?
>> 
>> I have not found tests for file local variables. Are there any? I can
>> write a bunch of basic tests but this will take a little more time.
>
> In this case, just testing the situation where all the variables are
> deleted would be enough.  It would be good to have additional tests
> for the file-local variables in general, but that doesn't have to be
> part of this patch.

I attached a patch which includes tests for adding and deleting file
local variables, also for the situation where all variables are deleted.

Daniel
0001-modify-file-local-variable-prop-line-Delete-em.patch (text/x-diff, 11.7 KB)
From 15f585671c981b25c4e7c716e9e8d897a4f6fa56 Mon Sep 17 00:00:00 2001
From: Daniel Mendler <[email protected]>
Date: Thu, 30 Jul 2026 07:53:17 +0200
Subject: [PATCH] modify-file-local-variable(-prop-line): Delete empty
 variable block

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 @@ modify-file-local-variable
 				       (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 @@ modify-file-local-variable
 	    (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 @@ modify-file-local-variable-prop-line
 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 @@ modify-file-local-variable-prop-line
 	    ;; 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 @@ modify-file-local-variable-prop-line
 		(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 @@ modify-file-local-variable-prop-line
 	  (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 @@ files-x-test-connection-local-special-variables
        `(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
-- 
2.47.3