Ihor Radchenko <[email protected]> writes:

>
> This sounds reasonable, but I am concerned about the case when no edit
> is done by the body passed to the org-preserve-local-variables macro,
> but we may still move the local variables around. That might be surprising.
>
> Also, there is
> (unless modified
>               (restore-buffer-modified-p nil))
>
> but with the new patch, we may edit the buffer even when MODIFIED is
> nil. This may lead to problems.

I made a change to the first patch so now the buffer position of the
local-variables is saved and used to put them back when the buffer is
unmodified.

I've also tacked on a new patch to make the macro hygienic.

Other then that everything is the same.

Tests pass after each patch on emacs 30.2.
Tests pass after final patch on emacs 28 and 29.
Tests pass after final patch with TZ set to UTC, Europe/Istanbul, and
America/New_York.

>From 7ac71f3e026d412d4597e5aa13f4c41c04d52df8 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:03:27 -0500
Subject: [PATCH 1/5] Preserve "Local Variables" block without preserving
 following text

* lisp/org-macs.el (org-preserve-local-variables): Extract "Local
Variables" block exactly to prevent preserving text that follows the
block.
---
 lisp/org-macs.el | 35 +++++++++++++++++++++++------------
 1 file changed, 23 insertions(+), 12 deletions(-)

diff --git a/lisp/org-macs.el b/lisp/org-macs.el
index 80ce42250..dbcabde78 100644
--- a/lisp/org-macs.el
+++ b/lisp/org-macs.el
@@ -247,23 +247,34 @@ org-preserve-local-variables
   "Execute BODY while preserving local variables."
   (declare (debug (body)))
   `(let ((local-variables
-	  (org-with-wide-buffer
-	   (goto-char (point-max))
-	   (let ((case-fold-search t))
-	     (and (re-search-backward "^[ \t]*# +Local Variables:"
-				      (max (- (point) 3000) 1)
-				      t)
-               (let ((buffer-undo-list t))
-	         (delete-and-extract-region (point) (point-max)))))))
+          (org-with-wide-buffer
+           (goto-char (point-max))
+           (let ((case-fold-search t))
+             (and (re-search-backward
+                   ,(rx-let ((prefix
+                              (seq line-start (zero-or-more whitespace)
+                                   "#" (one-or-more whitespace))))
+                      (rx prefix "Local Variables:"
+                          (one-or-more anychar)
+                          prefix "End:"
+                          (zero-or-more whitespace) (optional "\n")))
+                   (max (- (point) 3000) 1)
+                   t)
+                  (cons (match-beginning 0)
+                        (let ((buffer-undo-list t))
+                          (delete-and-extract-region (match-beginning 0)
+                                                     (match-end 0))))))))
          (tick-counter-before (buffer-modified-tick)))
      (unwind-protect (progn ,@body)
        (when local-variables
-	 (org-with-wide-buffer
-	  (goto-char (point-max))
-	  (unless (bolp) (insert "\n"))
+         (org-with-wide-buffer
           (let ((modified (< tick-counter-before (buffer-modified-tick)))
                 (buffer-undo-list t))
-	    (insert local-variables)
+            (if (not modified)
+                (goto-char (car local-variables))
+              (goto-char (point-max))
+              (unless (bolp) (insert "\n")))
+	    (insert (cdr local-variables))
             (unless modified
               (restore-buffer-modified-p nil))))))))
 

base-commit: 1328e518db2e5876c8768fb4c74769d5b518984a
-- 
2.52.0

>From d8655237da0d46e668e23e306b1c38d8776a89b7 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:05:35 -0500
Subject: [PATCH 2/5] Testing: Test moving a subtree with a "Local Variables"
 block

* testing/lisp/test-org.el (test-org/move-subtree): Add tests that
include a "Local Variables" block.
---
 testing/lisp/test-org.el | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 530584e8d..6ab7ed2b9 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -5914,6 +5914,22 @@ test-org/move-subtree
    (org-test-with-temp-text "* H1\n** H1.2<point>\n"
      (org-metaup)
      (buffer-string)))
+  ;; Local variables
+  (let ((local-variable-string "# Local Variables:
+# fill-column: 120
+# End:\n"))
+    (should
+     (equal (concat "* H2\n* H1\n" local-variable-string)
+            (org-test-with-temp-text
+                (concat "* H1<point>\n* H2\n" local-variable-string)
+              (org-metadown)
+              (buffer-string))))
+    (should
+     (equal (concat "* H2\n* H1\n* H3\n" local-variable-string)
+            (org-test-with-temp-text
+                (concat "* H1<point>\n* H2\n" local-variable-string "* H3\n")
+              (org-metadown)
+              (buffer-string)))))
   ;; With selection
   (should
    (equal "* T\n** H3\n** H1\n** H2\n"
-- 
2.52.0

>From ee0324bb0f918c87b8e898bd636398b1cea8b8a7 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:06:20 -0500
Subject: [PATCH 3/5] Preserve "Local Variables" block when inserting a heading

*lisp/org.el (org-insert-heading): When we are respecting content,
preserve the "Local Variables" block by using
`org-preserve-local-variables'.
---
 lisp/org.el | 77 +++++++++++++++++++++++++++--------------------------
 1 file changed, 39 insertions(+), 38 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index 7b455a18a..9386ec4b0 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -6637,44 +6637,45 @@ org-insert-heading
 	       (invisible-p (max (1- (point)) (point-min)))))
       ;; Position point at the location of insertion.  Make sure we
       ;; end up on a visible headline if INVISIBLE-OK is nil.
