Posts tagged "emacs":
Fault-tolerant Org Links
I’m sure many Org users have experienced this: you reorganize your notes, maybe renaming a file or moving a section to a different Org file, and a few weeks later you open a link in another note only to be greeted by an unexpected prompt: “No match - create this as a new heading?”. Org tries to be helpful, even creating a new buffer for the non-existent file, assuming all along that you are creating a wiki and normally insert in your text links to targets that don't exist yet. But what if that is not your use-case? What if, instead of popping a new buffer and disrupting your flow, you want to be told that you got a broken link (knowing full well that the link target exists somewhere)? Then you can utter an expletive and carry on reading whatever you were reading, or try to find the intended target and fix the link.
Broken Org links are an unfortunate fact of life when your files and headings change over time. In my case, I kept stumbling on dead links in my org notes that have been curated for decades and survived multiple moves between cloud storage providers, note management systems (remember remember.el
?), and other reorgs. I am not a big fan of spending a lot of time migrating my files and rewiring everything proactively. I wished for an Org setup that would detect a broken link and fix it right there and then, as I tried to follow it. In a sense, I wished for Org links to be fault-tolerant. At the same time, I didn't want a heavy solution, with its own consistency and maintenance burden, like globally unique Org Ids or a custom database.
I created a small set of tools to help detect and repair broken links in my Org files on the fly. My Org Link Repair
code consists of three little helpers:
- A checker hook
/org-test-file-link
that intercepts broken links before Org tries to apply its built-in 'nonexistent target' logic. - A transient menu
/olr-transient
to provide a quick interface for automated and manual broken link recovery tasks. - An interactive repair mode
/olr-manual-mode
that guides a user through fixing broken links one by one.
Together, these additions make it much easier to stay on top of link rot in my notes without altering how I normally create and use Org links. Let’s look at each part and how they work together in practice.
A side note on the UX: One of my design goals was to guide the user to perform the needed actions without relying on their familiarity with Org Link Repair
flow. I expect this flow to be exercised rarely enough that even a user who has done it before is not expected to remember key bindings or the steps to repair their broken link. The code should try to make the process seamless and straightforward.
The helpers that I show are meant as a starting point and can be adapted or extended. I implemented detection of broken file links and a manual (user-assisted) repair strategy, because file links were the ones breaking for me and the manual strategy is the most general (the correct target file may be in an abandoned Google drive, an encrypted file bundle, or anywhere). Other link types could be tested and different repair strategies could be implemented, including a fully automated strategy, if the likely target file location is known, or can be easily searched for. Even web links could be handled similarly: detect broken links to web pages that have disappeared, and rewrite them to use a web archive (like the Wayback machine).
If you can’t prevent links from breaking, at least make them easy to find and fix.
Catching Broken Links
The first thing to do is to change the value of org-link-search-must-match-exact-headline
from its default setting of query-to-create
. That eliminates the wiki-centric query to create a new heading when following a broken link. But it doesn't prevent Org from popping a new buffer for a link pointing to a nonexistent file name. To suppress that, we need to do a bit more work.
Luckily Org developers provided the org-open-at-point-functions
hook which makes it straightforward to intercept the link opening flow and detect a broken link due to non-existent file early. Here is my interceptor that checks for broken file links and bails out with a user error on non-existent files. It could be expanded to handle other link types and other broken link scenarios. Note that the error message tells the user what key binding to use to initiate the link repair.
(custom-set-variables '(org-link-search-must-match-exact-headline t)) (defun /org-test-file-link () "Check if the file link target file exists before following it." (let ((ctx (org-element-context))) (when (and (eq (org-element-type ctx) 'link) (string= (org-element-property :type ctx) "file")) (let ((file (org-element-property :path ctx))) ;; If the file exists, return nil to let org-open-at-point continue (if (not (file-exists-p file)) (user-error (concat "Target file not found; Use " (substitute-command-keys "\\[/olr-transient]") " to repair link to %s") file)))))) (add-hook 'org-open-at-point-functions #'/org-test-file-link)
A Transient Menu for Link Repair Tasks
I am using Emacs’ Transient library (the same engine behind Magit’s menus) to create a one-stop menu for all Org Link Repair
activities. The command /olr-transient
is a prefix command that, when invoked, pops up a transient menu with several relevant actions. This spares me from memorizing multiple separate commands or key bindings. I just hit one key sequence to get the menu, then select what I need. Here’s my initial definition of the transient menu:
(transient-define-prefix /olr-transient () "Transient menu for Org Link Repair." [:description "Org Link Repair transient: fix your broken links\n\n" ["Test/Repair" ("l" "Lint all links in the buffer" /org-lint-links :transient nil) ("m" "Manually find the new target" /olr-manual-mode :transient nil)] ["Display/Navigate" ("n" "Next link" org-next-link :transient t) ("p" "Previous link" org-previous-link :transient t) ("d" "Display toggle" org-toggle-link-display :transient t)] ["Other" ("q" "Quit" transient-quit-one :transient nil)]]) (global-set-key (kbd "<f2> <return>") #'/olr-transient)
The manual repair strategy is the only one offered for now. The menu also offers linting the links in the current buffer (I have a customized version of the built-in org-lint
for that), link navigation and display toggling commands.
Using a transient menu here feels like overkill for just a few commands, but I anticipate adding more link-related utilities over time. Even now, it’s nice to have a single “hub” for link management. I don’t use it every day, but when I suspect there might be broken links, I know where to go. It’s also convenient when a broken link does pop up unexpectedly. I can quickly bring up this menu and choose to repair it on the spot.
Manual Repair Strategy — Guided Link Fixing
This is the most general strategy which is why I implemented it first. The tradeoff is that it relies on the user knowing where the intended link target is and navigating to it. I found that I usually remember what happened to my abandoned Org files, even after years of not visiting them. I can usually recover them from an old archive, or one of my no-longer-used Dropbox accounts.
The strategy implements a global minor mode and a set of functions to initiate the repair flow and to complete it. When the user chooses to use this strategy, the code remembers the current location (the location of the broken link) and activates the /olr-manual-mode
minor mode while the user is free to do whatever they need to locate the correct target org file and a headline. A mode line lighter provides a visual clue that the repair flow is in progress. Once the target has been located, the user would hit C-c C-c
to complete the repair, which will interpret the current point as the intended link target. The code will replace the broken link at the starting location with the new link. The user is free to abandon the flow at any time with C-c C-k
.
Here is my code:
;; ;;; Org Link Repair - Manual (user-assisted) Strategy ;; (defvar /olr-manual-marker nil "Marker pointing at the original (broken) link.") (defvar /olr-manual-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-c") #'/olr-manual-complete) (define-key map (kbd "C-c C-k") #'/olr-manual-abort) map) "Keymap for `/olr-manual-mode'.") (easy-menu-define /olr-manual-mode-menu /olr-manual-mode-map "Menu for OLR Manual Mode" '("OrgLinkRepairManualMode" ["Complete" /olr-manual-complete t] ["Abort" /olr-manual-abort t])) (define-minor-mode /olr-manual-mode "Global minor mode for Org Link Repair manual strategy. When enabled, the marker pointing at the link at point is saved. The user is expected to navigate to where the link should be pointing at and call `/olr-manual-complete' to repair the link, or `/olr-manual-abort' to cancel. Attempting to enable this minor mode outside an Org-mode derivative, or if the point is not at an Org link will fail with a user error." :lighter " LinkRepair" :global t (if (not /olr-manual-mode) (setq /olr-manual-marker nil) (unless (derived-mode-p 'org-mode) (user-error "Not in an Org buffer")) (unless (eq (org-element-type (org-element-context)) 'link) (user-error "Not at an Org link")) (setq /olr-manual-marker (point-marker)) (message (substitute-command-keys "Manual link repair mode initiated. Navigate to intended link target, press \\[/olr-manual-complete] to complete, or \\[/olr-manual-abort] to abort." )))) (defun /olr-manual-complete () "Complete Org Link Repair by replacing the broken link at saved marker with a new link targeted at point. The user is expected to have navigated to the location of the new link target. This function will call `org-store-link', then use `org-insert-all-links' to replace the broken link, location of which was saved by `/olr-manual-mode'." (interactive) (org-store-link nil t) (unless (and /olr-manual-marker (marker-position /olr-manual-marker)) (error "OrgLinkRepair: Lost marker to the original link location")) (switch-to-buffer (marker-buffer /olr-manual-marker)) (goto-char (marker-position /olr-manual-marker)) (/olr-manual-mode -1) (let* ((oldctx (org-element-context)) (oldstart (org-element-property :begin oldctx)) (oldend (org-element-property :end oldctx)) oldlink newlink) ;; Delete the old link at point (when (and oldstart oldend) (setq oldlink (buffer-substring oldstart oldend)) (delete-region oldstart oldend)) ;; Insert the new link (org-insert-all-links 1 "" "") (let* ((newctx (org-element-context)) (newstart (org-element-property :begin newctx)) (newend (org-element-property :end newctx))) (goto-char newstart) (setq newlink (buffer-substring newstart newend))) ;; Notify the user: audibly+visibly (hopefully after auto-revert messages) (ding) (run-with-idle-timer 0.2 nil (lambda () (message (concat "Modified buffer by replacing link %s with %s." "\nSave the buffer to keep changes!") oldlink newlink))))) (defun /olr-manual-abort () "Abort manual Org Link Repair." (interactive) (unless (and /olr-manual-marker (marker-position /olr-manual-marker)) (error "OrgLinkRepair: Lost marker to the original link location")) (switch-to-buffer (marker-buffer /olr-manual-marker)) (goto-char (marker-position /olr-manual-marker)) (/olr-manual-mode -1) ;; Notify the user (message "Org Link Repair aborted."))
Limitations and Next Steps
Not a Complete Solution: This toolkit currently provides early interception for broken file links only. It could be extended to catch other link types if doing it early would be beneficial. For example opening web links may pop a browser window, which is annoying if we could know ahead of time that it will fail. The manual repair strategy will work for any link type, as long as it is supported by org-store-link
. Again, not for web links opened in a browser.
Manual Effort: While the repair mode makes fixing easier, it’s still a manual process. I have to find the new targets or decide to remove links. There’s room for automation, e.g. suggesting likely new locations for a file (perhaps by searching for a filename in a known directory). At the moment, I actually prefer the manual control, but smarter suggestions could speed things up.
Workflow UX: I experimented with making a nicer user experience during the manual link repair workflow. I wanted to make it visually clear that the user is in the workflow and is expected to either complete it or abort it. The global minor mode lighter in the mode line doesn't seem to be enough. I tried sticking a header-line at the top, displaying a banner message and key bindings to complete/abort, but it was not reliable, and didn't look great either. I have some other ideas to try, but if you have a suggestion please let me know.
Despite these limitations, the gain in convenience has been huge for me. I can freely rename files or reorganize headings, knowing that if I forget to update a reference, Emacs will help me catch it later. And fixing it is straightforward. This is a relatively small addition to my Emacs config (just a few dozen lines of Elisp), but it solves an annoying real problem that used to steal time and momentum. And by the way, I do have LLM generated test cases for this code (see my previous blog post).
Enjoy the malleability of Emacs and the freedom it gives you!
Discuss this post on Reddit.
Towards Auto-Generated ERT Unit Tests
Rigorous testing clearly benefits software projects, yet many Emacs Lisp packages have minimal tests. You might think manual testing during development is enough—but that only works if the code never changes and has no evolving dependencies. Automated tests, however, give you the confidence to modify code without fear of unintentionally breaking functionality. They quickly catch issues caused by changing dependencies, and coverage tools highlight tested and untested functionality.
Benefits aside, writing test cases can feel like a chore. As an enterprise software developer, I disliked it as much as anyone. But now, as I occasionally work on Emacs Lisp packages mostly for personal use, I'm finding that a lack of automated tests costs me valuable time. We've all experienced making seemingly harmless changes, only to discover obscure bugs weeks later that automated tests might have caught immediately.
I want automated tests for my Emacs Lisp code—whether it’s a published package or just a personal library of functions—but I'd rather not write them manually. I've long dreamed of using LLMs to generate test cases. So, is this approach already viable, particularly for Emacs Lisp unit tests? Writing unit tests feels like an ideal scenario for current LLMs that may lack extensive Emacs Lisp training: unit tests are simpler than integration or performance tests, less sensitive to hallucinations, and easy to adjust or discard if problematic.
At this stage, I'm not aiming for a sleek Emacs integration. I just want to see if the approach works. Using ChatGPT in a browser with some simple copy-pasting is enough. I started by asking ChatGPT (using the o3-mini-high
and 4o
models) to help set up ERT tests for my personal library functions, loaded from my init.el
. My goal was to run the tests externally, in batch mode, separate from my main Emacs instance.
ChatGPT performed reasonably well. After a few iterations, partly due to peculiarities in my init.el
configuration, I ended up with a test file containing a dummy test that I could successfully run externally using:
emacs --batch -Q \ -l test/test-pp-lib.el \ -f ert-run-tests-batch-and-exit
Next, I gave ChatGPT the code for my /org-next-visible-link
function (see my previous blog post) and asked it to generate a complete suite of unit tests aimed at maximizing coverage. The generated tests looked reasonable, but several failed due to small, silly issues. Some failures were caused by missing double backslashes to properly escape [
in looking-at
patterns. Others were due to ChatGPT "misunderstanding" the behavior of the function: if point is already at the beginning of a link, /org-next-visible-link
will skip to the next one. It was easy enough to fix these manually.
However, one test kept failing. It involved text visibility, which is the core aspect that /org-next-visible-link
is supposed to handle. I pasted the ERT error and backtrace into ChatGPT and asked it to find the problem. It claimed to have identified and fixed the issue, but the test failed again—a common pattern when LLMs hallucinate fixes that don’t actually work.
Next, I tried executing the test steps manually in a buffer. The test failed for me too! That’s when I realized the problem might not be in the test, but in my code. I asked ChatGPT to help me find the bug. I gave it a big hint: during manual testing, I noticed the failure occurred in a specific corner case, when a link was inside a folded section with no text following it. I explained that adding any text after the link would make the test pass. What happened next was quite impressive.
ChatGPT took its time performing its inference-based iterative reasoning. It spent about three minutes analyzing the function code, the test code, and the failed test backtrace. When it responded, it correctly identified a bug in my code, explained the underlying problem, and suggested a specific code change to fix it. I applied the fix, and the test passed.
No more excuses: Every non-trivial function deserves a unit test.
For completeness, here is the code of the function under test (with the fix), and the unit tests generated by ChatGPT.
;; Adapted from org-next-link to only consider visible links (defun /org-next-visible-link (&optional search-backward) "Move forward to the next visible link. When SEARCH-BACKWARD is non-nil, move backward." (interactive) (let ((pos (point)) (search-fun (if search-backward #'re-search-backward #'re-search-forward))) ;; Tweak initial position: make sure we do not match current link. (cond ((and (not search-backward) (looking-at org-link-any-re)) (goto-char (match-end 0))) (search-backward (pcase (org-in-regexp org-link-any-re nil t) (`(,beg . ,_) (goto-char beg))))) (catch :found (while (funcall search-fun org-link-any-re nil t) (let ((folded (org-invisible-p (match-beginning 0) t))) (when (or (not folded) (eq folded 'org-link)) (let ((context (save-excursion (unless search-backward (forward-char -1)) (org-element-context)))) (pcase (org-element-lineage context '(link) t) (link (goto-char (org-element-property :begin link)) (throw :found t))))))) (goto-char pos) ;; No further link found nil)))
;;; test-pp-lib.el —–– tests for pp-lib.el ;; Ensure test directory is on load-path so we can require test-helper (add-to-list 'load-path (file-name-directory #$)) (require 'test-helper) (require 'ert) (require 'org) ;; for org-mode, org-element, org-link-any-re (require 'cl-lib) ;; for cl-letf ;;; tests for `/org-next-visible-link' (ert-deftest org-next-visible-link-forward-basic () "Move to the first link in forward direction and return non-nil." (with-temp-buffer (insert "foo [[A]] bar [[B]] baz") (org-mode) (goto-char (point-min)) (should (/org-next-visible-link)) (should (looking-at "\\[\\[A\\]\\]")))) (ert-deftest org-next-visible-link-forward-second-link () "Subsequent `org-next-visible-link' should find the next link." (with-temp-buffer (insert "foo [[A]] bar [[B]] baz") (org-mode) (goto-char (point-min)) (/org-next-visible-link) (should (/org-next-visible-link)) (should (looking-at "\\[\\[B\\]\\]")))) (ert-deftest org-next-visible-link-forward-skip-current () "When point is at the beginning of a link, skip it and find the next." (with-temp-buffer (insert "[[A]] [[B]]") (org-mode) (goto-char (point-min)) (should (/org-next-visible-link)) (should (looking-at "\\[\\[B\\]\\]")))) (ert-deftest org-next-visible-link-backward-basic () "Move backward to the nearest previous link and return non-nil." (with-temp-buffer (insert "first [[A]] then [[B]] then [[C]]") (org-mode) (goto-char (point-max)) (should (/org-next-visible-link t)) (should (looking-at "\\[\\[C\\]\\]")))) (ert-deftest org-next-visible-link-backward-second-link () "Second backward invocation finds the prior link." (with-temp-buffer (insert "[[A]] [[B]]") (org-mode) (goto-char (point-max)) (/org-next-visible-link t) (should (/org-next-visible-link t)) (should (looking-at "\\[\\[A\\]\\]")))) (ert-deftest org-next-visible-link-backward-skip-current () "When point is on a link, backward skips it and finds the previous." (with-temp-buffer (insert "[[X]] [[Y]]") (org-mode) ;; position right at the beginning of Y (goto-char (point-min)) (/org-next-visible-link) ; forward to [[X]] ;; now move to Y (/org-next-visible-link) ;; test backward skip (goto-char (point)) (should (/org-next-visible-link t)) (should (looking-at "\\[\\[X\\]\\]")))) (ert-deftest org-next-visible-link-no-link () "With no links, returns nil and point does not move." (with-temp-buffer (insert "no links here") (org-mode) (goto-char (point-min)) (should-not (/org-next-visible-link)) (should (= (point) (point-min))))) (ert-deftest org-next-visible-link-skip-in-folded-headline () "Skip links that reside in a folded headline body." (with-temp-buffer (org-mode) (insert "* Heading1\n[[SKIP]]\n* Heading2\n[[FIND]]\n") ;; Fold the first subtree so its body (and the [[SKIP]] link) is hidden (goto-char (point-min)) (org-cycle) ;; this folds the subtree under Heading1 ;; Now search forward: should skip [[SKIP]] and land on [[FIND]] (goto-char (point-min)) (should (/org-next-visible-link)) (should (looking-at "\\[\\[FIND\\]\\]")) ;; And then no more (should-not (/org-next-visible-link)))) (ert-deftest org-next-visible-link-allow-org-link-invisible () "Find links hidden with `invisible='org-link` overlays." (with-temp-buffer (insert "foo [[HIDDEN]] [[VISIBLE]]") (org-mode) ;; hide first link with 'org-link (goto-char (point-min)) (re-search-forward org-link-any-re) (let ((ov (make-overlay (match-beginning 0) (match-end 0)))) (overlay-put ov 'invisible 'org-link)) (goto-char (point-min)) ;; should still hit HIDDEN first (should (/org-next-visible-link)) (should (looking-at "\\[\\[HIDDEN\\]\\]")) ;; then hit VISIBLE (should (/org-next-visible-link)) (should (looking-at "\\[\\[VISIBLE\\]\\]")) (should-not (/org-next-visible-link)))) ;;; test-pp-lib.el ends here
Even with the current generation of ChatGPT models, I can confidently say they can be used to generate useful ERT unit tests. Is it perfect? Of course not. For example, the org-next-visible-link-backward-skip-current
test contains a bug that, only by chance, doesn’t cause a failure. It also includes a useless (goto-char (point))
call. Future LLMs will only get better: they’ll be able to fix or improve existing tests and generate new ones to increase code coverage. And tighter integration with Emacs, eventually reaching the point where tests are fully auto-generated, is just a matter of time.
There's no need to wait, though. I am sure existing tools like gptel.el
and aider.el
could already be used to provide a tighter integration experience, if desired. Experimenting with other LLM providers and their models might also yield even better unit test generation results.
Meanwhile, I'm off to work on improving my ERT setup to streamline running tests and debugging my code. From now on, all the Emacs Lisp code I work on will be accompanied by unit tests. I encourage you to do the same.
Enjoy the malleability of Emacs and the freedom it gives you!
Discuss this post on Reddit.
The TAB Key in Org Mode, Reimagined
I don't know about you, but when I'm reading something in an Org file and spot a link I want to follow, I instinctively press TAB
to jump to it—just like I would in an Info or Help buffer. Using TAB
for such field navigation is a common pattern across many applications, including Emacs. It’s also nicely symmetric with Shift-TAB (S-TAB
), which typically navigates backward. But in Org mode, TAB
triggers local visibility cycling: folding and unfolding text under the current headline. S-TAB
cycles visibility globally, folding and unfolding all the headlines. (Granted, if you don’t use Info or navigate Help buffers with TAB
, you might not miss that behavior in Org mode.)
See, we have this dichotomy in Org mode: it's both an authoring tool and a task/notes manager. For document authoring, Org markup serves as a source format that's later exported for publishing. In this context, visibility cycling is essential for managing structure and reducing distractions while writing. As a task and notes manager, Org is used to track notes, TODO lists, schedules, and data—content that's often read in place and never exported. Visibility cycling still helps, but it's generally less critical than in authoring mode.
This reading workflow within Org files makes me long for features found in more reading-focused modes. Sure, I don’t treat my Org files as read-only; reading and editing are fluidly intertwined. Still, when I'm focused on reading, I want the TAB
key to handle navigation, not headline visibility cycling. And I don't want to switch to another mode like View mode just to get a better reading experience.
It's well known that the TAB
key is heavily overloaded in Emacs, especially in Org mode. Depending on context and configuration, it can perform one of four types of actions: line indentation, candidate completion (during editing), or field navigation and visibility cycling (during reading). As mentioned earlier, TAB
is commonly used for field navigation in Info and Help modes. But note that even Org mode uses it this way within tables. Its association with visibility cycling was unique to Org mode until recently, when it was made an option in Outline mode too.
Personally, I want to move in the opposite direction: removing visibility cycling from the list of TAB
-triggered actions. Three types of behavior are already plenty. I'd rather assign visibility control to a more complex keybinding and prioritize field navigation instead. I'm not a big fan of cycling in general (see my previous blog post), and would prefer to jump directly to specific folding levels. I also value consistency in keybindings, so unifying TAB
behavior across modes is important to me.
TAB
: indentation and completion when editing; field navigation when reading
I decided to give it a try and remap TAB
in Org mode to primarily perform field navigation. What exactly is considered a “field” is largely up to the user. In general, it should be a structural element in a file where a non-trivial action can be performed, making it useful to have an easy way to jump between them. For my setup, I chose to treat only links and headlines as fields, similar to how Info handles navigation. Of course, others might include property drawers, code blocks, custom buttons, or other interactive elements. I wouldn't overdo it though—too many fields and TAB
navigation loses its utility.
I remapped TAB
in Org mode to navigate to the next visible heading or link, and S-TAB
to move to the previous one. Headlines and links inside folded sections are skipped. For visibility cycling, I now rely on Org Speed Keys (a built in feature of Org mode).
Speed Keys let you trigger commands with a single keystroke when the point is at the beginning of a headline. They’re off by default but incredibly handy once enabled. A number of keys are predefined out of the box; for example, c
is already mapped to org-cycle
, which is what TAB
normally does in Org mode.
I’ve had Speed Keys enabled for ages (mainly using them for forward/backward headline navigation), but I had never used c
for visibility cycling—until now. And it gets even better: the combination of TAB
/ S-TAB
to jump between fields, followed by a speed key at the headline, turns out to be quite powerful.
What about the other actions TAB
usually performs in Org files? For now, I rely on M-x org-cycle
when needed. The org-cycle
command is quite sophisticated and can fall back to other TAB
behaviors like indentation when appropriate. That said, I’ve been using my custom TAB
/ S-TAB
bindings for months now and haven’t run into any situations where I missed the default behavior.
Want to give it a try? Here’s the code you can drop into your init.el
:
(defun /org-next-visible-heading-or-link (&optional arg) "Move to the next visible heading or link, whichever comes first. With prefix ARG and the point on a heading(link): jump over subsequent headings(links) to the next link(heading), respectively. This is useful to skip over a long series of consecutive headings(links)." (interactive "P") (let ((next-heading (save-excursion (org-next-visible-heading 1) (when (org-at-heading-p) (point)))) (next-link (save-excursion (when (/org-next-visible-link) (point))))) (when arg (if (and (org-at-heading-p) next-link) (setq next-heading nil) (if (and (looking-at org-link-any-re) next-heading) (setq next-link nil)))) (cond ((and next-heading next-link) (goto-char (min next-heading next-link))) (next-heading (goto-char next-heading)) (next-link (goto-char next-link))))) (defun /org-previous-visible-heading-or-link (&optional arg) "Move to the previous visible heading or link, whichever comes first. With prefix ARG and the point on a heading(link): jump over subsequent headings(links) to the previous link(heading), respectively. This is useful to skip over a long series of consecutive headings(links)." (interactive "P") (let ((prev-heading (save-excursion (org-previous-visible-heading 1) (when (org-at-heading-p) (point)))) (prev-link (save-excursion (when (/org-next-visible-link t) (point))))) (when arg (if (and (org-at-heading-p) prev-link) (setq prev-heading nil) (if (and (looking-at org-link-any-re) prev-heading) (setq prev-link nil)))) (cond ((and prev-heading prev-link) (goto-char (max prev-heading prev-link))) (prev-heading (goto-char prev-heading)) (prev-link (goto-char prev-link))))) ;; Adapted from org-next-link to only consider visible links (defun /org-next-visible-link (&optional search-backward) "Move forward to the next visible link. When SEARCH-BACKWARD is non-nil, move backward." (interactive) (let ((pos (point)) (search-fun (if search-backward #'re-search-backward #'re-search-forward))) ;; Tweak initial position: make sure we do not match current link. (cond ((and (not search-backward) (looking-at org-link-any-re)) (goto-char (match-end 0))) (search-backward (pcase (org-in-regexp org-link-any-re nil t) (`(,beg . ,_) (goto-char beg))))) (catch :found (while (funcall search-fun org-link-any-re nil t) (let ((folded (org-invisible-p nil t))) (when (or (not folded) (eq folded 'org-link)) (let ((context (save-excursion (unless search-backward (forward-char -1)) (org-element-context)))) (pcase (org-element-lineage context '(link) t) (link (goto-char (org-element-property :begin link)) (throw :found t))))))) (goto-char pos) ;; No further link found nil))) (defun /org-shifttab (&optional arg) "Move to the previous visible heading or link. If already at a heading, move first to its beginning. When inside a table, move to the previous field." (interactive "P") (cond ((org-at-table-p) (call-interactively #'org-table-previous-field)) ((and (not (bolp)) (org-at-heading-p)) (beginning-of-line)) (t (call-interactively #'/org-previous-visible-heading-or-link)))) (defun /org-tab (&optional arg) "Move to the next visible heading or link. When inside a table, re-align the table and move to the next field." (interactive) (cond ((org-at-table-p) (org-table-justify-field-maybe) (call-interactively #'org-table-next-field)) (t (call-interactively #'/org-next-visible-heading-or-link)))) (use-package org :config ;; RET should follow link when possible (moves to next field in tables) (setq org-return-follows-link t) ;; must be at the beginning of a headline to use it; ? for help (setq org-use-speed-commands t) ;; Customize some bindings (define-key org-mode-map (kbd "<tab>") #'/org-tab) (define-key org-mode-map (kbd "<backtab>") #'/org-shifttab) ;; Customize speed keys: modifying operations must be upper case (custom-set-variables '(org-speed-commands '(("Outline Navigation and Visibility") ("n" . (org-speed-move-safe 'org-next-visible-heading)) ("p" . (org-speed-move-safe 'org-previous-visible-heading)) ("f" . (org-speed-move-safe 'org-forward-heading-same-level)) ("b" . (org-speed-move-safe 'org-backward-heading-same-level)) ("u" . (org-speed-move-safe 'outline-up-heading)) ("j" . org-goto) ("c" . org-cycle) ("C" . org-shifttab) (" " . org-display-outline-path) ("s" . org-toggle-narrow-to-subtree) ("Editing") ("I" . (progn (forward-char 1) (call-interactively 'org-insert-heading-respect-content))) ("^" . org-sort) ("W" . org-refile) ("@" . org-mark-subtree) ("T" . org-todo) (":" . org-set-tags-command) ("Misc") ("?" . org-speed-command-help)))) )
A few comments about the code, for those interested:
- This is more of a proof-of-concept than optimized code ready for upstreaming.
- My
/org-next-visible-link
is a simplified version of the built-inorg-next-link
, tailored to the specific cases I care about. Honestly, I was surprised thatorg-next-link
doesn’t already do what I need. It jumps to the next link even if it’s inside a folded section, causing it to unfold. I have a hard time imagining why would anyone need that. - In
/org-tab
and/org-shifttab
, I preserved the default behavior oforg-cycle
within a table: it navigates between table fields. - I’ve also customized
org-speed-commands
to only bind editing actions to keys that require the Shift modifier. I like keeping lowercase keys reserved for non-destructive commands. As a next step, I may remap Space and Shift-Space to scroll the buffer. That would bring me even closer to a more consistent reading experience.
Enjoy the malleability of Emacs and the freedom it gives you!
Discuss this post on Reddit.
Speed Dialing Your Favorite Files
I may be dating myself, but I vividly remember setting up speed dials for my most frequently called numbers on my AT&T landline phone. In the early '90s, you could store a phone number in a numbered memory slot (referred to as "programming") and later dial your grandma, for example, by pressing SPD+2
. Retro is in—so if you're too young to remember that and want to know more, just ask your favorite LLM chatbot to fill you in.
Speed-dialing as a user experience concept is widespread, although we don't normally call it that anymore. It is implemented as a feature that I use many times a day in my web browser. I use Safari on a Mac and typically keep many tabs open. I pin the first few to frequently visited URLs, like https://planet.emacslife.com. I can quickly switch to one of them using the keyboard shortcut CMD+1..9
, always knowing which website I'll get. Other browsers offer similar functionality, though they may use different shortcuts, like CTRL+1..9
.
The two apps I use most often on my Mac are Safari and Emacs, and I wondered, “Why don't I have a similar speed-dialing feature in Emacs?” It would be incredibly useful to switch instantly to my important files for reading or jotting down notes. I also like to optimize my keybindings, and consistency plays a big role in that—whether it’s adopting Emacs keybindings elsewhere or bringing external shortcuts into Emacs. It would be great to use the same CMD+1..9
shortcut to recreate this functionality in Emacs.
But doesn’t Emacs already have Tab Bar and Tab Line features? Maybe one of them (I can never remember which is which) could be adapted or enhanced to do what I want. Note, however, that I’m talking about speed dialing files, not tabs. I don’t want to select a tab or cycle through them—I want to jump directly to a specific buffer that’s visiting a specific file. Tabs feel a bit unnatural in Emacs; they make sense in browsers, but in Emacs, we typically work with buffers by name.
Direct addressing—using a name or a short index—is both powerful and highly efficient. Cycling is the least efficient method (looking at you, CMD+TAB
). Completion is a middle ground—it requires extra keystrokes compared to direct addressing and is less predictable when the candidate list changes (in how many characters must be typed to get a single match). However, it’s essential when the list of candidates is long.
Direct Addressing > Completion > Cycling
In general, I prefer direct addressing whenever possible, completion when necessary, and cycling only as a last resort. Emacs' built-in bookmark-jump
falls into the completion category. It would be my next choice if the number of my frequently used files was above ten.
Another reason I avoid using tabs for this in Emacs is that I don’t want to waste screen real estate on a tab bar if I don’t have to. My speed dials are mostly static—I may change them occasionally, but if I assign 1
to school.org
and 2
to house.org
, I want to stick with that. Thanks to muscle memory, I don’t need to see the list in front of me at all times. Plus, accidentally switching to the wrong frequently used file isn’t a big deal—I can quickly flip through a few of them to find what I need.
The beauty of Emacs is that I can create a Safari-like speed-dial experience with just a couple of elisp expressions in my init.el
file.
;; ;; Speed Dialing Favorite Files ;; (defvar /speed-dial-list '(("⓵-todo" . "~/todo.org") ("⓶-emacs" . "~/para/areas/emacs.org") ("⓷-family" . "~/para/areas/family.org") ("⓸-house" . "~/para/areas/house.org") ("⓹-garden" . "~/para/areas/garden.org") ("⓺-42" . "~/para/areas/42.org") ("⓻-init" . "~/.emacs.d/init.el") ("⓼-O1" . "~/para/projects/proj1.org") ("⓽-O2" . "~/para/projects/proj2.org") ("⓾-O3" . "~/para/projects/proj3.org")) "List of speed-dial entries as (LABEL . FILENAME).") ;; Global keybindings for speed dialing using '<Super>' + digit (let ((i 1)) (dolist (entry /speed-dial-list) (keymap-global-set (format "s-%d" (mod i 10)) `(lambda() (interactive) (find-file-existing ,(cdr entry)))) (setq i (1+ i))))
As you can see, I use the <Super>
key modifier to define bindings that match my Safari shortcuts, CMD+1..9
. Note a little trick: using the mod function inside keymap-global-set
to get s-0
to invoke the tenth speed-dial entry.
Currently, the speed-dial bindings simply call the find-file-existing
function to switch to the corresponding buffer, opening the file if needed. But you can customize this further by using your own function for tailored behavior.
For example, you might use repeated presses of the same CMD+0..9
to change folding in an Org buffer, jump to a predefined heading, switch to a related buffer, or perform other context-specific actions.

Rather than visualizing the speed-dial entries as tabs, I found a way to display them without taking up valuable screen real estate. I simply splice the speed-dial labels into the Emacs frame title bar, which I don't really use for anything else. By default, it shows the current buffer name, but that information is also displayed in the mode line, which is where my eyes naturally go.
;; Inject my speed-dial list into the frame title (setq frame-title-format (concat (mapconcat #'car /speed-dial-list " ") " - %b"))
For my needs, displaying speed-dial entries in the Emacs frame title, followed by the current buffer name, works perfectly. My main Emacs frame is always wide enough to accommodate it. If I couldn’t use the frame title, I’d probably just open my init.el
whenever I needed to check which speed-dial number maps to which file. But you might find an even better approach that works for you.
Enjoy the malleability of Emacs and the freedom it gives you!
Discuss this post on Reddit.