Skip to content
nreplPublic

About

A small, language-agnostic nREPL client for Emacs

Resources

Stars

15 stars

Watchers

1 watching

Forks

Repository files navigation

neat

CI

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.

Status

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.

Project context

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:

  1. An official nREPL specification. Today the nREPL project is the de facto spec; a formal version is being drafted at nrepl/spec.nrepl.org. neat aims to keep pressure on the spec to stay genuinely language-agnostic by being a client that refuses to silently assume Clojure.
  2. Reference clients. A spec without independent client implementations is wishful thinking. neat is 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.
  3. 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.

Installation

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.

Modules

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-mode minor mode for source buffers.

Library users typically only need neat-bencode and neat-client.

Quick start

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.

Troubleshooting

Eldoc, completion, or M-. quietly do nothing

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.

Evaluation just sits there

Three usual suspects:

  • The eval is reading from stdin. Look at the minibuffer for a stdin prompt and answer it, REPL buffer or not. C-c C-d there sends end-of-file, and C-g interrupts the eval (or sends end-of-file, if the server can't interrupt).
  • The connection died. The mode-line shows [closed] and a ;; connection closed line appears in the REPL buffer.
  • The eval is actually running, just slowly. C-c C-c in the REPL (or C-c C-k in a source buffer) sends an interrupt op. Not every server has one, and neat tells you when yours doesn't.

Evaluation lands in the wrong namespace

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.el for a project-wide default.
  • Or swap neat-buffer-ns-function for 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.

M-x neat doesn't autofill the port

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-directory is used, which may not be where you expect.

M-: (neat-discover-port-file) RET shows what neat is finding.

M-. doesn't jump anywhere

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.

Something is happening on the wire but you can't tell what

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-log pops the buffer without toggling.
  • neat-clear-message-log wipes 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).

Generic Emacs debugging tools

A few Emacs builtins that pair well with neat:

  • M-x toggle-debug-on-quit. With it on, hitting C-g during 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 uses condition-case-unless-debug, so turning this on means buggy callbacks land in the debugger rather than being swallowed by a one-line message. See the Emacs Lisp Debugger manual.
  • M-x profiler-start, exercise the slow thing, then M-x profiler-report. Helpful when CAPF feels laggy or an eldoc lookup blocks longer than expected. See the Profiling manual.

Design

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.

Development

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.

License

Distributed under the GNU General Public License, version 3 or later. See LICENSE.

About

A small, language-agnostic nREPL client for Emacs

Resources

Stars

15 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages