<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://metaredux.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://metaredux.com/" rel="alternate" type="text/html" /><updated>2026-08-14T04:44:16+00:00</updated><id>https://metaredux.com/feed.xml</id><title type="html">Meta Redux</title><subtitle>Writings on programming and open-source by Bozhidar Batsov, creator of RuboCop, CIDER, and Prelude.</subtitle><author><name>Bozhidar Batsov</name></author><entry><title type="html">Projectile 3.4</title><link href="https://metaredux.com/posts/2026/08/13/projectile-3-4.html" rel="alternate" type="text/html" title="Projectile 3.4" /><published>2026-08-13T07:30:00+00:00</published><updated>2026-08-13T07:30:00+00:00</updated><id>https://metaredux.com/posts/2026/08/13/projectile-3-4</id><content type="html" xml:base="https://metaredux.com/posts/2026/08/13/projectile-3-4.html"><![CDATA[<p><a href="https://github.com/bbatsov/projectile/releases/tag/v3.4.0">Projectile 3.4</a> is out! After
the four releases that preceded it this summer, this one is polish rather than ambition.</p>

<p>Most of it came out of an annoyance I’d been living with for years without ever quite
naming it: a lot of what I work on isn’t one directory. <a href="https://github.com/clojure-emacs/cider">CIDER</a> is really CIDER plus
<a href="https://github.com/clojure-emacs/cider-nrepl">cider-nrepl</a> plus <a href="https://github.com/clojure-emacs/orchard">orchard</a> plus <a href="https://github.com/clojure-emacs/clj-refactor.el">clj-refactor</a> plus a
handful of others. <a href="https://github.com/rubocop/rubocop">RuboCop</a> is RuboCop plus <a href="https://github.com/rubocop/rubocop-ast">rubocop-ast</a> plus
<a href="https://github.com/rubocop/rubocop-rails">rubocop-rails</a> and the other extension gems plus <a href="https://github.com/rubocop/ruby-style-guide">the style
guide</a>. I’ve been maintaining families of repositories for the better
part of a decade.</p>

<p>Lately I’ve also been reaching for git worktrees far more than I used to, so the same
repository is now checked out two or three times on my disk at any given moment.</p>

<p>Projectile saw all of that as a pile of unrelated projects, which is exactly what it looks
like from the outside.</p>

<!--more-->

<h2 id="somewhere-else-in-the-same-project">Somewhere else in the same project</h2>

<p><code class="language-plaintext highlighter-rouge">projectile-switch-worktree</code> (<code class="language-plaintext highlighter-rouge">s-p W</code>) offers the other checkouts of the repository you’re
in, each labelled with whatever tells it apart - the branch for git, the workspace name for
Jujutsu:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Switch to worktree:
~/src/myapp-main/     (main)
~/src/myapp-hotfix/   (hotfix/crash-on-open)
</code></pre></div></div>

<p>Git worktrees are the obvious case. Projectile asks git about those, so a worktree you’ve
never opened in Emacs shows up anyway. Jujutsu workspaces work the same way.</p>

<p>The case I care about more is the one without any plumbing: a second <code class="language-plaintext highlighter-rouge">git clone</code> of the
same upstream. Same workflow, done by hand. I looked at my own <code class="language-plaintext highlighter-rouge">~/projects</code> while writing
this and found four separate clones of CIDER sitting in it. Nothing records those anywhere,
so Projectile can’t ask git to list them - it matches them among your known projects by
their remote instead.</p>

<h2 id="somewhere-else-in-the-same-effort">Somewhere else in the same <em>effort</em></h2>

<p>The other half is <code class="language-plaintext highlighter-rouge">projectile-switch-sibling-project</code> (<code class="language-plaintext highlighter-rouge">s-p n</code>), which offers the projects
related to the one you’re in rather than every project on the machine.</p>

<p>The interesting question was what “related” should mean. My first instinct was to compare
directory names and look for a shared prefix. Before writing anything I tried the
candidates out on my actual project directory, all ninety-odd repositories of it.</p>

<p>Comparing names does find <code class="language-plaintext highlighter-rouge">rubocop</code>, <code class="language-plaintext highlighter-rouge">rubocop-ast</code> and <code class="language-plaintext highlighter-rouge">rubocop-rails</code>. But grouping by the
<em>owner of the upstream remote</em> finds this:</p>

<p><img src="/assets/images/projectile-sibling-projects.png" alt="The sibling project prompt in CIDER, listing sixteen related projects including orchard, haystack, port and sayid" /></p>

<p>Half of those names have nothing in common with <code class="language-plaintext highlighter-rouge">cider</code>, and no amount of staring at
directory names would ever have related them. <a href="https://github.com/clojure-emacs/haystack">haystack</a> parses stack traces,
<a href="https://github.com/clojure-emacs/port">port</a> is a printer registry, <a href="https://github.com/clojure-emacs/sayid">sayid</a> is a tracing debugger. Nothing in those
names says “CIDER”.</p>

<p>What they do have in common is an org: they all live under
<a href="https://github.com/clojure-emacs">clojure-emacs</a> on GitHub. That’s not incidental, it’s how the project is
actually organised - the org is the boundary of the effort, and the individual repositories
are just where the pieces ended up when they got big enough to split out. Same story for
the <a href="https://github.com/rubocop">rubocop</a> org, and for <a href="https://github.com/nrepl">nrepl</a>. Once I saw that, keying on the
owner of the remote was obviously right, because it’s the same fact GitHub is already
recording for me.</p>

<p>It needs a bound, though, and the same experiment showed why: about 40% of my checkouts are
under my own GitHub account, and “we’re both under <code class="language-plaintext highlighter-rouge">bbatsov</code>” doesn’t relate anything. So an
inferred group covering more than a quarter of your known projects gets dropped and the
next signal takes over.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> If you’d rather just say what belongs together,
<code class="language-plaintext highlighter-rouge">projectile-project-groups</code> is there and is never second-guessed.</p>

<p>Switching is only the obvious thing to do with this. Now that Projectile can tell which
projects belong together, the same grouping could back a find-file or a search across the
whole family - one prompt that reaches every repository in the org rather than just the one
you happen to be sitting in. I go looking for “which of these fifteen repos defines this
var” often enough that I suspect it’s the more useful half. Nothing built yet, but that’s
where I expect this to go.</p>

<h2 id="the-commands-come-with-you">The commands come with you</h2>

<p>Once Projectile knows two directories are the same repository, the command history can
follow you between them.</p>

<p>Press <code class="language-plaintext highlighter-rouge">M-p</code> at the compile or test prompt in a worktree you made this morning and you get
the commands the project is actually built with, instead of an empty history. That’s <a href="https://github.com/bbatsov/projectile/issues/1786">an
issue from 2022</a> that a stale bot had
helpfully closed for me at some point.</p>

<p>What doesn’t follow you is anything that runs without asking - what a prompt is pre-filled
with, and what <code class="language-plaintext highlighter-rouge">projectile-repeat-last-command</code> replays. I had those shared too in the
first draft, and then watched a repeat in one worktree rebuild the tree next door. A
remembered command can carry absolute paths back to where it was typed.</p>

<h2 id="more-languages-fewer-surprises">More languages, fewer surprises</h2>

<p><code class="language-plaintext highlighter-rouge">projectile-run-test-at-point</code> (<code class="language-plaintext highlighter-rouge">s-p c .</code>) arrived in 3.1 knowing Python, Go and JS/TS. It
now also knows Ruby (both RSpec and Minitest), Rust, Elixir, Java, Erlang and F#.</p>

<p>Ruby is written the same way whichever framework you use, so there the project type picks
the runner rather than the syntax; Java’s picks between Maven and Gradle. Elixir tests get
addressed as <code class="language-plaintext highlighter-rouge">FILE:LINE</code>, because ExUnit can’t select a test by name from the command line.
OCaml deliberately gets nothing - its tests are ordinary values you register with Alcotest
or OUnit, so there’s no syntax to recognize.</p>

<p><code class="language-plaintext highlighter-rouge">projectile-find-file-of-kind</code> (<code class="language-plaintext highlighter-rouge">s-p j</code>) and <code class="language-plaintext highlighter-rouge">projectile-toggle-related-file</code> (<code class="language-plaintext highlighter-rouge">s-p J</code>)
learned Phoenix, Laravel and Next.js. Rails and Django had been the only frameworks with
file-kinds tables out of the box, which felt like a strange place to have stopped.</p>

<h2 id="the-reports-answer-back">The reports answer back</h2>

<p>The dashboard and the doctor from 3.3 both got a pass. They’re no longer plain text -
section headers, field labels and findings are faced by meaning, with findings colored by
severity and sorted so anything wanting your attention comes first. The faces only inherit
from standard ones, so your theme styles them without knowing Projectile exists.</p>

<p>The doctor’s findings now come with a button that acts on them:</p>

<p><img src="/assets/images/projectile-doctor-findings.png" alt="The doctor's findings section, with a warning about projectile-mode carrying an enable button, above four ok findings" /></p>

<p><code class="language-plaintext highlighter-rouge">[enable]</code> for a <code class="language-plaintext highlighter-rouge">projectile-mode</code> you forgot to turn on, <code class="language-plaintext highlighter-rouge">[enable caching]</code> on a big
uncached project, <code class="language-plaintext highlighter-rouge">[open dirconfig]</code>, <code class="language-plaintext highlighter-rouge">[edit .dir-locals.el]</code>. Pressing one regenerates the
report. Findings Projectile can’t act on stay plain advice.</p>

<p>And since a doctor report usually ends up pasted into an issue, <code class="language-plaintext highlighter-rouge">w</code> copies the buffer as
plain text, without the faces and buttons.</p>

<h2 id="odds-and-ends">Odds and ends</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">projectile-find-changed-file</code> (<code class="language-plaintext highlighter-rouge">s-p C</code>) completes over what git reports as staged,
unstaged or untracked - or, with a prefix argument, everything that differs from a
revision you pick.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-run-task</code> discovers rake tasks now, read out of your <code class="language-plaintext highlighter-rouge">Rakefile</code> and <code class="language-plaintext highlighter-rouge">.rake</code>
files rather than by running <code class="language-plaintext highlighter-rouge">rake -T</code>, which would load the whole application.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-ignored-project-patterns</code> is the regexp-matching sibling of
<code class="language-plaintext highlighter-rouge">projectile-ignored-projects</code>, so keeping a whole area of your machine out of the known
projects no longer needs a lambda.</li>
  <li>Messages Projectile emits on its own initiative are prefixed with <code class="language-plaintext highlighter-rouge">[Projectile]</code> now, and
the ones answering a command you just invoked aren’t. There were five different
conventions in there before, which I’d somehow never noticed.</li>
  <li>The known projects file, the frecency store and the session directory are resolved with
<code class="language-plaintext highlighter-rouge">locate-user-emacs-file</code>, so they land in the right place if your configuration lives in
<code class="language-plaintext highlighter-rouge">~/.config/emacs</code>. Nothing moves if it doesn’t.</li>
  <li>Elixir’s implementation/test toggle offers <code class="language-plaintext highlighter-rouge">foo_test.exs</code> rather than <code class="language-plaintext highlighter-rouge">foo_test.ex</code>, a
script ExUnit will actually run. Project types can declare their test file extension now.</li>
</ul>

<h2 id="upgrade-notes">Upgrade notes</h2>

<p>Nothing here should break a working setup, but two things are worth knowing.</p>

<p>A batch of options were renamed or folded together. The six
<code class="language-plaintext highlighter-rouge">projectile-&lt;cmd&gt;-use-comint-mode</code> options became one <code class="language-plaintext highlighter-rouge">projectile-use-comint-mode</code>;
<code class="language-plaintext highlighter-rouge">projectile-per-project-compilation-buffer</code> and <code class="language-plaintext highlighter-rouge">projectile-per-command-compilation-buffer</code>
became <code class="language-plaintext highlighter-rouge">projectile-compilation-buffer-scope</code>; and a handful of options that had broken
their own naming schemes were renamed to match their siblings.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> <strong>Every one of them is
still honored under its old name</strong>, so your config keeps working - you’ll just see an
obsolescence notice.</p>

<p>Two options are gone: <code class="language-plaintext highlighter-rouge">projectile-tags-file-name</code> and <code class="language-plaintext highlighter-rouge">projectile-go-project-test-function</code>
were only ever read as Projectile loaded, which means setting them from your init file
afterwards did precisely nothing.</p>

<p>And the command history is now the repository’s rather than the directory’s. Set
<code class="language-plaintext highlighter-rouge">projectile-command-history-scope</code> to <code class="language-plaintext highlighter-rouge">project</code> if you’d rather have it per directory.
Histories you already have are adopted, not dropped.</p>

<p>The full changelog is
<a href="https://github.com/bbatsov/projectile/blob/master/CHANGELOG.md">here</a>, and the manual is
at <a href="https://docs.projectile.mx/">docs.projectile.mx</a>. The cross-repository features have a
<a href="https://docs.projectile.mx/projectile/3.4/across_repositories.html">page of their own</a>,
limitations included.</p>

