Browse Source

Preserve file local variables during some operations

* lisp/org-macs.el (org-preserve-local-variables): New macro.
* lisp/org-footnote.el (org-footnote--clear-footnote-section):
(org-footnote--goto-local-insertion-point):
(org-footnote-create-definition):
(org-footnote-delete):
(org-footnote-renumber-fn:N):
(org-footnote-sort):
(org-footnote-normalize):
* lisp/org.el (org-move-subtree-down):
(org-copy-subtree):
(org-sort-entries):
(org-refile): Use new macro.
* testing/lisp/test-org-footnote.el (test-org-footnote/normalize):
(test-org-footnote/delete):
(test-org-footnote/sort):
(test-org-footnote/normalize):
* testing/lisp/test-org.el (test-org/sort-entries): Add tests.

Operations affected include copying, killing, refiling, archiving and
moving subtrees.  It also affects sorting, creating and deleting
footnotes.
Nicolas Goaziou 1 year ago
parent
commit
fdb2eb6701
5 changed files with 491 additions and 391 deletions
  1. 184 199
      lisp/org-footnote.el
  2. 18 0
      lisp/org-macs.el
  3. 194 188
      lisp/org.el
  4. 88 4
      testing/lisp/test-org-footnote.el
  5. 7 0
      testing/lisp/test-org.el

+ 184 - 199
lisp/org-footnote.el

@@ -313,23 +313,23 @@ otherwise."
 
 (defun org-footnote--clear-footnote-section ()
   "Remove all footnote sections in buffer and create a new one.
-New section is created at the end of the buffer, before any file
-local variable definition.  Leave point within the new section."
+New section is created at the end of the buffer.  Leave point
+within the new section."
   (when org-footnote-section
     (goto-char (point-min))
-    (let ((regexp
-	   (format "^\\*+ +%s[ \t]*$"
-		   (regexp-quote org-footnote-section))))
+    (let ((regexp (format "^\\*+ +%s[ \t]*$"
+			  (regexp-quote org-footnote-section))))
       (while (re-search-forward regexp nil t)
 	(delete-region
 	 (match-beginning 0)
-	 (progn (org-end-of-subtree t t)
-		(if (not (eobp)) (point)
-		  (org-footnote--goto-local-insertion-point)
-		  (skip-chars-forward " \t\n")
-		  (if (eobp) (point) (line-beginning-position)))))))
+	 (org-end-of-subtree t t))))
     (goto-char (point-max))
