I think I'll take my REPL neat
My parens black and my bed at three
CIDER's too sweet for me...-- Bozier
A small, language-agnostic nREPL client for Emacs.
Where other nREPL clients (e.g. monroe and CIDER) target Clojure specifically,
neat aims to be the purest language-agnostic nREPL client in the Emacs world:
no Clojure-flavored helpers, no hardcoded ops, no assumptions about your
project's build tool. It's useful directly (a REPL buffer plus a source-buffer
minor mode) and also as a library that other Emacs packages can build on.
This is an early, experimental project. Expect rough edges.
0.1.0 is the first tagged release. The codec, async dispatch, comint
REPL, and source-buffer minor mode are all in place; eldoc, CAPF,
xref find-definition, doc-lookup, namespace-aware eval, and stdin
handling work against any conformant server. See
CHANGELOG.md for the full inventory. Expect rough
edges - this is the start of the road, not the end.
neat is part of a broader push to make nREPL a healthy multi-language
ecosystem rather than a Clojure-only protocol. That effort has three
strands:
- An official nREPL specification. Today the nREPL project
is the de facto spec; a formal version is being drafted at
nrepl/spec.nrepl.org.
neataims to keep pressure on the spec to stay genuinely language-agnostic by being a client that refuses to silently assume Clojure. - Reference clients. A spec without independent client
implementations is wishful thinking.
neatis one such reference client, intentionally built on Emacs builtins and free of Clojure-specific helpers, so it can act as a baseline for what "compliant" should mean on the client side. - A compatibility test suite. That one lives in its own project, proof. It talks to a server the way clients do, runs a set of checks and tells you what's broken and which clients it breaks. If you work on an nREPL server, that's the tool to reach for. neat's own integration suite only tests the client.
neat isn't on MELPA yet. In the meantime the snippets below track
the main branch; to pin to a tagged release, swap :branch "main"
for :rev "v0.1.0" (with package-vc-install) or :branch "v0.1.0"
(with straight.el). Latest tag: v0.1.0.
Easiest path - package-vc-install on Emacs 29+:
;; latest from main
(package-vc-install
'(neat :url "https://github.com/nrepl/neat" :branch "main"))
;; pinned to a release
(package-vc-install
'(neat :url "https://github.com/nrepl/neat" :rev "v0.1.0"))On Emacs 30+ with use-package:
;; latest from main
(use-package neat
:vc (:url "https://github.com/nrepl/neat" :branch "main")
:commands (neat neat-mode))
;; pinned to a release
(use-package neat
:vc (:url "https://github.com/nrepl/neat" :rev "v0.1.0")
:commands (neat neat-mode))With straight.el:
;; latest from main
(straight-use-package
'(neat :type git :host github :repo "nrepl/neat"))
;; pinned to a release
(straight-use-package
'(neat :type git :host github :repo "nrepl/neat" :branch "v0.1.0"))Or combined with use-package:
(use-package neat
:straight (neat :type git :host github :repo "nrepl/neat")
:commands (neat neat-mode))For a manual checkout (e.g. while contributing):
(add-to-list 'load-path "/path/to/neat")
(require 'neat)neat-mode is a minor mode you turn on per source buffer; hook it onto
whichever languages you actually drive (clojure-mode, fennel-mode,
hy-mode, ...). The mode itself doesn't assume any specific language.
neat is a few small files instead of one big one, so other packages can
pick and choose:
neat-bencode.el- bencode encode/decode, no other dependencies.neat-client.el- connection management, request dispatch, nREPL ops.neat-repl.el- comint-derived REPL buffer.neat.el- entry point, customization group,neat-modeminor mode for source buffers.
Library users typically only need neat-bencode and neat-client.
Start an nREPL server. Anything that speaks the protocol will do; for a Clojure server the easy options are:
bb nrepl-server :port 7888
# or
lein repl :headless :port 7888
# or
clj -M:nrepl
Then in Emacs:
M-x neat RET localhost RET 7888 RET
A *neat: localhost:7888* buffer pops up with a prompt. Type an
expression, hit RET, see the result. Multi-line forms work too:
RET only submits when the input parses as balanced; otherwise it
inserts a newline so you can finish the form. Input history is
persisted between sessions in neat-repl-history-file and the prompt
follows the server's reported namespace (user> , myapp.core> , ...).
To evaluate from a source buffer:
M-x neat-mode
Bindings:
| Key | Command |
|---|---|
C-c C-e |
neat-eval-last-sexp |
C-c C-c |
neat-eval-defun |
C-c C-r |
neat-eval-region |
C-c C-b |
neat-eval-buffer |
C-c C-l |
neat-load-buffer-file |
C-c C-z |
neat-switch-to-repl |
C-c C-k |
neat-interrupt-eval |
C-c M-n |
neat-set-ns |
C-c C-d C-d |
neat-show-doc-at-point |
M-. |
xref-find-definitions |
M-, |
xref-go-back |
neat-eval-buffer ships the buffer contents as an eval op (each form
evaluated in turn, every value streamed back). neat-load-buffer-file
sends a load-file op instead, carrying the buffer's path and filename
so the server can attribute file and line numbers to errors. Use it
when you're loading an actual file from disk and care about good
diagnostics; use neat-eval-buffer when you're scratching around in
a buffer that may not even be on disk.
M-. is plain xref-find-definitions. neat-mode registers an xref
backend that asks the server's lookup op where the symbol at point
is defined and jumps there. M-, pops back through the standard
xref stack. Sources behind URLs we can't resolve locally (jar:...,
http:..., ...) yield no result; we don't try to extract files from
jars.
neat-set-ns (C-c M-n) sets the buffer-local namespace neat-ns,
which is sent as the ns field on every eval op from this buffer.
Set it explicitly per buffer, in a major-mode hook, or via
.dir-locals.el. For languages where the namespace is declared in
the source (Clojure's (ns foo.bar), etc.), swap in a parser via
neat-buffer-ns-function -- the default just returns neat-ns.
The CAPF, eldoc backend, and xref backend all rely on the standard
completions and lookup nREPL ops. neat checks the ops the server
lists in its describe reply, and when one of them is missing these
features stay out of the way - that's by design, a language-agnostic
client can't assume anything.
Diagnosis: turn on the message log (next-to-last entry below) and
look at the ops in the describe reply, or for unknown-op in a
status.
Three usual suspects:
- The eval is reading from stdin. Look at the minibuffer for a
stdinprompt and answer it, REPL buffer or not.C-c C-dthere sends end-of-file, andC-ginterrupts the eval (or sends end-of-file, if the server can't interrupt). - The connection died. The mode-line shows
[closed]and a;; connection closedline appears in the REPL buffer. - The eval is actually running, just slowly.
C-c C-cin the REPL (orC-c C-kin a source buffer) sends aninterruptop. Not every server has one, and neat tells you when yours doesn't.
By default neat sends no ns on eval, so the server uses whatever
its current namespace is. To pin a source buffer:
C-c M-n(neat-set-ns) interactively.((eval . (setq neat-ns "myapp.core")))in.dir-locals.elfor a project-wide default.- Or swap
neat-buffer-ns-functionfor one that derives the ns from the buffer (parsing a(ns ...)form, reading file metadata, etc.).
A namespace the server doesn't know (a typo in neat-ns, say) shows
up as a ;; namespace not found: ... line in the REPL, or in the echo
area when there's no REPL buffer.
The port comes from the nearest .nrepl-port file walking up from
the current buffer. Common reasons it doesn't fire:
- Your server writes a different filename: set
neat-port-file-name. - The file isn't on the path from the buffer to the project root.
- You're in a buffer not visiting a file:
default-directoryis used, which may not be where you expect.
M-: (neat-discover-port-file) RET shows what neat is finding.
The lookup op may have returned a URL we can't resolve locally -
jar: paths (Clojure stdlib source inside jars), http: URLs
(remote sources), and similar are skipped by design. Or the server
simply doesn't know where the symbol lives. The message log shows
the actual response.
M-x neat-toggle-message-log flips a global mirror. Both directions
land pretty-printed in *neat-messages*, prefixed with -->
(request), <-- (response), or !!! (internal note, e.g. a dropped
malformed message). The buffer is in neat-message-log-mode - q
buries, c clears. Windows scrolled to the end auto-follow new
entries; scroll up and the log stops chasing you.
Two more knobs worth knowing:
neat-show-message-logpops the buffer without toggling.neat-clear-message-logwipes it.neat-message-log-max-message-length(default 2000) truncates each message to keep huge values from making the buffer unusable.neat-message-log-max-buffer-lines(default 5000) trims the oldest entries on the fly so long sessions don't grow without bound.
Customize the buffer name via neat-message-log-buffer-name;
permanently enable logging with (setq neat-log-messages t).
A few Emacs builtins that pair well with neat:
M-x toggle-debug-on-quit. With it on, hittingC-gduring a hang drops into the debugger at exactly the stuck point - very useful when an eval, lookup, or completion call is wedged.M-x toggle-debug-on-error. Surfaces signaled errors instead of routing them to*Messages*. neat's callback dispatch usescondition-case-unless-debug, so turning this on means buggy callbacks land in the debugger rather than being swallowed by a one-linemessage. See the Emacs Lisp Debugger manual.M-x profiler-start, exercise the slow thing, thenM-x profiler-report. Helpful when CAPF feels laggy or an eldoc lookup blocks longer than expected. See the Profiling manual.
For the rationale behind the architecture (the module split, the
async dispatch model, the comint pipe-process trick, why we target
Emacs 28+, and so on) see doc/design.md.
The project uses Eldev and Buttercup.
eldev compile # byte-compile
eldev lint # lint
eldev test # run Buttercup suites
The default suite is the fast one. There's also an integration suite
that starts an nREPL server (nrepl/nrepl, through the Clojure CLI)
and drives the client and the REPL buffer end to end: rendering,
stdin, interrupts, error statuses and late output. It's gated behind
an env var since starting the JVM adds a few seconds, and it's
skipped when clojure isn't on your PATH:
NEAT_INTEGRATION=1 eldev test
The suite tests neat, not the server. To find out how well a server speaks nREPL, run proof against it.
Distributed under the GNU General Public License, version 3 or later. See
LICENSE.