This is another installment in the series of articles about the notable changesin CIDER 2.0. Today’s topic is one ofthose “ambitious ideas that lay dormant for ages” I keep mentioning: properinteractive macro stepping.The DreamCIDER has had macroexpansion support practically forever - C-c C-m expandsthe form before point into a dedicated buffer, a feature we inheritedspiritually from SLIME. It works, but it has always felt a bit… detached. Theexpansion lives in another buffer, divorced from the code you’re reading, andfor deeply nested macros you end up bouncing between buffers trying to keepyour bearings.Emacs Lisp hackers have long had something nicer:macrostep, a brilliant littlepackage that expands macros in place - right where they sit in your code - onestep at a time, and collapses them back when you’re done. I’ve wanted a Clojureversion of this for years. CIDER 2.0 finally ships one.Standing on shoulders, not on top of themI’m hardly the first person to want this. The idea of bridging CIDER andmacrostep goes back to at least2016, where a proof ofconcept wired up macrostep’s extension hooks to CIDER by injecting a couple ofhelper functions into your REPL and shuttling forms back and forth as strings.Later, macrostep-geiser - aScheme-oriented macrostep backend - grew CIDER support as well, and SLIMEitself ships a slime-macrostep contrib built on the same extension API. Sothe hooks were there, the hacks existed, and I could have blessed one of themand called it a day.I opted for a clean, from-scratch implementation in CIDER instead - mostly toprovide the best possible experience for Clojure programmers, without thecompromises the bridges had to make. macrostep’s extension API was designedaround Emacs Lisp’s happy circumstances, and Clojure violates most of them: macrostep-expand-1-function must be synchronous. That’s fine whenexpansion is an elisp function call; it’s less fine when it’s a networkround-trip to an nREPL server. (CIDER has been busy removing exactly thiskind of blocking call lately - asynchronous eldoc being the poster child.) The API traffics in forms - elisp data structures. But Clojure code isn’telisp data: keywords, reader tags, #() lambdas and namespaced maps have nofaithful elisp representation, so every bridge ends up round-tripping codethrough strings and hoping the quoting survives. The 2016 proof of conceptliterally did (format "(expand-once '%s)" form), which should make anyonea little nervous. macrostep-print-function expects the client to print and fontify theexpansion. In CIDER the server does the printing - that’s how we getnamespace tidying (when instead of clojure.core/when), print-optionhandling and metadata display for free. A client-side printer would have toreimplement all of that, badly.There’s also a less technical reason: lately I’ve grown rather averse toadding third-party dependencies to CIDER. A dependency is a bet on someoneelse’s continued enthusiasm, and such bets sometimes go bad - tellingly,macrostep itself spent a stretch unmaintained and now lives in a GitHub orgliterally named “emacsorphanage” (it has since found a new maintainer, but thepoint stands). CIDER is 14 years old and intends to stick around; over thatkind of horizon, owning ~600 lines of overlay code is cheaper than adoptingsomeone else’s semi-abandoned package. So we keptmacrostep’s brilliant UX ideas and its familiar keybindings, and left thecoupling behind.cider-macrostepThe entry point is cider-macrostep-expand (C-c M-m e). Put point after amacro form, invoke it, and the form is replaced inline with its one-stepexpansion, 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-expandablesub-forms are underlined, n/p hop between them, e (or RET) steps intoone, and c/q collapse one level or everything back to the original code.Your buffer is neveractually modified in a way that sticks - collapse everything and it’s exactly asit was.A couple of touches I’m particularly fond of: Every distinct gensym in the expansion gets its own color, so you can finallytrack where that temp__5825__auto__ flows through the expanded code. Onceyou’ve seen a for expansion with colorized gensyms, you won’t want to goback. E (cider-macrostep-expand-all) fully expands the form in one step, forwhen you don’t care about the journey. b (cider-macrostep-expand-in-buffer) runs the same stepping session in adedicated popup, leaving the source buffer untouched - handy when you’re insomeone else’s code and feel uneasy about inline rewrites, however temporary.The classic buffer got some love tooThe traditional macroexpansion buffer wasn’t neglected either: it grew a headerline showing the active expander and display options, n and t cyclenamespace display and metadata in place, g re-expands with the latest macrodefinition, and freshly expanded forms pulse briefly so your eye lands in theright place.The expansion commands also got more talkative: pointing them at an unresolvedsymbol now tells you whether the namespace simply isn’t loaded yet (evaluatethe buffer!) or you’ve got a typo, instead of silently doing nothing.1Why 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 hiddentax: the expansion of any non-trivial macro is a wall of gensyms and nestedlet*s that’s genuinely hard to read cold. Stepping through the expansion onelevel at a time, in place, with the gensyms color-coded, turns that wall intosomething you can actually follow. I did not expect macro debugging to becomefun, and yet here we are.All the details are in themacroexpansion docs.Give it a try the next time a macro surprises you - and keep hacking! Amusingly, we initially overdid these diagnostics - the guard refused toexpand let and fn (which are macros wearing special-form badges) andbroke a beloved trick of using macroexpansion to normalize reader syntaxlike ::auto/keywords. See#4111 - fixed rightafter 2.0. Even diagnostics need diagnostics. ↩