-    (org-footnote--goto-local-insertion-point)
+    ;; Clean-up blank lines at the end of the buffer.
+    (skip-chars-backward " \r\t\n")
+    (unless (bobp)
+      (forward-line)
+      (when (eolp) (insert "\n")))
+    (delete-region (point) (point-max))
     (when (and (cdr (assq 'heading org-blank-before-new-entry))
 	       (zerop (save-excursion (org-back-over-empty-lines))))
       (insert "\n"))
@@ -448,14 +448,8 @@ while collecting them."
   "Find insertion point for footnote, just before next outline heading.
 Assume insertion point is within currently accessible part of the buffer."
   (org-with-limited-levels (outline-next-heading))
-  ;; Skip file local variables.  See `modify-file-local-variable'.
-  (when (eobp)
-    (let ((case-fold-search t))
-      (re-search-backward "^[ \t]*# +Local Variables:"
-			  (max (- (point-max) 3000) (point-min))
-			  t)))
   (skip-chars-backward " \t\n")
-  (forward-line)
+  (unless (bobp) (forward-line))
   (unless (bolp) (insert "\n")))
 
 
@@ -676,21 +670,22 @@ Return buffer position at the beginning of the definition.  This
 function doesn't move point."
   (let ((label (org-footnote-normalize-label label))
 	electric-indent-mode)		; Prevent wrong indentation.
-    (org-with-wide-buffer
-     (cond
-      ((not org-footnote-section) (org-footnote--goto-local-insertion-point))
-      ((save-excursion
-	 (goto-char (point-min))
-	 (re-search-forward
-	  (concat "^\\*+[ \t]+" (regexp-quote org-footnote-section) "[ \t]*$")
-	  nil t))
-       (goto-char (match-end 0))
-       (forward-line)
-       (unless (bolp) (insert "\n")))
-      (t (org-footnote--clear-footnote-section)))
-     (when (zerop (org-back-over-empty-lines)) (insert "\n"))
-     (insert "[fn:" label "] \n")
-     (line-beginning-position 0))))
+    (org-preserve-local-variables
+     (org-with-wide-buffer
+      (cond
+       ((not org-footnote-section) (org-footnote--goto-local-insertion-point))
+       ((save-excursion
+	  (goto-char (point-min))
+	  (re-search-forward
+	   (concat "^\\*+[ \t]+" (regexp-quote org-footnote-section) "[ \t]*$")
+	   nil t))
+	(goto-char (match-end 0))
+	(forward-line)
+	(unless (bolp) (insert "\n")))
+       (t (org-footnote--clear-footnote-section)))
+      (when (zerop (org-back-over-empty-lines)) (insert "\n"))
+      (insert "[fn:" label "] \n")
+      (line-beginning-position 0)))))
 
 (defun org-footnote-delete-references (label)
   "Delete every reference to footnote LABEL.
@@ -733,31 +728,32 @@ and all references of a footnote label.
 
 If LABEL is non-nil, delete that footnote instead."
   (catch 'done
-    (let* ((nref 0) (ndef 0) x
-	   ;; 1. Determine LABEL of footnote at point.
-	   (label (cond
-		   ;; LABEL is provided as argument.
-		   (label)
-		   ;; Footnote reference at point.  If the footnote is
-		   ;; anonymous, delete it and exit instead.
-		   ((setq x (org-footnote-at-reference-p))
-		    (or (car x)
-			(progn
-			  (delete-region (nth 1 x) (nth 2 x))
-			  (message "Anonymous footnote removed")
-			  (throw 'done t))))
-		   ;; Footnote definition at point.
-		   ((setq x (org-footnote-at-definition-p))
-		    (car x))
-		   (t (error "Don't know which footnote to remove")))))
-      ;; 2. Now that LABEL is non-nil, find every reference and every
-      ;; definition, and delete them.
-      (setq nref (org-footnote-delete-references label)
-	    ndef (org-footnote-delete-definitions label))
-      ;; 3. Verify consistency of footnotes and notify user.
-      (org-footnote-auto-adjust-maybe)
-      (message "%d definition(s) of and %d reference(s) of footnote %s removed"
-	       ndef nref label))))
+    (org-preserve-local-variables
+     (let* ((nref 0) (ndef 0) x
+	    ;; 1. Determine LABEL of footnote at point.
+	    (label (cond
+		    ;; LABEL is provided as argument.
+		    (label)
+		    ;; Footnote reference at point.  If the footnote is
+		    ;; anonymous, delete it and exit instead.
+		    ((setq x (org-footnote-at-reference-p))
+		     (or (car x)
+			 (progn
+			   (delete-region (nth 1 x) (nth 2 x))
+			   (message "Anonymous footnote removed")
+			   (throw 'done t))))
+		    ;; Footnote definition at point.
+		    ((setq x (org-footnote-at-definition-p))
+		     (car x))
+		    (t (error "Don't know which footnote to remove")))))
+       ;; 2. Now that LABEL is non-nil, find every reference and every
+       ;; definition, and delete them.
+       (setq nref (org-footnote-delete-references label)
+	     ndef (org-footnote-delete-definitions label))
+       ;; 3. Verify consistency of footnotes and notify user.
+       (org-footnote-auto-adjust-maybe)
+       (message "%d definition(s) of and %d reference(s) of footnote %s removed"
+		ndef nref label)))))
 
 
 ;;;; Sorting, Renumbering, Normalizing
@@ -765,28 +761,25 @@ If LABEL is non-nil, delete that footnote instead."
 (defun org-footnote-renumber-fn:N ()
   "Order numbered footnotes into a sequence in the document."
   (interactive)
-  (let ((references (org-footnote--collect-references)))
-    (unwind-protect
-	(let* ((c 0)
-	       (references (cl-remove-if-not
-			    (lambda (r) (string-match-p "\\`[0-9]+\\'" (car r)))
-			    references))
-	       (alist (mapcar (lambda (l) (cons l (number-to-string (cl-incf c))))
-			      (delete-dups (mapcar #'car references)))))
-	  (org-with-wide-buffer
-	   ;; Re-number references.
-	   (dolist (ref references)
-	     (goto-char (nth 1 ref))
-	     (org-footnote--set-label (cdr (assoc (nth 0 ref) alist))))
-	   ;; Re-number definitions.
-	   (goto-char (point-min))
-	   (while (re-search-forward "^\\[fn:\\([0-9]+\\)\\]" nil t)
-	     (replace-match (or (cdr (assoc (match-string 1) alist))
-				;; Un-referenced definitions get
-				;; higher numbers.
-				(number-to-string (cl-incf c)))
-			    nil nil nil 1))))
-      (dolist (r references) (set-marker (nth 1 r) nil)))))
+  (let* ((c 0)
+	 (references (cl-remove-if-not
+		      (lambda (r) (string-match-p "\\`[0-9]+\\'" (car r)))
+		      (org-footnote--collect-references)))
+	 (alist (mapcar (lambda (l) (cons l (number-to-string (cl-incf c))))
+			(delete-dups (mapcar #'car references)))))
+    (org-with-wide-buffer
+     ;; Re-number references.
+     (dolist (ref references)
+       (goto-char (nth 1 ref))
+       (org-footnote--set-label (cdr (assoc (nth 0 ref) alist))))
+     ;; Re-number definitions.
+     (goto-char (point-min))
+     (while (re-search-forward "^\\[fn:\\([0-9]+\\)\\]" nil t)
+       (replace-match (or (cdr (assoc (match-string 1) alist))
+			  ;; Un-referenced definitions get higher
+			  ;; numbers.
+			  (number-to-string (cl-incf c)))
+		      nil nil nil 1)))))
 
 (defun org-footnote-sort ()
   "Rearrange footnote definitions in the current buffer.
@@ -795,129 +788,121 @@ references.  Also relocate definitions at the end of their
 relative section or within a single footnote section, according
 to `org-footnote-section'.  Inline definitions are ignored."
   (let ((references (org-footnote--collect-references)))
-    (unwind-protect
-	(let ((definitions (org-footnote--collect-definitions 'delete)))
-	  (org-with-wide-buffer
-	   (org-footnote--clear-footnote-section)
-	   ;; Insert footnote definitions at the appropriate location,
-	   ;; separated by a blank line.  Each definition is inserted
-	   ;; only once throughout the buffer.
-	   (let (inserted)
-	     (dolist (cell references)
-	       (let ((label (car cell))
-		     (nested (not (nth 2 cell)))
-		     (inline (nth 3 cell)))
-		 (unless (or (member label inserted) inline)
-		   (push label inserted)
-		   (unless (or org-footnote-section nested)
-		     ;; If `org-footnote-section' is non-nil, or
-		     ;; reference is nested, point is already at the
-		     ;; correct position.  Otherwise, move at the
-		     ;; appropriate location within the section
-		     ;; containing the reference.
-		     (goto-char (nth 1 cell))
-		     (org-footnote--goto-local-insertion-point))
-		   (insert "\n"
-			   (or (cdr (assoc label definitions))
-			       (format "[fn:%s] DEFINITION NOT FOUND." label))
-			   "\n"))))
-	     ;; Insert un-referenced footnote definitions at the end.
-	     (let ((unreferenced
-		    (cl-remove-if (lambda (d) (member (car d) inserted))
-				  definitions)))
-	       (dolist (d unreferenced) (insert "\n" (cdr d) "\n"))))))
-      ;; Clear dangling markers in the buffer.
-      (dolist (r references) (set-marker (nth 1 r) nil)))))
+    (org-preserve-local-variables
+     (let ((definitions (org-footnote--collect-definitions 'delete)))
+       (org-with-wide-buffer
+	(org-footnote--clear-footnote-section)
+	;; Insert footnote definitions at the appropriate location,
+	;; separated by a blank line.  Each definition is inserted
+	;; only once throughout the buffer.
+	(let (inserted)
+	  (dolist (cell references)
+	    (let ((label (car cell))
+		  (nested (not (nth 2 cell)))
+		  (inline (nth 3 cell)))
+	      (unless (or (member label inserted) inline)
+		(push label inserted)
+		(unless (or org-footnote-section nested)
+		  ;; If `org-footnote-section' is non-nil, or
+		  ;; reference is nested, point is already at the
+		  ;; correct position.  Otherwise, move at the
+		  ;; appropriate location within the section
+		  ;; containing the reference.
+		  (goto-char (nth 1 cell))
+		  (org-footnote--goto-local-insertion-point))
+		(insert "\n"
+			(or (cdr (assoc label definitions))
+			    (format "[fn:%s] DEFINITION NOT FOUND." label))
+			"\n"))))
+	  ;; Insert un-referenced footnote definitions at the end.
+	  (pcase-dolist (`(,label . ,definition) definitions)
+	    (unless (member label inserted)
+	      (insert "\n" definition "\n")))))))))
 
 (defun org-footnote-normalize ()
   "Turn every footnote in buffer into a numbered one."
   (interactive)
-  (let ((references (org-footnote--collect-references 'anonymous)))
-    (unwind-protect
-	(let ((n 0)
-	      (translations nil)
-	      (definitions nil))
-	  (org-with-wide-buffer
-	   ;; Update label for reference.  We need to do this before
-	   ;; clearing definitions in order to rename nested footnotes
-	   ;; before they are deleted.
-	   (dolist (cell references)
-	     (let* ((label (car cell))
-		    (anonymous (not label))
-		    (new
-		     (cond
-		      ;; In order to differentiate anonymous
-		      ;; references from regular ones, set their
-		      ;; labels to integers, not strings.
-		      (anonymous (setcar cell (cl-incf n)))
-		      ((cdr (assoc label translations)))
-		      (t (let ((l (number-to-string (cl-incf n))))
-			   (push (cons label l) translations)
-			   l)))))
-	       (goto-char (nth 1 cell))	; Move to reference's start.
-	       (org-footnote--set-label
-		(if anonymous (number-to-string new) new))
-	       (let ((size (nth 3 cell)))
-		 ;; Transform inline footnotes into regular references
-		 ;; and retain their definition for later insertion as
-		 ;; a regular footnote definition.
-		 (when size
-		   (let ((def (concat
-			       (format "[fn:%s] " new)
-			       (org-trim
-				(substring
-				 (delete-and-extract-region
-				  (point) (+ (point) size 1))
-				 1)))))
-		     (push (cons (if anonymous new label) def) definitions)
-		     (when org-footnote-fill-after-inline-note-extraction
-		       (org-fill-paragraph)))))))
-	   ;; Collect definitions.  Update labels according to ALIST.
-	   (let ((definitions
-		   (nconc definitions
-			  (org-footnote--collect-definitions 'delete)))
-		 (inserted))
-	     (org-footnote--clear-footnote-section)
-	     (dolist (cell references)
-	       (let* ((label (car cell))
-		      (anonymous (integerp label))
-		      (pos (nth 1 cell)))
-		 ;; Move to appropriate location, if required.  When
-		 ;; there is a footnote section or reference is
-		 ;; nested, point is already at the expected location.
-		 (unless (or org-footnote-section (not (nth 2 cell)))
-		   (goto-char pos)
-		   (org-footnote--goto-local-insertion-point))
-		 ;; Insert new definition once label is updated.
-		 (unless (member label inserted)
-		   (push label inserted)
-		   (let ((stored (cdr (assoc label definitions)))
-			 ;; Anonymous footnotes' label is already
-			 ;; up-to-date.
-			 (new (if anonymous label
-				(cdr (assoc label translations)))))
-		     (insert "\n"
-			     (cond
-			      ((not stored)
-			       (format "[fn:%s] DEFINITION NOT FOUND." new))
-			      (anonymous stored)
-			      (t
-			       (replace-regexp-in-string
-				"\\`\\[fn:\\(.*?\\)\\]" new stored nil nil 1)))
-			     "\n")))))
-	     ;; Insert un-referenced footnote definitions at the end.
-	     (let ((unreferenced
-		    (cl-remove-if (lambda (d) (member (car d) inserted))
-				  definitions)))
-	       (dolist (d unreferenced)
-		 (insert "\n"
-			 (replace-regexp-in-string
-			  org-footnote-definition-re
-			  (format "[fn:%d]" (cl-incf n))
-			  (cdr d))
-			 "\n"))))))
-      ;; Clear dangling markers.
-      (dolist (r references) (set-marker (nth 1 r) nil)))))
+  (org-preserve-local-variables
+   (let ((n 0)
+	 (translations nil)
+	 (definitions nil)
+	 (references (org-footnote--collect-references 'anonymous)))
+     (org-with-wide-buffer
+      ;; Update label for reference.  We need to do this before
+      ;; clearing definitions in order to rename nested footnotes
+      ;; before they are deleted.
+      (dolist (cell references)
+	(let* ((label (car cell))
+	       (anonymous (not label))
+	       (new
+		(cond
+		 ;; In order to differentiate anonymous references
+		 ;; from regular ones, set their labels to integers,
+		 ;; not strings.
+		 (anonymous (setcar cell (cl-incf n)))
+		 ((cdr (assoc label translations)))
+		 (t (let ((l (number-to-string (cl-incf n))))
+		      (push (cons label l) translations)
+		      l)))))
+	  (goto-char (nth 1 cell))	; Move to reference's start.
+	  (org-footnote--set-label
+	   (if anonymous (number-to-string new) new))
+	  (let ((size (nth 3 cell)))
+	    ;; Transform inline footnotes into regular references and
+	    ;; retain their definition for later insertion as
+	    ;; a regular footnote definition.
+	    (when size
+	      (let ((def (concat
+			  (format "[fn:%s] " new)
+			  (org-trim
+			   (substring
+			    (delete-and-extract-region
+			     (point) (+ (point) size 1))
+			    1)))))
+		(push (cons (if anonymous new label) def) definitions)
+		(when org-footnote-fill-after-inline-note-extraction
+		  (org-fill-paragraph)))))))
+      ;; Collect definitions.  Update labels according to ALIST.
+      (let ((definitions
+	      (nconc definitions
+		     (org-footnote--collect-definitions 'delete)))
+	    (inserted))
+	(org-footnote--clear-footnote-section)
+	(dolist (cell references)
+	  (let* ((label (car cell))
+		 (anonymous (integerp label))
+		 (pos (nth 1 cell)))
+	    ;; Move to appropriate location, if required.  When there
+	    ;; is a footnote section or reference is nested, point is
+	    ;; already at the expected location.
+	    (unless (or org-footnote-section (not (nth 2 cell)))
+	      (goto-char pos)
+	      (org-footnote--goto-local-insertion-point))
+	    ;; Insert new definition once label is updated.
+	    (unless (member label inserted)
+	      (push label inserted)
+	      (let ((stored (cdr (assoc label definitions)))
+		    ;; Anonymous footnotes' label is already
+		    ;; up-to-date.
+		    (new (if anonymous label
+			   (cdr (assoc label translations)))))
+		(insert "\n"
+			(cond
+			 ((not stored)
+			  (format "[fn:%s] DEFINITION NOT FOUND." new))
+			 (anonymous stored)
+			 (t
+			  (replace-regexp-in-string
+			   "\\`\\[fn:\\(.*?\\)\\]" new stored nil nil 1)))
+			"\n")))))
+	;; Insert un-referenced footnote definitions at the end.
+	(pcase-dolist (`(,label . ,definition) definitions)
+	  (unless (member label inserted)
+	    (insert "\n"
+		    (replace-regexp-in-string org-footnote-definition-re
+					      (format "[fn:%d]" (cl-incf n))
+					      definition)
+		    "\n"))))))))
 
 (defun org-footnote-auto-adjust-maybe ()
   "Renumber and/or sort footnotes according to user settings."

+ 18 - 0
lisp/org-macs.el

@@ -169,6 +169,24 @@ point nowhere."
   "Load FILE with optional arguments NOERROR and MUSTSUFFIX."
   `(load ,file 'noerror nil nil '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 "^[ \t]*# +Local Variables:"
+				      (max (- (point) 3000) 1)
+				      t)
+		  (delete-and-extract-region (point) (point-max)))))))
+     (unwind-protect (progn ,@body)
+       (when local-variables
+	 (org-with-wide-buffer
+	  (goto-char (point-max))
+	  (unless (bolp) (insert "\n"))
+	  (insert local-variables))))))
+
 
 
 ;;; Buffer

+ 194 - 188
lisp/org.el

@@ -8085,80 +8085,81 @@ case."
   "Move the current subtree down past ARG headlines of the same level."
   (interactive "p")
   (setq arg (prefix-numeric-value arg))
-  (let ((movfunc (if (> arg 0) 'org-get-next-sibling
-		   'org-get-last-sibling))
-	(ins-point (make-marker))
-	(cnt (abs arg))
-	(col (current-column))
-	beg beg0 end txt folded ne-beg ne-end ne-ins ins-end)
-    ;; Select the tree
-    (org-back-to-heading)
-    (setq beg0 (point))
-    (save-excursion
-      (setq ne-beg (org-back-over-empty-lines))
-      (setq beg (point)))
-    (save-match-data
-      (save-excursion (outline-end-of-heading)
-		      (setq folded (org-invisible-p)))
-      (progn (org-end-of-subtree nil t)
-	     (unless (eobp) (backward-char))))
-    (outline-next-heading)
-    (setq ne-end (org-back-over-empty-lines))
-    (setq end (point))
-    (goto-char beg0)
-    (when (and (> arg 0) (org-first-sibling-p) (< ne-end ne-beg))
-      ;; include less whitespace
-      (save-excursion
-	(goto-char beg)
-	(forward-line (- ne-beg ne-end))
-	(setq beg (point))))
-    ;; Find insertion point, with error handling
-    (while (> cnt 0)
-      (or (and (funcall movfunc) (looking-at org-outline-regexp))
-	  (progn (goto-char beg0)
-		 (user-error "Cannot move past superior level or buffer limit")))
-      (setq cnt (1- cnt)))
-    (when (> arg 0)
-      ;; Moving forward - still need to move over subtree
-      (org-end-of-subtree t t)
-      (save-excursion
-	(org-back-over-empty-lines)
-	(or (bolp) (newline))))
-    (setq ne-ins (org-back-over-empty-lines))
-    (move-marker ins-point (point))
-    (setq txt (buffer-substring beg end))
-    (org-save-markers-in-region beg end)
-    (delete-region beg end)
-    (org-remove-empty-overlays-at beg)
-    (or (= beg (point-min)) (outline-flag-region (1- beg) beg nil))
-    (or (bobp) (outline-flag-region (1- (point)) (point) nil))
-    (and (not (bolp)) (looking-at "\n") (forward-char 1))
-    (let ((bbb (point)))
-      (insert-before-markers txt)
-      (org-reinstall-markers-in-region bbb)
-      (move-marker ins-point bbb))
-    (or (bolp) (insert "\n"))
-    (setq ins-end (point))
-    (goto-char ins-point)
-    (org-skip-whitespace)
-    (when (and (< arg 0)
-	       (org-first-sibling-p)
-	       (> ne-ins ne-beg))
-      ;; Move whitespace back to beginning
-      (save-excursion
-	(goto-char ins-end)
-	(let ((kill-whole-line t))
-	  (kill-line (- ne-ins ne-beg)) (point)))
-      (insert (make-string (- ne-ins ne-beg) ?\n)))
-    (move-marker ins-point nil)
-    (if folded
-	(outline-hide-subtree)
-      (org-show-entry)
-      (org-show-children)
-      (org-cycle-hide-drawers 'children))
-    (org-clean-visibility-after-subtree-move)
-    ;; move back to the initial column we were at
-    (move-to-column col)))
+  (org-preserve-local-variables
+   (let ((movfunc (if (> arg 0) 'org-get-next-sibling
+		    'org-get-last-sibling))
+	 (ins-point (make-marker))
+	 (cnt (abs arg))
+	 (col (current-column))
+	 beg beg0 end txt folded ne-beg ne-end ne-ins ins-end)
+     ;; Select the tree
+     (org-back-to-heading)
+     (setq beg0 (point))
+     (save-excursion
+       (setq ne-beg (org-back-over-empty-lines))
+       (setq beg (point)))
+     (save-match-data
+       (save-excursion (outline-end-of-heading)
+		       (setq folded (org-invisible-p)))
+       (progn (org-end-of-subtree nil t)
+	      (unless (eobp) (backward-char))))
+     (outline-next-heading)
+     (setq ne-end (org-back-over-empty-lines))
+     (setq end (point))
+     (goto-char beg0)
+     (when (and (> arg 0) (org-first-sibling-p) (< ne-end ne-beg))
+       ;; include less whitespace
+       (save-excursion
+	 (goto-char beg)
+	 (forward-line (- ne-beg ne-end))
+	 (setq beg (point))))
+     ;; Find insertion point, with error handling
+     (while (> cnt 0)
+       (or (and (funcall movfunc) (looking-at org-outline-regexp))
+	   (progn (goto-char beg0)
+		  (user-error "Cannot move past superior level or buffer limit")))
+       (setq cnt (1- cnt)))
+     (when (> arg 0)
+       ;; Moving forward - still need to move over subtree
+       (org-end-of-subtree t t)
+       (save-excursion
+	 (org-back-over-empty-lines)
+	 (unless (bolp) (insert "\n"))))
+     (setq ne-ins (org-back-over-empty-lines))
+     (move-marker ins-point (point))
+     (setq txt (buffer-substring beg end))
+     (org-save-markers-in-region beg end)
+     (delete-region beg end)
+     (org-remove-empty-overlays-at beg)
+     (or (= beg (point-min)) (outline-flag-region (1- beg) beg nil))
+     (or (bobp) (outline-flag-region (1- (point)) (point) nil))
+     (and (not (bolp)) (looking-at "\n") (forward-char 1))
+     (let ((bbb (point)))
+       (insert-before-markers txt)
+       (org-reinstall-markers-in-region bbb)
+       (move-marker ins-point bbb))
+     (or (bolp) (insert "\n"))
+     (setq ins-end (point))
+     (goto-char ins-point)
+     (org-skip-whitespace)
+     (when (and (< arg 0)
+		(org-first-sibling-p)
+		(> ne-ins ne-beg))
+       ;; Move whitespace back to beginning.
+       (save-excursion
+	 (goto-char ins-end)
+	 (let ((kill-whole-line t))
+	   (kill-line (- ne-ins ne-beg)) (point)))
+       (insert (make-string (- ne-ins ne-beg) ?\n)))
+     (move-marker ins-point nil)
+     (if folded
+	 (outline-hide-subtree)
+       (org-show-entry)
+       (org-show-children)
+       (org-cycle-hide-drawers 'children))
+     (org-clean-visibility-after-subtree-move)
+     ;; Move back to the initial column we were at.
+     (move-to-column col))))
 
 (defvar org-subtree-clip ""
   "Clipboard for cut and paste of subtrees.
@@ -8185,35 +8186,36 @@ If FORCE-STORE-MARKERS is non-nil, store the relative locations
 of some markers in the region, even if CUT is non-nil.  This is
 useful if the caller implements cut-and-paste as copy-then-paste-then-cut."
   (interactive "p")
-  (let (beg end folded (beg0 (point)))
-    (if (called-interactively-p 'any)
-	(org-back-to-heading nil) ; take what looks like a subtree
-      (org-back-to-heading t)) ; take what is really there
-    (setq beg (point))
-    (skip-chars-forward " \t\r\n")
-    (save-match-data
-      (if nosubtrees
-	  (outline-next-heading)
-	(save-excursion (outline-end-of-heading)
-			(setq folded (org-invisible-p)))
-	(ignore-errors (org-forward-heading-same-level (1- n) t))
-	(org-end-of-subtree t t)))
-    ;; Include the end of an inlinetask
-    (when (and (featurep 'org-inlinetask)
-	       (looking-at-p (concat (org-inlinetask-outline-regexp)
-				     "END[ \t]*$")))
-      (end-of-line))
-    (setq end (point))
-    (goto-char beg0)
-    (when (> end beg)
-      (setq org-subtree-clip-folded folded)
-      (when (or cut force-store-markers)
-	(org-save-markers-in-region beg end))
-      (if cut (kill-region beg end) (copy-region-as-kill beg end))
-      (setq org-subtree-clip (current-kill 0))
-      (message "%s: Subtree(s) with %d characters"
-	       (if cut "Cut" "Copied")
-	       (length org-subtree-clip)))))
+  (org-preserve-local-variables
+   (let (beg end folded (beg0 (point)))
+     (if (called-interactively-p 'any)
+	 (org-back-to-heading nil)    ; take what looks like a subtree
+       (org-back-to-heading t))	      ; take what is really there
+     (setq beg (point))
+     (skip-chars-forward " \t\r\n")
+     (save-match-data
+       (if nosubtrees
+	   (outline-next-heading)
+	 (save-excursion (outline-end-of-heading)
+			 (setq folded (org-invisible-p)))
+	 (ignore-errors (org-forward-heading-same-level (1- n) t))
+	 (org-end-of-subtree t t)))
+     ;; Include the end of an inlinetask
+     (when (and (featurep 'org-inlinetask)
+		(looking-at-p (concat (org-inlinetask-outline-regexp)
+				      "END[ \t]*$")))
+       (end-of-line))
+     (setq end (point))
+     (goto-char beg0)
+     (when (> end beg)
+       (setq org-subtree-clip-folded folded)
+       (when (or cut force-store-markers)
+	 (org-save-markers-in-region beg end))
+       (if cut (kill-region beg end) (copy-region-as-kill beg end))
+       (setq org-subtree-clip (current-kill 0))
+       (message "%s: Subtree(s) with %d characters"
+		(if cut "Cut" "Copied")
+		(length org-subtree-clip))))))
 
 (defun org-paste-subtree (&optional level tree for-yank remove)
   "Paste the clipboard as a subtree, with modification of headline level.
@@ -8691,88 +8693,91 @@ function is being called interactively."
 	    (dcst (downcase sorting-type))
 	    (case-fold-search nil)
 	    (now (current-time)))
-        (sort-subr
-         (/= dcst sorting-type)
-         ;; This function moves to the beginning character of the "record" to
-         ;; be sorted.
-	 (lambda nil
-	   (if (re-search-forward re nil t)
-	       (goto-char (match-beginning 0))
-	     (goto-char (point-max))))
-         ;; This function moves to the last character of the "record" being
-         ;; sorted.
-	 (lambda nil
-	   (save-match-data
-	     (condition-case nil
-		 (outline-forward-same-level 1)
-	       (error
-		(goto-char (point-max))))))
-         ;; This function returns the value that gets sorted against.
-	 (lambda nil
-	   (cond
-	    ((= dcst ?n)
-	     (if (looking-at org-complex-heading-regexp)
-		 (string-to-number (org-sort-remove-invisible (match-string 4)))
-	       nil))
-	    ((= dcst ?a)
-	     (if (looking-at org-complex-heading-regexp)
-		 (funcall case-func (org-sort-remove-invisible (match-string 4)))
-	       nil))
-	    ((= dcst ?k)
-	     (or (get-text-property (point) :org-clock-minutes) 0))
-	    ((= dcst ?t)
-	     (let ((end (save-excursion (outline-next-heading) (point))))
-	       (if (or (re-search-forward org-ts-regexp end t)
-		       (re-search-forward org-ts-regexp-both end t))
-		   (org-time-string-to-seconds (match-string 0))
-		 (float-time now))))
-	    ((= dcst ?c)
-	     (let ((end (save-excursion (outline-next-heading) (point))))
-	       (if (re-search-forward
-		    (concat "^[ \t]*\\[" org-ts-regexp1 "\\]")
-		    end t)
-		   (org-time-string-to-seconds (match-string 0))
-		 (float-time now))))
-	    ((= dcst ?s)
-	     (let ((end (save-excursion (outline-next-heading) (point))))
-	       (if (re-search-forward org-scheduled-time-regexp end t)
-		   (org-time-string-to-seconds (match-string 1))
-		 (float-time now))))
-	    ((= dcst ?d)
-	     (let ((end (save-excursion (outline-next-heading) (point))))
-	       (if (re-search-forward org-deadline-time-regexp end t)
-		   (org-time-string-to-seconds (match-string 1))
-		 (float-time now))))
-	    ((= dcst ?p)
-	     (if (re-search-forward org-priority-regexp (point-at-eol) t)
-		 (string-to-char (match-string 2))
-	       org-default-priority))
-	    ((= dcst ?r)
-	     (or (org-entry-get nil property) ""))
-	    ((= dcst ?o)
-	     (when (looking-at org-complex-heading-regexp)
-	       (let* ((m (match-string 2))
-		      (s (if (member m org-done-keywords) '- '+)))
-		 (- 99 (funcall s (length (member m org-todo-keywords-1)))))))
-	    ((= dcst ?f)
-	     (if getkey-func
-		 (progn
-		   (setq tmp (funcall getkey-func))
-		   (when (stringp tmp) (setq tmp (funcall case-func tmp)))
-		   tmp)
-	       (error "Invalid key function `%s'" getkey-func)))
-	    (t (error "Invalid sorting type `%c'" sorting-type))))
-         nil
-         (cond
-          ((= dcst ?a) 'string<)
-          ((= dcst ?f)
-	   (or compare-func
-	       (and interactive?
-		    (org-read-function
-		     (concat "Function for comparing keys "
-			     "(empty for default `sort-subr' predicate): ")
-		     'allow-empty))))
-          ((member dcst '(?p ?t ?s ?d ?c ?k)) '<)))
+        (org-preserve-local-variables
+	 (sort-subr
+	  (/= dcst sorting-type)
+	  ;; This function moves to the beginning character of the
+	  ;; "record" to be sorted.
+	  (lambda nil
+	    (if (re-search-forward re nil t)
+		(goto-char (match-beginning 0))
+	      (goto-char (point-max))))
+	  ;; This function moves to the last character of the "record" being
+	  ;; sorted.
+	  (lambda nil
+	    (save-match-data
+	      (condition-case nil
+		  (outline-forward-same-level 1)
+		(error
+		 (goto-char (point-max))))))
+	  ;; This function returns the value that gets sorted against.
+	  (lambda ()
+	    (cond
+	     ((= dcst ?n)
+	      (if (looking-at org-complex-heading-regexp)
+		  (string-to-number
+		   (org-sort-remove-invisible (match-string 4)))
+		nil))
+	     ((= dcst ?a)
+	      (if (looking-at org-complex-heading-regexp)
+		  (funcall case-func
+			   (org-sort-remove-invisible (match-string 4)))
+		nil))
+	     ((= dcst ?k)
+	      (or (get-text-property (point) :org-clock-minutes) 0))
+	     ((= dcst ?t)
+	      (let ((end (save-excursion (outline-next-heading) (point))))
+		(if (or (re-search-forward org-ts-regexp end t)
+			(re-search-forward org-ts-regexp-both end t))
+		    (org-time-string-to-seconds (match-string 0))
+		  (float-time now))))
+	     ((= dcst ?c)
+	      (let ((end (save-excursion (outline-next-heading) (point))))
+		(if (re-search-forward
+		     (concat "^[ \t]*\\[" org-ts-regexp1 "\\]")
+		     end t)
+		    (org-time-string-to-seconds (match-string 0))
+		  (float-time now))))
+	     ((= dcst ?s)
+	      (let ((end (save-excursion (outline-next-heading) (point))))
+		(if (re-search-forward org-scheduled-time-regexp end t)
+		    (org-time-string-to-seconds (match-string 1))
+		  (float-time now))))
+	     ((= dcst ?d)
+	      (let ((end (save-excursion (outline-next-heading) (point))))
+		(if (re-search-forward org-deadline-time-regexp end t)
+		    (org-time-string-to-seconds (match-string 1))
+		  (float-time now))))
+	     ((= dcst ?p)
+	      (if (re-search-forward org-priority-regexp (point-at-eol) t)
+		  (string-to-char (match-string 2))
+		org-default-priority))
+	     ((= dcst ?r)
+	      (or (org-entry-get nil property) ""))
+	     ((= dcst ?o)
+	      (when (looking-at org-complex-heading-regexp)
+		(let* ((m (match-string 2))
+		       (s (if (member m org-done-keywords) '- '+)))
+		  (- 99 (funcall s (length (member m org-todo-keywords-1)))))))
+	     ((= dcst ?f)
+	      (if getkey-func
+		  (progn
+		    (setq tmp (funcall getkey-func))
+		    (when (stringp tmp) (setq tmp (funcall case-func tmp)))
+		    tmp)
+		(error "Invalid key function `%s'" getkey-func)))
+	     (t (error "Invalid sorting type `%c'" sorting-type))))
+	  nil
+	  (cond
+	   ((= dcst ?a) 'string<)
+	   ((= dcst ?f)
+	    (or compare-func
+		(and interactive?
+		     (org-read-function
+		      (concat "Function for comparing keys "
+			      "(empty for default `sort-subr' predicate): ")
+		      'allow-empty))))
+	   ((member dcst '(?p ?t ?s ?d ?c ?k)) '<))))
 	(when restore-clock?
 	  (move-marker org-clock-marker
 		       (1+ (next-single-property-change
@@ -11552,9 +11557,10 @@ prefix argument (`C-u C-u C-u C-c C-w')."
 	    (unless org-refile-keep
 	      (if regionp
 		  (delete-region (point) (+ (point) (- region-end region-start)))
-		(delete-region
-		 (and (org-back-to-heading t) (point))
-		 (min (1+ (buffer-size)) (org-end-of-subtree t t) (point)))))
+		(org-preserve-local-variables
+		 (delete-region
+		  (and (org-back-to-heading t) (point))
+		  (min (1+ (buffer-size)) (org-end-of-subtree t t) (point))))))
 	    (when (featurep 'org-inlinetask)
 	      (org-inlinetask-remove-END-maybe))
 	    (setq org-markers-to-move nil)

+ 88 - 4
testing/lisp/test-org-footnote.el

@@ -112,7 +112,33 @@
 	(org-footnote-new))
       (buffer-substring-no-properties
        (line-beginning-position -1)
-       (line-beginning-position 4))))))
+       (line-beginning-position 4)))))
+  ;; Do not alter file local variables when inserting new definition
+  ;; label.
+  (should
+   (equal "Paragraph[fn:1]
+
+\[fn:1] 
+# Local Variables:
+# foo: t
+# End:"
+	  (org-test-with-temp-text
+	      "Paragraph<point>\n# Local Variables:\n# foo: t\n# End:"
+	    (let ((org-footnote-section nil)) (org-footnote-new))
+	    (buffer-string))))
+  (should
+   (equal "Paragraph[fn:1]
+
+* Footnotes
+
+\[fn:1] 
+# Local Variables:
+# foo: t
+# End:"
+	  (org-test-with-temp-text
+	      "Paragraph<point>\n# Local Variables:\n# foo: t\n# End:"
+	    (let ((org-footnote-section "Footnotes")) (org-footnote-new))
+	    (buffer-string)))))
 
 (ert-deftest test-org-footnote/delete ()
   "Test `org-footnote-delete' specifications."
@@ -175,7 +201,15 @@
 	    (org-test-with-temp-text
 		"Text[fn:1]\n\n[fn:1] Definition.\n\n\nOther text."
 	      (org-footnote-delete "1")
-	      (buffer-string))))))
+	      (buffer-string)))))
+  ;; Preserve file local variables when deleting a footnote.
+  (should
+   (equal
+    "Paragraph\n# Local Variables:\n# foo: t\n# End:"
+    (org-test-with-temp-text
+	"Paragraph[fn:1]\n[fn:1] Def 1\n# Local Variables:\n# foo: t\n# End:"
+      (let ((org-footnote-section nil)) (org-footnote-delete "1"))
+      (buffer-string)))))
 
 (ert-deftest test-org-footnote/goto-definition ()
   "Test `org-footnote-goto-definition' specifications."
@@ -454,7 +488,32 @@ Text[fn:1][fn:4]
 "
     (org-test-with-temp-text "Text[fn:9]\n\n[fn:1] A\n[fn:9] B"
       (let ((org-footnote-section nil)) (org-footnote-sort))
-      (buffer-string)))))
+      (buffer-string))))
+  ;; When sorting, preserve file local variables.
+  (should
+   (equal "
+Paragraph[fn:1][fn:2]
+
+\[fn:1] Def 1
+
+\[fn:2] Def 2
+
+# Local Variables:
+# foo: t
+# End:"
+	  (org-test-with-temp-text
+	      "
+Paragraph[fn:1][fn:2]
+
+\[fn:2] Def 2
+
+\[fn:1] Def 1
+
+# Local Variables:
+# foo: t
+# End:"
+	    (let ((org-footnote-section nil)) (org-footnote-sort))
+	    (buffer-string)))))
 
 (ert-deftest test-org-footnote/renumber-fn:N ()
   "Test `org-footnote-renumber-fn:N' specifications."
@@ -569,7 +628,32 @@ Text[fn:1][fn:4]
     "Test[fn:1]\nNext\n\n[fn:1] def\n\n[fn:2] A\n"
     (org-test-with-temp-text "Test[fn::def]\nNext\n[fn:unref] A"
       (let ((org-footnote-section nil)) (org-footnote-normalize))
-      (buffer-string)))))
+      (buffer-string))))
+  ;; Preserve file local variables when normalizing.
+  (should
+   (equal "
+Paragraph[fn:1][fn:2]
+
+\[fn:1] Def 1
+
+\[fn:2] Def 2
+
+# Local Variables:
+# foo: t
+# End:"
+	  (org-test-with-temp-text
+	      "
+Paragraph[fn:foo][fn:bar]
+
+\[fn:bar] Def 2
+
+\[fn:foo] Def 1
+
+# Local Variables:
+# foo: t
+# End:"
+	    (let ((org-footnote-section nil)) (org-footnote-normalize))
+	    (buffer-string)))))
 
 
 (provide 'test-org-footnote)

+ 7 - 0
testing/lisp/test-org.el

@@ -2918,6 +2918,13 @@ SCHEDULED: <2017-05-06 Sat>
   :END:
 "
 	    (org-sort-entries nil ?k)
+	    (buffer-string))))
+  ;; Preserve file local variables when sorting.
+  (should
+   (equal "\n* A\n* B\n# Local Variables:\n# foo: t\n# End:\n"
+	  (org-test-with-temp-text
+	      "\n* B\n* A\n# Local Variables:\n# foo: t\n# End:"
+	    (org-sort-entries nil ?a)
 	    (buffer-string)))))
 
 (ert-deftest test-org/file-contents ()