-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrss.xml
More file actions
814 lines (714 loc) · 70.1 KB
/
rss.xml
File metadata and controls
814 lines (714 loc) · 70.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title><![CDATA[My Emacs Blog]]></title>
<description><![CDATA[My Emacs Blog]]></description>
<link>https://spepo.github.io/</link>
<lastBuildDate>Sun, 22 Jun 2025 10:18:07 -0700</lastBuildDate>
<item>
<title><![CDATA[Fault-tolerant Org Links]]></title>
<description><![CDATA[
<p>
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: <i>“No match - create this as a new heading?”</i>. 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.
</p>
<p>
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 <code>remember.el</code>?), 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.
</p>
<p>
I created a small set of tools to help detect and repair broken links in my Org files on the fly. My <code>Org Link Repair</code> code consists of three little helpers:
</p>
<ul class="org-ul">
<li>A checker hook <code>/org-test-file-link</code> that intercepts broken links before Org tries to apply its built-in 'nonexistent target' logic.</li>
<li>A transient menu <code>/olr-transient</code> to provide a quick interface for automated and manual broken link recovery tasks.</li>
<li>An interactive repair mode <code>/olr-manual-mode</code> that guides a user through fixing broken links one by one.</li>
</ul>
<p>
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.
</p>
<p>
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 <code>Org Link Repair</code> 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.
</p>
<p>
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).
</p>
<blockquote>
<p>
If you can’t prevent links from breaking, at least make them easy to find and fix.
</p>
</blockquote>
<div id="outline-container-orgd913ec7" class="outline-2">
<h2 id="orgd913ec7">Catching Broken Links</h2>
<div class="outline-text-2" id="text-orgd913ec7">
<p>
The first thing to do is to change the value of <code>org-link-search-must-match-exact-headline</code> from its default setting of <code>query-to-create</code>. 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.
</p>
<p>
Luckily Org developers provided the <code>org-open-at-point-functions</code> 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.
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(custom-set-variables '(org-link-search-must-match-exact-headline t))
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-test-file-link</span> ()
<span style="color: #2a486a; font-style: italic;">"Check if the file link target file exists before following it."</span>
(<span style="color: #5317ac; font-weight: bold;">let</span> ((ctx (org-element-context)))
(<span style="color: #5317ac; font-weight: bold;">when</span> (<span style="color: #5317ac; font-weight: bold;">and</span> (eq (org-element-type ctx) 'link)
(string= (org-element-property <span style="color: #8f0075; font-weight: bold;">:type</span> ctx) <span style="color: #2544bb;">"file"</span>))
(<span style="color: #5317ac; font-weight: bold;">let</span> ((file (org-element-property <span style="color: #8f0075; font-weight: bold;">:path</span> ctx)))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">If the file exists, return nil to let org-open-at-point continue</span>
(<span style="color: #5317ac; font-weight: bold;">if</span> (not (file-exists-p file))
(<span style="color: #813e00; font-weight: bold;">user-error</span> (concat <span style="color: #2544bb;">"Target file not found; Use "</span>
(substitute-command-keys <span style="color: #2544bb;">"\\[</span><span style="color: #0000c0;">/olr-transient</span><span style="color: #2544bb;">]"</span>)
<span style="color: #2544bb;">" to repair link to %s"</span>) <span style="color: #813e00; font-weight: bold;">file))))))</span>
(add-hook 'org-open-at-point-functions #'/org-test-file-link)
</pre>
</div>
</div>
</div>
<div id="outline-container-orgaaef726" class="outline-2">
<h2 id="orgaaef726">A Transient Menu for Link Repair Tasks</h2>
<div class="outline-text-2" id="text-orgaaef726">
<p>
I am using Emacs’ Transient library (the same engine behind Magit’s menus) to create a one-stop menu for all <code>Org Link Repair</code> activities. The command <code>/olr-transient</code> 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:
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span style="color: #5317ac; font-weight: bold;">transient-define-prefix</span> <span style="color: #721045;">/olr-transient</span> ()
<span style="color: #2a486a; font-style: italic;">"Transient menu for Org Link Repair."</span>
[<span style="color: #8f0075; font-weight: bold;">:description</span> <span style="color: #2544bb;">"Org Link Repair transient: fix your broken links\n\n"</span>
[<span style="color: #2544bb;">"Test/Repair"</span>
(<span style="color: #2544bb;">"l"</span> <span style="color: #2544bb;">"Lint all links in the buffer"</span> /org-lint-links <span style="color: #8f0075; font-weight: bold;">:transient</span> nil)
(<span style="color: #2544bb;">"m"</span> <span style="color: #2544bb;">"Manually find the new target"</span> /olr-manual-mode <span style="color: #8f0075; font-weight: bold;">:transient</span> nil)]
[<span style="color: #2544bb;">"Display/Navigate"</span>
(<span style="color: #2544bb;">"n"</span> <span style="color: #2544bb;">"Next link"</span> org-next-link <span style="color: #8f0075; font-weight: bold;">:transient</span> t)
(<span style="color: #2544bb;">"p"</span> <span style="color: #2544bb;">"Previous link"</span> org-previous-link <span style="color: #8f0075; font-weight: bold;">:transient</span> t)
(<span style="color: #2544bb;">"d"</span> <span style="color: #2544bb;">"Display toggle"</span> org-toggle-link-display <span style="color: #8f0075; font-weight: bold;">:transient</span> t)]
[<span style="color: #2544bb;">"Other"</span>
(<span style="color: #2544bb;">"q"</span> <span style="color: #2544bb;">"Quit"</span> transient-quit-one <span style="color: #8f0075; font-weight: bold;">:transient</span> nil)]])
(global-set-key (kbd <span style="color: #2544bb;">"<f2> <return>"</span>) #'/olr-transient)
</pre>
</div>
<p>
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 <code>org-lint</code> for that), link navigation and display toggling commands.
</p>
<p>
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.
</p>
</div>
</div>
<div id="outline-container-orgd253323" class="outline-2">
<h2 id="orgd253323">Manual Repair Strategy — Guided Link Fixing</h2>
<div class="outline-text-2" id="text-orgd253323">
<p>
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.
</p>
<p>
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 <code>/olr-manual-mode</code> 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 <code>C-c C-c</code> 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 <code>C-c C-k</code>.
</p>
<p>
Here is my code:
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #505050; font-style: italic;">;; </span>
<span style="color: #505050; font-style: italic;">;;; </span><span style="color: #505050; font-style: italic;">Org Link Repair - Manual (user-assisted) Strategy</span>
<span style="color: #505050; font-style: italic;">;; </span>
(<span style="color: #5317ac; font-weight: bold;">defvar</span> <span style="color: #00538b;">/olr-manual-marker</span> nil
<span style="color: #2a486a; font-style: italic;">"Marker pointing at the original (broken) link."</span>)
(<span style="color: #5317ac; font-weight: bold;">defvar</span> <span style="color: #00538b;">/olr-manual-mode-map</span>
(<span style="color: #5317ac; font-weight: bold;">let</span> ((map (make-sparse-keymap)))
(define-key map (kbd <span style="color: #2544bb;">"C-c C-c"</span>) #'/olr-manual-complete)
(define-key map (kbd <span style="color: #2544bb;">"C-c C-k"</span>) #'/olr-manual-abort)
map)
<span style="color: #2a486a; font-style: italic;">"Keymap for `</span><span style="color: #0000c0; font-style: italic;">/olr-manual-mode</span><span style="color: #2a486a; font-style: italic;">'."</span>)
(<span style="color: #5317ac; font-weight: bold;">easy-menu-define</span> /olr-manual-mode-menu /olr-manual-mode-map
<span style="color: #2a486a; font-style: italic;">"Menu for OLR Manual Mode"</span>
'(<span style="color: #2544bb;">"OrgLinkRepairManualMode"</span>
[<span style="color: #2544bb;">"Complete"</span> /olr-manual-complete t]
[<span style="color: #2544bb;">"Abort"</span> /olr-manual-abort t]))
(<span style="color: #5317ac; font-weight: bold;">define-minor-mode</span> <span style="color: #721045;">/olr-manual-mode</span>
<span style="color: #2a486a; font-style: italic;">"Global minor mode for Org Link Repair manual strategy.</span>
<span style="color: #2a486a; font-style: italic;">When enabled, the marker pointing at the link at point is saved. The user</span>
<span style="color: #2a486a; font-style: italic;">is expected to navigate to where the link should be pointing at and call</span>
<span style="color: #2a486a; font-style: italic;">`</span><span style="color: #0000c0; font-style: italic;">/olr-manual-complete</span><span style="color: #2a486a; font-style: italic;">' to repair the link, or `</span><span style="color: #0000c0; font-style: italic;">/olr-manual-abort</span><span style="color: #2a486a; font-style: italic;">' to cancel.</span>
<span style="color: #2a486a; font-style: italic;">Attempting to enable this minor mode outside an Org-mode derivative, or</span>
<span style="color: #2a486a; font-style: italic;">if the point is not at an Org link will fail with a user error."</span>
<span style="color: #8f0075; font-weight: bold;">:lighter</span> <span style="color: #2544bb;">" LinkRepair"</span>
<span style="color: #8f0075; font-weight: bold;">:global</span> t
(<span style="color: #5317ac; font-weight: bold;">if</span> (not /olr-manual-mode)
(<span style="color: #5317ac; font-weight: bold;">setq</span> /olr-manual-marker nil)
(<span style="color: #5317ac; font-weight: bold;">unless</span> (derived-mode-p 'org-mode)
(<span style="color: #813e00; font-weight: bold;">user-error</span> <span style="color: #2544bb;">"Not in an Org buffer"</span>))
(<span style="color: #5317ac; font-weight: bold;">unless</span> (eq (org-element-type (org-element-context)) 'link)
(<span style="color: #813e00; font-weight: bold;">user-error</span> <span style="color: #2544bb;">"Not at an Org link"</span>))
(<span style="color: #5317ac; font-weight: bold;">setq</span> /olr-manual-marker (point-marker))
(message
(substitute-command-keys
<span style="color: #2544bb;">"Manual link repair mode initiated. Navigate to intended link target,</span>
<span style="color: #2544bb;">press \\[</span><span style="color: #0000c0;">/olr-manual-complete</span><span style="color: #2544bb;">] to complete, or \\[</span><span style="color: #0000c0;">/olr-manual-abort</span><span style="color: #2544bb;">] to abort."</span>
))))
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/olr-manual-complete</span> ()
<span style="color: #2a486a; font-style: italic;">"Complete Org Link Repair by replacing the broken link at saved marker</span>
<span style="color: #2a486a; font-style: italic;">with a new link targeted at point.</span>
<span style="color: #2a486a; font-style: italic;">The user is expected to have navigated to the location of the new link target.</span>
<span style="color: #2a486a; font-style: italic;">This function will call `</span><span style="color: #0000c0; font-style: italic;">org-store-link</span><span style="color: #2a486a; font-style: italic;">', then use `</span><span style="color: #0000c0; font-style: italic;">org-insert-all-links</span><span style="color: #2a486a; font-style: italic;">' to</span>
<span style="color: #2a486a; font-style: italic;">replace the broken link, location of which was saved by `</span><span style="color: #0000c0; font-style: italic;">/olr-manual-mode</span><span style="color: #2a486a; font-style: italic;">'."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span>)
(org-store-link nil t)
(<span style="color: #5317ac; font-weight: bold;">unless</span> (<span style="color: #5317ac; font-weight: bold;">and</span> /olr-manual-marker (marker-position /olr-manual-marker))
(<span style="color: #813e00; font-weight: bold;">error</span> <span style="color: #2544bb;">"OrgLinkRepair: Lost marker to the original link location"</span>))
(switch-to-buffer (marker-buffer /olr-manual-marker))
(goto-char (marker-position /olr-manual-marker))
(/olr-manual-mode -1)
(<span style="color: #5317ac; font-weight: bold;">let*</span> ((oldctx (org-element-context))
(oldstart (org-element-property <span style="color: #8f0075; font-weight: bold;">:begin</span> oldctx))
(oldend (org-element-property <span style="color: #8f0075; font-weight: bold;">:end</span> oldctx))
oldlink newlink)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Delete the old link at point</span>
(<span style="color: #5317ac; font-weight: bold;">when</span> (<span style="color: #5317ac; font-weight: bold;">and</span> oldstart oldend)
(<span style="color: #5317ac; font-weight: bold;">setq</span> oldlink (buffer-substring oldstart oldend))
(delete-region oldstart oldend))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Insert the new link</span>
(org-insert-all-links 1 <span style="color: #2544bb;">""</span> <span style="color: #2544bb;">""</span>)
(<span style="color: #5317ac; font-weight: bold;">let*</span> ((newctx (org-element-context))
(newstart (org-element-property <span style="color: #8f0075; font-weight: bold;">:begin</span> newctx))
(newend (org-element-property <span style="color: #8f0075; font-weight: bold;">:end</span> newctx)))
(goto-char newstart)
(<span style="color: #5317ac; font-weight: bold;">setq</span> newlink (buffer-substring newstart newend)))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Notify the user: audibly+visibly (hopefully after auto-revert messages)</span>
(ding)
(run-with-idle-timer
0.2 nil
(<span style="color: #5317ac; font-weight: bold;">lambda</span> () (message (concat <span style="color: #2544bb;">"Modified buffer by replacing link %s with %s."</span>
<span style="color: #2544bb;">"\nSave the buffer to keep changes!"</span>)
oldlink newlink)))))
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/olr-manual-abort</span> ()
<span style="color: #2a486a; font-style: italic;">"Abort manual Org Link Repair."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span>)
(<span style="color: #5317ac; font-weight: bold;">unless</span> (<span style="color: #5317ac; font-weight: bold;">and</span> /olr-manual-marker (marker-position /olr-manual-marker))
(<span style="color: #813e00; font-weight: bold;">error</span> <span style="color: #2544bb;">"OrgLinkRepair: Lost marker to the original link location"</span>))
(switch-to-buffer (marker-buffer /olr-manual-marker))
(goto-char (marker-position /olr-manual-marker))
(/olr-manual-mode -1)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Notify the user</span>
(message <span style="color: #2544bb;">"Org Link Repair aborted."</span>))
</pre>
</div>
</div>
</div>
<div id="outline-container-org0000479" class="outline-2">
<h2 id="org0000479">Limitations and Next Steps</h2>
<div class="outline-text-2" id="text-org0000479">
<p>
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 <code>org-store-link</code>. Again, not for web links opened in a browser.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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 <a href="https://spepo.github.io/2025-04-30-towards-auto-generated-ert-unit-tests.html">my previous blog post</a>).
</p>
<blockquote>
<p>
Enjoy the malleability of Emacs and the freedom it gives you!
</p>
</blockquote>
<p>
Discuss this post on <a href="https://www.reddit.com/r/emacs/comments/1lht5mp/faulttolerant_org_links/">Reddit</a>.
</p>
</div>
</div>
<div class="taglist"><a href="https://spepo.github.io/tags.html">Tags</a>: <a href="https://spepo.github.io/tag-emacs.html">emacs</a> </div>]]></description>
<category><![CDATA[emacs]]></category>
<link>https://spepo.github.io/2025-06-22-fault-tolerant-org-links.html</link>
<guid>https://spepo.github.io/2025-06-22-fault-tolerant-org-links.html</guid>
<pubDate>Sun, 22 Jun 2025 11:01:00 -0700</pubDate>
</item>
<item>
<title><![CDATA[Towards Auto-Generated ERT Unit Tests]]></title>
<description><![CDATA[
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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 <code>o3-mini-high</code> and <code>4o</code> models) to help set up ERT tests for my personal library functions, loaded from my <code>init.el</code>. My goal was to run the tests externally, in batch mode, separate from my main Emacs instance.
</p>
<p>
ChatGPT performed reasonably well. After a few iterations, partly due to peculiarities in my <code>init.el</code> configuration, I ended up with a test file containing a dummy test that I could successfully run externally using:
</p>
<div class="org-src-container">
<pre class="src src-sh">emacs --batch -Q <span style="color: #2544bb;">\</span>
-l test/test-pp-lib.el <span style="color: #2544bb;">\</span>
-f ert-run-tests-batch-and-exit
</pre>
</div>
<p>
Next, I gave ChatGPT the code for my <code>/org-next-visible-link</code> function (see <a href="https://spepo.github.io/2025-03-29-the-tab-key-in-org-mode-reimagined.html">my previous blog post</a>) 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 <code>[</code> in <code>looking-at</code> patterns. Others were due to ChatGPT "misunderstanding" the behavior of the function: if point is already at the beginning of a link, <code>/org-next-visible-link</code> will skip to the next one. It was easy enough to fix these manually.
</p>
<p>
However, one test kept failing. It involved text visibility, which is the core aspect that <code>/org-next-visible-link</code> 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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<blockquote>
<p>
No more excuses: Every non-trivial function deserves a unit test.
</p>
</blockquote>
<p>
For completeness, here is the code of the function under test (with the fix), and the unit tests generated by ChatGPT.
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Adapted from org-next-link to only consider visible links</span>
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-next-visible-link</span> (<span style="color: #005a5f; font-weight: bold;">&optional</span> search-backward)
<span style="color: #2a486a; font-style: italic;">"Move forward to the next visible link.</span>
<span style="color: #2a486a; font-style: italic;">When SEARCH-BACKWARD is non-nil, move backward."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span>)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((pos (point))
(search-fun (<span style="color: #5317ac; font-weight: bold;">if</span> search-backward #'re-search-backward
#'re-search-forward)))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Tweak initial position: make sure we do not match current link.</span>
(<span style="color: #5317ac; font-weight: bold;">cond</span>
((<span style="color: #5317ac; font-weight: bold;">and</span> (not search-backward) (looking-at org-link-any-re))
(goto-char (match-end 0)))
(search-backward
(<span style="color: #5317ac; font-weight: bold;">pcase</span> (org-in-regexp org-link-any-re nil t)
(`(,beg . ,_) (goto-char beg)))))
(<span style="color: #5317ac; font-weight: bold;">catch</span> <span style="color: #0000c0;">:found</span>
(<span style="color: #5317ac; font-weight: bold;">while</span> (funcall search-fun org-link-any-re nil t)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((folded (org-invisible-p (match-beginning 0) t)))
(<span style="color: #5317ac; font-weight: bold;">when</span> (<span style="color: #5317ac; font-weight: bold;">or</span> (not folded) (eq folded 'org-link))
(<span style="color: #5317ac; font-weight: bold;">let</span> ((context (<span style="color: #5317ac; font-weight: bold;">save-excursion</span>
(<span style="color: #5317ac; font-weight: bold;">unless</span> search-backward (forward-char -1))
(org-element-context))))
(<span style="color: #5317ac; font-weight: bold;">pcase</span> (org-element-lineage context '(link) t)
(link
(goto-char (org-element-property <span style="color: #8f0075; font-weight: bold;">:begin</span> link))
(<span style="color: #5317ac; font-weight: bold;">throw</span> <span style="color: #0000c0;">:found</span> t)))))))
(goto-char pos)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">No further link found</span>
nil)))
</pre>
</div>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #505050; font-style: italic;">;;; </span><span style="color: #505050; font-style: italic;">test-pp-lib.el —–– tests for pp-lib.el</span>
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Ensure test directory is on load-path so we can require test-helper</span>
(add-to-list 'load-path (file-name-directory #$))
(<span style="color: #5317ac; font-weight: bold;">require</span> '<span style="color: #0000c0;">test-helper</span>)
(<span style="color: #5317ac; font-weight: bold;">require</span> '<span style="color: #0000c0;">ert</span>)
(<span style="color: #5317ac; font-weight: bold;">require</span> '<span style="color: #0000c0;">org</span>) <span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">for org-mode, org-element, org-link-any-re</span>
(<span style="color: #5317ac; font-weight: bold;">require</span> '<span style="color: #0000c0;">cl-lib</span>) <span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">for cl-letf</span>
<span style="color: #505050; font-style: italic;">;;; </span><span style="color: #505050; font-style: italic;">tests for `</span><span style="color: #0000c0; font-style: italic;">/org-next-visible-link</span><span style="color: #505050; font-style: italic;">'</span>
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-forward-basic</span> ()
<span style="color: #2a486a; font-style: italic;">"Move to the first link in forward direction and return non-nil."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"foo [[A]] bar [[B]] baz"</span>)
(org-mode)
(goto-char (point-min))
(should (/org-next-visible-link))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">A\\</span><span style="color: #2544bb;">]\\]"</span>))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-forward-second-link</span> ()
<span style="color: #2a486a; font-style: italic;">"Subsequent `</span><span style="color: #0000c0; font-style: italic;">org-next-visible-link</span><span style="color: #2a486a; font-style: italic;">' should find the next link."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"foo [[A]] bar [[B]] baz"</span>)
(org-mode)
(goto-char (point-min))
(/org-next-visible-link)
(should (/org-next-visible-link))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">B\\</span><span style="color: #2544bb;">]\\]"</span>))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-forward-skip-current</span> ()
<span style="color: #2a486a; font-style: italic;">"When point is at the beginning of a link, skip it and find the next."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"[[A]] [[B]]"</span>)
(org-mode)
(goto-char (point-min))
(should (/org-next-visible-link))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">B\\</span><span style="color: #2544bb;">]\\]"</span>))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-backward-basic</span> ()
<span style="color: #2a486a; font-style: italic;">"Move backward to the nearest previous link and return non-nil."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"first [[A]] then [[B]] then [[C]]"</span>)
(org-mode)
(goto-char (point-max))
(should (/org-next-visible-link t))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">C\\</span><span style="color: #2544bb;">]\\]"</span>))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-backward-second-link</span> ()
<span style="color: #2a486a; font-style: italic;">"Second backward invocation finds the prior link."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"[[A]] [[B]]"</span>)
(org-mode)
(goto-char (point-max))
(/org-next-visible-link t)
(should (/org-next-visible-link t))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">A\\</span><span style="color: #2544bb;">]\\]"</span>))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-backward-skip-current</span> ()
<span style="color: #2a486a; font-style: italic;">"When point is on a link, backward skips it and finds the previous."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"[[X]] [[Y]]"</span>)
(org-mode)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">position right at the beginning of Y</span>
(goto-char (point-min))
(/org-next-visible-link) <span style="color: #505050; font-style: italic;">; </span><span style="color: #505050; font-style: italic;">forward to [[X]]</span>
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">now move to Y</span>
(/org-next-visible-link)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">test backward skip</span>
(goto-char (point))
(should (/org-next-visible-link t))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">X\\</span><span style="color: #2544bb;">]\\]"</span>))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-no-link</span> ()
<span style="color: #2a486a; font-style: italic;">"With no links, returns nil and point does not move."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"no links here"</span>)
(org-mode)
(goto-char (point-min))
(should-not (/org-next-visible-link))
(should (= (point) (point-min)))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-skip-in-folded-headline</span> ()
<span style="color: #2a486a; font-style: italic;">"Skip links that reside in a folded headline body."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(org-mode)
(insert <span style="color: #2544bb;">"* Heading1\n[[SKIP]]\n* Heading2\n[[FIND]]\n"</span>)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Fold the first subtree so its body (and the [[SKIP]] link) is hidden</span>
(goto-char (point-min))
(org-cycle) <span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">this folds the subtree under Heading1</span>
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Now search forward: should skip [[SKIP]] and land on [[FIND]]</span>
(goto-char (point-min))
(should (/org-next-visible-link))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">FIND\\</span><span style="color: #2544bb;">]\\]"</span>))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">And then no more</span>
(should-not (/org-next-visible-link))))
(<span style="color: #5317ac; font-weight: bold;">ert-deftest</span> <span style="color: #721045;">org-next-visible-link-allow-org-link-invisible</span> ()
<span style="color: #2a486a; font-style: italic;">"Find links hidden with `</span><span style="color: #0000c0; font-style: italic;">invisible=</span><span style="color: #2a486a; font-style: italic;">'org-link` overlays."</span>
(<span style="color: #5317ac; font-weight: bold;">with-temp-buffer</span>
(insert <span style="color: #2544bb;">"foo [[HIDDEN]] [[VISIBLE]]"</span>)
(org-mode)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">hide first link with 'org-link</span>
(goto-char (point-min))
(re-search-forward org-link-any-re)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((ov (make-overlay (match-beginning 0) (match-end 0))))
(overlay-put ov 'invisible 'org-link))
(goto-char (point-min))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">should still hit HIDDEN first</span>
(should (/org-next-visible-link))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">HIDDEN\\</span><span style="color: #2544bb;">]\\]"</span>))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">then hit VISIBLE</span>
(should (/org-next-visible-link))
(should (looking-at <span style="color: #2544bb;">"\\[\\[</span><span style="color: #0000c0;">VISIBLE\\</span><span style="color: #2544bb;">]\\]"</span>))
(should-not (/org-next-visible-link))))
<span style="color: #505050; font-style: italic;">;;; </span><span style="color: #505050; font-style: italic;">test-pp-lib.el ends here</span>
</pre>
</div>
<p>
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 <code>org-next-visible-link-backward-skip-current</code> test contains a bug that, only by chance, doesn’t cause a failure. It also includes a useless <code>(goto-char (point))</code> 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.
</p>
<p>
There's no need to wait, though. I am sure existing tools like <code>gptel.el</code> and <code>aider.el</code> 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.
</p>
<p>
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.
</p>
<blockquote>
<p>
Enjoy the malleability of Emacs and the freedom it gives you!
</p>
</blockquote>
<p>
Discuss this post on <a href="https://www.reddit.com/r/emacs/comments/1keqj4n/towards_autogenerated_ert_unit_tests/">Reddit</a>.
</p>
<div class="taglist"><a href="https://spepo.github.io/tags.html">Tags</a>: <a href="https://spepo.github.io/tag-emacs.html">emacs</a> </div>]]></description>
<category><![CDATA[emacs]]></category>
<link>https://spepo.github.io/2025-04-30-towards-auto-generated-ert-unit-tests.html</link>
<guid>https://spepo.github.io/2025-04-30-towards-auto-generated-ert-unit-tests.html</guid>
<pubDate>Wed, 30 Apr 2025 11:14:00 -0700</pubDate>
</item>
<item>
<title><![CDATA[The TAB Key in Org Mode, Reimagined]]></title>
<description><![CDATA[
<p>
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 <code>TAB</code> to jump to it—just like I would in an Info or Help buffer. Using <code>TAB</code> for such field navigation is a common pattern across many applications, including Emacs. It’s also nicely symmetric with Shift-TAB (<code>S-TAB</code>), which typically navigates backward. But in Org mode, <code>TAB</code> triggers local visibility cycling: folding and unfolding text under the current headline. <code>S-TAB</code> cycles visibility globally, folding and unfolding all the headlines. (Granted, if you don’t use Info or navigate Help buffers with <code>TAB</code>, you might not miss that behavior in Org mode.)
</p>
<p>
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.
</p>
<p>
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 <code>TAB</code> 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.
</p>
<p>
It's well known that the <code>TAB</code> 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, <code>TAB</code> 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.
</p>
<p>
Personally, I want to move in the opposite direction: removing visibility cycling from the list of <code>TAB</code>-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 <a href="https://spepo.github.io/2025-02-18-speed-dial-your-favorite-files.html">my previous blog post</a>), and would prefer to jump directly to specific folding levels. I also value consistency in keybindings, so unifying <code>TAB</code> behavior across modes is important to me.
</p>
<blockquote>
<p>
<code>TAB</code>: indentation and completion when editing; field navigation when reading
</p>
</blockquote>
<p>
I decided to give it a try and remap <code>TAB</code> 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 <code>TAB</code> navigation loses its utility.
</p>
<p>
I remapped <code>TAB</code> in Org mode to navigate to the next visible heading or link, and <code>S-TAB</code> 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).
</p>
<p>
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, <code>c</code> is already mapped to <code>org-cycle</code>, which is what <code>TAB</code> normally does in Org mode.
</p>
<p>
I’ve had Speed Keys enabled for ages (mainly using them for forward/backward headline navigation), but I had never used <code>c</code> for visibility cycling—until now. And it gets even better: the combination of <code>TAB</code> / <code>S-TAB</code> to jump between fields, followed by a speed key at the headline, turns out to be quite powerful.
</p>
<p>
What about the other actions <code>TAB</code> usually performs in Org files? For now, I rely on <code>M-x org-cycle</code> when needed. The <code>org-cycle</code> command is quite sophisticated and can fall back to other <code>TAB</code> behaviors like indentation when appropriate. That said, I’ve been using my custom <code>TAB</code> / <code>S-TAB</code> bindings for months now and haven’t run into any situations where I missed the default behavior.
</p>
<p>
Want to give it a try? Here’s the code you can drop into your <code>init.el</code>:
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-next-visible-heading-or-link</span> (<span style="color: #005a5f; font-weight: bold;">&optional</span> arg)
<span style="color: #2a486a; font-style: italic;">"Move to the next visible heading or link, whichever comes first.</span>
<span style="color: #2a486a; font-style: italic;">With prefix ARG and the point on a heading(link): jump over subsequent</span>
<span style="color: #2a486a; font-style: italic;">headings(links) to the next link(heading), respectively. This is useful</span>
<span style="color: #2a486a; font-style: italic;">to skip over a long series of consecutive headings(links)."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span> <span style="color: #2544bb;">"P"</span>)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((next-heading (<span style="color: #5317ac; font-weight: bold;">save-excursion</span>
(org-next-visible-heading 1)
(<span style="color: #5317ac; font-weight: bold;">when</span> (org-at-heading-p) (point))))
(next-link (<span style="color: #5317ac; font-weight: bold;">save-excursion</span>
(<span style="color: #5317ac; font-weight: bold;">when</span> (/org-next-visible-link) (point)))))
(<span style="color: #5317ac; font-weight: bold;">when</span> arg
(<span style="color: #5317ac; font-weight: bold;">if</span> (<span style="color: #5317ac; font-weight: bold;">and</span> (org-at-heading-p) next-link)
(<span style="color: #5317ac; font-weight: bold;">setq</span> next-heading nil)
(<span style="color: #5317ac; font-weight: bold;">if</span> (<span style="color: #5317ac; font-weight: bold;">and</span> (looking-at org-link-any-re) next-heading)
(<span style="color: #5317ac; font-weight: bold;">setq</span> next-link nil))))
(<span style="color: #5317ac; font-weight: bold;">cond</span>
((<span style="color: #5317ac; font-weight: bold;">and</span> next-heading next-link) (goto-char (min next-heading next-link)))
(next-heading (goto-char next-heading))
(next-link (goto-char next-link)))))
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-previous-visible-heading-or-link</span> (<span style="color: #005a5f; font-weight: bold;">&optional</span> arg)
<span style="color: #2a486a; font-style: italic;">"Move to the previous visible heading or link, whichever comes first.</span>
<span style="color: #2a486a; font-style: italic;">With prefix ARG and the point on a heading(link): jump over subsequent</span>
<span style="color: #2a486a; font-style: italic;">headings(links) to the previous link(heading), respectively. This is useful</span>
<span style="color: #2a486a; font-style: italic;">to skip over a long series of consecutive headings(links)."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span> <span style="color: #2544bb;">"P"</span>)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((prev-heading (<span style="color: #5317ac; font-weight: bold;">save-excursion</span>
(org-previous-visible-heading 1)
(<span style="color: #5317ac; font-weight: bold;">when</span> (org-at-heading-p) (point))))
(prev-link (<span style="color: #5317ac; font-weight: bold;">save-excursion</span>
(<span style="color: #5317ac; font-weight: bold;">when</span> (/org-next-visible-link t) (point)))))
(<span style="color: #5317ac; font-weight: bold;">when</span> arg
(<span style="color: #5317ac; font-weight: bold;">if</span> (<span style="color: #5317ac; font-weight: bold;">and</span> (org-at-heading-p) prev-link)
(<span style="color: #5317ac; font-weight: bold;">setq</span> prev-heading nil)
(<span style="color: #5317ac; font-weight: bold;">if</span> (<span style="color: #5317ac; font-weight: bold;">and</span> (looking-at org-link-any-re) prev-heading)
(<span style="color: #5317ac; font-weight: bold;">setq</span> prev-link nil))))
(<span style="color: #5317ac; font-weight: bold;">cond</span>
((<span style="color: #5317ac; font-weight: bold;">and</span> prev-heading prev-link) (goto-char (max prev-heading prev-link)))
(prev-heading (goto-char prev-heading))
(prev-link (goto-char prev-link)))))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Adapted from org-next-link to only consider visible links</span>
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-next-visible-link</span> (<span style="color: #005a5f; font-weight: bold;">&optional</span> search-backward)
<span style="color: #2a486a; font-style: italic;">"Move forward to the next visible link.</span>
<span style="color: #2a486a; font-style: italic;">When SEARCH-BACKWARD is non-nil, move backward."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span>)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((pos (point))
(search-fun (<span style="color: #5317ac; font-weight: bold;">if</span> search-backward #'re-search-backward
#'re-search-forward)))
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Tweak initial position: make sure we do not match current link.</span>
(<span style="color: #5317ac; font-weight: bold;">cond</span>
((<span style="color: #5317ac; font-weight: bold;">and</span> (not search-backward) (looking-at org-link-any-re))
(goto-char (match-end 0)))
(search-backward
(<span style="color: #5317ac; font-weight: bold;">pcase</span> (org-in-regexp org-link-any-re nil t)
(`(,beg . ,_) (goto-char beg)))))
(<span style="color: #5317ac; font-weight: bold;">catch</span> <span style="color: #0000c0;">:found</span>
(<span style="color: #5317ac; font-weight: bold;">while</span> (funcall search-fun org-link-any-re nil t)
(<span style="color: #5317ac; font-weight: bold;">let</span> ((folded (org-invisible-p nil t)))
(<span style="color: #5317ac; font-weight: bold;">when</span> (<span style="color: #5317ac; font-weight: bold;">or</span> (not folded) (eq folded 'org-link))
(<span style="color: #5317ac; font-weight: bold;">let</span> ((context (<span style="color: #5317ac; font-weight: bold;">save-excursion</span>
(<span style="color: #5317ac; font-weight: bold;">unless</span> search-backward (forward-char -1))
(org-element-context))))
(<span style="color: #5317ac; font-weight: bold;">pcase</span> (org-element-lineage context '(link) t)
(link
(goto-char (org-element-property <span style="color: #8f0075; font-weight: bold;">:begin</span> link))
(<span style="color: #5317ac; font-weight: bold;">throw</span> <span style="color: #0000c0;">:found</span> t)))))))
(goto-char pos)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">No further link found</span>
nil)))
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-shifttab</span> (<span style="color: #005a5f; font-weight: bold;">&optional</span> arg)
<span style="color: #2a486a; font-style: italic;">"Move to the previous visible heading or link.</span>
<span style="color: #2a486a; font-style: italic;">If already at a heading, move first to its beginning. When inside a table,</span>
<span style="color: #2a486a; font-style: italic;">move to the previous field."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span> <span style="color: #2544bb;">"P"</span>)
(<span style="color: #5317ac; font-weight: bold;">cond</span>
((org-at-table-p) (call-interactively #'org-table-previous-field))
((<span style="color: #5317ac; font-weight: bold;">and</span> (not (bolp)) (org-at-heading-p)) (beginning-of-line))
(t (call-interactively #'/org-previous-visible-heading-or-link))))
(<span style="color: #5317ac; font-weight: bold;">defun</span> <span style="color: #721045;">/org-tab</span> (<span style="color: #005a5f; font-weight: bold;">&optional</span> arg)
<span style="color: #2a486a; font-style: italic;">"Move to the next visible heading or link.</span>
<span style="color: #2a486a; font-style: italic;">When inside a table, re-align the table and move to the next field."</span>
(<span style="color: #5317ac; font-weight: bold;">interactive</span>)
(<span style="color: #5317ac; font-weight: bold;">cond</span>
((org-at-table-p) (org-table-justify-field-maybe)
(call-interactively #'org-table-next-field))
(t (call-interactively #'/org-next-visible-heading-or-link))))
(<span style="color: #5317ac; font-weight: bold;">use-package</span> <span style="color: #0000c0;">org</span>
<span style="color: #8f0075; font-weight: bold;">:config</span>
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">RET should follow link when possible (moves to next field in tables)</span>
(<span style="color: #5317ac; font-weight: bold;">setq</span> org-return-follows-link t)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">must be at the beginning of a headline to use it; ? for help</span>
(<span style="color: #5317ac; font-weight: bold;">setq</span> org-use-speed-commands t)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Customize some bindings</span>
(define-key org-mode-map (kbd <span style="color: #2544bb;">"<tab>"</span>) #'/org-tab)
(define-key org-mode-map (kbd <span style="color: #2544bb;">"<backtab>"</span>) #'/org-shifttab)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Customize speed keys: modifying operations must be upper case</span>
(custom-set-variables
'(org-speed-commands
'((<span style="color: #2544bb;">"Outline Navigation and Visibility"</span>)
(<span style="color: #2544bb;">"n"</span> . (org-speed-move-safe 'org-next-visible-heading))
(<span style="color: #2544bb;">"p"</span> . (org-speed-move-safe 'org-previous-visible-heading))
(<span style="color: #2544bb;">"f"</span> . (org-speed-move-safe 'org-forward-heading-same-level))
(<span style="color: #2544bb;">"b"</span> . (org-speed-move-safe 'org-backward-heading-same-level))
(<span style="color: #2544bb;">"u"</span> . (org-speed-move-safe 'outline-up-heading))
(<span style="color: #2544bb;">"j"</span> . org-goto)
(<span style="color: #2544bb;">"c"</span> . org-cycle)
(<span style="color: #2544bb;">"C"</span> . org-shifttab)
(<span style="color: #2544bb;">" "</span> . org-display-outline-path)
(<span style="color: #2544bb;">"s"</span> . org-toggle-narrow-to-subtree)
(<span style="color: #2544bb;">"Editing"</span>)
(<span style="color: #2544bb;">"I"</span> . (<span style="color: #5317ac; font-weight: bold;">progn</span> (forward-char 1) (call-interactively 'org-insert-heading-respect-content)))
(<span style="color: #2544bb;">"^"</span> . org-sort)
(<span style="color: #2544bb;">"W"</span> . org-refile)
(<span style="color: #2544bb;">"@"</span> . org-mark-subtree)
(<span style="color: #2544bb;">"T"</span> . org-todo)
(<span style="color: #2544bb;">":"</span> . org-set-tags-command)
(<span style="color: #2544bb;">"Misc"</span>)
(<span style="color: #2544bb;">"?"</span> . org-speed-command-help))))
)
</pre>
</div>
<p>
A few comments about the code, for those interested:
</p>
<ol class="org-ol">
<li>This is more of a proof-of-concept than optimized code ready for upstreaming.</li>
<li>My <code>/org-next-visible-link</code> is a simplified version of the built-in <code>org-next-link</code>, tailored to the specific cases I care about. Honestly, I was surprised that <code>org-next-link</code> 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.</li>
<li>In <code>/org-tab</code> and <code>/org-shifttab</code>, I preserved the default behavior of <code>org-cycle</code> within a table: it navigates between table fields.</li>
<li>I’ve also customized <code>org-speed-commands</code> 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.</li>
</ol>
<blockquote>
<p>
Enjoy the malleability of Emacs and the freedom it gives you!
</p>
</blockquote>
<p>
Discuss this post on <a href="https://www.reddit.com/r/emacs/comments/1jmroa6/the_tab_key_in_org_mode_reimagined/">Reddit</a>.
</p>
<div class="taglist"><a href="https://spepo.github.io/tags.html">Tags</a>: <a href="https://spepo.github.io/tag-emacs.html">emacs</a> </div>]]></description>
<category><![CDATA[emacs]]></category>
<link>https://spepo.github.io/2025-03-29-the-tab-key-in-org-mode-reimagined.html</link>
<guid>https://spepo.github.io/2025-03-29-the-tab-key-in-org-mode-reimagined.html</guid>
<pubDate>Sat, 29 Mar 2025 08:01:00 -0700</pubDate>
</item>
<item>
<title><![CDATA[Speed Dialing Your Favorite Files]]></title>
<description><![CDATA[
<p>
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 <code>SPD+2</code>. 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.
</p>
<p>
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 <a href="https://planet.emacslife.com">https://planet.emacslife.com</a>. I can quickly switch to one of them using the keyboard shortcut <code>CMD+1..9</code>, always knowing which website I'll get. Other browsers offer similar functionality, though they may use different shortcuts, like <code>CTRL+1..9</code>.
</p>
<p>
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 <code>CMD+1..9</code> shortcut to recreate this functionality in Emacs.
</p>
<p>
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.
</p>
<p>
Direct addressing—using a name or a short index—is both powerful and highly efficient. Cycling is the least efficient method (looking at you, <code>CMD+TAB</code>). 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.
</p>
<blockquote>
<p>
Direct Addressing > Completion > Cycling
</p>
</blockquote>
<p>
In general, I prefer direct addressing whenever possible, completion when necessary, and cycling only as a last resort. Emacs' built-in <code>bookmark-jump</code> falls into the completion category. It would be my next choice if the number of my frequently used files was above ten.
</p>
<p>
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 <code>1</code> to <code>school.org</code> and <code>2</code> to <code>house.org</code>, 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.
</p>
<p>
The beauty of Emacs is that I can create a Safari-like speed-dial experience with just a couple of elisp expressions in my <code>init.el</code> file.
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #505050; font-style: italic;">;;</span>
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Speed Dialing Favorite Files</span>
<span style="color: #505050; font-style: italic;">;;</span>
(<span style="color: #5317ac; font-weight: bold;">defvar</span> <span style="color: #00538b;">/speed-dial-list</span>
'((<span style="color: #2544bb;">"⓵-todo"</span> . <span style="color: #2544bb;">"~/todo.org"</span>)
(<span style="color: #2544bb;">"⓶-emacs"</span> . <span style="color: #2544bb;">"~/para/areas/emacs.org"</span>)
(<span style="color: #2544bb;">"⓷-family"</span> . <span style="color: #2544bb;">"~/para/areas/family.org"</span>)
(<span style="color: #2544bb;">"⓸-house"</span> . <span style="color: #2544bb;">"~/para/areas/house.org"</span>)
(<span style="color: #2544bb;">"⓹-garden"</span> . <span style="color: #2544bb;">"~/para/areas/garden.org"</span>)
(<span style="color: #2544bb;">"⓺-42"</span> . <span style="color: #2544bb;">"~/para/areas/42.org"</span>)
(<span style="color: #2544bb;">"⓻-init"</span> . <span style="color: #2544bb;">"~/.emacs.d/init.el"</span>)
(<span style="color: #2544bb;">"⓼-O1"</span> . <span style="color: #2544bb;">"~/para/projects/proj1.org"</span>)
(<span style="color: #2544bb;">"⓽-O2"</span> . <span style="color: #2544bb;">"~/para/projects/proj2.org"</span>)
(<span style="color: #2544bb;">"⓾-O3"</span> . <span style="color: #2544bb;">"~/para/projects/proj3.org"</span>))
<span style="color: #2a486a; font-style: italic;">"List of speed-dial entries as (LABEL . FILENAME)."</span>)
<span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Global keybindings for speed dialing using '</span><span style="color: #0000c0; font-style: italic;"><Super></span><span style="color: #505050; font-style: italic;">' + digit</span>
(<span style="color: #5317ac; font-weight: bold;">let</span> ((i 1))
(<span style="color: #5317ac; font-weight: bold;">dolist</span> (entry /speed-dial-list)
(keymap-global-set (format <span style="color: #2544bb;">"s-%d"</span> (mod i 10))
`(<span style="color: #5317ac; font-weight: bold;">lambda</span>() (<span style="color: #5317ac; font-weight: bold;">interactive</span>) (find-file-existing ,(cdr entry))))
(<span style="color: #5317ac; font-weight: bold;">setq</span> i (1+ i))))
</pre>
</div>
<p>
As you can see, I use the <code><Super></code> key modifier to define bindings that match my Safari shortcuts, <code>CMD+1..9</code>. Note a little trick: using the mod function inside <code>keymap-global-set</code> to get <code>s-0</code> to invoke the tenth speed-dial entry.
</p>
<p>
Currently, the speed-dial bindings simply call the <code>find-file-existing</code> 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.
</p>
<p>
For example, you might use repeated presses of the same <code>CMD+0..9</code> to change folding in an Org buffer, jump to a predefined heading, switch to a related buffer, or perform other context-specific actions.
</p>
<figure>
<img src="https://spepo.github.io/static/2025-02/speed_dialing_screenshot1.png" alt="speed_dialing_screenshot.png">
</figure>
<p>
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.
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #505050; font-style: italic;">;; </span><span style="color: #505050; font-style: italic;">Inject my speed-dial list into the frame title</span>
(<span style="color: #5317ac; font-weight: bold;">setq</span> frame-title-format (concat (mapconcat #'car /speed-dial-list <span style="color: #2544bb;">" "</span>)
<span style="color: #2544bb;">" - %b"</span>))
</pre>
</div>
<p>
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 <code>init.el</code> 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.
</p>
<blockquote>
<p>
Enjoy the malleability of Emacs and the freedom it gives you!
</p>
</blockquote>
<p>
Discuss this post on <a href="https://www.reddit.com/r/emacs/comments/1iskl3w/speed_dialing_your_favorite_files/">Reddit</a>.
</p>
<div class="taglist"><a href="https://spepo.github.io/tags.html">Tags</a>: <a href="https://spepo.github.io/tag-emacs.html">emacs</a> </div>]]></description>
<category><![CDATA[emacs]]></category>
<link>https://spepo.github.io/2025-02-18-speed-dial-your-favorite-files.html</link>
<guid>https://spepo.github.io/2025-02-18-speed-dial-your-favorite-files.html</guid>
<pubDate>Tue, 18 Feb 2025 10:46:00 -0800</pubDate>
</item>
</channel>
</rss>