<feed xmlns="http://www.w3.org/2005/Atom"> <id>https://batsov.com/</id><title>(think)</title><subtitle>Bozhidar Batsov's personal blog</subtitle> <updated>2026-07-24T08:47:39+03:00</updated> <author> <name>Bozhidar Batsov</name> <uri>https://batsov.com/</uri> </author><link rel="self" type="application/atom+xml" href="https://batsov.com/feed.xml"/><link rel="alternate" type="text/html" hreflang="en" href="https://batsov.com/"/> <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator> <rights> © 2026 Bozhidar Batsov </rights> <icon>/assets/img/favicons/favicon.ico</icon> <logo>/assets/img/favicons/favicon-96x96.png</logo> <entry><title>Neocaml 0.9: A Better REPL, Dune/Opam Completion, and More Robustness</title><link href="https://batsov.com/articles/2026/06/24/neocaml-0-9-repl-completion-and-robustness/" rel="alternate" type="text/html" title="Neocaml 0.9: A Better REPL, Dune/Opam Completion, and More Robustness" /><published>2026-06-24T10:00:00+03:00</published> <updated>2026-06-24T10:00:00+03:00</updated> <id>https://batsov.com/articles/2026/06/24/neocaml-0-9-repl-completion-and-robustness/</id> <content type="html"><![CDATA[<p>It’s been a couple of months since the last <a href="https://github.com/bbatsov/neocaml">neocaml</a> release, and the reason is simple — for a while there I was genuinely out of ideas. Back when I shipped <a href="/articles/2026/03/25/neocaml-0-6-opam-dune-and-more/">0.6</a> I declared (again!) that I was done with new features, and this time I almost meant it. But ideas have a way of creeping back in, and 0.9 turned out to be a meaty release. Here are the highlights.</p><h2 id="a-much-nicer-repl-experience">A much nicer REPL experience</h2><p>The biggest chunk of work went into the REPL (toplevel) integration. I’m well aware that the OCaml toplevel isn’t terribly popular with seasoned OCaml developers — most of them reach for a proper build and a debugger instead. But I think newcomers get a lot of mileage out of a REPL, and (no surprise to anyone who’s followed my work) I’m a Lisper at heart with a real soft spot for interactive development. Clojure and Emacs Lisp spoiled me, and I want OCaml beginners to taste a bit of that too.</p><p>So, what’s new:</p><ul><li><strong>A dedicated REPL per project.</strong> The REPL buffer is now named after its project (e.g. <code class="language-plaintext highlighter-rouge">*OCaml: myproject*</code>), and the send commands route to the current buffer’s project REPL. You can have several projects running side by side without them stepping on each other.<li><p><strong>Choose your toplevel.</strong> The new <code class="language-plaintext highlighter-rouge">neocaml-repl-flavor</code> lets you pick between <code class="language-plaintext highlighter-rouge">ocaml</code>, <code class="language-plaintext highlighter-rouge">utop</code>, and <code class="language-plaintext highlighter-rouge">dune-utop</code>. Set it globally, or per project via <code class="language-plaintext highlighter-rouge">.dir-locals.el</code>:</p><div class="language-emacs-lisp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre><span class="p">((</span><span class="nv">neocaml-mode</span> <span class="o">.</span> <span class="p">((</span><span class="nv">neocaml-repl-flavor</span> <span class="o">.</span> <span class="nv">dune-utop</span><span class="p">))))</span>
</pre></div></div><p>The active flavor shows up in the REPL’s mode line, so you always know what you’re talking to.</p><li><strong>Send a phrase and step.</strong> <code class="language-plaintext highlighter-rouge">C-c C-n</code> (<code class="language-plaintext highlighter-rouge">neocaml-repl-send-phrase-and-step</code>) sends the phrase at point to the REPL and moves on to the next one — perfect for walking through a file top to bottom while you experiment.<li><strong><code class="language-plaintext highlighter-rouge">#require</code> from Emacs.</strong> <code class="language-plaintext highlighter-rouge">neocaml-repl-require</code> loads a <code class="language-plaintext highlighter-rouge">findlib</code> package into the running toplevel without you having to type the directive by hand.<li><strong>Restart on demand.</strong> <code class="language-plaintext highlighter-rouge">neocaml-repl-restart</code> kills and restarts the toplevel when things get into a weird state.</ul><h2 id="completion-for-opam-and-dune-no-lsp-required">Completion for Opam and Dune, no LSP required</h2><p>This one I’m particularly happy with. <code class="language-plaintext highlighter-rouge">.ml</code>/<code class="language-plaintext highlighter-rouge">.mli</code> files have <code class="language-plaintext highlighter-rouge">ocaml-lsp-server</code> to lean on for completion, but the auxiliary file formats have no language server at all. That’s exactly the kind of gap neocaml is meant to fill, so both <code class="language-plaintext highlighter-rouge">neocaml-dune-mode</code> and <code class="language-plaintext highlighter-rouge">neocaml-opam-mode</code> now ship a <code class="language-plaintext highlighter-rouge">completion-at-point</code> backend.</p><p>In a <code class="language-plaintext highlighter-rouge">dune</code> file you get completion for stanza names, the field names valid for the <em>enclosing</em> stanza, and library names inside <code class="language-plaintext highlighter-rouge">libraries</code>/<code class="language-plaintext highlighter-rouge">pps</code> fields:</p><pre><code class="language-dune">(library
 (name my_lib)
 (libraries str re|))   ; &amp;lt;- completes both your own libraries and installed findlib ones
