Emacs: Enhancing up and down subtree movement in outline-mode and outline-minor-mode

When editing outlined files (e.g., using the built-in outline-minor-mode, or packages like outline-indent.el, outline-yaml.el, etc.), handling subtrees efficiently can significantly enhance productivity, especially when working with complex documents. If you’re familiar with outline-mode or outline-minor-mode, you might have noticed that the built-in functions for moving subtrees up and down, outline-move-subtree-up and outline-move-subtree-down:

  • Blank line exclusion: outline-move-subtree-up and outline-move-subtree-down exclude the last blank line of the subtree when the outline-blank-line variable is set to t. Setting outline-blank-line to t is worthwhile because it retains a visible blank line after the subtree and before the next heading, improving the readability of the document.
  • Cursor position reset: After moving a subtree up or down, the cursor position is often reset, which can be disruptive during editing.

Here’s how you can address these issues by using custom functions to enhance subtree movement:

(require 'outline)

(defun my-advice-outline-hide-subtree (orig-fun &rest args)
  "Advice for `outline-hide-subtree'.
This ensures that the outline is folded correctly by
outline-move-subtree-up/down, preventing it from being unable to open the fold."
  (let ((outline-blank-line
         (if (bound-and-true-p my-outline-hide-subtree-blank-line-enabled)
             my-outline-hide-subtree-blank-line
           outline-blank-line)))
    (apply orig-fun args)))

(defun my-advice-outline-move-subtree-up-down (orig-fun &rest args)
  "Move the current subtree up/down past ARGS headlines of the same level.
This function ensures the last blank line is included, even when
`outline-blank-line' is set to t. It also restores the cursor position,
addressing the issue where the cursor might be reset after the operation."
  (interactive "p")
  (let ((column (current-column))
        (outline-blank-line nil)
        (my-outline-hide-subtree-blank-line-enabled t)
        (my-outline-hide-subtree-blank-line outline-blank-line))
    (apply orig-fun (or args 1))
    (move-to-column column)))

(advice-add 'outline-hide-subtree
            :around #'my-advice-outline-hide-subtree)
(advice-add 'outline-move-subtree-up
            :around #'my-advice-outline-move-subtree-up-down)
(advice-add 'outline-move-subtree-down
            :around #'my-advice-outline-move-subtree-up-down)Code language: Lisp (lisp)

I also recommend using Ctrl-Up and Ctrl-Down key bindings for moving subtrees up and down.

(define-key outline-mode-map (kbd "C-<up>") 'outline-move-subtree-up)
(define-key outline-mode-map (kbd "C-<down>") 'outline-move-subtree-down)

(defun my-setup-outline-minor-mode-keybindings ()
    "Set up keybindings for moving subtrees in `outline-minor-mode'."
  (define-key outline-minor-mode-map (kbd "C-<up>") 'outline-move-subtree-up)
  (define-key outline-minor-mode-map (kbd "C-<down>") 'outline-move-subtree-down))
(add-hook 'outline-minor-mode-hook 'my-setup-outline-minor-mode-keybindings) Code language: Lisp (lisp)

With this setup, you will ensure that every time you use outline-move-subtree-up or outline-move-subtree-down, the last blank line of the subtree is included and the cursor position is restored.