Meta Redux: Closing the Find-Usages Gap in CIDER

Wait 5 sec.

Next up in the series on the notable changes inCIDER 2.0: cross-references. Or, asmost people call the feature, “find usages” - for years the most commonlycited reason to run clojure-lsp alongside (orinstead of) CIDER. Let’s talk about why that gap existed and how we finallyclosed it.Why runtime xref wasn’t enoughCIDER has had runtime cross-referencing for a while: the cider/fn-refs opwalks the loaded vars in your REPL and reports which functions reference theone at point. It’s a genuinely cool trick - the REPL literally knows yourprogram - but as a “find usages” answer it has three structural problems: It only sees loaded code. Namespaces you haven’t required yet - oftenmost of the codebase - are invisible. It reports functions, not occurrences. Each hit points at the caller’sdefinition, not the exact call site, and a function that calls yours threetimes shows up once. It’s JVM-only, so ClojureScript users got nothing.clojure-lsp, by contrast, builds a static index of your whole project withclj-kondo’s analyzer and answers instantly, loaded or not. Foroccurrence-oriented questions (“show me every place this is used, so I canchange all of them”), static analysis is simply the right tool. No amount ofruntime cleverness fixes “the code isn’t loaded”.The fix: search the sourceSo CIDER 2.0 does the obvious thing we should have done years ago:xref-find-references (M-?) now finds references by searching theproject’s source files on disk. Unloaded code, cljs files, commented-outdrafts - 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 syntacticsearch, so an identically named var in another namespace can produce a falsepositive. But for the daily “where is this used?” question it turns out tobe remarkably close in practice, it requires zero extra infrastructure, and itcomposes 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 notextual trace in your source, but the runtime knows about them. Staticanalysis 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 thesource search explicitly, and outside a project the source mode gracefullyfalls back to the runtime search.)Beyond find usages: the who-* familyWhile closing the gap, we went further and built out a whole family ofSLIME-inspired cross-referencing commands under C-c C-w, most of themrendered as expandable trees: cider-who-calls / cider-who-is-called - the call graph, upward anddownward. Expand a caller to see its callers; spelunk as deep as you like: cider-who-implements - a protocol’s implementing types (inlinedefrecord/deftype implementations included) or a multimethod’s dispatchvalues, each jumping to the implementation’s source. Multimethods are a nicecase study in hybrid thinking: the method functions carry no source metadataat runtime, so CIDER locates the defmethod forms by - you guessed it -searching the source. cider-type-protocols / cider-protocols-with-method - the reverselookups: what does this type implement, and which protocols declare thismethod? cider-who-macroexpands - a macro’s use sites, found via source search,because macro invocations are expanded away at compile time and the runtimeliterally cannot see them.Much of this is powered by new ops in cider-nrepl (andOrchard underneath), and much ofthe inspiration came straight from SLIME and swank-clojure, which offeredwho-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 commonanswer I heard when I asked - then CIDER now covers you out of the box. If youuse 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 toreplicate them. The two continue towork fine side by side, and thenew async eldoc even yields politely so LSP-provided docs can compose withCIDER’s.My goal was never to “beat” clojure-lsp - it was to make a freshly installedCIDER 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 liveruntime on the other end of the wire.The full story is in thenavigation docs. Keephacking!