Posts
-
Stepping Through Macros in CIDER
This is another installment in the series of articles about the notable changes in CIDER 2.0. Today’s topic is one of those “ambitious ideas that lay dormant for ages” I keep mentioning: proper interactive macro stepping.
The Dream
CIDER has had macroexpansion support practically forever -
C-c C-mexpands the form before point into a dedicated buffer, a feature we inherited spiritually from SLIME. It works, but it has always felt a bit… detached. The expansion lives in another buffer, divorced from the code you’re reading, and for deeply nested macros you end up bouncing between buffers trying to keep your bearings.Emacs Lisp hackers have long had something nicer: macrostep, a brilliant little package that expands macros in place - right where they sit in your code - one step at a time, and collapses them back when you’re done. I’ve wanted a Clojure version of this for years. CIDER 2.0 finally ships one.
Standing on shoulders, not on top of them
I’m hardly the first person to want this. The idea of bridging CIDER and macrostep goes back to at least 2016, where a proof of concept wired up macrostep’s extension hooks to CIDER by injecting a couple of helper functions into your REPL and shuttling forms back and forth as strings. Later, macrostep-geiser - a Scheme-oriented macrostep backend - grew CIDER support as well, and SLIME itself ships a
slime-macrostepcontrib built on the same extension API. So the hooks were there, the hacks existed, and I could have blessed one of them and called it a day.I opted for a clean, from-scratch implementation in CIDER instead - mostly to provide the best possible experience for Clojure programmers, without the compromises the bridges had to make. macrostep’s extension API was designed around Emacs Lisp’s happy circumstances, and Clojure violates most of them:
macrostep-expand-1-functionmust be synchronous. That’s fine when expansion is an elisp function call; it’s less fine when it’s a network round-trip to an nREPL server. (CIDER has been busy removing exactly this kind of blocking call lately - asynchronous eldoc being the poster child.)- The API traffics in forms - elisp data structures. But Clojure code isn’t
elisp data: keywords, reader tags,
#()lambdas and namespaced maps have no faithful elisp representation, so every bridge ends up round-tripping code through strings and hoping the quoting survives. The 2016 proof of concept literally did(format "(expand-once '%s)" form), which should make anyone a little nervous. macrostep-print-functionexpects the client to print and fontify the expansion. In CIDER the server does the printing - that’s how we get namespace tidying (wheninstead ofclojure.core/when), print-option handling and metadata display for free. A client-side printer would have to reimplement all of that, badly.
There’s also a less technical reason: lately I’ve grown rather averse to adding third-party dependencies to CIDER. A dependency is a bet on someone else’s continued enthusiasm, and such bets sometimes go bad - tellingly, macrostep itself spent a stretch unmaintained and now lives in a GitHub org literally named “emacsorphanage” (it has since found a new maintainer, but the point stands). CIDER is 14 years old and intends to stick around; over that kind of horizon, owning ~600 lines of overlay code is cheaper than adopting someone else’s semi-abandoned package. So we kept macrostep’s brilliant UX ideas and its familiar keybindings, and left the coupling behind.
cider-macrostep
The entry point is
cider-macrostep-expand(C-c M-m e). Put point after a macro form, invoke it, and the form is replaced inline with its one-step expansion, highlighted so you can tell what’s expansion and what’s your code:(when-let [x (fetch-thing)] (process x)) ;; C-c M-m e => (let [temp__5825__auto__ (fetch-thing)] (when temp__5825__auto__ (let [x temp__5825__auto__] (process x))))From there you’re in
cider-macrostep-mode, where the further-expandable sub-forms are underlined,n/phop between them,e(orRET) steps into one, andc/qcollapse one level or everything back to the original code. Your buffer is never actually modified in a way that sticks - collapse everything and it’s exactly as it was.Here’s the whole flow in motion - expand, step into a nested macro, collapse back to the original code:

A couple of touches I’m particularly fond of:
- Every distinct gensym in the expansion gets its own color, so you can finally
track where that
temp__5825__auto__flows through the expanded code. Once you’ve seen aforexpansion with colorized gensyms, you won’t want to go back:

