2024-08-19 Opening all links in an Org subtree

A few days ago I had an extremely specific need. I had an Org mode subtree consisting entirely of a list of URLs, and I wanted to open them all in my browser.

Perhaps the simplest way to do that would be to use a keyboard macro. Since all the URLs started in the same position in their lines, navigating to the next link would be just C-n. Even if they were not, Org mode has the org-next-link command (bound to C-c C-x C-n), so navigating to the next and previous links (it has the “previous” counterpart, of course) is simple.

I decided, however, to write a custom command to do that, just to show how easy it is to extend Emacs (and Org mode) with your own code. (And it turned out that I needed just about 15 minutes to do that!) The only thing requiring a moment of thought is the stop condition for the loop I’m going to write. Unfortunately, the docstring of org-next-link doesn’t say what happens if there is no next link in the buffer. Well, looking at its source code told me that there are two ways of knowing if the search failed. Neither is very nice – one is to check the value of the org-link--search-failed variable (but the double dash indicates that it’s an “internal” variable, meaning its name and/or meaning is subject to change without notice), and the other is to check if the return value from org-next-link is the string "No further link found" (since in that case this is what that command tells the user in the echo area via message, which returns the string it printed). In an ideal world, org-next-link would have an interactive parameter, which – when set to nil – would prohibit it from polluting the echo area and make sure the return value clearly indicates the result of the search.

Of course, it’s not really a problem. I’m fine with using the internal variable – in the unlikely case the code of org-next-link changes and this no longer works, my function would just enter an infinite loop, open my links many times in the browser and require me to press C-g to exit the loop and close all the tabs it managed to open (when there is no next link, org-next-link wraps around). This would be slightly annoying, but not dangerous in any real sense. I’m also fine with seeing a spurious No further link found message, but I’ll make it not appear just to show how easy it is to do it.

(defun org-open-all-links-in-subtree ()
  "Open all the links in the current subtree.
Note: this uses Org's internal variable `org-link--search-failed'."
  (interactive)
  (save-excursion
    (save-restriction
      (org-narrow-to-subtree)
      (goto-char (point-min))
      (let ((inhibit-message t)
            (message-log-max nil))
        (setq org-link--search-failed nil)
        (while (progn (org-next-link)
                      (not org-link--search-failed))
          (org-open-at-point))))))

Note the strange shape of the while loop. We want first to try to get to the next link and only open it if that succeeds. That means that we start with org-next-link to get to the first link. (This would break if the point started on a link – Emacs would immediately move to the next one and not open the first. However, a subtree cannot start with a link – it should start with one or more stars – so the earlier (goto-char (point-min)) ensures that the starting point is not on a link. Well, it’s still not 100% reliable, for example in the case of an Org file beginning with a link and having no subtrees, but I didn’t care much for such edge cases with a convenience command I write for myself.) Then, if there is no next link (for example, when the subtree has no links at all, or when – on one of the subsequent iterations – we tried to move on from the last link), we exit the loop. Otherwise, we open the link at point and repeat.

Another thing I’d like to mention is how we avoid both polluting the echo area (using inhibit-message) and the *Messages* buffer (using message-log-max). This is probably something worth remembering (in fact, I didn’t remember these two variable names myself and had to look them up…).

The last thing I’d like to say here is that if you want to learn to write commands like this yourself, I recommend – as I usually do – the great Introduction to programming in Emacs Lisp by the late Robert J. Chassell first, and then Hacking your way around in Emacs by yours truly if you want more.

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode