emacs/lisp/+org.el

510 lines
21 KiB
EmacsLisp

;;; +org.el -*- lexical-binding: t; -*-
;;; Code:
(require 'org)
(require 'org-element)
(require 'ox)
;;; org-return-dwim - https://github.com/alphapapa/unpackaged.el
;; http://kitchingroup.cheme.cmu.edu/blog/2017/04/09/A-better-return-in-org-mode/
(defun +org-element-descendant-of (type element)
"Return non-nil if ELEMENT is a descendant of TYPE.
TYPE should be an element type, like `item' or `paragraph'.
ELEMENT should be a list like that returned by `org-element-context'."
;; MAYBE: Use `org-element-lineage'.
(when-let* ((parent (org-element-property :parent element)))
(or (eq type (car parent))
(+org-element-descendant-of type parent))))
(defun +org-return-dwim (&optional prefix)
"A helpful replacement for `org-return'. With PREFIX, call `org-return'.
On headings, move point to position after entry content. In
lists, insert a new item or end the list, with checkbox if
appropriate. In tables, insert a new row or end the table."
(interactive "P")
;; Auto-fill if enabled
(when auto-fill-function
(if (listp auto-fill-function)
(dolist (func auto-fill-function)
(funcall func))
(funcall auto-fill-function)))
(if prefix
;; Handle prefix args
(pcase prefix
('(4) (newline))
('(16) (newline 2))
;; this is ... not ideal. but whatever.
(_ (newline prefix)))
(cond
;; Act depending on context around point.
((and org-return-follows-link
(eq 'link (car (org-element-context))))
;; Link: Open it.
(org-open-at-point-global))
((org-at-heading-p)
;; Heading: Move to position after entry content.
;; NOTE: This is probably the most interesting feature of this function.
(let ((heading-start (org-entry-beginning-position)))
(goto-char (org-entry-end-position))
(cond ((and (org-at-heading-p)
(= heading-start (org-entry-beginning-position)))
;; Entry ends on its heading; add newline after
(end-of-line)
(insert "\n\n"))
(t
;; Entry ends after its heading; back up
(forward-line -1)
(end-of-line)
(when (org-at-heading-p)
;; At the same heading
(forward-line)
(insert "\n")
(forward-line -1))
(while (not
(looking-back
(rx (repeat 3 (seq (optional blank) "\n")))
nil))
(insert "\n"))
(forward-line -1)))))
((org-at-item-checkbox-p)
;; Checkbox: Insert new item with checkbox.
(org-insert-todo-heading nil))
((org-in-item-p)
;; Plain list
(let* ((context (org-element-context))
(first-item-p (eq 'plain-list (car context)))
(itemp (eq 'item (car context)))
(emptyp (eq (org-element-property :contents-begin context)
(org-element-property :contents-end context)))
(item-child-p
(+org-element-descendant-of 'item context)))
;; The original function from unpackaged just tested the (or ...) test
;; in this cond, in an if. However, that doesn't auto-end nested
;; lists. So I made this form a cond and added the (and...) test in
;; the first position, which is clunky (the delete-region... stuff
;; comes twice) and might not be needed. More testing, obviously, but
;; for now, it works well enough.
(cond ((and itemp emptyp)
(delete-region (line-beginning-position) (line-end-position))
(insert "\n\n"))
((or first-item-p
(and itemp (not emptyp))
item-child-p)
(org-insert-item))
(t (delete-region (line-beginning-position) (line-end-position))
(insert "\n")))))
((when (fboundp 'org-inlinetask-in-task-p)
(org-inlinetask-in-task-p))
;; Inline task: Don't insert a new heading.
(org-return))
((org-at-table-p)
(cond ((save-excursion
(beginning-of-line)
;; See `org-table-next-field'.
(cl-loop with end = (line-end-position)
for cell = (org-element-table-cell-parser)
always (equal (org-element-property :contents-begin cell)
(org-element-property :contents-end cell))
while (re-search-forward "|" end t)))
;; Empty row: end the table.
(delete-region (line-beginning-position) (line-end-position))
(org-return))
(t
;; Non-empty row: call `org-return'.
(org-return))))
(t
;; All other cases: call `org-return'.
(org-return)))))
(defun +org-table-copy-down (n)
"Call `org-table-copy-down', or `org-return' outside of a table.
N is passed to the functions."
(interactive "p")
(if (org-table-check-inside-data-field 'noerror)
(org-table-copy-down n)
(+org-return-dwim n)))
;;; org-fix-blank-lines - unpackaged.el
(defun +org-fix-blank-lines (&optional prefix)
"Ensure blank lines around headings.
Optional PREFIX argument operates on the entire buffer.
Drawers are included with their headings."
(interactive "P")
(org-map-entries (lambda ()
(org-with-wide-buffer
;; `org-map-entries' narrows the buffer, which
;; prevents us from seeing newlines before the
;; current heading, so we do this part widened.
(while (not (looking-back "\n\n" nil))
;; Insert blank lines before heading.
(insert "\n")))
(let ((end (org-entry-end-position)))
;; Insert blank lines before entry content
(forward-line)
(while (and (org-at-planning-p)
(< (point) (point-max)))
;; Skip planning lines
(forward-line))
(while (re-search-forward
org-drawer-regexp end t)
;; Skip drawers. You might think that
;; `org-at-drawer-p' would suffice, but for
;; some reason it doesn't work correctly when
;; operating on hidden text. This works, taken
;; from `org-agenda-get-some-entry-text'.
(re-search-forward "^[ \t]*:END:.*\n?" end t)
(goto-char (match-end 0)))
(unless (or (= (point) (point-max))
(org-at-heading-p)
(looking-at-p "\n"))
(insert "\n"))))
t
(if prefix
nil
'tree)))
;;; org-count-words
(defun +org-count-words-stupidly (start end &optional limit)
"Count words between START and END, ignoring a lot.
Since this function is, for some reason, pricy, the optional
parameter LIMIT sets a word limit at which to stop counting.
Once the function hits that number, it'll return -LIMIT
instead of the true count."
(interactive (list nil nil))
(cond ((not (called-interactively-p 'any))
(let ((words 0)
(continue t))
(save-excursion
(save-restriction
(narrow-to-region start end)
(goto-char (point-min))
(while (and continue
(< (point) (point-max)))
(cond
;; Ignore comments
((or (org-at-comment-p)
(org-in-commented-heading-p))
(forward-line))
;; Ignore headings
((or (org-at-heading-p))
(forward-line))
;; Ignore property and log drawers
((or (looking-at org-drawer-regexp)
(looking-at org-clock-drawer-re))
(search-forward ":END:" nil :noerror)
(forward-line))
;; Ignore DEADLINE and SCHEDULED keywords
((or (looking-at org-deadline-regexp)
(looking-at org-scheduled-regexp)
(looking-at org-closed-time-regexp))
(forward-line))
;; Ignore tables
((org-at-table-p) (forward-line))
;; Ignore hyperlinks, but count the descriptions
((looking-at org-link-bracket-re)
(when-let ((desc (match-string-no-properties 5)))
(save-match-data
(setq words (+ words
(length (remove ""
(org-split-string
desc "\\W")))))))
(goto-char (match-end 0)))
;; Ignore source blocks
((org-in-src-block-p) (forward-line))
;; Ignore blank lines
((looking-at "^$")
(forward-line))
;; Count everything else
(t
;; ... unless it's in a few weird contexts
(let ((contexts (org-context)))
(cond ((or (assoc :todo-keyword contexts)
(assoc :priority contexts)
(assoc :keyword contexts)
(assoc :checkbox contexts))
(forward-word-strictly))
(t (setq words (1+ words))
(if (and limit
(> words limit))
(setq words (- limit)
continue nil))
(forward-word-strictly)))))))))
words))
((use-region-p)
(message "%d words in region"
(+org-count-words-stupidly (region-beginning)
(region-end))))
(t
(message "%d words in buffer"
(+org-count-words-stupidly (point-min)
(point-max))))))
;;; org-insert-link-dwim - https://xenodium.com/emacs-dwim-do-what-i-mean/
(defun +org-insert-link-dwim (&optional interactivep)
"Like `org-insert-link' but with personal dwim preferences."
(interactive '(t))
(let* ((point-in-link (org-in-regexp org-link-any-re 1))
(clipboard-url (when (string-match-p
(rx (sequence bos
(or "http"
"gemini"
"gopher")))
(current-kill 0))
(current-kill 0)))
(region-content (when (region-active-p)
(buffer-substring-no-properties (region-beginning)
(region-end))))
(org-link (when clipboard-url
(org-link-make-string
clipboard-url
(or region-content
(read-string "title: "
(with-current-buffer
(url-retrieve-synchronously
clipboard-url)
(dom-text
(car
(dom-by-tag (libxml-parse-html-region
(point-min)
(point-max))
'title))))))))))
(if interactivep
(cond ((and region-content clipboard-url (not point-in-link))
(delete-region (region-beginning) (region-end))
(insert org-link))
((and clipboard-url (not point-in-link))
(insert org-link))
(t
(call-interactively 'org-insert-link)))
org-link)))
;;; Navigate headings with widening
(defun +org-next-heading-widen (arg)
"Find the ARGth next org heading, widening if necessary."
(interactive "p")
(let ((current-point (point))
(point-target (if (> arg 0) (point-max) (point-min))))
(org-next-visible-heading arg)
(when (and (buffer-narrowed-p)
(= (point) point-target)
(or (and (> arg 0))
(and (< arg 0)
(= (point) current-point))))
(widen)
(org-next-visible-heading arg))))
(defun +org-previous-heading-widen (arg)
"Find the ARGth previous org heading, widening if necessary."
(interactive "p")
(+org-next-heading-widen (- arg)))
;;; Hooks & Advice
(defvar +org-before-save-prettify-buffer t
"Prettify org buffers before saving.")
(put '+org-before-save-prettify-buffer 'safe-local-variable #'booleanp)
(defun +org-before-save@prettify-buffer ()
(when +org-before-save-prettify-buffer
(save-mark-and-excursion
(mark-whole-buffer)
;;(org-fill-paragraph nil t)
(+org-fix-blank-lines t)
(org-align-tags t))))
(defun +org-delete-backward-char (N)
"Keep tables aligned while deleting N characters backward.
When deleting backwards, in tables this function will insert
whitespace in front of the next \"|\" separator, to keep the
table aligned. The table will still be marked for re-alignment
if the field did fill the entire column, because, in this case
the deletion might narrow the column."
(interactive "p")
(save-match-data
(org-check-before-invisible-edit 'delete-backward)
(if (and (= N 1)
(not overwrite-mode)
(not (org-region-active-p))
(not (eq (char-before) ?|))
(save-excursion (skip-chars-backward " \t") (not (bolp)))
(looking-at-p ".*?|")
(org-at-table-p))
(progn (forward-char -1) (org-delete-char 1))
(backward-delete-char-untabify N)
(org-fix-tags-on-the-fly))))
;;; Smarter {super,sub}scripts
;; https://old.reddit.com/r/emacs/comments/qzlzm0/what_are_your_top_key_bindings_rebindings_minor/hmwyhm3/
;; I don't use this currently because I found out about
;; `org-pretty-entities-include-sub-superscripts', which really does exactly
;; what I wanted.
(defface +org-script-markers '((t :inherit shadow))
"Face to be used for sub/superscripts markers i.e., ^, _, {, }.")
;; Hiding the super and subscript markers is extremely annoying
;; since any remotely complex equation becomes a chore. And leaving
;; it not raised is jarring to the eye. So this fontifies the
;; buffer just like how auctex does -- use a muted colour to
;; highlight the markup and raise the script.
(defun +org-raise-scripts (limit)
"Differences from `org-raise-scripts' are:
- It doesn't actually hide the markup used for super and subscript.
- It uses a custom face to highlight the markup: +org-script-markers.
- It doesn't require `org-pretty-entities' to be t."
(when (and org-pretty-entities-include-sub-superscripts
(re-search-forward
(if (eq org-use-sub-superscripts t)
org-match-substring-regexp
org-match-substring-with-braces-regexp)
limit t))
(let* ((pos (point)) table-p comment-p
(mpos (match-beginning 3))
(emph-p (get-text-property mpos 'org-emphasis))
(link-p (get-text-property mpos 'mouse-face))
(keyw-p (eq 'org-special-keyword (get-text-property mpos 'face))))
(goto-char (point-at-bol))
(setq table-p (looking-at-p org-table-dataline-regexp)
comment-p (looking-at-p "^[ \t]*#[ +]"))
(goto-char pos)
;; Handle a_b^c
(when (member (char-after) '(?_ ?^)) (goto-char (1- pos)))
(unless (or comment-p emph-p link-p keyw-p)
(put-text-property (match-beginning 3) (match-end 0)
'display
(if (equal (char-after (match-beginning 2)) ?^)
;; (nth (if table-p 3 1) org-script-display)
(nth 3 org-script-display)
;; (nth (if table-p 2 0) org-script-display)
(nth 2 org-script-display)))
(put-text-property (match-beginning 2) (match-end 2)
'face 'vz/org-script-markers)
(when (and (eq (char-after (match-beginning 3)) ?{)
(eq (char-before (match-end 3)) ?}))
(put-text-property (match-beginning 3) (1+ (match-beginning 3))
'face '+org-script-markers)
(put-text-property (1- (match-end 3)) (match-end 3)
'face '+org-script-markers)))
t)))
;; Extra link types
(defun +org-tel-open (number _)
"Notify the user of what phone NUMBER to call."
(message "Call: %s" number))
;; Make a horizontal rule!
(defun +org-horizontal-rule ()
"Make a horizontal rule after the current line."
(interactive nil org-mode)
(unless (eq (line-beginning-position) (line-end-position))
(end-of-line)
(newline))
(dotimes (_ fill-column)
(insert "-")))
;; Follow links, DWIM style
(defun +org-open-at-point-dwim (&optional arg)
"Open thing at point, or if there isn't something, list things."
(interactive "P")
(save-excursion
(let* ((this-char-type (org-element-type (org-element-context)))
(prev-char-type (ignore-errors
(save-excursion
(backward-char)
(org-element-type (org-element-context)))))
(types '(citation citation-reference clock comment comment-block
footnote-definition footnote-reference headline
inline-src-block inlinetask keyword link
node-property planning src-block timestamp))
(type this-char-type))
(when (and (memq this-char-type types) (memq prev-char-type types))
(backward-char)
(setq type prev-char-type)) ; what the fuckckckckck
;; Okay, so this ^ is pretty janky and doesn't /really/ work that well,
;; especially on DEADLINE (and probably SCHEDULED) lines. However, since
;; I really just want to open the list of URLs /most of the time/, I'm
;; fixing it like this instead.
(unless (and (memq type types)
(ignore-errors (org-open-at-point arg)))
(while (not
(progn
(org-back-to-heading)
(car (org-offer-links-in-entry (current-buffer) (point) 1))))
(org-up-heading-all 1))
(org-open-at-point arg)))))
;;; Open local HTML files with `browse-url'
(defun +org-open-html (file-path link-string)
"Open FILE-PATH with `browse-url'.
This function is intended to use with `org-file-apps'. See the
documentation of that function for a description of the two
arguments here, FILE-PATH and LINK-STRING."
(message "Opening %s (%s)..." file-path link-string)
(browse-url file-path))
(defun +org-insert-horizontal-rule (prefix)
"Insert a horizontal rule (-----) after the current line.
With PREFIX, insert before the current line."
(interactive "P")
(if prefix
(move-beginning-of-line nil)
(move-end-of-line nil)
(forward-line 1))
(insert "-----\n"))
;;; Make code snippets in org-mode easier to type
;; http://mbork.pl/2022-01-17_Making_code_snippets_in_Org-mode_easier_to_type
(defun +org-insert-backtick ()
"Insert a backtick using `org-self-insert-command'."
(interactive)
(setq last-command-event ?`)
(call-interactively #'org-self-insert-command))
(defvar-local +org-insert-tilde-language nil
"Default language name in the current Org file.
If nil, `org-insert-tilde' after 2 tildes inserts an \"example\"
block. If a string, it inserts a \"src\" block with the given
language name.")
(defun +org-insert-tilde ()
"Insert a tilde using `org-self-insert-command'."
(interactive)
(if (string= (buffer-substring-no-properties (- (point) 3) (point))
"\n~~")
(progn (delete-char -2)
(if +org-insert-tilde-language
(insert (format "#+begin_src %s\n#+end_src"
+org-insert-tilde-language))
(insert "#+begin_example\n#+end_example"))
(forward-line -1)
(if (string= +org-insert-tilde-language "")
(move-end-of-line nil)
;;(org-edit-special) ; Useful really only with splits.
))
(setq last-command-event ?~)
(call-interactively #'org-self-insert-command)))
(provide '+org)
;;; +org.el ends here