Thank you for the bug report!  It is clear, concise, and easy to
replicate.

I have come up with a fix attached as 4 patches.

Tests pass after each commit on emacs 30.
Tests pass on emacs 28, and 29 after final commit.

Emacs parses the local variables in `hack-local-variables--find-variables'.
There it ignores the local variables if they are not terminated with "End:", so
I think we too can ignore them if they aren't properly terminated.

I've implemented a change where we pull out the "Local Variable" block with
more precision using the "End:".

At first I wanted to only save the "Local Variable" block if it was after the
last headline but I realized that it would still be used by emacs if it was
within the last 3000 characters of the file so I scratched that idea.

I've also included a patch to ensure that inserting a new headline with
C-<return> inserts it before the local variables.

>From 47aff54ee31db54ed58446f1f9786b8c9dd2c403 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:03:27 -0500
Subject: [PATCH 1/4] 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 | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/lisp/org-macs.el b/lisp/org-macs.el
index c3be41d02..8b75e80ef 100644
--- a/lisp/org-macs.el
+++ b/lisp/org-macs.el
@@ -250,11 +250,18 @@ org-preserve-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)))))))
+             (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)
+                  (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

base-commit: 72db4de02d0080d78a5b4dda137ffdb1b1e7140f
-- 
2.51.2

>From 3bbe2c065ee4a50ecd200d9465ff03a4ef6c7db8 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:05:35 -0500
Subject: [PATCH 2/4] 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 e7769ba6c..09b9a09c4 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -5905,6 +5905,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.51.2

>From 951e37575e6ddbbab3f55801014be1d2c51e1843 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:06:20 -0500
Subject: [PATCH 3/4] 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 d0b349487..1a43a8cce 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -6616,44 +6616,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.51.2

>From 00e3309a6e26afcd4f44f36d0efbac439ce19f47 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 4 Dec 2025 12:06:44 -0500
Subject: [PATCH 4/4] 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 09b9a09c4..0c90bcef7 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -1880,6 +1880,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.51.2

Reply via email to