Closing the Find-Usages Gap in CIDER
Next up in the series on the notable changes in CIDER 2.0: cross-references. Or, as most people call the feature, “find usages” - for years the most commonly cited reason to run clojure-lsp alongside (or instead of) CIDER. Let’s talk about why that gap existed and how we finally closed it.
Why runtime xref wasn’t enough
CIDER has had runtime cross-referencing for a while: the cider/fn-refs op
walks the loaded vars in your REPL and reports which functions reference the
one at point. It’s a genuinely cool trick - the REPL literally knows your
program - but as a “find usages” answer it has three structural problems:
- It only sees loaded code. Namespaces you haven’t required yet - often most of the codebase - are invisible.
- It reports functions, not occurrences. Each hit points at the caller’s definition, not the exact call site, and a function that calls yours three times shows up once.
- It’s JVM-only, so ClojureScript users got nothing.
clojure-lsp, by contrast, builds a static index of your whole project with clj-kondo’s analyzer and answers instantly, loaded or not. For occurrence-oriented questions (“show me every place this is used, so I can change all of them”), static analysis is simply the right tool. No amount of runtime cleverness fixes “the code isn’t loaded”.
The fix: search the source
So CIDER 2.0 does the obvious thing we should have done years ago:
xref-find-references (M-?) now finds references by searching the
project’s source files on disk. Unloaded code, cljs files, commented-out
drafts - if the name occurs in the project, you’ll see the exact occurrence,
in the standard xref UI you already use for everything else in Emacs.
Is a source scan as smart as clj-kondo’s full analysis? No - it’s a syntactic search, so an identically named var in another namespace can produce a false positive. But for the daily “where is this used?” question it turns out to be remarkably close in practice, it requires zero extra infrastructure, and it composes with what only CIDER has: the running REPL.
That composition is configurable via cider-xref-references-mode:
source(the default) - occurrences from the project’s files.runtime- the historical loaded-vars behavior.both- source occurrences first, plus the runtime hits the scan can’t see. And there are such hits: references generated by macro expansion leave no textual trace in your source, but the runtime knows about them. Static analysis can’t ever tell you those; your REPL can.
(There’s also cider-xref-fn-refs-in-source, C-c C-? s, when you want the
source search explicitly, and outside a project the source mode gracefully
falls back to the runtime search.)
Beyond find usages: the who-* family
While closing the gap, we went further and built out a whole family of
SLIME-inspired cross-referencing commands under C-c C-w, most of them
rendered as expandable trees:
-
cider-who-calls/cider-who-is-called- the call graph, upward and downward. Expand a caller to see its callers; spelunk as deep as you like:
cider-who-implements- a protocol’s implementing types (inlinedefrecord/deftypeimplementations included) or a multimethod’s dispatch values, each jumping to the implementation’s source. Multimethods are a nice case study in hybrid thinking: the method functions carry no source metadata at runtime, so CIDER locates thedefmethodforms by - you guessed it - searching the source.cider-type-protocols/cider-protocols-with-method- the reverse lookups: what does this type implement, and which protocols declare this method?cider-who-macroexpands- a macro’s use sites, found via source search, because macro invocations are expanded away at compile time and the runtime literally cannot see them.
Much of this is powered by new ops in cider-nrepl (and
Orchard underneath), and much of
the inspiration came straight from SLIME and swank-clojure, which offered
who-calls decades ago. Sometimes nothing beats revisiting the classics.
So do you still need clojure-lsp?
If you were running clojure-lsp primarily for find-usages - the most common answer I heard when I asked - then CIDER now covers you out of the box. If you use it for project-wide renames, unused-var linting, or editing without a REPL, carry on; those are real strengths of static analysis and CIDER doesn’t try to replicate them. The two continue to work fine side by side, and the new async eldoc even yields politely so LSP-provided docs can compose with CIDER’s.
My goal was never to “beat” clojure-lsp - it was to make a freshly installed CIDER answer the questions every Clojure programmer asks a dozen times a day, with no extra moving parts, and with the one advantage nobody else has: a live runtime on the other end of the wire.
The full story is in the navigation docs. Keep hacking!