I write a lot of things using Org-Mode. One function I have wanted is auto-completion of links that already exist in the current Org-Mode buffer. I have created custom links for abbreviations, epigraphs, dates, and glossary of terms.
I spent a bit of time writing that function. I remembered Org-Roam’s completion functions, so I started there for inspiration.
Writing Some Emacs Lisp
I looked to org-roam-complete-link-at-point for inspiration. I need a function that returns the text of the links. Along with the text, I would need the raw-link
.
Below is jf/org-links-with-text
, the function I wrote.
Here’s the link to jf/org-links-with-text.
I updated the jf/org-links-with-text
to handle links without labels/text.
(defun jf/org-links-with-text (&optional given-link)
"Return the `distinct-' `org-mode' links in the
`current-buffer'.
Each element of the list will be a `propertize' string where the
string value is the text of the link and the \"link\" property
will be the :raw-link.
When provided a GIVEN-LINK stop processing when we encounter the
first matching link."
(let ((links
(org-element-map
(org-element-parse-buffer)
'link
(lambda (link)
(when-let* ((left (org-element-property :contents-begin link))
(right (org-element-property :contents-end link)))
(let ((returning
(propertize
(buffer-substring-no-properties left right)
'link (org-element-property :raw-link link))))
(if given-link
(when (string= given-link returning)
returning)
returning))))
nil
given-link)))
;; Ensure that we have a distinct list.
(if (listp links)
(-distinct links)
(list links))))
The above loops through all link
elements. Assembling a propertized string with each link it encounters. When provided a given-link
it halts processing on the first match. And then returns a list of the matches.
I reference Org Element API when writing the function.
Here are some examples of the propertized string section of the code:
- Given
[[https://orgmode.org][Org-Mode]]
then return the stringOrg-Mode
with a'link
property ofhttps://orgmode.org
. - Given
[[denote:20230506T202945][Title of Note]]
then return the stringTitle of Note
with a'link
property ofdenote:20230506T202945
.
In other words, the CAPF
function I’m developing will handle all Org-Mode style links.
With that function, I turned to the inspiration of the org-roam-complete-link-at-point. Below is the function I wrote. Here’s the link to jf/org-capf-links.
;; Cribbed from `org-roam' org-roam-complete-link-at-point
(defun jf/org-capf-links ()
"Complete links."
(when (and (thing-at-point 'symbol)
(not (org-in-src-block-p))
(not (save-match-data (org-in-regexp org-link-any-re))))
;; We want the symbol so that links such performing completion on
;; "org-mode" will look for links with the text of org-mode and
;; then replace the text "org-mode" with the returned link.
(let ((bounds (bounds-of-thing-at-point 'symbol)))
(list (car bounds) (cdr bounds)
;; Call without parameters, getting a links (filtered by CAPF
;; magic)
(jf/org-links-with-text)
:exit-function
(lambda (text _status)
;; We want the properties of that link. In the case of one
;; match, the provided text will have the 'link property.
;; However if the
(let ((link (car (jf/org-links-with-text text))))
(delete-char (- (length text)))
(insert "[[" (get-text-property 0 'link link) "]"
"[" text "]]")))
;; Proceed with the next completion function if the returned
;; titles do not match. This allows the default Org capfs or
;; custom capfs of lower priority to run.
:exclusive 'no))))
The above jf/org-capf-links
function has three significant parts:
The (when (and…
section guards running in a context where things might get confusing.
The (bounds-of-thing-at-point 'symbol)
section checks the current item; I could use either 'symbol
or 'word
; but 'symbol
means I can complete or links that have dashes.
The :exit-function
, and this is where I spent significant time. In my first round of testing, I had a simple Org-Mode buffer that had one link. When I called the Completion at Point Function (CaPF 📖) function (via TAB) the lambda
’s text
parameter would have the propertized value.
However, when I had multiple candidates, and selected one, the lambda
’s text
parameter would not have the propertized value. Hence, I had to go and find again the property.
Last, I wire this into my Org-Mode. To test the functions prior, I had already done this. I use the Corfu and Cape packages. Below is the configuration for my Org-Mode CaPFs:
(defun jf/org-capf ()
"The `completion-at-point-functions' I envision using for `org-mode'."
(setq-local completion-at-point-functions
(list (cape-super-capf
#'jf/org-capf-links
#'tempel-expand
#'cape-file))))
(add-hook ‘org-mode-hook #’jf/org-capf)
Conclusion
Given that I write between one thousand and four thousand words per day in Org-Mode and I do a lot of linking to code, glossaries, and external sites, I felt it worth the time and energy to write up a CaPF that could help reduce context shifting.
Now, when I write, I can use my TAB completion to provide link candidates to insert.