Linking to and exporting function definitions in Org Mode

| emacs, org
  • [2024-01-11 Thu]: Added ?link=1 to copy the context link
  • 2023-09-12: added a way to force the defun to start open with ?open=1
  • 2023-09-05: fixed the completion to include defun:

I'd like to write more blog posts about little Emacs hacks, and I'd like to do it with less effort. Including source code is handy even when it's missing some context from other functions defined in the same file, since sometimes people pick up ideas and having the source code right there means less flipping between links. When I'm working inside my config file or other literate programming documents, I can just write my blog post around the function definitions. When I'm talking about Emacs Lisp functions defined elsewhere, though, it's a little more annoying to copy the function definition and put it in a source block, especially if there are updates.

The following code creates a defun link type that exports the function definition. It works for functions that can be located with find-function, so only functions loaded from .el files, but that does what I need for now. Probably once I post this, someone will mention a much more elegant way to do things. Anyway, it makes it easier to use org-store-link to capture a link to the function, insert it into a blog post, navigate back to the function, and export HTML.

(defun my-org-defun-complete ()
  "Return function definitions."
  (concat "defun:"
          (completing-read
           "Function: "
           #'help--symbol-completion-table
           #'fboundp
           'confirm
           nil nil))) ;    (and fn (symbol-name fn)) ?

(defun my-org-defun-link-description (link description)
  "Add documentation string as part of the description"
  (unless description
    (when (string-match "defun:\\(.+\\)" link)
      (let ((symbol (intern (match-string 1 link))))
        (when (documentation symbol)
          (concat (symbol-name symbol) ": "
                  (car (split-string (documentation symbol) "\n"))))))))

(defun my-org-defun-open-complete ()
  "Return function definitions."
  (concat "defun-open:"
          (completing-read
           "Function: "
           #'help--symbol-completion-table
           #'fboundp
           'confirm
           nil nil)))

(defun my-org-defun-open-export (link description format _)
  (my-org-defun-export (concat link (if (string-match "\\?" link) "&open=1" "?open=1")) description format _))

(defun my-org-defun-export (link description format _)
  "Export the function."
  (let (symbol params path-and-query)
    (if (string-match "\\?" link)
        (setq path-and-query (url-path-and-query (url-generic-parse-url link))
              symbol (car path-and-query)
              params (url-parse-query-string (cdr path-and-query)))
      (setq symbol link))
    (save-window-excursion
      (my-org-defun-open symbol)
      (let ((function-body (buffer-substring (point)
                                             (progn (forward-sexp) (point))))
            body)
        (pcase format
          ((or '11ty 'html)
           (setq body
                 (if (assoc-default "bare" params 'string=)
                     (format "<div class=\"org-src-container\"><pre class=\"src src-emacs-lisp\">%s</pre></div>"
                             (org-html-do-format-code function-body "emacs-lisp" nil nil nil nil))
                   (format "<details%s><summary>%s</summary><div class=\"org-src-container\"><pre class=\"src src-emacs-lisp\">%s</pre></div></details>"
                           (if (assoc-default "open" params 'string=) " open"
                             "")
                           (or description
                               (and (documentation (intern symbol))
                                    (concat
                                     symbol
                                     ": "
                                     (car (split-string (documentation (intern symbol)) "\n"))))
                               symbol)
                           (org-html-do-format-code function-body "emacs-lisp" nil nil nil nil))))
           (when (assoc-default "link" params)
             (setq body (format "%s<div><a href=\"%s\">Context</a></div>" body (my-copy-link))))
           body)
          (`ascii function-body)
          (_ function-body))))))

(defun my-org-defun-store ()
  "Store a link to the function."
  (when (derived-mode-p 'emacs-lisp-mode)
    (org-link-store-props :type "defun"
                          :link (concat "defun:" (lisp-current-defun-name)))))

(defun my-org-defun-open (symbol &rest _)
  "Jump to the function definition.
If it's from a tangled file, follow the link."
  (find-function (intern (replace-regexp-in-string "\\?.*$" "" symbol)))
  (when (re-search-backward "^;; \\[\\[file:" nil t)
    (goto-char (match-end 0))
    (org-open-at-point-global)
    (when (re-search-forward (concat "( *defun +" (regexp-quote (replace-regexp-in-string "\\?.*$" "" symbol)))
                             nil t)
      (goto-char (match-beginning 0)))))

(org-link-set-parameters "defun" :follow #'my-org-defun-open
                         :export #'my-org-defun-export
                         :complete #'my-org-defun-complete
                         :insert-description #'my-org-defun-link-description
                         :store #'my-org-def-store)

(org-link-set-parameters "defun-open" :follow #'my-org-defun-open
                         :export #'my-org-defun-open-export
                         :complete #'my-org-defun-open-complete
                         :insert-description #'my-org-defun-link-description
                         :store #'my-org-def-store)

my-copy-link is at https://sachachua.com/dotemacs#web-link.

TODO Still allow linking to the file

Sometimes I want to link to a defun and sometimes I want to link to the file itself. Maybe I can have a file link with the same kind of scoping so that it kicks in only when defun: would also kick in.

(defun my-org-defun-store-file-link ()
  "Store a link to the file itself."
  (when (derived-mode-p 'emacs-lisp-mode)
    (org-link-store-props :type "file"
                          :link (concat "file:" (buffer-file-name)))))
(with-eval-after-load 'org
  (org-link-set-parameters "_file" :store #'my-org-defun-store-file-link))
View org source for this post
This is part of my Emacs configuration.
You can comment with Disqus or you can e-mail me at sacha@sachachua.com.