Files
nursery/lisp/org-format.el
2023-12-26 14:24:42 +13:00

170 lines
5.8 KiB
EmacsLisp
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; org-format.el --- Auto-format org buffers. -*- lexical-binding: t; -*-
;;; Commentary:
;; Adapted from: https://emacs.stackexchange.com/a/28268
;; Example configuration:
;;
;; (use-package org-format
;; :hook (org-mode . org-format-on-save-mode))
;;; Code:
(require 'org)
(require 'org-capture-detect)
(require 'thingatpt)
(defgroup org-format nil
"Automatically format org buffers on save."
:group 'productivity
:prefix "org-format-")
(defcustom org-format-blank-lines-before-subheadings 1
"Number of blank lines between a heading and preceding content.
Only applies to subheadings."
:group 'org-format
:type 'integer)
(defcustom org-format-blank-lines-before-first-heading 1
"Number of blank lines between a heading and preceding content.
Only applies to first level-1 heading in the document, and
supercedes the setting for
`org-format-blank-lines-before-level-1-headings'."
:group 'org-format
:type 'integer)
(defcustom org-format-blank-lines-before-level-1-headings 1
"Number of blank lines between a heading and preceding content.
Only applies to level-1 headings in the document."
:group 'org-format
:type 'integer)
(defcustom org-format-blank-lines-before-content 0
"Number of blank lines after the heading line and any property drawers."
:group 'org-format
:type 'integer)
(defcustom org-format-blank-lines-before-meta 0
"Number of blank lines between headers and subsequent planning & drawers."
:group 'org-format
:type 'integer)
(defcustom org-format-align-all-tables t
"Whether to align tables on save."
:group 'org-format
:type 'boolean)
(defun org-format--ensure-empty-lines (n)
(save-excursion
(goto-char (line-beginning-position))
(unless (bobp)
(forward-char -1)
(let ((start (point)))
(when (search-backward-regexp (rx (not (any space "\n"))))
(ignore-errors
(forward-char 1)
(delete-region (point) start))))
(insert (make-string n ?\n)))))
(defun org-format--in-archived-heading-p ()
(save-excursion
(when (org-before-first-heading-p)
(org-forward-heading-same-level 1))
(let ((tags (org-get-tags)))
(seq-contains-p tags org-archive-tag))))
(defun org-format--delete-blank-lines ()
"Modified version of `delete-blank-lines'."
(beginning-of-line)
(when (looking-at "[ \t]*$")
(delete-region (point)
(if (re-search-backward "[^ \t\n]" nil t)
(progn (forward-line 1) (point))
(point-min))))
;; Handle the special case where point is followed by newline and eob.
;; Delete the line, leaving point at eob.
(when (looking-at "^[ \t]*\n\\'")
(delete-region (point) (point-max))))
(defun org-format--headings (scope)
(let ((seen-first-heading-p))
(org-map-entries (lambda ()
;; Widen so we can see space preceding the current
;; headline.
(org-with-wide-buffer
(let* ((level (car (org-heading-components)))
(headline-spacing (cond
((and (equal 1 level) (not seen-first-heading-p))
(setq seen-first-heading-p t)
org-format-blank-lines-before-first-heading)
((equal 1 level)
org-format-blank-lines-before-level-1-headings)
(t
org-format-blank-lines-before-subheadings))))
(org-format--ensure-empty-lines headline-spacing)))
(unless (and (fboundp 'org-transclusion-within-transclusion-p)
(org-transclusion-within-transclusion-p))
(forward-line 1)
(org-format--delete-blank-lines)
(org-format--ensure-empty-lines org-format-blank-lines-before-meta)
(org-end-of-meta-data t)
(org-format--ensure-empty-lines org-format-blank-lines-before-content)))
t
scope)))
(defun org-format--transclusions ()
(while (search-forward-regexp (rx bol "#+transclude:") nil t)
(save-excursion
(unless (search-forward ":only-content" (line-end-position) t)
(goto-char (line-beginning-position))
(org-format--ensure-empty-lines org-format-blank-lines-before-subheadings)))))
;;;###autoload
(defun org-format-buffer ()
"Format the current `org-mode' buffer."
(interactive)
(unless (org-capture-detect)
(let ((scope (when (org-format--in-archived-heading-p)
;; archive files can be enormous--just format the heading at
;; point after archiving.
'tree)))
(org-with-wide-buffer
(when org-format-align-all-tables
(org-table-map-tables #'org-table-align t))
(org-format--headings scope)
;; Clean up trailing whitespace.
(goto-char (point-max))
(org-format--delete-blank-lines)
;; Format transcluded headings as if they were really there.
(goto-char (point-min))
(org-format--transclusions)))))
;; NB: Set this higher than the default to avoid interfering with things like
;; org-transclusion, etc.
(defvar org-format-on-save-mode-hook-depth 95)
;;;###autoload
(define-minor-mode org-format-on-save-mode
"Minor mode to enable formatting on buffer save in `org-mode'."
:lighter nil
(cond
(org-format-on-save-mode
(add-hook 'before-save-hook 'org-format-buffer org-format-on-save-mode-hook-depth t))
(t
(remove-hook 'before-save-hook 'org-format-buffer t))))
(provide 'org-format)
;;; org-format.el ends here