-      (org-with-limited-levels
-       (if (not current-level) (outline-next-heading) ;before first headline
-	 (org-back-to-heading invisible-ok)
-	 (when (equal arg '(16)) (org-up-heading-safe))
-	 (org-end-of-subtree invisible-ok 'to-heading)))
-      ;; At `point-max', if the file does not have ending newline,
-      ;; create one, so that we are not appending stars at non-empty
-      ;; line.
-      (unless (bolp) (insert "\n"))
-      (when (and blank? (save-excursion
-                          (backward-char)
-                          (org-before-first-heading-p)))
-        (insert "\n")
-        (backward-char))
-      (when (and (not current-level) (not (eobp)) (not (bobp)))
-        (when (org-at-heading-p) (insert "\n"))
-        (backward-char))
-      (unless (and blank? (org-previous-line-empty-p))
-	(org-N-empty-lines-before-current (if blank? 1 0)))
-      (insert stars " " "\n")
-      ;; Move point after stars.
-      (backward-char)
-      ;; Retain blank lines before next heading.
-      (funcall maybe-add-blank-after blank?)
-      ;; When INVISIBLE-OK is non-nil, ensure newly created headline
-      ;; is visible.
-      (unless invisible-ok
-        (if (eq org-fold-core-style 'text-properties)
-	    (cond
-	     ((org-fold-folded-p
-               (max (point-min)
-                    (1- (line-beginning-position))))
-	      (org-fold-region (line-end-position 0) (line-end-position) nil))
-	     (t nil))
-          (pcase (get-char-property-and-overlay (point) 'invisible)
-	    (`(outline . ,o)
-	     (move-overlay o (overlay-start o) (line-end-position 0)))
-	    (_ nil)))))
+      (org-preserve-local-variables
+       (org-with-limited-levels
+        (if (not current-level) (outline-next-heading) ;before first headline
+          (org-back-to-heading invisible-ok)
+          (when (equal arg '(16)) (org-up-heading-safe))
+          (org-end-of-subtree invisible-ok 'to-heading)))
+       ;; At `point-max', if the file does not have ending newline,
+       ;; create one, so that we are not appending stars at non-empty
+       ;; line.
+       (unless (bolp) (insert "\n"))
+       (when (and blank? (save-excursion
+                           (backward-char)
+                           (org-before-first-heading-p)))
+         (insert "\n")
+         (backward-char))
+       (when (and (not current-level) (not (eobp)) (not (bobp)))
+         (when (org-at-heading-p) (insert "\n"))
+         (backward-char))
+       (unless (and blank? (org-previous-line-empty-p))
+         (org-N-empty-lines-before-current (if blank? 1 0)))
+       (insert stars " " "\n")
+       ;; Move point after stars.
+       (backward-char)
+       ;; Retain blank lines before next heading.
+       (funcall maybe-add-blank-after blank?)
+       ;; When INVISIBLE-OK is non-nil, ensure newly created headline
+       ;; is visible.
+       (unless invisible-ok
+         (if (eq org-fold-core-style 'text-properties)
+             (cond
+              ((org-fold-folded-p
+                (max (point-min)
+                     (1- (line-beginning-position))))
+               (org-fold-region (line-end-position 0) (line-end-position) nil))
+              (t nil))
+           (pcase (get-char-property-and-overlay (point) 'invisible)
+             (`(outline . ,o)
+              (move-overlay o (overlay-start o) (line-end-position 0)))
+             (_ nil))))))
      ;; At a headline...
      ((org-at-heading-p)
       (cond ((bolp)
-- 
2.52.0

>From dd551c23ea81e7ae54f8f90c79a02ede87e3cd91 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:06:44 -0500
Subject: [PATCH 4/5] Testing: Test inserting a heading with a "Local
 Variables" block

* testing/lisp/test-org.el (test-org/insert-heading): Test inserting a
heading with a "Local Variables" block.
---
 testing/lisp/test-org.el | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 6ab7ed2b9..4f3dd433e 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -1889,6 +1889,20 @@ test-org/insert-heading
 	  (org-test-with-temp-text "<point>P"
 	    (org-insert-heading)
 	    (buffer-string))))
+  ;; Move local variable string to end when we respect the content
+  (let ((local-variable-string "# Local Variables:
+# fill-column: 120
+# End:\n"))
+    (should
+     (equal (concat "* \n" local-variable-string)
+            (org-test-with-temp-text local-variable-string
+              (org-insert-heading-respect-content)
+              (buffer-string))))
+    (should
+     (equal (concat "* H\n* \n" local-variable-string)
+            (org-test-with-temp-text (concat "* H<point>\n" local-variable-string)
+              (org-insert-heading-respect-content)
+              (buffer-string)))))
   ;; In the middle of a line, split the line if allowed, otherwise,
   ;; insert the headline at its end.
   (should
-- 
2.52.0

>From 72fc0cc275184f1b460fbb25bb32daa11841fb62 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 8 Jan 2026 21:22:44 -0500
Subject: [PATCH 5/5] org-preserve-local-variables: Make macro hygenic

* lisp/org-macs.el (org-preserve-local-variables): Use
`org-with-gensyms' to avoid defining local variables.
---
 lisp/org-macs.el | 63 ++++++++++++++++++++++++------------------------
 1 file changed, 32 insertions(+), 31 deletions(-)

diff --git a/lisp/org-macs.el b/lisp/org-macs.el
index dbcabde78..5afccad07 100644
--- a/lisp/org-macs.el
+++ b/lisp/org-macs.el
@@ -246,37 +246,38 @@ org-load-noerror-mustsuffix
 (defmacro org-preserve-local-variables (&rest body)
   "Execute BODY while preserving local variables."
   (declare (debug (body)))
-  `(let ((local-variables
-          (org-with-wide-buffer
-           (goto-char (point-max))
-           (let ((case-fold-search t))
-             (and (re-search-backward
-                   ,(rx-let ((prefix
-                              (seq line-start (zero-or-more whitespace)
-                                   "#" (one-or-more whitespace))))
-                      (rx prefix "Local Variables:"
-                          (one-or-more anychar)
-                          prefix "End:"
-                          (zero-or-more whitespace) (optional "\n")))
-                   (max (- (point) 3000) 1)
-                   t)
-                  (cons (match-beginning 0)
-                        (let ((buffer-undo-list t))
-                          (delete-and-extract-region (match-beginning 0)
-                                                     (match-end 0))))))))
-         (tick-counter-before (buffer-modified-tick)))
-     (unwind-protect (progn ,@body)
-       (when local-variables
-         (org-with-wide-buffer
-          (let ((modified (< tick-counter-before (buffer-modified-tick)))
-                (buffer-undo-list t))
-            (if (not modified)
-                (goto-char (car local-variables))
-              (goto-char (point-max))
-              (unless (bolp) (insert "\n")))
-	    (insert (cdr local-variables))
-            (unless modified
-              (restore-buffer-modified-p nil))))))))
+  (org-with-gensyms (local-variables tick-counter-before)
+    `(let ((,local-variables
+            (org-with-wide-buffer
+             (goto-char (point-max))
+             (let ((case-fold-search t))
+               (and (re-search-backward
+                     ,(rx-let ((prefix
+                                (seq line-start (zero-or-more whitespace)
+                                     "#" (one-or-more whitespace))))
+                        (rx prefix "Local Variables:"
+                            (one-or-more anychar)
+                            prefix "End:"
+                            (zero-or-more whitespace) (optional "\n")))
+                     (max (- (point) 3000) 1)
+                     t)
+                    (cons (match-beginning 0)
+                          (let ((buffer-undo-list t))
+                            (delete-and-extract-region (match-beginning 0)
+                                                       (match-end 0))))))))
+           (,tick-counter-before (buffer-modified-tick)))
+       (unwind-protect (progn ,@body)
+         (when ,local-variables
+           (org-with-wide-buffer
+            (let ((modified (< ,tick-counter-before (buffer-modified-tick)))
+                  (buffer-undo-list t))
+              (if (not modified)
+                  (goto-char (car ,local-variables))
+                (goto-char (point-max))
+                (unless (bolp) (insert "\n")))
+              (insert (cdr ,local-variables))
+              (unless modified
+                (restore-buffer-modified-p nil)))))))))
 
 ;;;###autoload
 (defmacro org-element-with-disabled-cache (&rest body)
-- 
2.52.0

Reply via email to