2025-09-15
Tags: emacs
Helpful [1] is a fantastic Emacs package that drastically improves
the builtin [40m[35m`help-mode`[39m[49m.
One of the particularly nice features is finding references to a
particular symbol.
Unfortunately it can be painfully slow in practice due to actually
parsing every loaded Elisp file:
[1m[37m([0m[37mrequire[0m[1m[37m [0m[33m'benchmark[0m[1m[37m)[0m[1m[37m[0m
[1m[37m([0m[1m[36mbenchmark-elapse[0m[1m[37m [0m[1m[37m([0m[1m[36melisp-refs-function[0m[1m[37m [0m[37m#'car[0m[1m[37m))[0m[1m[37m[0m
[1m[37m24.001221[0m
Instead of walking the ASTs of every file, why not do a regex
search?
It sacrifices correctness (and completely ignores the type of the
symbol), but it turns out to be orders of magnitude faster in
practice.
Bonus points for using a fast tool like ripgrep and extra bonus
points for completing the work asynchronously so as not to block
Emacs's main thread.
I don't mean to denigrate elisp-refs [2]; the author
clearly has put a lot of thought into performance and it's
only natural that using an approach that heavily cuts
corners together with a tool implemented in optimized
machine code instead of Elisp (which is especially hampered
by GC performance) will lead to faster results.
I'm a ripgrep junkie and I prefer it for grokking most
codebases, but the speed comes at the cost of having to
sift through many false positives.
I use the wonderful deadgrep [3] package to do just that:
[1m[37m([0m[37mwhen[0m[1m[37m [0m[1m[37m([0m[1m[36mlocate-library[0m[1m[37m [0m[33m"deadgrep"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mrequire[0m[1m[37m [0m[33m'deadgrep[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mfset[0m[1m[37m [0m[33m'deadgrep--arguments-orig[0m[1m[37m [0m[1m[37m([0m[37msymbol-function[0m[1m[37m [0m[37m#'[0m[1m[36mdeadgrep--arguments[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefine-advice[0m[1m[37m [0m[1m[36mhelpful--all-references[0m[1m[37m [0m[1m[37m([0m[37m:override[0m[1m[37m [0m[1m[37m([0m[1m[36mbutton[0m[1m[37m)[0m[1m[37m [0m[1m[36mfysh/use-deadgrep[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mcl-letf*[0m[1m[37m [0m[1m[37m((([0m[37msymbol-function[0m[1m[37m [0m[33m'deadgrep--arguments[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[33m&rest[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[36m`[0m[1m[37m([0m[33m"--follow"[0m[1m[37m [0m[33m"--type=elisp"[0m[1m[37m [0m[33m"--type=gzip"[0m[1m[37m [0m[33m"--search-zip"[0m[1m[37m [0m[1m[36m,@[0m[1m[37m([0m[1m[36mbutlast[0m[1m[37m [0m[1m[37m([0m[37mapply[0m[1m[37m [0m[37m#'[0m[1m[36mdeadgrep--arguments-orig[0m[1m[37m [0m[1m[36margs[0m[1m[37m))[0m[1m[37m [0m[1m[36m,[0m[1m[36mlisp-directory[0m[1m[37m [0m[1m[36m,[0m[1m[37m([0m[1m[36m-first[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[1m[36mp[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mstring-suffix-p[0m[1m[37m [0m[33m"/share/emacs/site-lisp"[0m[1m[37m [0m[1m[36mp[0m[1m[37m))[0m[1m[37m [0m[1m[36mload-path[0m[1m[37m)))))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mdeadgrep[0m[1m[37m [0m[1m[37m([0m[37msymbol-name[0m[1m[37m [0m[1m[37m([0m[1m[36mbutton-get[0m[1m[37m [0m[1m[36mbutton[0m[1m[37m [0m[33m'symbol[0m[1m[37m))[0m[1m[37m [0m[1m[36mdefault-directory[0m[1m[37m))))[0m[1m[37m[0m
The code is quite hacky because deadgrep is not designed to allow
passing multiple directories in a single search, but this gets the
job done.
[1m[37m([0m[37mwith-temp-buffer[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mlet[0m[1m[37m [0m[1m[37m(([0m[1m[36mbutton[0m[1m[37m [0m[1m[37m([0m[1m[36mmake-button[0m[1m[37m [0m[1m[37m([0m[37mpoint-min[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[37mpoint-max[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mtime-to-draw[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mtime-to-completion[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mbutton-put[0m[1m[37m [0m[1m[36mbutton[0m[1m[37m [0m[33m'symbol[0m[1m[37m [0m[37m#'car[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37msetq[0m[1m[37m [0m[1m[36mtime-to-completion[0m[1m[37m [0m[1m[37m([0m[1m[36mbenchmark-elapse[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37msetq[0m[1m[37m [0m[1m[36mtime-to-draw[0m[1m[37m [0m[1m[37m([0m[1m[36mbenchmark-elapse[0m[1m[37m [0m[1m[37m([0m[1m[36mhelpful--all-references[0m[1m[37m [0m[1m[36mbutton[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mwhile[0m[1m[37m [0m[1m[36mdeadgrep--running[0m[1m[37m [0m[1m[37m([0m[1m[36msit-for[0m[1m[37m [0m[1m[36m0.1[0m[1m[37m [0m[33m'nodisp[0m[1m[37m))))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mformat[0m[1m[37m [0m[33m"Time to first draw: %s\nTime to completion: %s"[0m[1m[37m [0m[1m[36mtime-to-draw[0m[1m[37m [0m[1m[36mtime-to-completion[0m[1m[37m)))[0m[1m[37m[0m
[1m[37mTime to first draw: 0.084542[0m
[1m[37mTime to completion: 38.184606[0m
In this pathological case the total time is slower than using
[40m[35m`elisp-refs-function`[39m[49m because there are almost
four times as many matches because of comments/docstrings and
partial matches like [40m`mapcar`[49m or [40m`car-safe`[49m
(including symbols that aren't functions like [40m`byte-
car`[49m).
The major difference is that while the performance of
[40m[35m`elisp-refs-*`[39m[49m functions[^fn:1] are roughly
constant regardless of the total number of references to a symbol,
using ripgrep is significantly faster for terms with fewer than 10k
matches (not to mention that you can browse the results
immediately).
If you want to remove the partial matches, you could use the
following advice instead:
[1m[37m([0m[37mdefine-advice[0m[1m[37m [0m[1m[36mhelpful--all-references[0m[1m[37m [0m[1m[37m([0m[37m:override[0m[1m[37m [0m[1m[37m([0m[1m[36mbutton[0m[1m[37m)[0m[1m[37m [0m[1m[36mfysh/use-deadgrep[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mcl-letf*[0m[1m[37m [0m[1m[37m((([0m[37msymbol-function[0m[1m[37m [0m[33m'deadgrep--arguments[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[33m&rest[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[36m`[0m[1m[37m([0m[33m"--follow"[0m[1m[37m [0m[33m"--type=elisp"[0m[1m[37m [0m[33m"--type=gzip"[0m[1m[37m [0m[33m"--search-zip"[0m[1m[37m [0m[1m[36m,@[0m[1m[37m([0m[1m[36mbutlast[0m[1m[37m [0m[1m[37m([0m[37mapply[0m[1m[37m [0m[37m#'[0m[1m[36mdeadgrep--arguments-orig[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m [0m[1m[36m3[0m[1m[37m)[0m[1m[37m [0m[33m"--no-fixed-strings"[0m[1m[37m [0m[33m"--"[0m[1m[37m [0m[1m[36m,[0m[1m[37m([0m[37mcar[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m [0m[1m[36m,[0m[1m[36mlisp-directory[0m[1m[37m [0m[1m[36m,[0m[1m[37m([0m[1m[36m-first[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[1m[36mp[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mstring-suffix-p[0m[1m[37m [0m[33m"/share/emacs/site-lisp"[0m[1m[37m [0m[1m[36mp[0m[1m[37m))[0m[1m[37m [0m[1m[36mload-path[0m[1m[0m...
[1m[37m [0m[1m[37m([0m[1m[36mdeadgrep[0m[1m[37m [0m[1m[37m([0m[37mformat[0m[1m[37m [0m[33m"(\\(|#?'| )(%s) "[0m[1m[37m [0m[1m[37m([0m[37msymbol-name[0m[1m[37m [0m[1m[37m([0m[1m[36mbutton-get[0m[1m[37m [0m[1m[36mbutton[0m[1m[37m [0m[33m'symbol[0m[1m[37m)))[0m[1m[37m [0m[1m[36mdefault-directory[0m[1m[37m)))[0m[1m[37m[0m
This unfortunately will highlight the entire match instead of just
the capturing group, so I prefer not to use it (althgough the speed
is mostly the same, if not a bit faster).
[^fn:1]: [40m[35m`elisp-refs-symbol`[39m[49m is faster than its
counterparts due to reduced implementation complexity
References:
(HTM) [1] Helpful
(HTM) [2] elisp-refs
(HTM) [3] deadgrep
>=================================================================<
(DIR) Blog
(DIR) Writeups
(DIR) jp
copyright 2026 George Huebner
(HTM) email