Welcome to my Emacs blog
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.