E(cider-macrostep-expand-all) fully expands the form in one step, for when you don’t care about the journey.b(cider-macrostep-expand-in-buffer) runs the same stepping session in a dedicated popup, leaving the source buffer untouched - handy when you’re in someone else’s code and feel uneasy about inline rewrites, however temporary.
The classic buffer got some love too
The traditional macroexpansion buffer wasn’t neglected either: it grew a header line showing the active expander and display options,
nandtcycle namespace display and metadata in place,gre-expands with the latest macro definition, and freshly expanded forms pulse briefly so your eye lands in the right place.The expansion commands also got more talkative: pointing them at an unresolved symbol now tells you whether the namespace simply isn’t loaded yet (evaluate the buffer!) or you’ve got a typo, instead of silently doing nothing.1
Why bother?
Macros are the part of Clojure people are most likely to describe as “magic”, and the standard advice - “just macroexpand it” - has always carried a hidden tax: the expansion of any non-trivial macro is a wall of gensyms and nested
let*s that’s genuinely hard to read cold. Stepping through the expansion one level at a time, in place, with the gensyms color-coded, turns that wall into something you can actually follow. I did not expect macro debugging to become fun, and yet here we are.All the details are in the macroexpansion docs. Give it a try the next time a macro surprises you - and keep hacking!
-
Amusingly, we initially overdid these diagnostics - the guard refused to expand
letandfn(which are macros wearing special-form badges) and broke a beloved trick of using macroexpansion to normalize reader syntax like::auto/keywords. See #4111 - fixed right after 2.0. Even diagnostics need diagnostics. ↩
-
clj-refactor.el 4.0
Hot on the heels of CIDER 2.0, clj-refactor.el 4.0 is out! It’s the first major release of the project in almost five years, and this time around the version bump is not just ceremonial - 4.0 is the biggest batch of user-facing improvements clj-refactor has seen in ages, plus a healthy dose of long-overdue spring cleaning.
Read More -
Simplifying Session Management in CIDER
The CIDER 2.0 announcement was, by necessity, a mile wide and an inch deep - there was simply too much to cover. So I’m planning to make up for that with a small series of articles, each shedding a bit more light on one notable change and the reasoning behind it. This is the first one, and it tackles the area that has probably generated more confusion (and bug reports) than any other over the years: session management.
Truth be told, I meant to write this article back when the work landed in CIDER 1.22,1 but I was so busy wrapping up CIDER 2.0 that I’m only getting to it now, as part of this series. Better late than never, right?
A bit of history
Back in the pre-history of CIDER (before 0.18), there were no sessions at all - just connections, and a very simple rule: commands went to the most recently used connection that made sense for your buffer. Crude? Sure. But it mostly worked OK, and - crucially - everyone understood it.
CIDER 0.18 replaced this with Sesman-based session management. Sessions grouped related REPLs together and could be linked to projects, directories and buffers, which made multi-project workflows a lot more robust. Shortly afterwards, in 0.19, we introduced the concept of friendly sessions (I wrote about it back in Happy New CIDER!) - sessions that CIDER would infer for buffers that had no explicit links, so that things Just Worked without any setup.
I’ll be honest: I was never a big fan of the friendly-session concept. It was one more piece of terminology for users to learn (“linked? friendly? current?”), and its behavior felt a bit like magic in some cases - your buffer would get associated with some REPL, and it wasn’t always obvious why that one. Magic is great when it works and infuriating when it doesn’t, and session inference worked… most of the time.
The breaking point
Over the years the friendly-session matching logic quietly accumulated complexity. To decide whether a session was friendly to your buffer, CIDER would check whether the file was on the session’s classpath (fetched over nREPL and rescanned regularly), and, failing that, try to match the buffer’s namespace against the namespaces loaded in the REPL. Clever! Also: slow, hard to predict, and hard to debug.
Then it got worse. The matcher runs on a very hot path - the mode-line needs to know the current REPL, so it effectively ran on every redisplay. At some point a change made it call
file-truenameover every classpath root on each of those runs, and Clojure buffers without a connected REPL became visibly laggy (#3933 - if you’ve ever felt CIDER make plain editing sluggish, that was probably it).The performance bug was fixable in place. But while staring at the code I realized the classpath scan never needed to be there at all. Friendly sessions were originally introduced for essentially one use case: making
cider-find-var(M-.) into a dependency’s source land in a buffer that still talks to your project’s REPL. That’s a navigation problem, not an inference problem.The fix: less magic, more predictability
So #3935 restructured the whole thing around two simple rules:
- When you jump to a dependency’s source via a CIDER navigation command, the buffer gets pinned to the session you came from. Evaluation, completion and friends keep targeting that REPL, no inference required.
- For everything else, a session is friendly to a buffer simply when the buffer’s file lives under the session’s project directory. That’s one cheap string comparison instead of a classpath scan.
That’s the entire model now. It killed the redisplay lag, dropped a couple of nREPL round-trips we used to make on every connection, and - more importantly to me - you can now predict what CIDER will do without consulting the source code.
Is the new behavior dumber than the old one? Slightly, and deliberately so. It’s still smarter than the pre-0.18 “last used connection” rule - project boundaries are respected, and dependency buffers follow the session you were working in. The one thing you lose is auto-association for files outside the project that you open by non-CIDER means (say, a plain
find-fileinto a jar). For those you can link a session explicitly (C-c C-s pand friends) - or use the next thing.Default sessions: opting out of inference entirely
While I was in the area, I also added (back) a default session facility - old-timers may remember that the pre-sesman connection era had a similar notion. If you’d rather have absolute predictability than any cleverness:
M-x cider-set-default-sessionPick a session, and from that point on every REPL lookup - evaluation, completion, documentation, the lot - goes to it, regardless of which buffer you’re in.
M-x cider-clear-default-sessionreverts to the normal project-based association. No linking, no friendliness, no inference - just “always use this one, until I say otherwise”.The two styles, side by side
Say you’re working on two projects at once - a backend and a frontend:
~/code/backendwith a Clojure REPL~/code/frontendwith a shadow-cljs (ClojureScript) REPL
With the default (inference-based) behavior, things flow like you’d hope:
- You open
~/code/backend/src/api/handler.cljand evaluate a form - it goes to the backend REPL, because the file lives under that session’s project directory. - You press
M-.on aringvar and land in a jar - the dependency buffer is pinned to the backend session, so completion and evaluation keep working against the backend REPL. - You switch to
~/code/frontend/src/ui/views.cljs- commands now target the frontend REPL. No links, no configuration, no surprises.
Now the contrasting style. Suppose you’re spending the afternoon debugging the backend, but you keep bouncing between projects, scratch buffers, and random files scattered around your disk, and you want all of it to hit the backend REPL:
M-x cider-set-default-session RET backend- Every buffer - the frontend sources included - now talks to the backend REPL. What you evaluate is what you get, everywhere.
- When you’re done:
M-x cider-clear-default-session, and the project-based behavior above kicks back in.
The full dispatch pipeline (pinned buffer → default session → linked/friendly session → REPL type filter → recency) is documented in detail in the session management docs, including an ASCII flowchart I’m unreasonably proud of. See in particular the sections on friendly sessions and the default session.
Closing thoughts
Session management is one of those areas where the “smart” solution and the good solution turned out to be different things. Seven years of friendly sessions taught me that users don’t actually want their tools to be clever - they want them to be predictable, and fast, and clever only when the cleverness is cheap and explainable in one sentence. “Your project’s files use your project’s REPL” passes that bar; “your buffer matches some session’s classpath” never did.
More posts about the notable changes in CIDER 2.0 are coming soon. Until then - keep hacking!
-
In retrospect these changes should have probably happened in 2.0, given that they altered some long-standing behavior. In my defense, when I was working on them I still hadn’t decided whether 2.0 would be the next release or something in the more distant future. ↩
-
CIDER 2.0: Sky is the Limit
Two weeks ago I wrote that CIDER 2.0 was brewing. Today the brew is ready - CIDER 2.0 (“Terceira”)1 is officially out! I promised the release would follow the preview within a week or two if nothing serious surfaced, and for once in my life I’m actually on schedule.
The preview post covered the big themes in detail - the transient menus, the call-graph browsers,
cider-macrostep, the revamped tracing and enlighten, the ClojureScript improvements - so I won’t rehash all of that here. Instead I’ll focus on what changed between the preview and the release, and on the bigger picture of what CIDER 2.0 is actually about.What CIDER 2.0 is about
Looking back at the (enormous) changelog, the release boils down to four themes:
- Tackle some ambitious ideas that had been lying dormant for ages - inline macro stepping, rich (content-type) results, source-aware cross-referencing. Some of the issues closed by this release were filed the better part of a decade ago.
- Polish the “understand your code” toolbox - the debugger, the macroexpansion facilities, tracing, enlighten, the stacktraces and the cross-references all got a serious amount of love.
- Make the whole CIDER experience more consistent and discoverable - transient menus everywhere, one tree-view widget shared by all the browsers, and a naming cleanup that brought a bunch of stragglers in line.
- Fix old annoyances - the friendly-session complexity that 1.22 started taming (remember the redisplay lag fix and default sessions?), the find-references gaps, the flaky SSH tunnels, the confused stdin handling.
Notice what’s not on that list - a pile of shiny new features. There are a few genuinely new things in 2.0, of course, but the heart of this release is that most of CIDER’s important features got overhauled (tastefully, I hope) or made more robust and faster. After 14 years you accumulate a lot of good ideas with rough edges; 2.0 is me going over them with fine-grit sandpaper.
What landed after the preview
Quite a lot, as it turns out - the last two weeks were busy. The headliners:
- Rich results
are now on by default. Evaluate something that returns an image
and it renders inline; a result that points to external content (a file, a
URL) gets a
[show content]button that fetches it only when you press it. HTML renders as formatted text, URLs are clickable. This works for regularC-x C-e-style evaluations too, not just in the REPL (configurable viacider-eval-rich-content-destination). Fun fact: content-type support was added way back in 0.17, disabled in 0.25 after it got a bit overzealous with the fetching, and the interactive-eval part was requested in 2018. Better late than never, right? - The transient story got finished. The debugger and the inspector now have
menus of their own (
?andmrespectively), and many menus grew argument flags - pick a pretty-printer per invocation, set test selectors once and reuse them across runs, toggle the refresh modes, pass aliases at jack-in. As before, your muscle memory is safe - the menus only help when you pause. - There’s a new
cider-doctorcommand that checks your Emacs setup and your active session for common problems (version mismatches, stale byte-code, leftover obsolete config) and produces a copy-pasteable report. My hope is that it will make “CIDER doesn’t work” bug reports a thing of the past - or at least give us something to look at when they arrive. - Pending evaluations now show an animated spinner overlay right at the form you’re evaluating, instead of a spinner in the mode-line of a REPL buffer you probably can’t even see.
- The debugger got
dusted off properly: quitting a debug session finally
restores point to where you started - an issue
filed in 2016 - the
force-step-out key works again, and
cider-nrepl0.62 fixed a batch of instrumentation bugs (records surviving instrumentation, clear errors for forms too big to instrument, and a few crashes). - Stdin handling
got a long overdue overhaul - input prompts are routed to the
session that actually asked for input, cancelling a prompt now interrupts the
evaluation (instead of quietly letting it continue), and
C-c C-dsends EOF for code that reads until end of input. - Clicking a stack frame for a top-level anonymous function now jumps to the
actual source instead of
clojure.core/fn- a bug from 2020 - and ClojureScript frames render theirns/fnproperly. - A big consistency pass over the options: the REPL history browser is now
cider-history, the inline-result options became a coherentcider-eval-result-*family, and the six per-buffer auto-select options collapsed into a singlecider-auto-select-buffer. Every old name keeps working as an obsolete alias, so nothing breaks. - And a long tail of robustness work - a slow memory leak on the
eldoc/completion path,
cider-classpathon Windows, formatting no longer corrupting multi-line strings, theme-aware colors for the nREPL message log, and plenty more of the same ilk.
The documentation also got restructured to be more approachable - there’s a proper quickstart now, a keybindings reference page, dedicated pages on using CIDER alongside
clojure-lspandclojure-ts-mode, and a guide for full-stack Clojure + ClojureScript projects. The manual has grown organically for over a decade, and it showed; hopefully finding things is much easier now.Upgrading
Despite the big scary version number, upgrading should be uneventful. All the renames ship with obsolete aliases, the transient menus preserve the classic keybindings, and the only removals are commands that had been no-ops for years. The one bit of muscle memory you may need to adjust:
cider-macroexpand-allmoved fromC-c M-mtoC-c M-m a, asC-c M-mis now a prefix for all the macroexpansion commands. If anything feels off after the upgrade,M-x cider-doctoris your friend.Fourteen years later
CIDER 0.1 (well,
nrepl.el0.1) was released on July 10th, 2012 - fourteen years (and five days) ago.2 I’ve been reflecting on this a lot lately. Fourteen years is an eternity in our line of work - entire ecosystems have come and gone in that time - and yet here we are, still innovating, still improving, still moving forward. I dare say CIDER 2.0 is the strongest release in the project’s history, and it’s certainly the one I’ve enjoyed working on the most.3None of this would have been possible without the people and organizations who have supported the project over the years - everyone who contributed code, reported issues, wrote about CIDER, answered questions, or backed the project financially. A special thanks to Clojurists Together for their long-standing support, and to everyone who took the snapshot for a spin after the preview post and shared feedback - several rough edges got filed down because of you.
So, go play with CIDER 2.0! Kick the tires, explore the menus, crack open some values in the inspector, step through a macro or two. And if CIDER makes your work a little nicer every day, consider supporting its future development
- that’s what keeps CIDER and friends going.
Where to from here? The sky is the limit. The REPL is the inspiration. The best is always yet to come…
Keep hacking!
-
Continuing the Azores naming streak started by 1.22 (“São Miguel”). “Terceira” literally means “the third” in Portuguese, which is a slightly confusing name for a 2.0 release, but naming things has never been my strong suit. ↩
-
The full origin story is in CIDER Turns 10, if you’re curious how a prototype hacked on a flight to San Francisco ended up here. ↩
-
That I can remember. My memory is not what it used to be. ↩
-
Lowering the Drawbridge
Drawbridge 0.4 is out! If your reaction is “Draw-what now?”, I can’t really blame you - Drawbridge is easily the most obscure project in the nREPL stable, and it has spent most of its life in a state best described as “technically maintained”. I’ve set out to change that recently, and this post is both a release announcement and the story of a 14-year-old project that never quite lived up to its potential. Hopefully, until now.
Read More
Subscribe via RSS | View Older Posts