</code></pre><p>The library candidates combine your project’s own libraries with whatever’s installed in the active Opam switch. And it’s switch-aware — if there’s a project-local switch (an <code class="language-plaintext highlighter-rouge">_opam/</code> directory), neocaml detects it and queries it via <code class="language-plaintext highlighter-rouge">opam exec --</code>, without any configuration on your part. The results are cached per project, so it stays snappy.</p><p>In <code class="language-plaintext highlighter-rouge">opam</code> files you get completion for field and section names, and package names inside <code class="language-plaintext highlighter-rouge">depends</code>/<code class="language-plaintext highlighter-rouge">depopts</code>/<code class="language-plaintext highlighter-rouge">conflicts</code> (sourced from <code class="language-plaintext highlighter-rouge">opam list</code>):</p><pre><code class="language-opam">depends: [
  "dune" {&amp;gt;= "3.0"}
  "cmd|"   ; &amp;lt;- completes to cmdliner and friends
]
</code></pre><p>Both backends can be toggled off via <code class="language-plaintext highlighter-rouge">neocaml-dune-complete-libraries</code> and <code class="language-plaintext highlighter-rouge">neocaml-opam-complete-packages</code> if you’d rather not have them.</p><h2 id="robustness-improvements">Robustness improvements</h2><p>A good chunk of this release is the unglamorous but important work of making things just behave correctly:</p><ul><li><strong>Character literals and quoted strings at the syntactic layer.</strong> Tree-sitter fontifies <code class="language-plaintext highlighter-rouge">'"'</code>, <code class="language-plaintext highlighter-rouge">'('</code>, and <code class="language-plaintext highlighter-rouge">{|raw "strings"|}</code> correctly, but the syntax table underneath was getting confused — which broke <code class="language-plaintext highlighter-rouge">forward-sexp</code>, <code class="language-plaintext highlighter-rouge">delete-pair</code>, and <code class="language-plaintext highlighter-rouge">electric-pair-mode</code> around those constructs. The fix was a good old <code class="language-plaintext highlighter-rouge">syntax-propertize-function</code>. I wrote up the whole story over on Emacs Redux in <a href="https://emacsredux.com/blog/2026/06/22/tree-sitter-modes-still-need-a-syntax-table/">Tree-sitter Modes Still Need a Syntax Table</a>, if you’re into mode-writing internals.<li><strong><code class="language-plaintext highlighter-rouge">project.el</code> integration.</strong> A directory with a <code class="language-plaintext highlighter-rouge">dune-project</code> file is now recognized as a project root (even without version control), <code class="language-plaintext highlighter-rouge">_build/</code> and <code class="language-plaintext highlighter-rouge">_opam/</code> are ignored, and <code class="language-plaintext highlighter-rouge">compile-command</code> defaults to <code class="language-plaintext highlighter-rouge">dune build</code> in dune projects.</ul><p>There’s more in there too — <code class="language-plaintext highlighter-rouge">ocamlformat</code> integration (<code class="language-plaintext highlighter-rouge">C-c C-f</code>), clickable URLs and bug references in comments, a font-lock level selector, and richer menus across the modes.</p><h2 id="ocaml-55-support-and-the-abi-14-balancing-act">OCaml 5.5 support, and the ABI 14 balancing act</h2><p><a href="https://discuss.ocaml.org/t/ocaml-5-5-0-released/18265">OCaml 5.5 was released</a> on June 19th, so this felt like a good moment to ship 5.5 support in neocaml. The <code class="language-plaintext highlighter-rouge">ocaml</code> and <code class="language-plaintext highlighter-rouge">ocaml-interface</code> grammars now track tree-sitter-ocaml v0.25.0, which brings the 5.5 grammar along with it.</p><p>There’s a wrinkle here that’s worth explaining, because it’s shaped the last few releases. A tree-sitter grammar gets compiled into a parser that speaks a particular <strong>ABI version</strong>, and Emacs can only load parsers up to the latest ABI supported by the <code class="language-plaintext highlighter-rouge">libtree-sitter</code> it was <em>built against</em> — it’s not the Emacs version itself that sets the ceiling. In practice a lot of Emacs 30 builds out there (notably Homebrew’s on macOS) are linked against tree-sitter 0.24, which tops out at <strong>ABI 14</strong>; you need an Emacs built against tree-sitter 0.25+ to load <strong>ABI 15</strong> grammars. The trouble is that the tree-sitter 0.25 CLI now generates ABI 15 parsers by default, so any grammar regenerated with current tooling produces something those builds simply can’t load — you install it and it just errors out. Emacs 31 will ship with newer tree-sitter and make ABI 15 the common case, but it’s not out yet. (This isn’t a neocaml problem as such; it’s been biting tree-sitter modes across the ecosystem.)</p><p>After a few users ran into exactly this, I’ve made a deliberate decision: <strong>stick to ABI 14 grammars until Emacs 31 is widely available.</strong> That effort started a couple of releases back — in 0.8.1 I lowered the ABI requirement from 15 to 14 across the opam, dune, and ocamllex modes, switched the menhir recipe to <a href="https://github.com/tmcgilchrist/tree-sitter-menhir">tmcgilchrist/tree-sitter-menhir</a>, and pinned ocamllex back to v0.24.0, all of which target ABI 14 (<a href="https://github.com/bbatsov/neocaml/issues/42">#42</a>). 0.9 extends that policy to the core OCaml grammars.</p><p>The catch with v0.25.0 is precisely that it generates an ABI 15 parser. Happily, the 5.5 grammar didn’t actually need any ABI 15 features — the bump rode along with the CLI upgrade — so an ABI 14 regeneration of the very same grammar is a drop-in. Big thanks to <a href="https://github.com/314eter">314eter</a>, the <code class="language-plaintext highlighter-rouge">tree-sitter-ocaml</code> maintainer, for cutting a <code class="language-plaintext highlighter-rouge">v0.25.0-abi14</code> tag for exactly this purpose (<a href="https://github.com/tree-sitter/tree-sitter-ocaml/issues/141">#141</a>). The one snag was that tagging normally triggers releases to NPM, crates.io, and PyPI, so I sent a small PR to skip publishing for ABI-suffixed tags (<a href="https://github.com/tree-sitter/tree-sitter-ocaml/pull/142">#142</a>), and the tag followed. neocaml now pins both grammars to it.</p><h2 id="roadmap-and-docs">Roadmap and docs</h2><p>If you’re curious where neocaml is headed, I’ve started keeping a <a href="https://github.com/bbatsov/neocaml/blob/main/ROADMAP.md">ROADMAP.md</a> with ideas and guiding principles (short version: tree-sitter first, lean on the LSP stack for <code class="language-plaintext highlighter-rouge">.ml</code>/<code class="language-plaintext highlighter-rouge">.mli</code>, and own the auxiliary modes that have no language server). The project also has a proper documentation site now at <a href="https://neocaml.org">neocaml.org</a>, so there’s a real home for the details beyond the README.</p><h2 id="give-it-a-try">Give it a Try</h2><p>As always — update from <a href="https://melpa.org/#/neocaml">MELPA</a>, play with it, and let me know how it goes. The full list of changes is in the <a href="https://github.com/bbatsov/neocaml/releases/tag/v0.9.0">0.9.0 release notes</a>. Bug reports, feature requests, and pull requests are all welcome on <a href="https://github.com/bbatsov/neocaml">GitHub</a>.</p><p>That’s all from me, folks! Keep hacking!</p>]]></content> <author> <name>bbatsov</name> </author> <summary>It’s been a couple of months since the last neocaml release, and the reason is simple — for a while there I was genuinely out of ideas. Back when I shipped 0.6 I declared (again!) that I was done with new features, and this time I almost meant it. But ideas have a way of creeping back in, and 0.9 turned out to be a meaty release. Here are the highlights. A much nicer REPL experience The bigge...</summary> </entry> <entry><title>Emacs loves AsciiDoc</title><link href="https://batsov.com/articles/2026/06/11/emacs-loves-asciidoc/" rel="alternate" type="text/html" title="Emacs loves AsciiDoc" /><published>2026-06-11T09:00:00+03:00</published> <updated>2026-06-11T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/06/11/emacs-loves-asciidoc/</id> <content type="html"><![CDATA[<p>Regular readers know I have a soft spot for <a href="https://asciidoc.org/">AsciiDoc</a> – I’ve written about it more than once, and it’s the markup behind the documentation of most of my bigger OSS projects (CIDER, nREPL, Projectile, RuboCop). It hits a sweet spot Markdown never quite reaches: rich enough for proper technical writing (admonitions, includes, real tables, cross-references), without dragging in the full ceremony of something like DocBook.</p><p>There was just one problem, and it nagged at me for years: editing AsciiDoc in Emacs was never much fun.</p><h2 id="background">Background</h2><p>For years the only real option was <code class="language-plaintext highlighter-rouge">adoc-mode</code>, and it was showing its age. The font-locking was uneven, it predated modern Asciidoctor syntax, and it lagged far behind what <code class="language-plaintext highlighter-rouge">markdown-mode</code> (and <code class="language-plaintext highlighter-rouge">org-mode</code>) offered. So I’d catch myself reaching for Markdown in situations where AsciiDoc was clearly the better tool – not because Markdown was better, but because <code class="language-plaintext highlighter-rouge">markdown-mode</code> was. That always bugged me.</p><p>Eventually the itch got bad enough that I did something about it. Back in 2023 I <a href="https://metaredux.com/posts/2023/03/12/adoc-mode-reborn.html">took over maintenance of adoc-mode</a>, which had been dormant for years, and set myself a clear goal: get it to feature parity with <code class="language-plaintext highlighter-rouge">markdown-mode</code>. More recently I also started a brand-new <a href="https://github.com/bbatsov/asciidoc-mode">asciidoc-mode</a> built on top of tree-sitter. The target for both is the same – AsciiDoc support in Emacs that’s on par with Markdown, and ideally not far off from Org.</p><h2 id="less-is-more-unless-its-modes">Less is more, unless it’s modes</h2><p>Why two modes? They scratch slightly different itches:</p><ul><li><code class="language-plaintext highlighter-rouge">adoc-mode</code> is the classic, regexp-based mode. It works on any reasonably modern Emacs, has no external dependencies, and now ships a pile of interactive features (markup commands, list editing, image preview, tempo templates).<li><code class="language-plaintext highlighter-rouge">asciidoc-mode</code> is the newer, tree-sitter-based mode. It leans on a real grammar for robust, accurate highlighting and good performance in large documents. It’s leaner by design, and it needs Emacs 30.1+ with tree-sitter.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></ul><p>If you want the kitchen sink on any Emacs, reach for <code class="language-plaintext highlighter-rouge">adoc-mode</code>. If you’re on a recent Emacs and want highlighting backed by an actual parser, try <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>. Pick whichever fits your setup – both are actively maintained.</p><h2 id="recent-developments">Recent developments</h2><p>I’ve spent a good chunk of the past month bringing both modes to a place I’m genuinely happy with, and they’re now mostly feature-complete as far as I’m concerned. Some highlights:</p><ul><li><strong>Modern AsciiDoc, not legacy AsciiDoc.py.</strong> Both modes now track the modern Asciidoctor spec – something I’d <a href="/articles/2024/02/22/asciidoc-language-specification/">wanted to do for a while</a>. <code class="language-plaintext highlighter-rouge">+text+</code> and <code class="language-plaintext highlighter-rouge">++text++</code> are treated as passthroughs rather than monospace, curved-quote syntax (<code class="language-plaintext highlighter-rouge">"`text`"</code>) is recognized, the block ID shorthand <code class="language-plaintext highlighter-rouge">[#id]</code> is supported, checklists are highlighted, and the menus and templates dropped a bunch of deprecated AsciiDoc.py leftovers.<li><strong>More uniform font-locking.</strong> I went through the faces in both modes and aligned them with <code class="language-plaintext highlighter-rouge">markdown-mode</code> and <code class="language-plaintext highlighter-rouge">org-mode</code>: bold renders as plain bold, emphasis as plain italic, structural markup (delimiters, list markers) fades into the background, and dedicated semantic faces cover metadata, URLs, footnotes, highlights, and roles. Switching between Markdown and AsciiDoc no longer feels jarring.<li><strong>Navigation that actually works.</strong> Clickable cross-references and links, an <code class="language-plaintext highlighter-rouge">xref</code> backend over anchors, context-aware completion, and – for <code class="language-plaintext highlighter-rouge">adoc-mode</code> – cross-file Antora cross-reference resolution.<li><strong>Tooling integration.</strong> Asciidoctor-backed preview/export and a Flymake checker that reports parser errors inline.<li><strong>Native code-block fontification.</strong> <code class="language-plaintext highlighter-rouge">[source,LANG]</code> blocks in <code class="language-plaintext highlighter-rouge">asciidoc-mode</code> are highlighted using the language’s own major mode, just like in <code class="language-plaintext highlighter-rouge">markdown-mode</code> and <code class="language-plaintext highlighter-rouge">org-mode</code>. <code class="language-plaintext highlighter-rouge">adoc-mode</code> had added support for this a while back.</ul><p>Getting <code class="language-plaintext highlighter-rouge">asciidoc-mode</code> where I wanted it also meant sending a few fixes upstream to the <a href="https://github.com/cathaysia/tree-sitter-asciidoc">tree-sitter-asciidoc</a> grammar it builds on. A grammar-backed mode is only as good as its grammar, so some of the work happened a layer below Emacs. If you’re curious about that side of things, I wrote up the broader experience in <a href="/articles/2026/02/27/building-emacs-major-modes-with-treesitter-lessons-learned/">building Emacs major modes with Tree-sitter</a>.</p><h2 id="give-it-a-try">Give it a try</h2><p>If you’ve been avoiding AsciiDoc in Emacs because the tooling wasn’t there – I get it, that was me too. But the situation is genuinely different now. Grab <a href="https://github.com/bbatsov/adoc-mode">adoc-mode</a> or <a href="https://github.com/bbatsov/asciidoc-mode">asciidoc-mode</a>, point it at one of your <code class="language-plaintext highlighter-rouge">.adoc</code> files, and see how it feels.</p><p>And please share your feedback. Both projects are at the stage where real-world usage surfaces the rough edges I can’t see myself. Bug reports, ideas, and PRs are all very welcome on the respective issue trackers.</p><p>Together we can make AsciiDoc a first-class citizen of the Emacs ecosystem!<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p><p>That’s all from me, folks! Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>Also, I really wanted to name something <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>Afterwards we’ll aim for world domination, of course. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>Regular readers know I have a soft spot for AsciiDoc – I’ve written about it more than once, and it’s the markup behind the documentation of most of my bigger OSS projects (CIDER, nREPL, Projectile, RuboCop). It hits a sweet spot Markdown never quite reaches: rich enough for proper technical writing (admonitions, includes, real tables, cross-references), without dragging in the full ceremony of...</summary> </entry> <entry><title>nREPL Forever</title><link href="https://batsov.com/articles/2026/05/20/nrepl-forever/" rel="alternate" type="text/html" title="nREPL Forever" /><published>2026-05-20T09:00:00+03:00</published> <updated>2026-05-20T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/05/20/nrepl-forever/</id> <content type="html"><![CDATA[<p>Last week I <a href="/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/">announced Port</a>, a small prepl client for Emacs. That post focused on Port itself, but writing it left me with the itch to do a follow-up on the bigger picture, because the socket REPL / prepl story is one I’ve been meaning to write up for years.</p><p>If you’ve been around Clojure long enough, you remember the chatter. Socket REPL landed in Clojure 1.8 (January 2016), prepl in Clojure 1.10 (December 2018), and for a couple of years there was a steady stream of posts, tweets, and Slack threads to the effect of “this is what we should be building tools on. nREPL is on the way out.” Some serious people put their weight behind that idea, and some of them went and built tools to prove it.</p><p>Now it’s 2026 and we can take stock.</p><h2 id="sky-is-the-limit">Sky is the limit</h2><p>The pitch was good. Socket REPL is just <em>the</em> Clojure REPL exposed on a TCP port. prepl wraps it with a structured printer so the bytes coming back are EDN-tagged maps (<code class="language-plaintext highlighter-rouge">:ret</code>, <code class="language-plaintext highlighter-rouge">:out</code>, <code class="language-plaintext highlighter-rouge">:err</code>, <code class="language-plaintext highlighter-rouge">:tap</code>) instead of a human-readable prompt. Both ship with Clojure itself. No external server library, no middleware, no third-party namespaces. You start a JVM, you bind a port, you’re done.</p><p>The intellectual case for moving off nREPL had been made by Rich Hickey himself, most clearly in a <a href="https://groups.google.com/g/clojure-dev/c/Dl3Stw5iRVA/m/IHoVWiJz5UIJ">March 2015 clojure-dev post</a> that’s worth reading in full. Rich didn’t actually attack nREPL by name in that message. What he did was argue carefully for what a REPL <em>is</em>: a thing that reads characters, evaluates forms, prints results, and loops, with those streams available to user code so that things like nested REPLs and debuggers compose naturally. The money line:</p><blockquote><p>While framing and RPC orientation might make things easier for someone who just wants to implement an eval window, it makes the resulting service strictly less powerful than a REPL.</p></blockquote><p>His proposal, in the same post, was that tools should open <em>multiple</em> connections to the running program: one for the human-facing stream, and dedicated channels for IDE operations. The socket REPL (which landed in 1.8 the following January) and prepl (which arrived in 1.10) were the official implementation of that worldview.</p><p>A handful of editor projects took the cue and built clients:</p><ul><li><a href="https://tutkain.flowthing.me/">Tutkain</a> for Sublime Text<li><a href="https://github.com/mauricioszabo/atom-chlorine">Chlorine</a> for Atom<li><a href="https://github.com/Olical/conjure">Conjure</a> for Neovim (in its early Rust incarnation)<li><a href="https://github.com/tonsky/Clojure-Sublimed">Clojure-Sublimed</a> by Nikita Tonsky<li>a steady drip of smaller experiments around <a href="https://github.com/Unrepl/unrepl"><code class="language-plaintext highlighter-rouge">unrepl</code></a>, <a href="https://github.com/Olical/propel"><code class="language-plaintext highlighter-rouge">propel</code></a>, and friends</ul><p>It was real momentum. If you were following Clojure tooling in 2018-2020, it genuinely felt like nREPL might be the past, and the future would be some combination of socket REPL plus a thin self-installing protocol on top of it. You can find a fair number of “RIP nREPL” hot takes from that period if you go looking.</p><h2 id="reality-check">Reality Check</h2><p>I went and surveyed each of those projects recently while working on Port. The pattern is depressingly consistent:</p><p><strong>Tutkain</strong> started on prepl. In November 2021, its v0.11 release explicitly stopped using prepl message framing and switched to a hand-rolled EDN-RPC protocol that Tutkain boots onto the raw socket REPL by sending it a base64-encoded blob. The new protocol has request ids, op dispatch (<code class="language-plaintext highlighter-rouge">:eval</code>, <code class="language-plaintext highlighter-rouge">:lookup</code>, <code class="language-plaintext highlighter-rouge">:completions</code>, <code class="language-plaintext highlighter-rouge">:test</code>, <code class="language-plaintext highlighter-rouge">:apropos</code>, <code class="language-plaintext highlighter-rouge">:interrupt</code>, …), and server-managed thread bindings. In other words: Tutkain grew into nREPL, just spelled differently.</p><p><strong>Chlorine</strong> never used prepl directly. It used socket REPL plus an <code class="language-plaintext highlighter-rouge">unrepl</code>-style upgrade blob. Its author’s successor project, <a href="https://github.com/mauricioszabo/lazuli">Lazuli</a>, abandoned the whole approach in favor of nREPL. The <a href="https://mauricio.szabo.link/blog/2024/11/09/goodbye-chlorine-and-welcome-lazuli/">post-mortem</a> is worth reading and is fairly blunt: tools that attempted prepl went back to nREPL because, honestly, it’s simply better.</p><p><strong>Conjure</strong> had a prepl client in its early Rust days. The current Lua/Fennel rewrite ships only an nREPL client. The author’s reasoning in the release notes was that nREPL “has complete ecosystem adoption and brilliant ClojureScript support.”</p><p><strong>Clojure-Sublimed</strong> technically still talks to a raw socket REPL, but only after sending it an EDN-printing prelude that upgrades the REPL to a structured protocol of tonsky’s own design. His <a href="https://tonsky.me/blog/clojure-sublimed-3/">post on the topic</a> is one of the most thoughtful pieces I’ve read on Clojure REPL design, and his conclusion is roughly: the bare socket REPL is <em>more</em> useful than prepl because you can install your own protocol on top of it. Which is true. But notice that everyone who reached that conclusion ended up reinventing the same wheel: ids, ops, request/response correlation, completion support, lookup, interrupts. You know, the things nREPL has had since 2010.</p><p>So the trajectory looks roughly like this:</p><ol><li>Editor decides nREPL is too heavy or an undesirable external dependency and starts on prepl.<li>Editor discovers prepl has no ids, no ops, no interrupts, no server-side completion, no namespace tracking, no test runner integration, etc.<li>Editor rolls a custom protocol on top of socket REPL, or…<li>Editor gives up and goes to nREPL.</ol><p>Pure prepl clients are nearly extinct in the wild. The one I found that qualifies is <a href="https://github.com/Olical/propel">propel</a> by Oliver Caldwell (of Conjure fame), which is delightful, about 70 lines of Clojure, and explicitly synchronous (one outstanding eval at a time). That works! But it’s not a foundation for the kind of feature set people expect from an editor.</p><h2 id="real-repl-vs-tool-friendly-repl">“Real” REPL vs. tool-friendly REPL</h2><p>Here’s where I land. Rich isn’t wrong that prepl is closer to a “real” REPL in the strict sense. prepl genuinely is a more faithful encoding of read-eval-print: each form goes in, each result comes out, and the semantics match what you’d get at the standard REPL prompt.</p><p>The thing is, “real REPL” is not the property you optimize for when you’re building editor tooling. The properties editor tooling actually needs are:</p><ul><li>A way to <strong>correlate</strong> a request with its response when output and results are interleaved.<li>A way to <strong>multiplex</strong> – one connection, several logical conversations.<li><strong>Server-side hooks</strong> for the operations every IDE expects: completion, lookup, go-to-definition, find-references, test running, stacktrace structuring, interrupt.<li>A <strong>protocol stable enough</strong> that ten different editors can target it without each one inventing its own dialect.</ul><p>nREPL was <em>explicitly designed</em> for those properties. The <a href="https://nrepl.org/nrepl/design/overview.html">ops, middleware, and transport</a> abstractions exist precisely because the people building it knew the consumers are not humans typing at a prompt, they’re programs negotiating a session.</p><p>Calling nREPL “not a real REPL” is technically defensible and practically beside the point. Nobody on the consuming end is confused about what nREPL is <em>for</em>.</p><h2 id="taking-stock-eight-years-later">Taking stock, eight years later</h2><p>I <a href="https://metaredux.com/posts/2018/10/29/nrepl-redux.html">wrote about nREPL’s revival in 2018</a>. At that point I had just finished migrating the project out of Clojure Contrib, and the goal was to give it a real home and a working development process. It was a lot of work, but in hindsight things played out pretty well.</p><p>Looking at where things ended up:</p><ul><li><strong>nREPL itself</strong> is healthier than it has ever been. Active maintainers, a proper <a href="https://nrepl.org">manual</a>, a steady release cadence, an actual ecosystem organization on GitHub.<li><strong>Most popular Clojure editors</strong> support it. <a href="https://github.com/clojure-emacs/cider">CIDER</a>, <a href="https://calva.io">Calva</a>, <a href="https://cursive-ide.com">Cursive</a> (via its own client), Conjure, <a href="https://github.com/liquidz/vim-iced">vim-iced</a>, you name it.<li><strong><a href="https://babashka.org">babashka</a></strong> ships with nREPL built in. You boot a <code class="language-plaintext highlighter-rouge">bb</code> and you get an nREPL server, no extra dependencies. That’s how a lot of people use nREPL in scripting contexts today, and it’s been a hit.<li><strong><a href="https://github.com/basilisp-lang/basilisp">basilisp</a></strong> (the Clojure dialect on Python) has <a href="https://github.com/basilisp-lang/basilisp-nrepl-async">nREPL support</a>. nREPL running on Python, talking to Emacs, evaluating Clojure. Nice.<li><strong><a href="https://github.com/clojure/clojure-clr">ClojureCLR</a></strong> has a working nREPL story now, and <strong><a href="https://jank-lang.org">jank</a></strong> (the C++ Clojure) has nREPL on its roadmap too.<li>The middleware ecosystem (<a href="https://github.com/clojure-emacs/cider-nrepl"><code class="language-plaintext highlighter-rouge">cider-nrepl</code></a>, <a href="https://github.com/clojure-emacs/refactor-nrepl"><code class="language-plaintext highlighter-rouge">refactor-nrepl</code></a>, <a href="https://github.com/nrepl/piggieback"><code class="language-plaintext highlighter-rouge">piggieback</code></a>, <a href="https://github.com/clojure-emacs/sayid"><code class="language-plaintext highlighter-rouge">sayid</code></a>, <a href="https://github.com/nrepl/drawbridge"><code class="language-plaintext highlighter-rouge">drawbridge</code></a>, …) is alive, well, and continues to add features.</ul><p>Meanwhile prepl is, as best as I can tell, mostly a curiosity. It got me a side project I had fun with. It did not displace nREPL.</p><h2 id="epilogue">Epilogue</h2><p>The history of tooling protocols is full of cases where “purer”, “simpler”, or “more elegant” lost to “shipped, documented, and battle-tested.” LSP beat fifteen ad-hoc language protocols. DAP beat the same fifteen debuggers. nREPL beat prepl in the (Clojure) editor space.</p><p>It’s not that the simpler thing is bad. prepl is a fine, elegant little protocol, and there’s a real case for embedding it in CI scripts, ops automation, deployment pipelines, or anywhere you want to drive a Clojure VM programmatically without pulling in a server library. Use it there.</p><p>But for editor tooling? The Clojure community made an enormous, multi-year, multi-tool investment in nREPL. We have the protocol, the middleware, the manual, the books, the conference talks. nREPL works, it’s actively maintained, it’s increasingly portable across Clojure dialects, and the design decisions that Rich called out as un-REPL-like are the exact ones that make it a good substrate for editors.</p><p>So I’ll say what I felt awkward saying back in 2018: nREPL forever. It’s the right abstraction for the job, and it’s not going anywhere.</p><p>One more thing. After finishing Port I got curious what a minimal nREPL client would look like by comparison, so I went and built one. As you can imagine, it turned out to be significantly simpler. If that sounds interesting, take a look at <a href="/articles/2026/05/20/neat-a-language-agnostic-nrepl-client-for-emacs/">neat</a>, a small, language-agnostic nREPL client for Emacs.</p><p>Keep hacking!</p>]]></content> <author> <name>bbatsov</name> </author> <summary>Last week I announced Port, a small prepl client for Emacs. That post focused on Port itself, but writing it left me with the itch to do a follow-up on the bigger picture, because the socket REPL / prepl story is one I’ve been meaning to write up for years. If you’ve been around Clojure long enough, you remember the chatter. Socket REPL landed in Clojure 1.8 (January 2016), prepl in Clojure 1....</summary> </entry> <entry><title>neat: a language-agnostic nREPL client for Emacs</title><link href="https://batsov.com/articles/2026/05/20/neat-a-language-agnostic-nrepl-client-for-emacs/" rel="alternate" type="text/html" title="neat: a language-agnostic nREPL client for Emacs" /><published>2026-05-20T09:00:00+03:00</published> <updated>2026-05-20T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/05/20/neat-a-language-agnostic-nrepl-client-for-emacs/</id> <content type="html"><![CDATA[<blockquote><p>I think I’ll take my REPL neat <br /> My parens black and my bed at three <br /> CIDER’s too sweet for me…</p><p>– Bozier</p></blockquote><p>Last week I <a href="/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/">announced Port</a>, a small prepl client for Emacs. Today I’m following it up with another small Emacs package. Meet <a href="https://github.com/nrepl/neat">neat</a>, a tiny, deliberately language-agnostic nREPL client.</p><h2 id="why">Why?</h2><p>For years I’ve been hearing some version of the same request: “could CIDER work with my non-Clojure nREPL server?”. Babashka, Basilisp, nREPL-CLR, even some homegrown servers people built on top of nREPL for languages I’d never heard of.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> The answer was always the same kind of squishy “sort of, in theory, with caveats”, because while bare nREPL is genuinely language-agnostic, CIDER is not. CIDER was built for Clojure and assumes Clojure pretty much everywhere.</p><p>I always thought the right answer was “let’s gradually make CIDER more language-agnostic.” That’s the kind of plan that sounds reasonable until you actually try it.</p><p>The thing that pushed me over the edge was, oddly enough, building Port. Port is small, focused, and doesn’t try to be CIDER. Working on it for a couple of weeks reminded me how (deceptively) productive it is to start from a clean slate when the new requirements don’t match the assumptions baked into a mature codebase. Trying to retrofit CIDER into a language-agnostic shape would have meant fighting with every helper that ever assumed <code class="language-plaintext highlighter-rouge">clojure.repl</code> exists, every middleware contract <code class="language-plaintext highlighter-rouge">cider-nrepl</code> defines, every project-type heuristic that knows about <code class="language-plaintext highlighter-rouge">deps.edn</code> and <code class="language-plaintext highlighter-rouge">project.clj</code> and nothing else. A whole lot of “is the server Clojure, or is it the other thing?” branches. The Port experience reaffirmed that the right move for a genuinely different client is a <em>new project</em>, not a thousand cuts to an existing one.</p><p>So <code class="language-plaintext highlighter-rouge">neat</code> was born. The name is short, says what it does (it’s neat, both in the small-and-tidy sense and in the “no deps, no special assumptions, just the protocol” sense), and conveniently leaves room for puns I haven’t fully committed to yet. I might land on a backronym one day. For now it’s just “neat”.</p><h2 id="what-neat-actually-is">What neat actually is</h2><p>neat is a small Emacs nREPL client. The code is split across four files:</p><ul><li><code class="language-plaintext highlighter-rouge">neat-bencode.el</code>: bencode encode/decode.<li><code class="language-plaintext highlighter-rouge">neat-client.el</code>: TCP connections, request dispatch, the standard nREPL ops.<li><code class="language-plaintext highlighter-rouge">neat-repl.el</code>: a comint-derived REPL buffer.<li><code class="language-plaintext highlighter-rouge">neat.el</code>: the entry point, customization group, and <code class="language-plaintext highlighter-rouge">neat-mode</code> minor mode for source buffers.</ul><p>It only uses Emacs builtins. There are no external runtime dependencies, not even on <code class="language-plaintext highlighter-rouge">clojure-mode</code>, because neat doesn’t assume Clojure on the other end. If you write <code class="language-plaintext highlighter-rouge">clojure-mode</code>, <code class="language-plaintext highlighter-rouge">fennel-mode</code>, <code class="language-plaintext highlighter-rouge">hy-mode</code>, or anything else that talks nREPL, you turn on <code class="language-plaintext highlighter-rouge">neat-mode</code> in that buffer and it just works.</p><p>The connection routing is also intentionally library-friendly. There’s a buffer-local <code class="language-plaintext highlighter-rouge">neat-current-connection</code> override so downstream packages can implement their own routing logic, plus a global default for the simple “one server at a time” case that most people will want.</p><p>Capability discovery is done at connect time via the nREPL <code class="language-plaintext highlighter-rouge">describe</code> op. neat doesn’t hardcode “this server has completions, this one doesn’t” assumptions. If the server reports a <code class="language-plaintext highlighter-rouge">completions</code> op, the CAPF backend lights up (with type annotations next to each candidate, when the server provides them). If it reports <code class="language-plaintext highlighter-rouge">lookup</code>, eldoc starts working and <code class="language-plaintext highlighter-rouge">M-.</code> jumps to definitions via an xref backend. If neither is there, you still get a perfectly serviceable raw REPL.</p><h2 id="basic-usage">Basic usage</h2><p>Start an nREPL server. Anything that speaks the protocol will do. For a Clojure server:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre><td class="rouge-code"><pre>clj -M:nrepl
# or
bb nrepl-server :port 7888
# or
lein repl :headless :port 7888
</pre></div></div><p>Then in Emacs:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>M-x neat RET localhost RET 7888 RET
</pre></div></div><p>A REPL buffer pops up, the prompt follows the server’s reported namespace, and you can type expressions at it. Multi-line input works because <code class="language-plaintext highlighter-rouge">RET</code> only submits when the form parses as balanced under <code class="language-plaintext highlighter-rouge">neat-repl-input-syntax-table</code> (Emacs Lisp syntax by default, which is close enough for any Lisp). Input history is persisted across sessions.</p><p>If there’s a <code class="language-plaintext highlighter-rouge">.nrepl-port</code> file in the project, the prompt defaults to its contents, so <code class="language-plaintext highlighter-rouge">M-x neat RET RET</code> is enough to connect.</p><p>To evaluate from a source buffer, turn on the minor mode:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>M-x neat-mode
</pre></div></div><p>The familiar bindings are there, intentionally compatible with what CIDER users expect:</p><table><thead><tr><th>Key<th>Command<tbody><tr><td><code class="language-plaintext highlighter-rouge">C-c C-e</code><td><code class="language-plaintext highlighter-rouge">neat-eval-last-sexp</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-c</code><td><code class="language-plaintext highlighter-rouge">neat-eval-defun</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-r</code><td><code class="language-plaintext highlighter-rouge">neat-eval-region</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-b</code><td><code class="language-plaintext highlighter-rouge">neat-eval-buffer</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-l</code><td><code class="language-plaintext highlighter-rouge">neat-load-buffer-file</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-z</code><td><code class="language-plaintext highlighter-rouge">neat-switch-to-repl</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-k</code><td><code class="language-plaintext highlighter-rouge">neat-interrupt-eval</code><tr><td><code class="language-plaintext highlighter-rouge">C-c M-n</code><td><code class="language-plaintext highlighter-rouge">neat-set-ns</code><tr><td><code class="language-plaintext highlighter-rouge">C-c C-d C-d</code><td><code class="language-plaintext highlighter-rouge">neat-show-doc-at-point</code><tr><td><code class="language-plaintext highlighter-rouge">M-.</code><td><code class="language-plaintext highlighter-rouge">xref-find-definitions</code><tr><td><code class="language-plaintext highlighter-rouge">M-,</code><td><code class="language-plaintext highlighter-rouge">xref-go-back</code></table><p><code class="language-plaintext highlighter-rouge">neat-eval-buffer</code> ships the buffer contents as an <code class="language-plaintext highlighter-rouge">eval</code> op; <code class="language-plaintext highlighter-rouge">neat-load-buffer-file</code> uses the standard <code class="language-plaintext highlighter-rouge">load-file</code> op instead, so the server can attribute file and line numbers to errors. Use the latter when you’re actually loading a file from disk and care about good diagnostics.</p><p><code class="language-plaintext highlighter-rouge">neat-set-ns</code> sets the buffer-local <code class="language-plaintext highlighter-rouge">neat-ns</code>, which gets sent as the <code class="language-plaintext highlighter-rouge">ns</code> field on every <code class="language-plaintext highlighter-rouge">eval</code> op from that buffer. For languages where the namespace is declared in the source (Clojure’s <code class="language-plaintext highlighter-rouge">(ns foo.bar)</code>, etc.), swap in a parser via <code class="language-plaintext highlighter-rouge">neat-buffer-ns-function</code>.</p><p>For juggling multiple connections, <code class="language-plaintext highlighter-rouge">M-x neat-list-connections</code> opens a tabulated-list buffer with one row per live connection, where you can set the default or disconnect interactively.</p><p>That’s roughly the whole user-facing surface today. There’s no jack-in command, no inspector, no debugger, no test runner. Likely there will never be, but if you need those you should probably be using CIDER anyways…</p><h2 id="should-you-use-it">Should you use it?</h2><p>If you write Clojure and CIDER works for you, keep using CIDER. It’s mature, full-featured, and supported, and I’m going to keep working on it for as long as people use it. Nothing about neat changes that.</p><p>But if you find yourself in one of these situations:</p><ul><li>you write a non-Clojure language whose runtime ships an nREPL server, and you’ve been muddling through with a half-supported CIDER setup,<li>you write Clojure but you value minimalism and don’t need the full CIDER feature set,<li>you’re building an Emacs package that needs to talk nREPL and you want a small, dependency-free library to build on,<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></ul><p>then neat might be a better fit. It’s small enough that you can read the whole thing in an afternoon, and the library/UI split (<code class="language-plaintext highlighter-rouge">neat-bencode</code> and <code class="language-plaintext highlighter-rouge">neat-client</code> are perfectly usable from other packages) is genuinely designed for downstream consumers.</p><h2 id="the-bigger-picture">The bigger picture</h2><p>neat is part of a broader push I’ve been chewing on for a while now: making nREPL a healthy multi-language ecosystem rather than a Clojure-only protocol. That push has three legs:</p><ol><li><strong>An actual nREPL specification.</strong> The <a href="https://github.com/nrepl/spec.nrepl.org">spec.nrepl.org</a> draft is (will be) the formal version of what today is “whatever nREPL the project does”.<li><strong>Reference clients.</strong> neat is one. The point of building a deliberately Clojure-free client is that it stress-tests the spec. Anywhere neat ends up needing to special-case the server, the spec has a gap.<li><strong>A compatibility test suite.</strong> The parameterised integration suite in neat already runs the same assertions against multiple servers and surfaces real divergences (Clojure batching <code class="language-plaintext highlighter-rouge">(println "hi")</code> into a single <code class="language-plaintext highlighter-rouge">out</code> message where Basilisp emits two, for example). I’d like to grow this into a portable suite that any nREPL server can self-check against.</ol><p>This is also why I keep teasing a “reference CLI client” in conversations. An editor client is one thing, but a small command-line nREPL client written in a non-Lisp language would be a much sharper test of how language-agnostic the protocol really is. neat is plausibly a precursor to that. Time will tell how far I push this; for now I just wanted to get the Emacs side moving.</p><h2 id="thanks">Thanks</h2><p>As always, big thanks to <a href="https://www.clojuriststogether.org/">Clojurists Together</a> and everyone supporting my open source work. You make it possible for me to keep tweaking and improving CIDER, nREPL, clj-refactor, and friends, and occasionally try something “neat” on the side. <code class="language-plaintext highlighter-rouge">neat</code> isn’t replacing any of the existing Clojure tooling for Emacs. It’s just another tool in the box for the people who want it.</p><p>Feedback, ideas, and contributions are most welcome over at the <a href="https://github.com/nrepl/neat/issues">issue tracker</a>.</p><p>Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p><a href="https://github.com/clojure-emacs/cider/issues/3905">https://github.com/clojure-emacs/cider/issues/3905</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>For a long time I planned to extract CIDER’s nREPL client code into a reusable package, but now that we have <code class="language-plaintext highlighter-rouge">neat</code> I probably will finally abandon this idea. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>I think I’ll take my REPL neat My parens black and my bed at three CIDER’s too sweet for me… – Bozier Last week I announced Port, a small prepl client for Emacs. Today I’m following it up with another small Emacs package. Meet neat, a tiny, deliberately language-agnostic nREPL client. Why? For years I’ve been hearing some version of the same request: “could CIDER work with my non-Cloju...</summary> </entry> <entry><title>Port: a minimalist prepl client for Emacs</title><link href="https://batsov.com/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/" rel="alternate" type="text/html" title="Port: a minimalist prepl client for Emacs" /><published>2026-05-12T09:00:00+03:00</published> <updated>2026-05-12T09:00:00+03:00</updated> <id>https://batsov.com/articles/2026/05/12/port-a-minimalist-prepl-client-for-emacs/</id> <content type="html"><![CDATA[<p>For ages I’ve had “add prepl support to CIDER” sitting somewhere in the back of my head. CIDER is built firmly around nREPL, but <a href="https://clojure.org/reference/repl_and_main#prepl">prepl</a> ships with Clojure itself, and the appeal of dropping the external REPL server requirement is obvious. Recently, as part of a broader internals cleanup “mini-project” in CIDER, I finally sat down and put a prototype together: <a href="https://github.com/clojure-emacs/cider/pull/3899">cider#3899</a>.</p><p>The good news is that the prototype sort of worked. The bad news is that the more I poked at it, the more I kept running into the same pattern. CIDER assumes ops, sessions, request ids, and a whole structured protocol that prepl simply doesn’t have. The amount of CIDER code that would need to grow “is this nREPL or prepl?” branches added up quickly, and I’d be papering over prepl’s limitations in dozens of subtle places. The exercise was fun, but it ended up reaffirming my long-standing belief that nREPL is a much better fit for editor tooling than prepl is.</p><p>The exercise did leave me thinking though. What if, instead of bolting prepl onto CIDER, I built a small standalone client in the spirit of <a href="https://github.com/clojure-emacs/inf-clojure">inf-clojure</a> and <a href="https://github.com/sanel/monroe">monroe</a>? Something tiny and focused that doesn’t have to pretend to be CIDER, and where prepl’s quirks would be the design rather than something to work around.</p><p>Conveniently, I was on vacation in Portugal at the time, where I spent a few days in Porto, and the name pretty much picked itself. <a href="https://github.com/clojure-emacs/port">Port</a> was born. It kept us firmly in the land of fun, drink-inspired Clojure-on-editor names: CIDER, <a href="https://calva.io">Calva</a> (after Calvados, the apple brandy), and now Port (the famous fortified wine). The protocol Port talks to is <code class="language-plaintext highlighter-rouge">prepl</code>, over a TCP <strong>port</strong>, so the pun was hard to pass up.</p><p>This time around I didn’t manage to land on a backronym I love (at least not yet). The contenders so far:</p><ul><li>prepl omnipotent repl toolkit (my favorite so far)<li>prepl-operated repl toolkit<li>peak optimized repl toolkit</ul><p>Naming is hard. I remain open to better suggestions. :D</p><h2 id="scope-or-what-port-isnt">Scope, or what Port isn’t</h2><p>Port is a side project. I don’t plan to invest serious time in it past the point I consider it feature-complete, which won’t be far beyond what’s already there. The deliberate goal is to keep it simple and focused, and its feature set will stay close to inf-clojure and monroe. <strong>Port is not competing with CIDER.</strong> If you want the full feature set (debugger, inspector, test runner, profiler, structured stacktraces, refactor support), CIDER + <code class="language-plaintext highlighter-rouge">cider-nrepl</code> is, and will remain, the way.</p><p>What Port gives you today is a small, dependable Clojure REPL that you can hook into Emacs without any external dependencies, just a stock Clojure JVM with a prepl listening on a port.</p><h2 id="a-few-architectural-notes">A few architectural notes</h2><p>If you’re up for the long version, <a href="https://github.com/clojure-emacs/port/blob/main/doc/design.md">doc/design.md</a> goes deep. Here’s the short version of what prepl gives you compared to nREPL:</p><ul><li><strong>No bencode</strong>. prepl emits EDN-tagged maps, one per line. This might be a feature or a problem, depending on your perspective.<li><strong>No middleware</strong>. Whatever the server prints is what you get. No interception, no extension surface.<li><strong>No sessions</strong>. There’s one thread per TCP connection.<li><strong>No ops</strong>. You send a Clojure form, the server evaluates it, and prints back tagged messages: <code class="language-plaintext highlighter-rouge">:ret</code>, <code class="language-plaintext highlighter-rouge">:out</code>, <code class="language-plaintext highlighter-rouge">:err</code>, <code class="language-plaintext highlighter-rouge">:tap</code>, plus an <code class="language-plaintext highlighter-rouge">:exception true</code> flag on errors.<li><strong>No request id</strong>. This is the main issue. Tags identify the <em>kind</em> of message, not which request produced it.</ul><p>That last point is the central design constraint. If you want to issue a request and reliably read back its result without accidentally consuming output from an unrelated background <code class="language-plaintext highlighter-rouge">future</code> or <code class="language-plaintext highlighter-rouge">tap&amp;gt;</code>, you need to layer correlation on top of the protocol. Port does this with two tricks:</p><ol><li><strong>Two sockets per session.</strong> A user socket drives the REPL buffer with raw streaming output, and a separate tool socket carries helper-command requests. Background prints from <code class="language-plaintext highlighter-rouge">future</code>/<code class="language-plaintext highlighter-rouge">agent</code> on the user thread don’t bleed into the tool channel.<li><strong>A bootstrap form.</strong> On connect, the tool socket evaluates a one-shot form that defines a <code class="language-plaintext highlighter-rouge">port.tooling/-eval</code> wrapper. Every subsequent helper call goes through it, which captures <code class="language-plaintext highlighter-rouge">*out*</code> / <code class="language-plaintext highlighter-rouge">*err*</code> and returns a tagged map containing the request id. The client matches the id against a pending-callback registry.</ol><p>This is what nREPL provides via sessions and ops, just reinvented at the TCP layer. It’s a fair amount of work for something nREPL gives you for free, which only strengthens my view that nREPL is the better protocol for editor tooling. Still, it was an interesting and educational exercise.</p><h2 id="zero-dependencies">Zero dependencies</h2><p>One thing I’m fairly proud of: Port has no hard dependencies. You’ll want either <code class="language-plaintext highlighter-rouge">clojure-mode</code> or <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> installed for the source-buffer side of things, but Port itself only soft-depends on them via runtime <code class="language-plaintext highlighter-rouge">fboundp</code> checks. Hook it onto whichever one(s) you actually use. I intend to keep it that way. Dependency creep is a real problem in the Emacs (every?) ecosystem, and a small package should stay small.</p><h2 id="whats-there-in-01">What’s there in 0.1</h2><p>I tagged <a href="https://github.com/clojure-emacs/port/releases/tag/v0.1.0">v0.1.0</a> yesterday. It’s small but already perfectly usable:</p><ul><li><code class="language-plaintext highlighter-rouge">M-x port</code> “jacks in” (bootstraps) (auto-detects <code class="language-plaintext highlighter-rouge">deps.edn</code> / <code class="language-plaintext highlighter-rouge">project.clj</code> / <code class="language-plaintext highlighter-rouge">bb.edn</code>, starts a server and connects to it)<li>single-buffer REPL with persistent input history, completion, and eldoc at the prompt<li>interactive evaluation from source buffers with pretty-printed results<li>structured stacktrace buffer with cause chain and navigable frames<li><code class="language-plaintext highlighter-rouge">M-.</code> find-definition that follows into jar sources<li>doc/source/apropos/macroexpand helpers</ul><p>MELPA submission is queued up next. After that, expect Port to be in <a href="/articles/2025/11/16/burst-driven-development-my-approach-to-oss-projects-maintenance/">burst-driven</a> maintenance mode like most of my smaller projects.</p><p>Feedback, ideas, and contributions are most welcome. The <a href="https://github.com/clojure-emacs/port/issues">issue tracker</a> is the right place.</p><h2 id="closing-thoughts">Closing thoughts</h2><p>Funny thing, I’d never actually written any code against prepl until I started this project. It was fun to spend some quality time with the “competition” of my beloved nREPL. Working with a different protocol always teaches you something about the one you’re used to, and I came away from this with a renewed appreciation for both: prepl is genuinely elegant for what it is, and nREPL is genuinely well-designed for what we use it for.</p><p>Big thanks to <a href="https://www.clojuriststogether.org/">Clojurists Together</a> and everyone else who supports my OSS Clojure work. You rock! Now if you’ll excuse me, I have new releases of CIDER, clj-refactor, and refactor-nrepl to get back to.</p><p>Keep hacking!</p>]]></content> <author> <name>bbatsov</name> </author> <summary>For ages I’ve had “add prepl support to CIDER” sitting somewhere in the back of my head. CIDER is built firmly around nREPL, but prepl ships with Clojure itself, and the appeal of dropping the external REPL server requirement is obvious. Recently, as part of a broader internals cleanup “mini-project” in CIDER, I finally sat down and put a prototype together: cider#3899. The good news is that ...</summary> </entry> </feed>
