Completion at Point Function (CAPF) for Org-Mode Links

Leveraging cape and org-element-map

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.

update:

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 string Org-Mode with a 'link property of https://orgmode.org.
  • Given [[denote:20230506T202945][Title of Note]] then return the string Title of Note with a 'link property of denote: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.