(this is a slightly modified extract from my Doom Emacs configuration)
While writing with Org mode, I frequently need to insert links to other headings within my local document. I started by doing this manually, inserting a CUSTOM_ID
property in the destination headline, and then creating the link.
Later, I discovered and now normally use counsel-org-link
(part of counsel, which is included and enabled by default with Ivy in Doom Emacs) for linking between headings in an Org document. It shows me a searchable list of all the headings in the current document, and allows selecting one, automatically creating a link to it. Since it doesn’t have a keybinding by default, let’s start by giving it one (C-c l l is the default +links
section in Doom Emacs):
(map! :after counsel :map org-mode-map
"C-c l l h" #'counsel-org-link)
I also configure counsel-outline-display-style
so that only the headline title is inserted into the link, instead of its full path within the document.
(after! counsel
(setq counsel-outline-display-style 'title))
counsel-org-link
uses org-id
as its backend, which generates IDs using UUIDs and stores them in the ID
property. I prefer using human-readable IDs stored in the CUSTOM_ID
property of each heading, so we need to make some changes.
First, configure org-id
to use CUSTOM_ID
if it exists. This instructs org-id
to grab those IDs when using the org-store-link
function (funny that org-id
knows how to recognize and use CUSTOM_ID
, but not how to generate them).
(after! org-id
;; Do not create ID if a CUSTOM_ID exists
(setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))
Second, I override counsel-org-link-action
, which is the function that actually generates and inserts the link, with a custom function that computes and inserts human-readable CUSTOM_ID
links. This is supported by a few auxiliary functions for generating and storing the CUSTOM_ID
.
(defun zz/make-id-for-title (title)
"Return an ID based on TITLE."
(let* ((new-id (replace-regexp-in-string "[^[:alnum:]]" "-" (downcase title))))
new-id))
(defun zz/org-custom-id-create ()
"Create and store CUSTOM_ID for current heading."
(let* ((title (or (nth 4 (org-heading-components)) ""))
(new-id (zz/make-id-for-title title)))
(org-entry-put nil "CUSTOM_ID" new-id)
(org-id-add-location new-id (buffer-file-name (buffer-base-buffer)))
new-id))
(defun zz/org-custom-id-get-create (&optional where force)
"Get or create CUSTOM_ID for heading at WHERE.
If FORCE is t, always recreate the property."
(org-with-point-at where
(let ((old-id (org-entry-get nil "CUSTOM_ID")))
;; If CUSTOM_ID exists and FORCE is false, return it
(if (and (not force) old-id (stringp old-id))
old-id
;; otherwise, create it
(zz/org-custom-id-create)))))
;; Now override counsel-org-link-action
(after! counsel
(defun counsel-org-link-action (x)
"Insert a link to X.
X is expected to be a cons of the form (title . point), as passed
by `counsel-org-link'.
If X does not have a CUSTOM_ID, create it based on the headline
title."
(let* ((id (zz/org-custom-id-get-create (cdr x))))
(org-insert-link nil (concat "#" id) (car x)))))
Ta-da! Now using counsel-org-link
inserts nice, human-readable links.
(tip of the hat: I got a lot of inspiration and some code for this from Emacs Org-mode: Use good header ids!)