<p>Five releases in six weeks is not a pace I intend to keep up, and this is the natural place
for the burst to land. The big pieces from 3.0 through 3.3 have had their corners sanded
down, and what’s left on my list is smaller and less interesting to write about. Which is
roughly where a fifteen-year-old package ought to be.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Which is why <code class="language-plaintext highlighter-rouge">projectile</code> itself comes back with no siblings on my machine. The cap is working; the answer is a configured group. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">projectile-global-ignore-file-patterns</code>, <code class="language-plaintext highlighter-rouge">projectile-cmd-hist-ignoredups</code>, <code class="language-plaintext highlighter-rouge">projectile-related-files-fn-function</code>, <code class="language-plaintext highlighter-rouge">projectile-auto-discover</code>, and the three reviewable-search options that were named after replace. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Emacs" /><category term="Projectile" /><summary type="html"><![CDATA[Projectile 3.4 is out! After the four releases that preceded it this summer, this one is polish rather than ambition. Most of it came out of an annoyance I’d been living with for years without ever quite naming it: a lot of what I work on isn’t one directory. CIDER is really CIDER plus cider-nrepl plus orchard plus clj-refactor plus a handful of others. RuboCop is RuboCop plus rubocop-ast plus rubocop-rails and the other extension gems plus the style guide. I’ve been maintaining families of repositories for the better part of a decade. Lately I’ve also been reaching for git worktrees far more than I used to, so the same repository is now checked out two or three times on my disk at any given moment. Projectile saw all of that as a pile of unrelated projects, which is exactly what it looks like from the outside.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">RuboCop 1.89: Project-Wide Analysis with Rubydex</title><link href="https://metaredux.com/posts/2026/08/05/rubocop-1-89.html" rel="alternate" type="text/html" title="RuboCop 1.89: Project-Wide Analysis with Rubydex" /><published>2026-08-05T07:00:00+00:00</published><updated>2026-08-05T07:00:00+00:00</updated><id>https://metaredux.com/posts/2026/08/05/rubocop-1-89</id><content type="html" xml:base="https://metaredux.com/posts/2026/08/05/rubocop-1-89.html"><![CDATA[<p><a href="https://github.com/rubocop/rubocop/releases/tag/v1.89.0">RuboCop 1.89</a> is out, and it comes with the usual assortment of new cops
and bug fixes. But there’s one theme running through this release that I’m
particularly excited about, and that’s what I want to talk about today: RuboCop
is finally learning to look beyond a single file.</p>

<!--more-->

<h2 id="the-single-file-curse-limitation">The single-file curse (limitation)</h2>

<p>For its entire existence RuboCop has analyzed your code one file at a time. That’s
not an accident - it’s a deliberate design choice that has served the project
extremely well. A file-at-a-time analyzer is simple, it’s fast, and it
parallelizes trivially across all your cores. It’s a big part of why RuboCop can
lint a huge codebase in a few seconds.</p>

<p>The catch is that a cop looking at a single file is essentially working with
blinders on. It has no idea what’s defined in the rest of your project. And that
turns out to be a real limitation for a whole class of checks:</p>

<ul>
  <li>Is this method a duplicate of one defined in another file? No way to tell.</li>
  <li>Does this constant actually refer to what I think it does, given the surrounding
namespaces? Can’t resolve it without the whole picture.</li>
  <li>Does this class really need a <code class="language-plaintext highlighter-rouge">super</code> call in its constructor, or does none of
its ancestors define <code class="language-plaintext highlighter-rouge">initialize</code>? RuboCop can’t see the ancestry, so it has to
play it safe.</li>
</ul>

<p>Faced with questions like these, cops have only ever had two options: bail out and
say nothing (a false negative), or guess and risk yelling at perfectly good code
(a false positive). Neither is great. Over the years we’ve papered over this with
configuration knobs - things like <code class="language-plaintext highlighter-rouge">AllowedParentClasses</code> for <code class="language-plaintext highlighter-rouge">Lint/MissingSuper</code>,
where you manually list the base classes RuboCop can’t see for itself. It works,
but it’s the kind of busywork a tool should be doing for you.</p>

<h2 id="enter-rubydex">Enter rubydex</h2>

<p><a href="https://github.com/Shopify/rubydex">rubydex</a> is a library from Shopify that does exactly what RuboCop
historically couldn’t: it builds a project-wide index of your code - a graph of
every declaration and reference, with the ancestry and resolution worked out. Give
a cop access to that index and suddenly it can answer all those cross-file
questions with confidence.</p>

<p>We introduced optional support for rubydex back in <a href="https://docs.rubocop.org/rubocop/usage/project_index.html">1.87</a>, and 1.89
is where it really starts to pay off.</p>

<p>I want to stress the word <em>optional</em> here. rubydex is a separate gem with native
(Rust) extensions, and using it means paying for an indexing pass and some extra
memory. That’s a perfectly reasonable trade for the extra smarts, but it’s not a
trade everyone wants to make, and it shouldn’t be forced on anyone. RuboCop’s
default experience should stay lean and dependency-light - no native code, no
index, no surprises. So rubydex is opt-in, and I fully expect it’ll stay that way
for the foreseeable future. If you want the cross-file superpowers, you flip a
switch; if you don’t, nothing changes for you.</p>

<p>Turning it on is two steps. Add the gem to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'rubydex'</span><span class="p">,</span> <span class="ss">require: </span><span class="kp">false</span>
</code></pre></div></div>

<p>And enable the flag in your <code class="language-plaintext highlighter-rouge">.rubocop.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">AllCops</span><span class="pi">:</span>
  <span class="na">UseProjectIndex</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Everything below happens automatically once the index is available - there’s no
per-cop opt-in. And if the gem isn’t installed, RuboCop just prints a friendly
warning and falls back to its usual file-local behavior.</p>

<h2 id="what-the-index-unlocks-in-189">What the index unlocks in 1.89</h2>

<p>This release wires the index into a good chunk of the cop library. Here’s a tour
of what you get.</p>

<p>There are a few brand-new cops that simply couldn’t exist without a project-wide
view:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Lint/NameTypo</code> is my personal favorite - it catches typos in constant and
method names by checking them against everything defined in your project and
suggesting the closest match. So <code class="language-plaintext highlighter-rouge">Services::UserCraetor.new</code> gets flagged with a
helpful “did you mean <code class="language-plaintext highlighter-rouge">UserCreator</code>?”, and the same goes for a fat-fingered
method call like <code class="language-plaintext highlighter-rouge">Report.generate_sumary</code>. It’s careful to only speak up when
it’s confident (the name has to be unresolvable <em>and</em> have a close sibling), so
it stays quiet on genuinely external names.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/DeprecatedReference</code> flags calls and constant references to any API you’ve
marked with a YARD <code class="language-plaintext highlighter-rouge">@deprecated</code> tag, anywhere in the project. Tag a method as
deprecated in one file, and every stray use of it elsewhere lights up. It’s a
lovely way to drive a gradual migration off some old code.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/UnusedPrivateMethod</code> does project-wide dead-code detection - it reports
private methods that are never referenced anywhere in the indexed project. It’s
disabled by default and best used as an occasional sweep, since it can’t see
reflective calls, but it’s great for a spring cleaning.</li>
</ul>

<p>A whole bunch of existing cops got sharper too, either catching things they used
to miss or backing off where they used to nag:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Lint/DuplicateMethods</code> now catches duplicates that live in <em>different</em> files -
the same method defined twice across two files was previously invisible to it.
If you’re deliberately redefining a method (a monkey patch, say), the self-alias
trick (<code class="language-plaintext highlighter-rouge">alias foo foo</code> right before the redefinition) signals your intent and
keeps things quiet.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/ConstantReassignment</code> likewise catches a constant that’s reassigned in
another file, not just within the current one.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/MissingSuper</code> no longer nags you about a missing <code class="language-plaintext highlighter-rouge">super</code> when the index can
prove that none of the class’s ancestors actually defines <code class="language-plaintext highlighter-rouge">initialize</code> - in that
case <code class="language-plaintext highlighter-rouge">super</code> would only reach the no-op <code class="language-plaintext highlighter-rouge">Object#initialize</code>. This is the one that
makes those <code class="language-plaintext highlighter-rouge">AllowedParentClasses</code> lists mostly unnecessary; RuboCop figures out
your abstract base classes on its own now.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/ConstantResolution</code> used to be so noisy it was practically unusable without
a pile of <code class="language-plaintext highlighter-rouge">Only</code>/<code class="language-plaintext highlighter-rouge">Ignore</code> configuration. With the index it reports only genuinely
ambiguous constants - ones that resolve differently through the surrounding
nesting than they would fully qualified - which finally makes it practical to
turn on.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/InheritException</code> now spots classes that inherit from <code class="language-plaintext highlighter-rouge">Exception</code>
indirectly, through a parent class defined elsewhere in your project.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/Documentation</code> accepts a reopened class or module as documented as long as
<em>any</em> of its definition sites carries a doc comment, so you’re not forced to
repeat yourself at every reopening.</li>
  <li>A cluster of cops learned to stop producing false positives once they can see the
whole picture: <code class="language-plaintext highlighter-rouge">Style/MissingRespondToMissing</code> accepts a <code class="language-plaintext highlighter-rouge">respond_to_missing?</code>
defined in another reopening of the class; <code class="language-plaintext highlighter-rouge">Style/StaticClass</code> leaves alone
classes that are subclassed elsewhere; <code class="language-plaintext highlighter-rouge">Style/RedundantConstantBase</code> can now flag
a redundant leading <code class="language-plaintext highlighter-rouge">::</code> inside a namespace when it proves the constant resolves
identically without it; and <code class="language-plaintext highlighter-rouge">Naming/PredicatePrefix</code> and
<code class="language-plaintext highlighter-rouge">Naming/AccessorMethodName</code> stop suggesting renames for methods that override an
ancestor defined elsewhere in the project.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/ClassAndModuleChildren</code> uses the index to make its (unsafe) autocorrection
a lot more reliable - it resolves the real namespace kind (<code class="language-plaintext highlighter-rouge">class</code> vs <code class="language-plaintext highlighter-rouge">module</code>)
instead of guessing, and skips compacting a definition when doing so would blow
up with a <code class="language-plaintext highlighter-rouge">NameError</code> at load time.</li>
</ul>

<p>And a couple of things happening under the hood are worth calling out:</p>

<ul>
  <li>The index now always covers your whole project, regardless of which files a
particular run happens to inspect. That means linting a single file reports the
same cross-file offenses as a full run - no more results that depend on how you
invoked RuboCop.</li>
  <li>If you want ancestry chains that reach into your gems to resolve too (think a
model inheriting from a framework base class), the new
<code class="language-plaintext highlighter-rouge">AllCops/ProjectIndexIncludesGems</code> option indexes your bundle’s sources as well.
On RuboCop’s own repo that took the share of classes with a fully resolvable
ancestry from about 20% to about 97%, at the cost of some extra memory.</li>
  <li>The language server got smarter about the index - it now builds it once per
session and reuses it across requests, rebuilding only when you save a file, so
editing a buffer doesn’t pay the indexing cost on every keystroke.</li>
</ul>

<h2 id="still-early-days">Still early days</h2>

<p>I don’t want to oversell any of this. We’re at the very beginning of RuboCop’s
cross-file journey, and there’s a lot we haven’t tackled yet. The list of checks
that would benefit from a project-wide view is long, and I suspect the most
interesting applications are still ahead of us. rubydex itself is young and
evolving, and so is our integration with it.</p>

<p>But I think it’s a genuinely promising start. A year ago the idea of RuboCop
catching a constant typo across files, or reasoning about a class’s real ancestry,
would have been a non-starter. Now it’s a config flag away.</p>

<p>If any of this sounds useful, I’d love for you to give it a try - add <code class="language-plaintext highlighter-rouge">rubydex</code> to
your <code class="language-plaintext highlighter-rouge">Gemfile</code>, flip <code class="language-plaintext highlighter-rouge">UseProjectIndex</code> on, and see what it turns up in your
codebase. And please <a href="https://github.com/rubocop/rubocop/issues">tell us how it goes</a>. The feature is still marked
experimental, and real-world feedback is exactly what we need to figure out where
to take it next. Bug reports, false positives, ideas for new cross-file cops - all
of it is welcome.</p>

<p>Big thanks to everyone who has been kicking the tires on the project index and
reporting their findings. Here’s to seeing beyond a single file.</p>

<p>Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Ruby" /><category term="RuboCop" /><summary type="html"><![CDATA[RuboCop 1.89 is out, and it comes with the usual assortment of new cops and bug fixes. But there’s one theme running through this release that I’m particularly excited about, and that’s what I want to talk about today: RuboCop is finally learning to look beyond a single file.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Leveling Up CIDER’s ClojureScript Support</title><link href="https://metaredux.com/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html" rel="alternate" type="text/html" title="Leveling Up CIDER’s ClojureScript Support" /><published>2026-07-29T14:00:00+00:00</published><updated>2026-07-29T14:00:00+00:00</updated><id>https://metaredux.com/posts/2026/07/29/leveling-up-ciders-clojurescript-support</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html"><![CDATA[<p>Continuing the series on the notable changes in
<a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0</a>, let’s talk about
ClojureScript - forever the trickier sibling in the CIDER family.</p>

<p>I’ll start with a confession I’ve made before: I rarely use ClojureScript
myself, which is a big part of why its support in CIDER has historically lagged
behind Clojure’s. Every “State of CIDER” survey reminds me of this, usually in
the comments section, occasionally in all caps. So in the 2.0 cycle I decided
to stop feeling vaguely guilty about it and actually do something - across
every layer of the stack: CIDER itself, <code class="language-plaintext highlighter-rouge">cider-nrepl</code>, and
<a href="https://github.com/nrepl/piggieback">Piggieback</a>.</p>

<h2 id="first-a-strategic-decision">First, a strategic decision</h2>

<p>The most important ClojureScript change in CIDER 2.0 isn’t a feature - it’s a
decision about what <em>not</em> to build. Some of CIDER’s most powerful tools (the
debugger, enlighten, tracing, profiling) are deeply tied to JVM runtime
introspection, and porting them to ClojureScript would be a massive effort with
a poor cost/benefit ratio. Rather than keeping them in eternal “maybe someday”
limbo, we’ve explicitly scoped them as Clojure-only and focused the actual work
on the things cljs users hit every day: evaluation, testing, error reporting,
and clear behavior everywhere else.</p>

<p>That last part matters more than it sounds. Historically, invoking a
JVM-only command in a ClojureScript REPL would fail in some confusing way - a
cryptic error, a JVM-flavored result, or silence. Now the ops themselves report
a <code class="language-plaintext highlighter-rouge">clojure-only</code> status, and CIDER tells you plainly that the command isn’t
supported for ClojureScript. Knowing what a tool <em>won’t</em> do is half of trusting
it.</p>

<h2 id="what-actually-got-better">What actually got better</h2>

<ul>
  <li>The regular test commands (<code class="language-plaintext highlighter-rouge">cider-test-run-ns-tests</code> and
friends) now work in ClojureScript REPLs, asynchronous <code class="language-plaintext highlighter-rouge">cljs.test/async</code>
tests included. Previously CIDER just refused, and you were stuck evaluating
<code class="language-plaintext highlighter-rouge">(run-tests)</code> by hand like an animal.</li>
  <li>Expanding your own macros (e.g.
ones brought in via <code class="language-plaintext highlighter-rouge">:refer-macros</code>) used to silently echo the form back
unexpanded - a bug filed all the way back in
<a href="https://github.com/clojure-emacs/cider/issues/2099">2017</a>. The compiler
environment is now threaded to the analyzer properly, and it just works.</li>
  <li><code class="language-plaintext highlighter-rouge">cider-nrepl</code> now resolves the
ClojureScript compiler environment through a provider chain, with a dedicated
shadow-cljs provider - so the static-analysis ops keep working in a shadow
REPL that never loads Piggieback.</li>
  <li>The new <code class="language-plaintext highlighter-rouge">cider-tap</code> viewer works with ClojureScript
too: a runtime helper buffers tapped values and the JVM side streams them to
Emacs. (Tapped cljs values aren’t inspectable - they live in the JS runtime -
but you see them as they happen.)</li>
  <li>ClojureScript stack frames now render their
<code class="language-plaintext highlighter-rouge">ns/fn</code> properly instead of degrading to <code class="language-plaintext highlighter-rouge">nil/nil</code>, and unqualified core vars
resolve against <code class="language-plaintext highlighter-rouge">cljs.core</code> rather than falling back to <code class="language-plaintext highlighter-rouge">clojure.core</code>
(which quietly broke things like indentation metadata).</li>
  <li>A recent ClojureScript on the classpath (whose
Closure compiler wants JDK 21+) no longer crashes <code class="language-plaintext highlighter-rouge">cider-nrepl</code> at startup on
an older JDK - you get a Clojure-only session instead of no session.</li>
  <li>Piggieback itself got a round of bug fixes in the
0.6.x/0.7.0 releases - it’s easy to forget it exists (which is rather the
point of it), but it powers most cljs REPLs CIDER talks to.</li>
</ul>

<p>The documentation kept pace too: the new
<a href="https://docs.cider.mx/cider/cljs/up_and_running.html#full-stack-clojure-clojurescript-projects">full-stack Clojure + ClojureScript guide</a>
covers the two-REPLs-one-project setup that trips up nearly everyone, and the
<a href="https://docs.cider.mx/cider/cljs/overview.html">ClojureScript docs</a> got a
general refresh.</p>

<h2 id="an-unexpected-assist">An unexpected assist</h2>

<p>Fun aside: this is the area where AI coding agents helped me the most during
the 2.0 cycle. My ClojureScript experience is limited, but between the
excellent bug reports from the community and the ability to quickly prototype
and test fixes against real shadow-cljs and figwheel setups, problems that had
been “someone who knows cljs should look at this someday” for years finally got
fixed. Make of that what you will.</p>

<h2 id="whats-next">What’s next</h2>

<p>I keep pondering some form of “native” shadow-cljs support, given that
shadow-cljs is what most ClojureScript users actually run these days. That’s
still very much in the hammock phase, so don’t hold me to it - but the
direction is clear: fewer moving parts, clearer errors, and honesty about
what’s supported.</p>

<p>If you’re a ClojureScript user, I’d genuinely love to hear how 2.0 feels in
your daily work - the feedback loop is what keeps this improving. Keep hacking!</p>

<h2>Articles in the Series</h2>

<ul>

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0: Sky is the Limit</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/16/simplifying-session-management-in-cider.html">Simplifying Session Management in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/17/stepping-through-macros-in-cider.html">Stepping Through Macros in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/23/making-cider-more-discoverable.html">Making CIDER More Discoverable</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER's Completion</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html">Closing the Find-Usages Gap in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/28/sharpening-ciders-debugging-tools.html">Sharpening CIDER's Debugging Tools</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html">Leveling Up CIDER's ClojureScript Support</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->
 <!-- posts for -->
</ul>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="CIDER 2.0" /><category term="Emacs" /><category term="Clojure" /><category term="ClojureScript" /><category term="CIDER" /><summary type="html"><![CDATA[Continuing the series on the notable changes in CIDER 2.0, let’s talk about ClojureScript - forever the trickier sibling in the CIDER family. I’ll start with a confession I’ve made before: I rarely use ClojureScript myself, which is a big part of why its support in CIDER has historically lagged behind Clojure’s. Every “State of CIDER” survey reminds me of this, usually in the comments section, occasionally in all caps. So in the 2.0 cycle I decided to stop feeling vaguely guilty about it and actually do something - across every layer of the stack: CIDER itself, cider-nrepl, and Piggieback. First, a strategic decision The most important ClojureScript change in CIDER 2.0 isn’t a feature - it’s a decision about what not to build. Some of CIDER’s most powerful tools (the debugger, enlighten, tracing, profiling) are deeply tied to JVM runtime introspection, and porting them to ClojureScript would be a massive effort with a poor cost/benefit ratio. Rather than keeping them in eternal “maybe someday” limbo, we’ve explicitly scoped them as Clojure-only and focused the actual work on the things cljs users hit every day: evaluation, testing, error reporting, and clear behavior everywhere else. That last part matters more than it sounds. Historically, invoking a JVM-only command in a ClojureScript REPL would fail in some confusing way - a cryptic error, a JVM-flavored result, or silence. Now the ops themselves report a clojure-only status, and CIDER tells you plainly that the command isn’t supported for ClojureScript. Knowing what a tool won’t do is half of trusting it. What actually got better The regular test commands (cider-test-run-ns-tests and friends) now work in ClojureScript REPLs, asynchronous cljs.test/async tests included. Previously CIDER just refused, and you were stuck evaluating (run-tests) by hand like an animal. Expanding your own macros (e.g. ones brought in via :refer-macros) used to silently echo the form back unexpanded - a bug filed all the way back in 2017. The compiler environment is now threaded to the analyzer properly, and it just works. cider-nrepl now resolves the ClojureScript compiler environment through a provider chain, with a dedicated shadow-cljs provider - so the static-analysis ops keep working in a shadow REPL that never loads Piggieback. The new cider-tap viewer works with ClojureScript too: a runtime helper buffers tapped values and the JVM side streams them to Emacs. (Tapped cljs values aren’t inspectable - they live in the JS runtime - but you see them as they happen.) ClojureScript stack frames now render their ns/fn properly instead of degrading to nil/nil, and unqualified core vars resolve against cljs.core rather than falling back to clojure.core (which quietly broke things like indentation metadata). A recent ClojureScript on the classpath (whose Closure compiler wants JDK 21+) no longer crashes cider-nrepl at startup on an older JDK - you get a Clojure-only session instead of no session. Piggieback itself got a round of bug fixes in the 0.6.x/0.7.0 releases - it’s easy to forget it exists (which is rather the point of it), but it powers most cljs REPLs CIDER talks to. The documentation kept pace too: the new full-stack Clojure + ClojureScript guide covers the two-REPLs-one-project setup that trips up nearly everyone, and the ClojureScript docs got a general refresh. An unexpected assist Fun aside: this is the area where AI coding agents helped me the most during the 2.0 cycle. My ClojureScript experience is limited, but between the excellent bug reports from the community and the ability to quickly prototype and test fixes against real shadow-cljs and figwheel setups, problems that had been “someone who knows cljs should look at this someday” for years finally got fixed. Make of that what you will. What’s next I keep pondering some form of “native” shadow-cljs support, given that shadow-cljs is what most ClojureScript users actually run these days. That’s still very much in the hammock phase, so don’t hold me to it - but the direction is clear: fewer moving parts, clearer errors, and honesty about what’s supported. If you’re a ClojureScript user, I’d genuinely love to hear how 2.0 feels in your daily work - the feedback loop is what keeps this improving. Keep hacking! Articles in the Series CIDER 2.0: Sky is the Limit Simplifying Session Management in CIDER Stepping Through Macros in CIDER Making CIDER More Discoverable Modernizing CIDER's Completion Closing the Find-Usages Gap in CIDER Sharpening CIDER's Debugging Tools Leveling Up CIDER's ClojureScript Support]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Sharpening CIDER’s Debugging Tools</title><link href="https://metaredux.com/posts/2026/07/28/sharpening-ciders-debugging-tools.html" rel="alternate" type="text/html" title="Sharpening CIDER’s Debugging Tools" /><published>2026-07-28T08:00:00+00:00</published><updated>2026-07-28T08:00:00+00:00</updated><id>https://metaredux.com/posts/2026/07/28/sharpening-ciders-debugging-tools</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/28/sharpening-ciders-debugging-tools.html"><![CDATA[<p>The series on the notable changes in
<a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0</a> rolls on. This time: the
“what is my code <em>actually</em> doing?” toolbox - the debugger, tracing,
enlighten, and the new tap viewer. This was the part of the release I enjoyed
working on the most, and the part that needed the most love.</p>

<h2 id="the-debugger-dusted-off">The debugger, dusted off</h2>

<p>CIDER’s interactive debugger is one of its most impressive features and,
paradoxically, one of its least reliable ones. Instrumenting arbitrary Clojure
code is <em>hard</em> - the debugger rewrites your forms to capture locals at every
step, and the corner cases are endless. Over the 2.0 cycle (and the 0.62.x
releases of <code class="language-plaintext highlighter-rouge">cider-nrepl</code>) a whole family of long-standing instrumentation bugs
got fixed:</p>

<ul>
  <li>Record literals embedded in code survive instrumentation instead of being
quietly downgraded to plain maps - which used to break protocol dispatch in
anything that compiled routes or components into records (compojure users
know the pain).</li>
  <li><code class="language-plaintext highlighter-rouge">defrecord</code>/<code class="language-plaintext highlighter-rouge">deftype</code> inline methods no longer blow up with the infamous
<code class="language-plaintext highlighter-rouge">Unable to resolve symbol: STATE__</code> error - the instrumenter now sensibly
skips the method bodies, which compile to real JVM methods that can’t
capture debugger state. <code class="language-plaintext highlighter-rouge">#dbg</code> on a bare collection literal triggered the
same error; fixed too. And heavily destructured argument lists used to
crash instrumentation in their own special way - not anymore.</li>
  <li>A form too large to instrument (yes, that’s a JVM limitation - <code class="language-plaintext highlighter-rouge">Method code
too large!</code>) now degrades gracefully: CIDER retries without local capture and
tells you what happened, instead of surfacing a raw compiler error.</li>
  <li>Quitting a debug session uses nREPL’s interrupt machinery instead of the
deprecated <code class="language-plaintext highlighter-rouge">Thread.stop</code>, so it keeps working on modern JDKs where
<code class="language-plaintext highlighter-rouge">Thread.stop</code> is simply gone.</li>
</ul>

<p>The UX got attention too. Quitting the debugger with <code class="language-plaintext highlighter-rouge">q</code> finally restores point
to where you started the session - a request filed in
<a href="https://github.com/clojure-emacs/cider/issues/1595">2016</a> - instead of
stranding you at the last breakpoint. The force-step-out key works again. And
all the debugger’s single-key commands are now proper named commands with a
transient menu (<code class="language-plaintext highlighter-rouge">?</code>) listing them, so you’re never stuck trying to remember
whether locals was <code class="language-plaintext highlighter-rouge">l</code> or <code class="language-plaintext highlighter-rouge">L</code>.</p>

<h2 id="tracing-grew-a-home">Tracing grew a home</h2>

<p><code class="language-plaintext highlighter-rouge">clojure.tools.trace</code>-style tracing has been in CIDER forever, but the output
was always interleaved into the REPL, where it fought with your actual work.
CIDER 2.0 gives traces a dedicated, live-streaming <code class="language-plaintext highlighter-rouge">*cider-trace*</code> buffer:</p>

<p><img src="/assets/images/cider-trace-buffer.png" alt="The *cider-trace* buffer showing a nested call tree with return values" /></p>

<p>Calls fold and unfold (<code class="language-plaintext highlighter-rouge">TAB</code>, or <code class="language-plaintext highlighter-rouge">F</code>/<code class="language-plaintext highlighter-rouge">U</code> for everything at once), <code class="language-plaintext highlighter-rouge">n</code>/<code class="language-plaintext highlighter-rouge">p</code> move
between calls, and <code class="language-plaintext highlighter-rouge">.</code> jumps to a function’s definition. <code class="language-plaintext highlighter-rouge">cider-list-traced</code>
answers the eternal “wait, what did I even trace?”, and <code class="language-plaintext highlighter-rouge">cider-untrace-all</code>
cleans the slate.</p>

<h2 id="enlighten-back-from-the-dead">Enlighten, back from the dead</h2>

<p><a href="https://docs.cider.mx/cider/debugging/enlighten.html">Enlighten</a> - the mode
that displays the values of locals inline as your code runs - has been in
“experimental” limbo since 2016. It finally got a proper overhaul: a real test
suite, fixes for the same record/deftype instrumentation bugs as the debugger
(they share machinery), and - importantly - manners. You can now enlighten a
single form with <code class="language-plaintext highlighter-rouge">cider-enlighten-defun-at-point</code> instead of flipping a global
mode, and <code class="language-plaintext highlighter-rouge">cider-enlighten-stop</code> turns everything off at once, rather than
making you re-evaluate every function in penance.</p>

<p>Every local and every intermediate result, right there in the buffer:</p>

<p><img src="/assets/images/cider-enlighten.png" alt="Enlighten showing argument and return values inline in the source" /></p>

<h2 id="tap-into-your-programs">Tap into your programs</h2>

<p>New in 2.0: <code class="language-plaintext highlighter-rouge">cider-tap</code>, a buffer that streams every value sent to <code class="language-plaintext highlighter-rouge">tap&gt;</code> and
lets you crack any of them open in the inspector with <code class="language-plaintext highlighter-rouge">RET</code>. <code class="language-plaintext highlighter-rouge">tap&gt;</code> has quietly
become the Clojure community’s favorite debugging primitive, and now you don’t
need an external tool like Portal or Reveal for the basic workflow - though
those remain great if you want more. (ClojureScript taps stream too; they’re
just not inspectable, since the values live in the JS runtime.)</p>

<p>It’s <code class="language-plaintext highlighter-rouge">println</code> debugging, minus the println guilt.</p>

<h2 id="the-connective-tissue">The connective tissue</h2>

<p>A few related quality-of-life items round out the picture: stack frames for
top-level anonymous functions jump to their actual source instead of
<code class="language-plaintext highlighter-rouge">clojure.core/fn</code> (a bug from
<a href="https://github.com/clojure-emacs/cider/issues/3157">2020</a>), ClojureScript
frames render their <code class="language-plaintext highlighter-rouge">ns/fn</code> properly, and the macroexpansion tooling - a
debugging tool in its own right - got a full makeover that deserves (and will
get) its own article.</p>

<p>None of these tools is new. That’s rather the point: the 2.0 debugging story
is mostly the existing tools becoming trustworthy. A debugger you don’t trust
is worse than no debugger at all.</p>

<p>The <a href="https://docs.cider.mx/cider/debugging/debugger.html">debugging docs</a> cover
everything in detail. Keep hacking!</p>

<h2>Articles in the Series</h2>

<ul>

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0: Sky is the Limit</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/16/simplifying-session-management-in-cider.html">Simplifying Session Management in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/17/stepping-through-macros-in-cider.html">Stepping Through Macros in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/23/making-cider-more-discoverable.html">Making CIDER More Discoverable</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER's Completion</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html">Closing the Find-Usages Gap in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/28/sharpening-ciders-debugging-tools.html">Sharpening CIDER's Debugging Tools</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html">Leveling Up CIDER's ClojureScript Support</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->
 <!-- posts for -->
</ul>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="CIDER 2.0" /><category term="Emacs" /><category term="Clojure" /><category term="CIDER" /><summary type="html"><![CDATA[The series on the notable changes in CIDER 2.0 rolls on. This time: the “what is my code actually doing?” toolbox - the debugger, tracing, enlighten, and the new tap viewer. This was the part of the release I enjoyed working on the most, and the part that needed the most love. The debugger, dusted off CIDER’s interactive debugger is one of its most impressive features and, paradoxically, one of its least reliable ones. Instrumenting arbitrary Clojure code is hard - the debugger rewrites your forms to capture locals at every step, and the corner cases are endless. Over the 2.0 cycle (and the 0.62.x releases of cider-nrepl) a whole family of long-standing instrumentation bugs got fixed: Record literals embedded in code survive instrumentation instead of being quietly downgraded to plain maps - which used to break protocol dispatch in anything that compiled routes or components into records (compojure users know the pain). defrecord/deftype inline methods no longer blow up with the infamous Unable to resolve symbol: STATE__ error - the instrumenter now sensibly skips the method bodies, which compile to real JVM methods that can’t capture debugger state. #dbg on a bare collection literal triggered the same error; fixed too. And heavily destructured argument lists used to crash instrumentation in their own special way - not anymore. A form too large to instrument (yes, that’s a JVM limitation - Method code too large!) now degrades gracefully: CIDER retries without local capture and tells you what happened, instead of surfacing a raw compiler error. Quitting a debug session uses nREPL’s interrupt machinery instead of the deprecated Thread.stop, so it keeps working on modern JDKs where Thread.stop is simply gone. The UX got attention too. Quitting the debugger with q finally restores point to where you started the session - a request filed in 2016 - instead of stranding you at the last breakpoint. The force-step-out key works again. And all the debugger’s single-key commands are now proper named commands with a transient menu (?) listing them, so you’re never stuck trying to remember whether locals was l or L. Tracing grew a home clojure.tools.trace-style tracing has been in CIDER forever, but the output was always interleaved into the REPL, where it fought with your actual work. CIDER 2.0 gives traces a dedicated, live-streaming *cider-trace* buffer: Calls fold and unfold (TAB, or F/U for everything at once), n/p move between calls, and . jumps to a function’s definition. cider-list-traced answers the eternal “wait, what did I even trace?”, and cider-untrace-all cleans the slate. Enlighten, back from the dead Enlighten - the mode that displays the values of locals inline as your code runs - has been in “experimental” limbo since 2016. It finally got a proper overhaul: a real test suite, fixes for the same record/deftype instrumentation bugs as the debugger (they share machinery), and - importantly - manners. You can now enlighten a single form with cider-enlighten-defun-at-point instead of flipping a global mode, and cider-enlighten-stop turns everything off at once, rather than making you re-evaluate every function in penance. Every local and every intermediate result, right there in the buffer: Tap into your programs New in 2.0: cider-tap, a buffer that streams every value sent to tap&gt; and lets you crack any of them open in the inspector with RET. tap&gt; has quietly become the Clojure community’s favorite debugging primitive, and now you don’t need an external tool like Portal or Reveal for the basic workflow - though those remain great if you want more. (ClojureScript taps stream too; they’re just not inspectable, since the values live in the JS runtime.) It’s println debugging, minus the println guilt. The connective tissue A few related quality-of-life items round out the picture: stack frames for top-level anonymous functions jump to their actual source instead of clojure.core/fn (a bug from 2020), ClojureScript frames render their ns/fn properly, and the macroexpansion tooling - a debugging tool in its own right - got a full makeover that deserves (and will get) its own article. None of these tools is new. That’s rather the point: the 2.0 debugging story is mostly the existing tools becoming trustworthy. A debugger you don’t trust is worse than no debugger at all. The debugging docs cover everything in detail. Keep hacking! Articles in the Series CIDER 2.0: Sky is the Limit Simplifying Session Management in CIDER Stepping Through Macros in CIDER Making CIDER More Discoverable Modernizing CIDER's Completion Closing the Find-Usages Gap in CIDER Sharpening CIDER's Debugging Tools Leveling Up CIDER's ClojureScript Support]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Projectile 3.3</title><link href="https://metaredux.com/posts/2026/07/27/projectile-3-3.html" rel="alternate" type="text/html" title="Projectile 3.3" /><published>2026-07-27T08:15:00+00:00</published><updated>2026-07-27T08:15:00+00:00</updated><id>https://metaredux.com/posts/2026/07/27/projectile-3-3</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/27/projectile-3-3.html"><![CDATA[<p><a href="https://github.com/bbatsov/projectile/releases/tag/v3.3.0">Projectile 3.3</a> is out! That’s
the fourth release this month, which probably tells you something about how much fun I’ve
been having with Projectile lately.</p>

<p>This one is mostly about a single theme - Projectile knowing more about your project
without you having to tell it anything.</p>

<!--more-->

<h2 id="tasks-you-never-configured">Tasks you never configured</h2>

<p>Projectile has had named tasks for a while now - you put an alist in <code class="language-plaintext highlighter-rouge">projectile-tasks</code>
(or in your project’s <code class="language-plaintext highlighter-rouge">.dir-locals.el</code>) and <code class="language-plaintext highlighter-rouge">projectile-run-task</code> (<code class="language-plaintext highlighter-rouge">s-p c x</code>) offers
them with completion. Useful, but there was a catch that always bugged me: the variable
starts out empty, so the command did precisely nothing until you sat down and filled it in.</p>

<p>Meanwhile every project I work on already has its tasks written down somewhere. The npm
scripts are in <code class="language-plaintext highlighter-rouge">package.json</code>. The Makefile targets are in the Makefile. So now Projectile
just reads them:</p>

<p><img src="/assets/images/projectile-discovered-tasks.png" alt="The projectile-run-task prompt listing npm scripts and Makefile targets discovered in the project" /></p>

<p>That’s a fresh checkout with zero Projectile configuration. Out of the box it knows about
npm scripts (running them through whichever package manager your lock file implies - npm,
yarn, pnpm or bun), Deno tasks, Composer scripts, <code class="language-plaintext highlighter-rouge">just</code> recipes, go-task tasks and Makefile
targets. The tool name is part of the task name, so <code class="language-plaintext highlighter-rouge">npm:build</code> and <code class="language-plaintext highlighter-rouge">make:build</code> can happily
coexist and you can see at a glance where something came from.</p>

<p>If you don’t want this, <code class="language-plaintext highlighter-rouge">projectile-discover-tasks</code> turns it off. If you want more of it,
<code class="language-plaintext highlighter-rouge">projectile-task-providers</code> is just a list of functions that take a project root and return
an alist - adding support for your favorite task runner is a few lines.</p>

<h2 id="a-lot-more-project-types">A lot more project types</h2>

<p>Projectile ships with a registry of project types, and I finally sat down and went through
it properly. The result is 34 new types.</p>

<p>The one that surprised me most was that a <code class="language-plaintext highlighter-rouge">package.json</code> without a lock file next to it
matched nothing at all - such a project came out as <code class="language-plaintext highlighter-rouge">generic</code>, with no compile or test
command. That can’t have been a rare situation. Also missing: Bun, Deno, uv (which has
pretty much taken over new Python projects), plain Composer, Laravel, Xcode, Flutter,
Terraform, Pulumi, Helm, Ansible, <code class="language-plaintext highlighter-rouge">just</code>, mise, Gleam, Babashka, scala-cli, Godot,
PlatformIO, and the static site generators (Hugo, Jekyll, Zola, MkDocs, Quarto).</p>

<p>Going through the registry also turned up three bugs that had been sitting there quietly:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">angular</code> type could never match anything. It listed two markers, <code class="language-plaintext highlighter-rouge">angular.json</code> and
<code class="language-plaintext highlighter-rouge">.angular-cli.json</code>, and marker lists are ANDed - but a project has one or the other,
never both. So the type had been dead since the day <code class="language-plaintext highlighter-rouge">angular.json</code> was added.</li>
  <li><code class="language-plaintext highlighter-rouge">pyproject.toml</code> was checked before <code class="language-plaintext highlighter-rouge">django</code>, <code class="language-plaintext highlighter-rouge">python-poetry</code>, <code class="language-plaintext highlighter-rouge">python-pipenv</code> and
<code class="language-plaintext highlighter-rouge">python-tox</code>. Since just about every Python project has a <code class="language-plaintext highlighter-rouge">pyproject.toml</code> these days,
those four types effectively never matched. A Django project would come out as
<code class="language-plaintext highlighter-rouge">python-toml</code> and lose its file kinds along with its commands.</li>
  <li><code class="language-plaintext highlighter-rouge">php-symfony</code> required an <code class="language-plaintext highlighter-rouge">app</code> directory, which Symfony dropped in version 3, and a
<code class="language-plaintext highlighter-rouge">vendor</code> directory, which only exists after someone runs composer. Modern Symfony
projects weren’t detected at all.</li>
</ul>

<p>I’ve also brought the stale markers up to date (Bazel’s <code class="language-plaintext highlighter-rouge">MODULE.bazel</code>, Gradle’s Kotlin
DSL, Zig’s <code class="language-plaintext highlighter-rouge">build.zig</code>) and fixed a pile of lifecycle commands that had rotted - Symfony’s
console moved to <code class="language-plaintext highlighter-rouge">bin/</code> years ago, the Dart CLI absorbed <code class="language-plaintext highlighter-rouge">pub</code>, <code class="language-plaintext highlighter-rouge">setup.py build</code> is
deprecated by the PyPA, and poetry projects run pytest rather than unittest discovery.</p>

<p>There’s a new <code class="language-plaintext highlighter-rouge">(:any "file1" "file2")</code> form for markers, which is what made most of this
expressible as plain data rather than yet another predicate function.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<h2 id="monorepos">Monorepos</h2>

<p>If you work in a monorepo you know the problem: the repository is one project as far as
git (and therefore Projectile) is concerned, but you’re usually working on one piece of it,
and building or testing the whole thing is a waste of time.</p>

<p>Projectile now understands that a directory holding a manifest of its own is a <em>subproject</em>.
<code class="language-plaintext highlighter-rouge">projectile-find-file-in-subproject</code> (<code class="language-plaintext highlighter-rouge">s-p c m f</code>) asks which one you want and then
scopes file completion to it:</p>

<p><img src="/assets/images/projectile-find-file-in-subproject.png" alt="The Subproject prompt listing packages/ui, services/api and services/web" /></p>

<p>And every lifecycle command has a subproject variant now - <code class="language-plaintext highlighter-rouge">s-p c m c</code> compiles, <code class="language-plaintext highlighter-rouge">s-p c m t</code>
tests, <code class="language-plaintext highlighter-rouge">s-p c m r</code> runs, and so on. They use the project’s own command and simply run it in
the nearest subproject, which is exactly what workspace-aware tools like Cargo, npm and
Maven need in order to scope their work.</p>

<p>There were subproject compile and test commands before this release, but they only looked
for the marker of the project’s <em>own</em> type, which meant they were useless in a polyglot
repository - a Rust crate sitting under a <code class="language-plaintext highlighter-rouge">package.json</code> project was invisible to them. Now
the markers come from every registered project type, so it works regardless of what the repo
looks like from the top.</p>

<h2 id="two-commands-for-looking-at-a-project">Two commands for looking at a project</h2>

<p><code class="language-plaintext highlighter-rouge">projectile-dashboard</code> (<code class="language-plaintext highlighter-rouge">s-p P</code>) summarizes the project you’re in:</p>

<p><img src="/assets/images/projectile-dashboard.png" alt="The Projectile dashboard showing project info, version control status, recent files and tasks" /></p>

<p>Everything in there is a button, so you can jump to a file, run a task or kick off a build
without leaving the buffer. It never indexes the project and only queries git when the
project is a local git repo, which makes it cheap enough to use as your
<code class="language-plaintext highlighter-rouge">projectile-switch-project-action</code> - which is exactly what I do now.</p>

<p>The other one is <code class="language-plaintext highlighter-rouge">projectile-doctor</code> (<code class="language-plaintext highlighter-rouge">s-p H</code>), which explains how Projectile sees the
current project:</p>

<p><img src="/assets/images/projectile-doctor.png" alt="The Projectile doctor report showing project root, type, indexing method and available tools" /></p>

<p>Root, which detection function found it and on which marker, the type and its marker, the
indexing method and the exact command that will run, which external tools are available, the
cache state, the effective ignore rules - and then a list of findings, each either <code class="language-plaintext highlighter-rouge">ok</code> or a
concrete suggestion. It’s plain text, meant to be pasted into a bug report. Selfishly, I’m
hoping this saves us both some back-and-forth in the issue tracker.</p>

<h2 id="odds-and-ends">Odds and ends</h2>

<p>A few smaller things worth knowing about:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">projectile-todos</code> (<code class="language-plaintext highlighter-rouge">s-p s t</code>) collects the project’s <code class="language-plaintext highlighter-rouge">TODO</code>/<code class="language-plaintext highlighter-rouge">FIXME</code> annotations into
the reviewable search buffer.</li>
  <li>Project-scoped bookmarks - <code class="language-plaintext highlighter-rouge">s-p B s</code>, <code class="language-plaintext highlighter-rouge">s-p B j</code>, <code class="language-plaintext highlighter-rouge">s-p B d</code>. They’re plain Emacs bookmarks,
Projectile just scopes the completion to the current project.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-replace-undo</code> (<code class="language-plaintext highlighter-rouge">s-p u</code>) reverts the last replace you applied from the
review buffer, and it’s careful about it - a file whose text has changed since is reported
and left alone.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-per-command-compilation-buffer</code> gives compile, test and run separate
compilation buffers, so running the tests no longer discards your build output.</li>
  <li>An optional Embark and Marginalia integration, wired up with <code class="language-plaintext highlighter-rouge">with-eval-after-load</code> so
neither package becomes a dependency.</li>
</ul>

<h2 id="bug-fixes">Bug fixes</h2>

<p>Three worth calling out.</p>

<p>The first is embarrassing and recent: with asynchronous indexing enabled, <code class="language-plaintext highlighter-rouge">projectile-find-file</code>
could show “Projectile is indexing” and spin forever. Projectile was waiting on the indexing
process’s <em>sentinel</em>, and Emacs doesn’t actually promise to run one while a command sits
waiting on the process. It now waits on the process itself and collects the output on its
own if the sentinel doesn’t show up.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> Thanks to Mike Crowe for a bug report with enough
detail in it (<code class="language-plaintext highlighter-rouge">strace</code> output included) that I could work backwards to the cause.</p>

<p>The second: in CMake projects, editing the compile command at the prompt didn’t stick, and
you’d get the default offered again on the next run. That was a side effect of a fix from
3.2 that made function-valued commands re-resolve on every run so the preset picker could
prompt again. It threw away your edit along with the cached command. Both behaviors work
now - your edit is remembered, the picker still gets to prompt.</p>

<p>The third one has been open since 2016. If you opened a file that git ignores, Projectile
would happily add it to the file cache, and it would then keep showing up in
<code class="language-plaintext highlighter-rouge">projectile-find-file</code> even though indexing had correctly left it out. Projectile’s own
ignore rules simply don’t know anything about <code class="language-plaintext highlighter-rouge">.gitignore</code>, so it now asks git directly
before caching a file it hasn’t seen before.</p>

<h2 id="upgrade-notes">Upgrade notes</h2>

<p>A few changes might alter what you see, so it’s worth knowing about them before you upgrade:</p>

<ul>
  <li>Alien indexing now honors Projectile’s ignore rules, which it used to skip entirely. This
means alien projects will list <em>fewer</em> files than before. Set
<code class="language-plaintext highlighter-rouge">projectile-alien-honors-ignores</code> to <code class="language-plaintext highlighter-rouge">nil</code> if you want the old behavior.</li>
  <li>The ignore configuration speaks gitignore patterns everywhere now, matched the same way by
every indexing method. Mostly this just works, but a leading <code class="language-plaintext highlighter-rouge">*</code> used to mean “at any
depth” and is now a plain wildcard, so <code class="language-plaintext highlighter-rouge">*node_modules</code> should become <code class="language-plaintext highlighter-rouge">node_modules</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-globally-ignored-directories</code> now includes the build and dependency
directories of the common ecosystems - <code class="language-plaintext highlighter-rouge">node_modules</code>, <code class="language-plaintext highlighter-rouge">target</code>, <code class="language-plaintext highlighter-rouge">__pycache__</code>, <code class="language-plaintext highlighter-rouge">.venv</code>,
<code class="language-plaintext highlighter-rouge">.next</code>, <code class="language-plaintext highlighter-rouge">.terraform</code> and a couple of dozen more. Directories that projects sometimes
do commit (<code class="language-plaintext highlighter-rouge">vendor</code>, <code class="language-plaintext highlighter-rouge">dist</code>, <code class="language-plaintext highlighter-rouge">public</code>, <code class="language-plaintext highlighter-rouge">build</code>) are deliberately not on the list.</li>
</ul>

<p>The full changelog is <a href="https://github.com/bbatsov/projectile/blob/master/CHANGELOG.md">here</a>,
and the manual is at <a href="https://docs.projectile.mx/">docs.projectile.mx</a>.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Gradle is a good example - <code class="language-plaintext highlighter-rouge">build.gradle</code>, <code class="language-plaintext highlighter-rouge">build.gradle.kts</code>, <code class="language-plaintext highlighter-rouge">settings.gradle</code> and <code class="language-plaintext highlighter-rouge">settings.gradle.kts</code> all mark a Gradle project, and a project will have some of them but not all. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>See <a href="https://github.com/bbatsov/projectile/pull/2124">https://github.com/bbatsov/projectile/pull/2124</a>. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Emacs" /><category term="Projectile" /><summary type="html"><![CDATA[Projectile 3.3 is out! That’s the fourth release this month, which probably tells you something about how much fun I’ve been having with Projectile lately. This one is mostly about a single theme - Projectile knowing more about your project without you having to tell it anything.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Closing the Find-Usages Gap in CIDER</title><link href="https://metaredux.com/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html" rel="alternate" type="text/html" title="Closing the Find-Usages Gap in CIDER" /><published>2026-07-27T07:30:00+00:00</published><updated>2026-07-27T07:30:00+00:00</updated><id>https://metaredux.com/posts/2026/07/27/closing-the-find-usages-gap-in-cider</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html"><![CDATA[<p>Next up in the series on the notable changes in
<a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0</a>: cross-references. Or, as
most people call the feature, “find usages” - for years the most commonly
cited reason to run <a href="https://clojure-lsp.io/">clojure-lsp</a> alongside (or
instead of) CIDER. Let’s talk about why that gap existed and how we finally
closed it.</p>

<h2 id="why-runtime-xref-wasnt-enough">Why runtime xref wasn’t enough</h2>

<p>CIDER has had <em>runtime</em> cross-referencing for a while: the <code class="language-plaintext highlighter-rouge">cider/fn-refs</code> 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:</p>

<ul>
  <li>It only sees <strong>loaded</strong> code. Namespaces you haven’t required yet - often
most of the codebase - are invisible.</li>
  <li>It reports <em>functions</em>, not <em>occurrences</em>. Each hit points at the caller’s
definition, not the exact call site, and a function that calls yours three
times shows up once.</li>
  <li>It’s JVM-only, so ClojureScript users got nothing.</li>
</ul>

<p>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”.</p>

<h2 id="the-fix-search-the-source">The fix: search the source</h2>

<p>So CIDER 2.0 does the obvious thing we should have done years ago:
<code class="language-plaintext highlighter-rouge">xref-find-references</code> (<code class="language-plaintext highlighter-rouge">M-?</code>) now finds references by <strong>searching the
project’s source files</strong> on disk. Unloaded code, <code class="language-plaintext highlighter-rouge">cljs</code> 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. To
borrow the franchise that has been handing programmers debugging metaphors
for over two decades now: the runtime is the Matrix, a tidy compiled illusion
of your program, and to see every place a thing is really used you sometimes
have to unplug and look at the source itself.<sup id="fnref:matrix" role="doc-noteref"><a href="#fn:matrix" class="footnote" rel="footnote">1</a></sup></p>

<p>Here’s the difference in one picture - the same query on
<code class="language-plaintext highlighter-rouge">orchard.misc/require-and-resolve</code>, first in runtime mode, then in source
mode:</p>

<p><img src="/assets/images/cider-xref-runtime-vs-source.gif" alt="Find-usages of orchard.misc/require-and-resolve: the runtime search returns only the loaded callers, one per calling function, while the source search finds every occurrence across the project - including the alias-qualified uses in other namespaces and a cljc file the REPL never loaded" /></p>

<p>The runtime knows about three callers - and points you at each <em>caller’s</em>
definition. The source scan turns up all ten actual occurrences, across five
files, including the ones written as <code class="language-plaintext highlighter-rouge">misc/require-and-resolve</code> in namespaces
the REPL never loaded. That’s the gap, in one screenshot.</p>

<p>Now, “search the source” makes it sound like a <code class="language-plaintext highlighter-rouge">grep</code>, and I want to be clear
that it isn’t a dumb one. Say you’re chasing <code class="language-plaintext highlighter-rouge">orchard.misc/require-and-resolve</code>.
The search runs in three stages:</p>

<ol>
  <li>First CIDER asks the REPL to resolve the symbol at point to its fully
qualified name, <code class="language-plaintext highlighter-rouge">orchard.misc/require-and-resolve</code>. The REPL is right there
on the other end of the wire, so why guess when you can ask?</li>
  <li>A fast first pass (<code class="language-plaintext highlighter-rouge">ripgrep</code>, via Emacs’ own <code class="language-plaintext highlighter-rouge">xref-matches-in-files</code>) finds
every file that so much as mentions <code class="language-plaintext highlighter-rouge">require-and-resolve</code>, purely to narrow
the field.</li>
  <li>Then the Clojure-aware part. For each of those files, CIDER reads its
<code class="language-plaintext highlighter-rouge">(ns ...)</code> form to learn <em>how</em> that file pulls in <code class="language-plaintext highlighter-rouge">orchard.misc</code> - aliased
as <code class="language-plaintext highlighter-rouge">[orchard.misc :as misc]</code>, brought in with <code class="language-plaintext highlighter-rouge">:refer [require-and-resolve]</code>,
or is this <code class="language-plaintext highlighter-rouge">orchard.misc</code> itself? - and builds a regexp that matches only
the forms that file could legitimately use: the qualified
<code class="language-plaintext highlighter-rouge">orchard.misc/require-and-resolve</code>, the aliased <code class="language-plaintext highlighter-rouge">misc/require-and-resolve</code>,
or a bare <code class="language-plaintext highlighter-rouge">require-and-resolve</code> where the namespace declaration makes that
valid. The requires inside the <code class="language-plaintext highlighter-rouge">ns</code> form are excluded, so the import line
doesn’t show up as a “usage”.<sup id="fnref:fix" role="doc-noteref"><a href="#fn:fix" class="footnote" rel="footnote">2</a></sup></li>
</ol>

<p>Is any of this as smart as clj-kondo’s full analysis? No - it’s ultimately a
syntactic search, so an identically named var in another namespace can still
sneak through as a false positive. But because it’s ns-aware rather than a
blind text match, a bare <code class="language-plaintext highlighter-rouge">require-and-resolve</code> in some file that never requires
<code class="language-plaintext highlighter-rouge">orchard.misc</code> (and has a <code class="language-plaintext highlighter-rouge">require-and-resolve</code> of its own) won’t be mistaken
for yours. For the daily “where is this used?” question it turns out to be
remarkably close to the real thing in practice, it requires zero extra
infrastructure, and it composes with what only CIDER has: the running REPL.</p>

<p>That composition is configurable via <code class="language-plaintext highlighter-rouge">cider-xref-references-mode</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">source</code> (the default) - occurrences from the project’s files.</li>
  <li><code class="language-plaintext highlighter-rouge">runtime</code> - the historical loaded-vars behavior.</li>
  <li><code class="language-plaintext highlighter-rouge">both</code> - source occurrences first, plus the runtime hits the scan can’t see.
And there <em>are</em> 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.</li>
</ul>

<p>(There’s also <code class="language-plaintext highlighter-rouge">cider-xref-fn-refs-in-source</code>, <code class="language-plaintext highlighter-rouge">C-c C-? s</code>, when you want the
source search explicitly, and outside a project the source mode gracefully
falls back to the runtime search.)</p>

<h2 id="beyond-find-usages-the-who--family">Beyond find usages: the who-* family</h2>

<p>While closing the gap, we went further and built out a whole family of
SLIME-inspired cross-referencing commands under <code class="language-plaintext highlighter-rouge">C-c C-w</code>, most of them
rendered as expandable trees:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">cider-who-calls</code> / <code class="language-plaintext highlighter-rouge">cider-who-is-called</code> - the call graph, upward and
downward. Expand a caller to see <em>its</em> callers; spelunk as deep as you like:</p>

    <p><img src="/assets/images/cider-who-calls-tree.png" alt="The cider-who-calls tree, expanded two levels up the call graph" /></p>
  </li>
  <li><code class="language-plaintext highlighter-rouge">cider-who-implements</code> - a protocol’s implementing types (inline
<code class="language-plaintext highlighter-rouge">defrecord</code>/<code class="language-plaintext highlighter-rouge">deftype</code> implementations 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 the <code class="language-plaintext highlighter-rouge">defmethod</code> forms by - you guessed it -
searching the source.</li>
  <li><code class="language-plaintext highlighter-rouge">cider-type-protocols</code> / <code class="language-plaintext highlighter-rouge">cider-protocols-with-method</code> - the reverse
lookups: what does this type implement, and which protocols declare this
method?</li>
  <li><code class="language-plaintext highlighter-rouge">cider-who-macroexpands</code> - 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.</li>
</ul>

<p>Much of this is powered by new ops in <code class="language-plaintext highlighter-rouge">cider-nrepl</code> (and
<a href="https://github.com/clojure-emacs/orchard">Orchard</a> underneath), and much of
the inspiration came straight from SLIME and swank-clojure, which offered
<code class="language-plaintext highlighter-rouge">who-calls</code> back when Clojure itself was barely out of the crib. And here’s
the part I love: the whole “go read the source instead of trusting the
runtime” instinct was already there in swank-clojure’s implementation. Its
<code class="language-plaintext highlighter-rouge">who-calls</code> would find candidate callers among the loaded vars, sure, but then
it went and <em>read the actual source form of each one off disk</em> and walked the
parsed code looking for your symbol, rather than believing whatever the
compiled runtime claimed. It was still anchored to loaded code - it never
scanned the whole project the way CIDER 2.0 does - but the core idea, that the
source on disk is the ground truth and the runtime is just a convenient
approximation, predates this release by about fifteen years. Good ideas don’t
expire. Sometimes nothing beats revisiting the classics.<sup id="fnref:source" role="doc-noteref"><a href="#fn:source" class="footnote" rel="footnote">3</a></sup></p>

<h2 id="so-do-you-still-need-clojure-lsp">So do you still need clojure-lsp?</h2>

<p>If you were running clojure-lsp <em>primarily</em> 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
<a href="https://docs.cider.mx/cider/config/lsp.html">work fine side by side</a>, and the
new async eldoc even yields politely so LSP-provided docs can compose with
CIDER’s.</p>

<p>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.</p>

<p>The full story is in the
<a href="https://docs.cider.mx/cider/usage/navigation.html">navigation docs</a>. Keep
hacking!</p>

<h2>Articles in the Series</h2>

<ul>

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0: Sky is the Limit</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/16/simplifying-session-management-in-cider.html">Simplifying Session Management in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/17/stepping-through-macros-in-cider.html">Stepping Through Macros in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/23/making-cider-more-discoverable.html">Making CIDER More Discoverable</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER's Completion</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html">Closing the Find-Usages Gap in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/28/sharpening-ciders-debugging-tools.html">Sharpening CIDER's Debugging Tools</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html">Leveling Up CIDER's ClojureScript Support</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->
 <!-- posts for -->
</ul>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:matrix" role="doc-endnote">
      <p>As Morpheus puts it: “Unfortunately, no one can be told what the
Matrix is. You have to see it for yourself.” Same with find-usages,
really - I can tell you a var is used in seven places, but until you’ve
seen the actual call sites you don’t really know what changing it will
break. <a href="#fnref:matrix" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fix" role="doc-endnote">
      <p>Full disclosure: matching the aliased and namespace-qualified forms
correctly only landed <em>after</em> 2.0 - the 2.0.x releases had a bug where the
source scan quietly skipped files that referenced a var through its alias,
which is of course the common case. The fix will ship in CIDER 2.1, which
doesn’t have a release date yet. I’m hoping to get back into a rhythm of
cutting a new CIDER release every month or two, so it shouldn’t be a long
wait. <a href="#fnref:fix" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:source" role="doc-endnote">
      <p>“The path of the One ends at the Source.” The Architect was
talking about Zion, but he might as well have been describing every
debugging session that ends with you finally opening the file and reading
the code instead of theorizing about it. <a href="#fnref:source" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="CIDER 2.0" /><category term="Emacs" /><category term="Clojure" /><category term="CIDER" /><summary type="html"><![CDATA[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. To borrow the franchise that has been handing programmers debugging metaphors for over two decades now: the runtime is the Matrix, a tidy compiled illusion of your program, and to see every place a thing is really used you sometimes have to unplug and look at the source itself.1 Here’s the difference in one picture - the same query on orchard.misc/require-and-resolve, first in runtime mode, then in source mode: The runtime knows about three callers - and points you at each caller’s definition. The source scan turns up all ten actual occurrences, across five files, including the ones written as misc/require-and-resolve in namespaces the REPL never loaded. That’s the gap, in one screenshot. Now, “search the source” makes it sound like a grep, and I want to be clear that it isn’t a dumb one. Say you’re chasing orchard.misc/require-and-resolve. The search runs in three stages: First CIDER asks the REPL to resolve the symbol at point to its fully qualified name, orchard.misc/require-and-resolve. The REPL is right there on the other end of the wire, so why guess when you can ask? A fast first pass (ripgrep, via Emacs’ own xref-matches-in-files) finds every file that so much as mentions require-and-resolve, purely to narrow the field. Then the Clojure-aware part. For each of those files, CIDER reads its (ns ...) form to learn how that file pulls in orchard.misc - aliased as [orchard.misc :as misc], brought in with :refer [require-and-resolve], or is this orchard.misc itself? - and builds a regexp that matches only the forms that file could legitimately use: the qualified orchard.misc/require-and-resolve, the aliased misc/require-and-resolve, or a bare require-and-resolve where the namespace declaration makes that valid. The requires inside the ns form are excluded, so the import line doesn’t show up as a “usage”.2 Is any of this as smart as clj-kondo’s full analysis? No - it’s ultimately a syntactic search, so an identically named var in another namespace can still sneak through as a false positive. But because it’s ns-aware rather than a blind text match, a bare require-and-resolve in some file that never requires orchard.misc (and has a require-and-resolve of its own) won’t be mistaken for yours. For the daily “where is this used?” question it turns out to be remarkably close to the real thing 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 (inline defrecord/deftype implementations 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 the defmethod forms 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 back when Clojure itself was barely out of the crib. And here’s the part I love: the whole “go read the source instead of trusting the runtime” instinct was already there in swank-clojure’s implementation. Its who-calls would find candidate callers among the loaded vars, sure, but then it went and read the actual source form of each one off disk and walked the parsed code looking for your symbol, rather than believing whatever the compiled runtime claimed. It was still anchored to loaded code - it never scanned the whole project the way CIDER 2.0 does - but the core idea, that the source on disk is the ground truth and the runtime is just a convenient approximation, predates this release by about fifteen years. Good ideas don’t expire. Sometimes nothing beats revisiting the classics.3 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! Articles in the Series CIDER 2.0: Sky is the Limit Simplifying Session Management in CIDER Stepping Through Macros in CIDER Making CIDER More Discoverable Modernizing CIDER's Completion Closing the Find-Usages Gap in CIDER Sharpening CIDER's Debugging Tools Leveling Up CIDER's ClojureScript Support As Morpheus puts it: “Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.” Same with find-usages, really - I can tell you a var is used in seven places, but until you’ve seen the actual call sites you don’t really know what changing it will break. &#8617; Full disclosure: matching the aliased and namespace-qualified forms correctly only landed after 2.0 - the 2.0.x releases had a bug where the source scan quietly skipped files that referenced a var through its alias, which is of course the common case. The fix will ship in CIDER 2.1, which doesn’t have a release date yet. I’m hoping to get back into a rhythm of cutting a new CIDER release every month or two, so it shouldn’t be a long wait. &#8617; “The path of the One ends at the Source.” The Architect was talking about Zion, but he might as well have been describing every debugging session that ends with you finally opening the file and reading the code instead of theorizing about it. &#8617;]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Modernizing CIDER’s Completion</title><link href="https://metaredux.com/posts/2026/07/25/modernizing-cider-completion.html" rel="alternate" type="text/html" title="Modernizing CIDER’s Completion" /><published>2026-07-25T06:00:00+00:00</published><updated>2026-07-25T06:00:00+00:00</updated><id>https://metaredux.com/posts/2026/07/25/modernizing-cider-completion</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/25/modernizing-cider-completion.html"><![CDATA[<p>CIDER’s code completion has quietly gotten quite good over the years, and I
don’t think it gets enough credit. It’s all built on Emacs’s standard
<code class="language-plaintext highlighter-rouge">completion-at-point</code>, so it works with whatever completion UI you prefer -
the built-in one, <a href="https://github.com/minad/corfu">Corfu</a>,
<a href="https://company-mode.github.io/">company</a> - without any special setup. Under
the hood <a href="https://github.com/alexander-yakushev/compliment">compliment</a> does the
heavy lifting for Clojure (and
<a href="https://github.com/clojure-emacs/clj-suitable">clj-suitable</a> for
ClojureScript), which means smart, backend-driven matching: <code class="language-plaintext highlighter-rouge">mai</code> completes to
<code class="language-plaintext highlighter-rouge">map-indexed</code>, <code class="language-plaintext highlighter-rouge">cji</code> to <code class="language-plaintext highlighter-rouge">clojure.java.io</code>, and an unimported <code class="language-plaintext highlighter-rouge">BiFun</code> to
<code class="language-plaintext highlighter-rouge">java.util.function.BiFunction</code>. The candidates come back ranked by the backend
and are context-aware - it knows when you’re inside a <code class="language-plaintext highlighter-rouge">-&gt;</code> or completing a
<code class="language-plaintext highlighter-rouge">deftype</code> field.</p>

<p>Lately I’ve been giving the Emacs side of things some attention, to bring it in
line with the modern completion stack so many of us now use - Vertico, Corfu,
Consult, Marginalia and friends. Two changes are worth calling out.</p>

<p>The first is about the symbol prompts. A number of CIDER commands ask you for a
Clojure symbol when there’s nothing at point - <code class="language-plaintext highlighter-rouge">cider-doc</code>, <code class="language-plaintext highlighter-rouge">cider-find-var</code>
and the like. Historically those prompts used the older completion machinery, so
your <code class="language-plaintext highlighter-rouge">completing-read</code> UI didn’t kick in and you were left with <code class="language-plaintext highlighter-rouge">TAB</code> and a
<code class="language-plaintext highlighter-rouge">*Completions*</code> buffer. There’s now <code class="language-plaintext highlighter-rouge">cider-use-completing-read-for-symbol</code> (off
by default for now); turn it on and those prompts go through <code class="language-plaintext highlighter-rouge">completing-read</code>
over a collection that queries the running REPL lazily as you type. Vertico,
Ivy, Helm - whatever you drive <code class="language-plaintext highlighter-rouge">completing-read</code> with - just works, and the
candidates carry their type and namespace.</p>

<p>The second is smaller, but I like it a lot: annotations now line up in a proper
column instead of trailing raggedly after each candidate.</p>

<p><img src="/assets/images/cider-completion-annotations.png" alt="CIDER completion annotations" /></p>

<p>This comes from an <code class="language-plaintext highlighter-rouge">affixation-function</code>, the richer successor to the old
<code class="language-plaintext highlighter-rouge">annotation-function</code>, so every frontend that understands it - the built-in
<code class="language-plaintext highlighter-rouge">*Completions*</code>, Corfu, Vertico - renders the aligned version. company keeps
showing its own trailing annotations, same as before.</p>

<p>Both changes are in the latest CIDER MELPA build and will ship in the next
stable release. As always, I’d love to hear how they work out for you.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<p><strong>P.S.</strong> If you use Embark, I wrote up a fun way to act on Clojure symbols with
it - documentation, jump-to-definition, inspect and so on - over on
<a href="https://emacsredux.com/blog/2026/07/25/cider-and-projectile-meet-embark/">Emacs Redux</a>.</p>

<h2>Articles in the Series</h2>

<ul>

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0: Sky is the Limit</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/16/simplifying-session-management-in-cider.html">Simplifying Session Management in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/17/stepping-through-macros-in-cider.html">Stepping Through Macros in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/23/making-cider-more-discoverable.html">Making CIDER More Discoverable</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER's Completion</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html">Closing the Find-Usages Gap in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/28/sharpening-ciders-debugging-tools.html">Sharpening CIDER's Debugging Tools</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html">Leveling Up CIDER's ClojureScript Support</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->
 <!-- posts for -->
</ul>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="CIDER 2.0" /><category term="CIDER" /><category term="Clojure" /><category term="Emacs" /><category term="Completion" /><summary type="html"><![CDATA[CIDER’s code completion has quietly gotten quite good over the years, and I don’t think it gets enough credit. It’s all built on Emacs’s standard completion-at-point, so it works with whatever completion UI you prefer - the built-in one, Corfu, company - without any special setup. Under the hood compliment does the heavy lifting for Clojure (and clj-suitable for ClojureScript), which means smart, backend-driven matching: mai completes to map-indexed, cji to clojure.java.io, and an unimported BiFun to java.util.function.BiFunction. The candidates come back ranked by the backend and are context-aware - it knows when you’re inside a -&gt; or completing a deftype field. Lately I’ve been giving the Emacs side of things some attention, to bring it in line with the modern completion stack so many of us now use - Vertico, Corfu, Consult, Marginalia and friends. Two changes are worth calling out. The first is about the symbol prompts. A number of CIDER commands ask you for a Clojure symbol when there’s nothing at point - cider-doc, cider-find-var and the like. Historically those prompts used the older completion machinery, so your completing-read UI didn’t kick in and you were left with TAB and a *Completions* buffer. There’s now cider-use-completing-read-for-symbol (off by default for now); turn it on and those prompts go through completing-read over a collection that queries the running REPL lazily as you type. Vertico, Ivy, Helm - whatever you drive completing-read with - just works, and the candidates carry their type and namespace. The second is smaller, but I like it a lot: annotations now line up in a proper column instead of trailing raggedly after each candidate. This comes from an affixation-function, the richer successor to the old annotation-function, so every frontend that understands it - the built-in *Completions*, Corfu, Vertico - renders the aligned version. company keeps showing its own trailing annotations, same as before. Both changes are in the latest CIDER MELPA build and will ship in the next stable release. As always, I’d love to hear how they work out for you. That’s all I have for you today. Keep hacking! P.S. If you use Embark, I wrote up a fun way to act on Clojure symbols with it - documentation, jump-to-definition, inspect and so on - over on Emacs Redux. Articles in the Series CIDER 2.0: Sky is the Limit Simplifying Session Management in CIDER Stepping Through Macros in CIDER Making CIDER More Discoverable Modernizing CIDER's Completion Closing the Find-Usages Gap in CIDER Sharpening CIDER's Debugging Tools Leveling Up CIDER's ClojureScript Support]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Making CIDER More Discoverable</title><link href="https://metaredux.com/posts/2026/07/23/making-cider-more-discoverable.html" rel="alternate" type="text/html" title="Making CIDER More Discoverable" /><published>2026-07-23T15:50:00+00:00</published><updated>2026-07-23T15:50:00+00:00</updated><id>https://metaredux.com/posts/2026/07/23/making-cider-more-discoverable</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/23/making-cider-more-discoverable.html"><![CDATA[<p>This series about the notable changes in
<a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0</a> continues with the change
you’ll bump into first, whatever your workflow: the transient menus, and the
broader push to make CIDER’s functionality discoverable.</p>

<h2 id="the-problem-cider-is-huge">The problem: CIDER is huge</h2>

<p>CIDER has well over 300 interactive commands. I’ll admit something I’ve said
before: there are features in CIDER that even <em>I</em> forget exist, and I wrote
half of them. For users, historically, the options for finding functionality
were:</p>

<ul>
  <li>memorize cryptic key chords (<code class="language-plaintext highlighter-rouge">C-c C-w i</code>, anyone?)</li>
  <li>grep the (very long) manual</li>
  <li>read the source</li>
  <li>stumble on a feature by accident three years in and feel robbed</li>
</ul>

<p>That’s not great for a tool whose whole pitch is making you more productive.
The Emacs answer to this problem was demonstrated years ago by
<a href="https://magit.vc/">Magit</a>: <a href="https://github.com/magit/transient">transient</a>
menus, which turn every prefix into a self-documenting popup. It took us
embarrassingly long to follow suit, but CIDER 2.0 finally does.</p>

<h2 id="transient-everywhere">Transient everywhere</h2>

<p>Every command group in CIDER now opens a transient menu: <code class="language-plaintext highlighter-rouge">cider-eval-menu</code> at
<code class="language-plaintext highlighter-rouge">C-c C-v</code>, <code class="language-plaintext highlighter-rouge">cider-doc-menu</code> at <code class="language-plaintext highlighter-rouge">C-c C-d</code>, and likewise for test, namespace,
macroexpand, profile, trace and references. A top-level <code class="language-plaintext highlighter-rouge">cider-menu</code> ties them
all together, and even the debugger (<code class="language-plaintext highlighter-rouge">?</code> mid-session) and the inspector (<code class="language-plaintext highlighter-rouge">m</code>)
got menus of their own. Jack-in and connect live in <code class="language-plaintext highlighter-rouge">cider-start-menu</code> at
<code class="language-plaintext highlighter-rouge">C-c C-x</code>.</p>

<p>Here’s the evaluation menu, which is a good illustration of the problem the
menus solve - I doubt many people knew all of this was hiding behind <code class="language-plaintext highlighter-rouge">C-c C-v</code>:</p>

<p><img src="/assets/images/cider-eval-menu.png" alt="The CIDER evaluation transient menu, listing every evaluation command" /></p>

<p>One design constraint was non-negotiable: <strong>your muscle memory is safe</strong>. These menus
replace bare prefix keymaps, so every existing keybinding works exactly as
before, at full speed - <code class="language-plaintext highlighter-rouge">C-c C-v e</code> still evaluates instantly, menu or no
menu. And if you’d rather not see the menus at all unless you actually
hesitate mid-chord, set <code class="language-plaintext highlighter-rouge">transient-show-popup</code> to a short delay and they’ll
appear only in that moment of doubt - which is precisely when you need them.</p>

<p>Transient also gave us something the old keymaps never could: <em>arguments</em>.
Menus now carry flags for the things that vary per invocation - pick a
pretty-printer with <code class="language-plaintext highlighter-rouge">--print-fn=</code>, set test selectors with
<code class="language-plaintext highlighter-rouge">--include=</code>/<code class="language-plaintext highlighter-rouge">--exclude=</code> and reuse them across runs, toggle
<code class="language-plaintext highlighter-rouge">cider-ns-refresh</code>’s modes explicitly, pass Clojure CLI aliases at jack-in
time. All those “this command behaves differently with a prefix argument”
paragraphs in the manual are becoming visible checkboxes instead.</p>

<p>The test menu shows this nicely - set the selectors once and every run below
them picks them up:</p>

<p><img src="/assets/images/cider-test-menu.png" alt="The CIDER test transient menu with its include and exclude selector arguments" /></p>

<h2 id="discovery-beyond-menus">Discovery beyond menus</h2>

<p>The menus are the headline, but the discoverability push in the 2.0 cycle went
wider:</p>

<ul>
  <li>A new <a href="https://docs.cider.mx/cider/keybindings.html">keybindings reference page</a>
collects every binding in one place, and the printable
<a href="https://github.com/clojure-emacs/cider/tree/master/refcard">refcard</a> was
brought back up to date.</li>
  <li>The REPL’s shouty welcome banner is gone, replaced by a one-line hint; the
getting-started material now lives in a summonable reference card
(<code class="language-plaintext highlighter-rouge">C-c C-h</code>, or the <code class="language-plaintext highlighter-rouge">,refcard</code> REPL shortcut) - available when you want it,
invisible when you don’t.</li>
  <li>CIDER now warns (once per session) when you use a deprecated keybinding, so
bindings can actually be retired someday without silently breaking people.
<code class="language-plaintext highlighter-rouge">M-x cider-list-deprecated-keybindings</code> shows what’s on the way out.</li>
  <li>The 1.22 cycle’s big audit already made the mode menus expose dozens of
commands that were technically present but practically invisible; 2.0 builds
on that foundation.</li>
  <li>Even <code class="language-plaintext highlighter-rouge">cider-doctor</code> is discoverability of a sort - it surfaces the problems
in your setup that you’d otherwise discover one confusing bug report at a
time.</li>
</ul>

<h2 id="the-philosophy">The philosophy</h2>

<p>If I had to compress the 2.0 discoverability work into one sentence: <em>the
features were always there; now the tool tells you about them</em>. Documentation
is where knowledge goes to be forgotten - the only reliable place to teach a
user about a feature is inside the workflow itself, at the moment of
hesitation. Transient menus are exactly that, and Magit proved the pattern
scales to enormous command sets.</p>

<p>If some menu feels wrong - a missing command, a flag that should exist, a
grouping that doesn’t match how you think - please file an issue. This part of
CIDER is young and very much open to feedback.</p>

<p>The <a href="https://docs.cider.mx/cider/keybindings.html">keybindings docs</a> have the
full picture. Keep hacking!</p>

<h2>Articles in the Series</h2>

<ul>

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0: Sky is the Limit</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/16/simplifying-session-management-in-cider.html">Simplifying Session Management in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/17/stepping-through-macros-in-cider.html">Stepping Through Macros in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/23/making-cider-more-discoverable.html">Making CIDER More Discoverable</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER's Completion</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html">Closing the Find-Usages Gap in CIDER</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/posts/2026/07/28/sharpening-ciders-debugging-tools.html">Sharpening CIDER's Debugging Tools</a></li>
    <!-- tags if -->

  
    <li><a href="/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html">Leveling Up CIDER's ClojureScript Support</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->
 <!-- posts for -->
</ul>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="CIDER 2.0" /><category term="Emacs" /><category term="Clojure" /><category term="CIDER" /><summary type="html"><![CDATA[This series about the notable changes in CIDER 2.0 continues with the change you’ll bump into first, whatever your workflow: the transient menus, and the broader push to make CIDER’s functionality discoverable. The problem: CIDER is huge CIDER has well over 300 interactive commands. I’ll admit something I’ve said before: there are features in CIDER that even I forget exist, and I wrote half of them. For users, historically, the options for finding functionality were: memorize cryptic key chords (C-c C-w i, anyone?) grep the (very long) manual read the source stumble on a feature by accident three years in and feel robbed That’s not great for a tool whose whole pitch is making you more productive. The Emacs answer to this problem was demonstrated years ago by Magit: transient menus, which turn every prefix into a self-documenting popup. It took us embarrassingly long to follow suit, but CIDER 2.0 finally does. Transient everywhere Every command group in CIDER now opens a transient menu: cider-eval-menu at C-c C-v, cider-doc-menu at C-c C-d, and likewise for test, namespace, macroexpand, profile, trace and references. A top-level cider-menu ties them all together, and even the debugger (? mid-session) and the inspector (m) got menus of their own. Jack-in and connect live in cider-start-menu at C-c C-x. Here’s the evaluation menu, which is a good illustration of the problem the menus solve - I doubt many people knew all of this was hiding behind C-c C-v: One design constraint was non-negotiable: your muscle memory is safe. These menus replace bare prefix keymaps, so every existing keybinding works exactly as before, at full speed - C-c C-v e still evaluates instantly, menu or no menu. And if you’d rather not see the menus at all unless you actually hesitate mid-chord, set transient-show-popup to a short delay and they’ll appear only in that moment of doubt - which is precisely when you need them. Transient also gave us something the old keymaps never could: arguments. Menus now carry flags for the things that vary per invocation - pick a pretty-printer with --print-fn=, set test selectors with --include=/--exclude= and reuse them across runs, toggle cider-ns-refresh’s modes explicitly, pass Clojure CLI aliases at jack-in time. All those “this command behaves differently with a prefix argument” paragraphs in the manual are becoming visible checkboxes instead. The test menu shows this nicely - set the selectors once and every run below them picks them up: Discovery beyond menus The menus are the headline, but the discoverability push in the 2.0 cycle went wider: A new keybindings reference page collects every binding in one place, and the printable refcard was brought back up to date. The REPL’s shouty welcome banner is gone, replaced by a one-line hint; the getting-started material now lives in a summonable reference card (C-c C-h, or the ,refcard REPL shortcut) - available when you want it, invisible when you don’t. CIDER now warns (once per session) when you use a deprecated keybinding, so bindings can actually be retired someday without silently breaking people. M-x cider-list-deprecated-keybindings shows what’s on the way out. The 1.22 cycle’s big audit already made the mode menus expose dozens of commands that were technically present but practically invisible; 2.0 builds on that foundation. Even cider-doctor is discoverability of a sort - it surfaces the problems in your setup that you’d otherwise discover one confusing bug report at a time. The philosophy If I had to compress the 2.0 discoverability work into one sentence: the features were always there; now the tool tells you about them. Documentation is where knowledge goes to be forgotten - the only reliable place to teach a user about a feature is inside the workflow itself, at the moment of hesitation. Transient menus are exactly that, and Magit proved the pattern scales to enormous command sets. If some menu feels wrong - a missing command, a flag that should exist, a grouping that doesn’t match how you think - please file an issue. This part of CIDER is young and very much open to feedback. The keybindings docs have the full picture. Keep hacking! Articles in the Series CIDER 2.0: Sky is the Limit Simplifying Session Management in CIDER Stepping Through Macros in CIDER Making CIDER More Discoverable Modernizing CIDER's Completion Closing the Find-Usages Gap in CIDER Sharpening CIDER's Debugging Tools Leveling Up CIDER's ClojureScript Support]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">clj-suitable 0.8.0: Closing the Gap with Compliment</title><link href="https://metaredux.com/posts/2026/07/20/clj-suitable-0-8-0.html" rel="alternate" type="text/html" title="clj-suitable 0.8.0: Closing the Gap with Compliment" /><published>2026-07-20T09:00:00+00:00</published><updated>2026-07-20T09:00:00+00:00</updated><id>https://metaredux.com/posts/2026/07/20/clj-suitable-0-8-0</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/20/clj-suitable-0-8-0.html"><![CDATA[<blockquote>
  <p>You had me at <code class="language-plaintext highlighter-rouge">js/</code>.</p>

  <p>– Jerry Maguire, on ClojureScript interop completion</p>
</blockquote>

<p><a href="https://github.com/clojure-emacs/clj-suitable/releases/tag/v0.8.0">clj-suitable 0.8.0</a> is out! If the name doesn’t ring a bell, that’s
rather the point - <a href="https://github.com/clojure-emacs/clj-suitable">clj-suitable</a> is the small library that quietly
powers ClojureScript code completion in CIDER, Calva, and pretty much anything
else that talks to a cljs REPL over nREPL. Think of it as the ClojureScript
counterpart to what <a href="https://github.com/alexander-yakushev/compliment">compliment</a><sup id="fnref:complement" role="doc-noteref"><a href="#fn:complement" class="footnote" rel="footnote">1</a></sup> does for Clojure. For
years it lagged well behind its Clojure sibling, and this release is my attempt
to finally close that gap - to make cljs completion, if you’ll forgive me, a
touch more <em>suitable</em>.</p>

<p>This is also a direct follow-up to the <a href="/posts/2026/07/01/demystifying-piggieback.html">Piggieback work</a> I wrote
about a couple of weeks ago. Once I had the cljs REPL plumbing back in decent
shape, fixing up the completion story sitting on top of it was the obvious next
move.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">2</a></sup></p>

<!--more-->

<h2 id="the-mission-catch-up-with-compliment">The mission: catch up with compliment</h2>

<p>Clojure programmers have had really good completion for years, thanks to
compliment. ClojureScript programmers got a paler version of the same idea -
the basics worked, but all the little touches that make completion feel smart
were missing. My goal for this cycle was simple to state and less simple to
deliver: teach clj-suitable the tricks compliment has had all along. I think
we’re kind of there now.</p>

<p>Here’s what that means in practice, mostly borrowed straight from compliment’s
playbook:</p>

<ul>
  <li>Fuzzy matching. You no longer have to type a prefix - <code class="language-plaintext highlighter-rouge">cs</code> now completes to
<code class="language-plaintext highlighter-rouge">clojure.string</code> and <code class="language-plaintext highlighter-rouge">rkv</code> to <code class="language-plaintext highlighter-rouge">reduce-kv</code>, the same subsequence matching
you’re used to on the Clojure side.</li>
  <li>Smarter ranking. Candidates are ordered the way compliment orders them -
vars from the current namespace first, then <code class="language-plaintext highlighter-rouge">cljs.core</code>, then everything
else. The thing you actually want tends to be at the top instead of buried
alphabetically.</li>
  <li>Local bindings. Completion now sees the bindings from the surrounding form -
<code class="language-plaintext highlighter-rouge">let</code>, <code class="language-plaintext highlighter-rouge">loop</code>, <code class="language-plaintext highlighter-rouge">fn</code>, <code class="language-plaintext highlighter-rouge">for</code>, <code class="language-plaintext highlighter-rouge">doseq</code> and friends - including destructured
ones. Type <code class="language-plaintext highlighter-rouge">first|</code> inside <code class="language-plaintext highlighter-rouge">(let [{:keys [first-name]} m] ...)</code> and you’ll
get <code class="language-plaintext highlighter-rouge">first-name</code>, which previously you would not.</li>
  <li>Referred vars. Inside a <code class="language-plaintext highlighter-rouge">(:require [clojure.string :refer [jo|]])</code> clause you
now get <code class="language-plaintext highlighter-rouge">join</code>, scoped to that one namespace rather than the whole world.</li>
  <li>Context awareness. Special forms are only offered at the head of a list, so
<code class="language-plaintext highlighter-rouge">if</code>, <code class="language-plaintext highlighter-rouge">let</code> and company stop showing up as candidates in argument position
where they make no sense.</li>
</ul>

<p>None of these are revolutionary on their own, but together they’re the
difference between completion that feels like an afterthought and completion
that feels like it belongs.</p>

<h2 id="a-bit-of-backstory">A bit of backstory</h2>

<p>There’s a nice irony in chasing compliment, because for a while ClojureScript
completion actually lived <em>inside</em> it. Back in 2019 Andrea Richiardi
<a href="https://github.com/alexander-yakushev/compliment/pull/62">ported</a> the <a href="https://github.com/clojure-emacs/cljs-tooling">cljs-tooling</a> completion machinery - the same
code CIDER used for cljs at the time - straight into compliment, and there was
even a <a href="https://github.com/alexander-yakushev/compliment/pull/72">follow-up attempt</a> to pull clj-suitable’s JavaScript interop
completions in alongside it. (You can still spot the heritage: a few functions
in clj-suitable’s current source are marked “Ported from compliment.”)</p>

<p>In the end we went the other way around: instead of growing compliment to cover
ClojureScript, we consolidated the ClojureScript side in clj-suitable and
<a href="https://github.com/alexander-yakushev/compliment/pull/74">reverted</a> the port. That sounds like wasted effort, but it wasn’t -
compliment’s pluggable <em>custom source</em> architecture is exactly what made the
split clean. clj-suitable just registers itself as another source, so tools get
Clojure and ClojureScript completion side by side without compliment having to
know a thing about cljs.</p>

<p><a href="https://github.com/rksm">Robert Krahn</a> had started clj-suitable earlier that year for the
dynamic, runtime-introspection side, and the static ClojureScript completion
found its permanent home there too. In hindsight it was clearly the right call:
cljs completion gets to grow (and break, and get fixed) on its own schedule,
and compliment stays focused and lean. A good architecture is the kind that
makes the split you didn’t plan for feel obvious after the fact.</p>

<h2 id="why-clojurescript-makes-this-harder">Why ClojureScript makes this harder</h2>

<p>Completion for Clojure is almost unfairly simple. Your code runs on the same
JVM as the nREPL server, so compliment can just reflect on the live thing -
real vars, real namespaces, real Java classes, all sitting in the same process.
Ask a question, get an answer.</p>

<p>ClojureScript doesn’t get to be that lucky, because it lives in two worlds at
once. The compiler is a Clojure program running on the JVM, and it’s the source
of truth for namespaces, vars and their metadata - so static completion reads
the ClojureScript <em>compiler state</em>, not your running program. But your actual
program runs somewhere else entirely: a Node process, a browser tab, maybe a
React Native app on a phone, reachable only across a REPL bridge. When you want
to complete JavaScript interop - the methods on <code class="language-plaintext highlighter-rouge">js/console</code>, say - there’s
nothing on the JVM to reflect on. You have to ship a bit of code across that
bridge, run it <em>in the JS runtime</em>, and read back what a live object actually
exposes.</p>

<p>That one fact is where all the complexity comes from. The bridge isn’t even a
single thing - a piggieback-driven <code class="language-plaintext highlighter-rouge">cljs.repl</code> runtime evaluates differently
from shadow-cljs, and clj-suitable has to speak both. The runtime can vanish
under you - refresh a browser tab and the namespace you loaded is gone. And
poking at a JS object to list its properties can have <em>side effects</em>, because a
property getter is just code that runs. (If you’ve ever wondered why
clj-suitable is so careful to only evaluate things that genuinely look like
interop, that’s why.)</p>

<p>So a completion request that looks like one operation from the editor is really
two very different machines under the hood - one reading compiler state on the
JVM, one evaluating code in a JS runtime you don’t control. Here’s the whole
picture:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  editor (CIDER / Calva)
     |   complete: prefix + context
     v
  nREPL server (one JVM) - cider-nrepl + clj-suitable
     |
     +-- static ---&gt; compliment + clj-suitable's cljs source
     |               reads the ClojureScript compiler state
     |               (namespaces, vars, locals, keywords) - stays on the JVM
     |
     +-- dynamic --&gt; only for JS interop forms
                        |   eval introspection code across the REPL bridge
                        v
              piggieback (cljs.repl)   or   shadow-cljs
                        |
                        v
              Node / browser / React Native  (the JS runtime)
              suitable.js-introspection reads a live object's
              properties and methods, and sends them back
     |
     v
  candidates from both paths, merged and returned to the editor
</code></pre></div></div>

<p>Two paths, one answer. Clojure completion has only ever needed the top half of
that diagram.</p>

<h2 id="dynamic-completion-tightened-up">Dynamic completion, tightened up</h2>

<p>That dynamic path is the fiddly one, and it’s where this release did most of
its sanding. The interop completion already worked - <code class="language-plaintext highlighter-rouge">(.| js/console)</code> would
offer you <code class="language-plaintext highlighter-rouge">log</code>, <code class="language-plaintext highlighter-rouge">warn</code> and the rest - but it had some rough edges:</p>

<ul>
  <li>Completing interop no longer clobbers your REPL history. Poking at
<code class="language-plaintext highlighter-rouge">(.| js/some-obj)</code> used to quietly overwrite <code class="language-plaintext highlighter-rouge">*1</code>/<code class="language-plaintext highlighter-rouge">*2</code>/<code class="language-plaintext highlighter-rouge">*3</code> with the
introspection result; now your last real value stays put where it belongs.</li>
  <li>The introspection namespace is loaded once per session instead of on every
single completion request. On a Node REPL that’s a needless round-trip gone
from every keystroke.</li>
  <li>The browser-runtime path got hardened. I chased down a couple of
long-standing “no completions in the browser” reports, stood up a real
headless-Chrome integration test to reproduce them, and fixed a lurking
crash along the way. (The short version of the investigation: the old
failures came from an inlined build that current CIDER no longer produces, so
most of you were never affected - but now there are tests making sure it
stays that way.)</li>
</ul>

<p>The <a href="https://github.com/clojure-emacs/clj-suitable/blob/master/CHANGELOG.md">full changelog</a> has everything that didn’t make the highlights.</p>

<h2 id="one-more-thing">One more thing</h2>

<p>0.8.0 gets the headline, but it stands on the <a href="https://github.com/clojure-emacs/clj-suitable/releases/tag/v0.7.0">0.7.0</a> release from a week
earlier, which did the unglamorous groundwork: modern dependencies
(ClojureScript 1.12, compliment 0.8.0, shadow-cljs 3.x), a move from CircleCI
to GitHub Actions, a <code class="language-plaintext highlighter-rouge">tools.build</code>-based build, and - crucially for my
sanity - actual integration tests that drive real Node and browser runtimes
instead of trusting things to work.</p>

<p>Actually, I only thought about improving how clj-suitable works when I
started to update its dependencies and CI setup.</p>

<h2 id="epilogue">Epilogue</h2>

<blockquote>
  <p>No sufficiently useful system can be both complete and consistent.</p>

  <p>– Kurt Gödel, subtweeting every autocomplete ever</p>
</blockquote>

<p>If you use CIDER, you don’t have to do anything to get any of this - it’ll ship
to you as part of the upcoming <strong>CIDER 2.1</strong>. Calva and other nREPL-based tools
that depend on clj-suitable will pick it up on their own schedule.</p>

<p>As always, this stands on the shoulders of others. Huge thanks to Alex
Yakushev, whose compliment is both the benchmark I was chasing and the source
of a good chunk of these ideas; to Andrea Richiardi, who did much of the early
work bridging ClojureScript completion and compliment; and to
<a href="https://github.com/rksm">Robert Krahn</a> for creating clj-suitable in the first place and giving me
such a solid foundation to build on.</p>

<p>Is any of this <em>complete</em>? Of course not - completeness is a horizon, not a
destination, and of all people a completion library should be the first to
admit it (see the gentleman up top). But clj-suitable <em>suits</em> ClojureScript a
good deal better than it did a month ago, and that was rather the point.</p>

<p>Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:complement" role="doc-endnote">
      <p>The completion library, spelled with an <em>i</em>. Not the nice thing
you say to someone, and - I really cannot stress this enough - not
<em>complement</em> with an <em>e</em>. <a href="#fnref:complement" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:1" role="doc-endnote">
      <p>One thing invariably leads to another with this stuff. You set out to fix
a REPL env wrapper and three weeks later you’re writing a headless-Chrome
test harness. No regrets. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Clojure" /><category term="ClojureScript" /><category term="nREPL" /><category term="CIDER" /><summary type="html"><![CDATA[You had me at js/. – Jerry Maguire, on ClojureScript interop completion clj-suitable 0.8.0 is out! If the name doesn’t ring a bell, that’s rather the point - clj-suitable is the small library that quietly powers ClojureScript code completion in CIDER, Calva, and pretty much anything else that talks to a cljs REPL over nREPL. Think of it as the ClojureScript counterpart to what compliment1 does for Clojure. For years it lagged well behind its Clojure sibling, and this release is my attempt to finally close that gap - to make cljs completion, if you’ll forgive me, a touch more suitable. This is also a direct follow-up to the Piggieback work I wrote about a couple of weeks ago. Once I had the cljs REPL plumbing back in decent shape, fixing up the completion story sitting on top of it was the obvious next move.2 The completion library, spelled with an i. Not the nice thing &#8617; One thing invariably leads to another with this stuff. You set out to fix &#8617;]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Sayid 0.8</title><link href="https://metaredux.com/posts/2026/07/18/sayid-0-8.html" rel="alternate" type="text/html" title="Sayid 0.8" /><published>2026-07-18T07:00:00+00:00</published><updated>2026-07-18T07:00:00+00:00</updated><id>https://metaredux.com/posts/2026/07/18/sayid-0-8</id><content type="html" xml:base="https://metaredux.com/posts/2026/07/18/sayid-0-8.html"><![CDATA[<p><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md">Sayid 0.8</a> is out! It’s the third release since I
<a href="/posts/2026/07/01/sayid-redux.html">brought Sayid back from the dead</a>
a couple of weeks ago, and it has a clear theme: making the tool easy to pick
up. The revival releases were mostly about the engine - bounding the recording,
consolidating the API, getting the data out. This one is about the experience.
If you’ve ever bounced off Sayid because you couldn’t figure out what to press,
or what it was trying to tell you, 0.8 is for you.</p>

<!--more-->

<h2 id="it-started-with-a-bug-report">It started with a bug report</h2>

<p>Shortly after the revival post, someone reported that pressing <code class="language-plaintext highlighter-rouge">c i</code> in the
workspace view - “inspect this captured value” - printed <code class="language-plaintext highlighter-rouge">Def'd as $s/*</code> and
then… nothing. The fix turned out to be a one-liner: Sayid was calling a
CIDER function whose signature changed years ago, and nobody had noticed since.
Which tells you everything about how many people were actually using that
command.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>The one-liner was easy, but the report got under my skin. If the inspector
integration could sit broken for years, what else about Sayid was quietly
hostile to anyone trying it for the first time? So I sat down and did a proper
UX audit of the Emacs client, wrote down everything that made me wince, and 0.8
is the result.</p>

<p>Here’s the current state of affairs in one take - trace, run, explore:</p>

<p><img src="https://raw.githubusercontent.com/clojure-emacs/sayid/master/doc/images/sayid-workflow.gif" alt="The Sayid workflow - trace some namespaces, run your code, explore the recording" /></p>

<h2 id="catching-up-06-and-07">Catching up: 0.6 and 0.7</h2>

<p>Before we get to the UX work, a quick recap of the two releases I never got
around to announcing (the revival post covered things up to 0.5).</p>

<p><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md">Sayid 0.6</a> rebuilt inner tracing - the mode that records every
intermediate expression <em>inside</em> a function - on top of <code class="language-plaintext highlighter-rouge">tools.analyzer.jvm</code>.
The old implementation re-read your source and rewrote raw forms, with special
cases for individual macros; the new one works off the analyzed AST. That
killed the long-standing bug where an inner-traced <code class="language-plaintext highlighter-rouge">try/catch</code> would swallow
exceptions, along with the per-macro special-casing that made the old
instrumenter so fragile.</p>

<p><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md">Sayid 0.7</a> made the trace itself data. <code class="language-plaintext highlighter-rouge">sayid.data/trace-data</code>
returns the recorded call tree as plain Clojure data with the live captured
values, and <code class="language-plaintext highlighter-rouge">tap-trace!</code> sends it to <code class="language-plaintext highlighter-rouge">tap&gt;</code>, so you can explore a recording in
<a href="https://github.com/djblue/portal">Portal</a> or your data tool of choice. It also added <code class="language-plaintext highlighter-rouge">sayid.golden</code> - capture
a run’s call tree as a baseline, then assert future runs still match it, which
is a surprisingly pleasant way to pin down the behavior of gnarly legacy code
before refactoring it.</p>

<h2 id="the-highlights">The highlights</h2>

<p>Now, the 0.8 goodies. The full list is in the <a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md">changelog</a>, but here’s what I’m
most excited about:</p>

<ul>
  <li>There’s a proper entry point now: <code class="language-plaintext highlighter-rouge">C-c s</code> pops up a <code class="language-plaintext highlighter-rouge">transient</code> menu
(<code class="language-plaintext highlighter-rouge">sayid-menu</code>) that groups the commands along Sayid’s core loop - trace
something, run your code, explore the recording - and shows you how much is
traced and recorded right now, or what’s missing (a REPL, the middleware) when
you’re not connected. The menu uses the same key sequences as the classic
prefix map, so your muscle memory keeps working, and <code class="language-plaintext highlighter-rouge">sayid-use-menu</code> brings
the plain keymap back if popups aren’t your thing.</li>
</ul>

<p><img src="https://raw.githubusercontent.com/clojure-emacs/sayid/master/doc/images/sayid-menu.png" alt="The Sayid menu" /></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sayid-trace-fn</code> (<code class="language-plaintext highlighter-rouge">C-c s t t</code>) is the new “just trace this” command. You no
longer need to know what inner and outer traces are before you can trace your
first function - the default does the right thing, and you can graduate to
the fancier variants later.</li>
  <li>Empty views teach instead of scolding. Opening the workspace before anything
was recorded used to greet you with an error; now you get the buffer anyway,
with a short walkthrough of how to get data into it. The first five minutes
with Sayid should no longer require reading the manual.</li>
  <li>The commands talk back like a human. Tracing a function now tells you what
happened and what to do next (“Outer-traced acme.checkout/subtotal - run some
code, then <code class="language-plaintext highlighter-rouge">C-c s w</code> shows what was recorded”), and trying to enable a trace
that doesn’t exist tells you the actual problem instead of pretending
everything went fine.</li>
  <li>The workspace tree is now the one view to rule them all: on any call you can
inspect a captured value in CIDER’s inspector (<code class="language-plaintext highlighter-rouge">c i</code> - yes, it works now),
def it to a var for REPL poking (<code class="language-plaintext highlighter-rouge">c d</code>), pretty-print it (<code class="language-plaintext highlighter-rouge">c p</code>), or copy an
expression that reproduces the call (<code class="language-plaintext highlighter-rouge">c r</code>). <code class="language-plaintext highlighter-rouge">C-c s f</code> - show the recorded
calls of the form at point - renders there too.</li>
  <li>A pile of small courtesies that add up: tracing a function no longer steals
your window with a popup, <code class="language-plaintext highlighter-rouge">g</code> refreshes the Sayid buffers like every other
Emacs buffer, and resetting the workspace asks before irreversibly dropping
your traces and recording.</li>
  <li>The client and the middleware now tolerate version skew - if your <code class="language-plaintext highlighter-rouge">deps.edn</code>
pins an older Sayid jar than your Emacs package expects, things degrade
politely instead of erroring in strange ways.</li>
  <li>The Emacs client now requires CIDER 2.0, which let me drop a bunch of
compatibility shims and lean on the new <code class="language-plaintext highlighter-rouge">cider-tree-view</code> throughout.</li>
  <li>And the README finally <em>shows</em> the tool instead of just describing it -
screenshots and the GIF above included. A picture of a call tree is worth a
thousand words about one.</li>
</ul>

<h2 id="upgrading-notes">Upgrading notes</h2>

<p>Nothing here should break a working setup, but two things are worth knowing.
The Emacs package now requires CIDER 2.0 (released earlier this week), so
they’ll need to be upgraded together. And <code class="language-plaintext highlighter-rouge">C-c s</code> now opens the menu instead of
acting as a bare prefix - every old key sequence still works exactly as typed,
but if you prefer the old silent prefix, set <code class="language-plaintext highlighter-rouge">sayid-use-menu</code> to <code class="language-plaintext highlighter-rouge">nil</code>. A few
commands got more consistent names (<code class="language-plaintext highlighter-rouge">sayid-trace-fn-outer</code> and friends); the
old names live on as obsolete aliases.</p>

<h2 id="onward">Onward</h2>

<p>When I wrote the revival post I asked people to kick the tyres and tell me what
feels rough. This release is what acting on that feedback looks like, and it’s
exactly the kind of contribution I need more of - a two-line bug report turned
into the biggest usability overhaul in the project’s history. So thanks to
everyone who has been trying out the revived Sayid, and please keep the reports
coming on the <a href="https://github.com/clojure-emacs/sayid/issues">issue tracker</a>.</p>

<p><code class="language-plaintext highlighter-rouge">[mx.cider/sayid "0.8.0"]</code> is on Clojars, the Emacs package is on MELPA, and
tracing your first function is now a single <code class="language-plaintext highlighter-rouge">C-c s t t</code> away. Give it a spin!</p>

<p>Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>In fairness, it also tells you something about me - the bug shipped with
my own resurrection releases. Reviving a decade-old codebase means
inheriting a decade of API drift, and some of it only surfaces when a real
user presses a real key. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Clojure" /><category term="nREPL" /><category term="CIDER" /><category term="Emacs" /><category term="Debugging" /><summary type="html"><![CDATA[Sayid 0.8 is out! It’s the third release since I brought Sayid back from the dead a couple of weeks ago, and it has a clear theme: making the tool easy to pick up. The revival releases were mostly about the engine - bounding the recording, consolidating the API, getting the data out. This one is about the experience. If you’ve ever bounced off Sayid because you couldn’t figure out what to press, or what it was trying to tell you, 0.8 is for you.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>