neorepl 0.5 is out! When I announced neorepl last month, I wrote that I’d always wanted a terminal REPL that felt like CIDER, without the Emacs part. A few releases later I think neorepl is mostly there, so this one went out with “CIDER in a terminal” for a tagline. It also has everything from the “What’s next” list in the 0.4 post, which almost never happens with my plans.

A line editor that knows Clojure

Editing in neorepl

That’s where you spend most of your time in a REPL, so that’s where most of the work went:

  • The arglists of the function you’re calling show up under the input, the way eldoc shows them in CIDER, with the parameter the argument at the cursor goes into in bold, and the arities that can’t take that many arguments faint. Macros get matched by their shape, so the docstring of a defn lines up with doc-string?. The lookups happen in the background now, so typing never waits on the server.
  • Enter in an unfinished form starts a line indented the way cljfmt would indent it.
  • The delimiter that pairs with the one at the cursor gets highlighted, and a closer that pairs with nothing, or with the wrong kind, shows up in red. JLine’s own idea of paren matching was to move the cursor to the opener for half a second, which always made me think I’d pressed the wrong key.
  • C-x Enter evaluates only the form at the cursor and gives you your input back, which is great for checking a piece of a bigger expression without retyping it. And after an evaluation the prompt shows at its right whether it worked, and how long it took. I borrowed both ideas from clojure-cli.repl, and I’m not sorry at all.
  • C-x C-e opens what you’ve typed in your $EDITOR, as it does in bash, for when a form outgrows the line editor.
  • set editing-mode vi in ~/.inputrc gets you vi’s keys. neorepl takes the readline settings JLine knows from there, and what’s under $if neorepl is for neorepl alone.
  • Commands get highlighted as you type them, and a command that doesn’t exist shows up in red before you press Enter.

Not everyone likes that much going on around the cursor, so ,set lists the REPL’s settings and turns them on and off, C-x C-h, C-x C-p and C-x C-q toggle eldoc, pretty printing and quiet mode, and ~/.config/neorepl/config.edn says what they start as:

{:eldoc false
 :print-length 100
 :autoreload true}

Finding your way around

Browsing namespaces and specs

,ns without a name opens a full-screen browser of the project’s namespaces, or every loaded one after you press a. Type / and part of a name to narrow them down, → to see a namespace’s public vars with their docs, Enter to switch to it and e to open it in your editor. ,spec does the same for the spec registry: a spec’s form, the specs it names one key away, and x for an example generated from it, as long as test.check is on the server’s classpath. (With cider-nrepl, x only works since 0.5.1, which came out today as well. I found that bug recording the GIF above.)

,refs lists the functions that use a var, and ,deps the ones a function uses, courtesy of cider-nrepl’s fn-refs and fn-deps. They come in handy before you change a function’s arguments, and even more so for coding agents, which get them as neorepl refs and the MCP refs tool. They only look through the project’s code that’s loaded, mind you.

Reloading and friends

Reloading what changed

,autoreload has the REPL reload the namespaces whose files changed before each evaluation, and before ,test, ,inspect and ,ns, so you’re never calling yesterday’s version of a function. It’s clj-reload under cider-nrepl, which reloads the namespaces that depend on the changed ones too, in the right order, and keeps what defonce defined. When a reload fails you get its error, rather than an evaluation against half-reloaded code.

Coding agents need this even more than people do, as they edit files all day long and forget to load them about as often. neorepl eval --reload and the reload option of the MCP eval tool take care of that.

Taps and profiling

A few more things CIDER users will recognize:

  • Values sent to tap> show up above the prompt, and ,tap opens the last one in the inspector, with cider-nrepl 0.64 or newer.
  • What the server’s own threads print, like a web server’s handlers, shows up above the prompt as well, instead of getting lost in the server’s terminal, with nREPL 1.5 or newer and babashka.
  • ,profile times every call of the functions you name, or of a whole namespace, and without a name shows what it measured: how many calls each function had, and how long they took, on average, in all, at the shortest and the longest. neorepl profile does the same from the shell.

Beside your editor

The CLI was meant for scripts from day one, and it turns out an editor doesn’t need a plugin to use it. neorepl eval -f FILE --line N:COL - evaluates the top-level form at the cursor in the file’s namespace, with the buffer’s text on standard input, so unsaved changes count too, and neorepl eval --ns NS - does the same for a selection. Helix can’t send a buffer to a command, so there the keys save the file first, and that’s all it takes:

[keys.normal."+"]
e = [":write", ":sh neorepl eval -s editor -f %{buffer_name} --line %{cursor_line}:%{cursor_column}"]
l = [":write", ":sh neorepl load %{buffer_name}"]

doc/editors.md has complete setups for Neovim, Emacs and Helix. (Yes, Emacs. Please don’t tell anyone.) The REPL sees whatever the editor evaluates, as they share a server, so with the REPL in a split next to your editor you get a pretty decent setup out of a few lines of config. The setups evaluate in a session of their own, called editor, so ,sessions editor takes the REPL there, and ,pst shows the last error your editor ran into.

More servers, tested for real

neorepl’s tests run against fakes that behave like cider-nrepl, which says nothing about the next cider-nrepl, or the next babashka. So now CI also runs neorepl against the real thing: nREPL with cider-nrepl, started by the Clojure CLI and by Leiningen, babashka’s server, ClojureScript on Node through piggieback and through shadow-cljs, nbb and Basilisp. That turned up a fair share of bugs on both sides of the wire. Running a ClojureScript namespace’s tests through piggieback failed with a circular dependency, a ClojureScript test’s error came with the line NaN, code that threw on nbb didn’t fail neorepl eval, and source, macroexpand and apropos didn’t work on Basilisp. All fixed now.

neorepl needs babashka 1.13.227 or newer these days, as that’s the first release with all the JLine and socket classes it used to work around.

Making the neighbours better, round three

  • I filed a bunch of issues for nbb’s nREPL server, about errors, sessions sharing state and in-ns, and Michiel fixed them within a day or two. They’ll be in the next nbb release. Thanks again, Michiel! On the babashka side, the test output going to the wrong message and info knowing nothing about namespaces are fixed on master.
  • Building on cider-nrepl’s profiler and taps turned up a few rough edges in its ops for them, all filed by now.
  • The nREPL fixes I wrote about last time are merged and waiting for the next nREPL release, and I found a few more ways for an interrupted evaluation to leave a session in a bad state. Interrupts are hard.
  • The piggieback changes are still in review. No news there, I’m afraid.

What’s next

0.6 is about getting neorepl to more people:

  • neorepl on Clojars, which also lets you install it as a Clojure tool and run it inside your project’s JVM, plus an uberjar and Homebrew
  • proper Windows support, at long last
  • a documentation site, as the README is getting rather long
  • the frames of ClojureScript errors, from the JavaScript stack, which have been “next” since 0.2

After that comes 1.0, where the CLI’s exit codes, its edn and JSON output and the MCP tools become a contract I promise to keep.

Try it

$ bbin install io.github.nrepl/neorepl
$ cd my-project
$ neorepl

The CHANGELOG has everything I skipped here, and the README has the details. Please keep the issues and ideas coming!

Keep hacking!