A minimalist Clojure interactive programming environment for Emacs, in the spirit of CIDER and monroe, built on top of Clojure's built-in prepl instead of nREPL.
Status: prototype / MVP. Expect rough edges.
M-x portjacks in (auto-detectsdeps.edn/project.clj/bb.edn), orM-x port-connectattaches to a prepl you started yourself.- Single-buffer REPL with persistent input history.
- Interactive evaluation from source buffers (
C-c C-e,C-c C-c, ...), pretty-printed viaclojure.pprintwith configurable length/level caps. - Structured stacktrace buffer for exceptions: cause chain, ex-data, navigable frames.
- Eldoc, completion-at-point, doc/source/apropos/macroexpand helpers,
and
M-.find-definition that follows into jar sources.
prepl (clojure.core.server/io-prepl) is a streaming text protocol that
ships with Clojure itself. No bencode, no middleware, no sessions: send a
Clojure form, read back a sequence of EDN messages tagged :ret, :out,
:err, or :tap. That makes Port small, portable, and easy to reason about,
at the cost of features that nREPL middleware would otherwise provide.
Where CIDER asks the server (via middleware) for things like documentation
or completion, Port leans on the same trick inf-clojure uses: just evaluate
a Clojure form (e.g. (clojure.repl/doc foo)) and read what the REPL prints
back. monroe is a bit different: it talks plain nREPL, so most of what it
needs (eval, interrupt, load-file, session management) is already a built-in
op; it only falls back to sending forms for things stock nREPL doesn't cover.
The Clojure-on-Emacs landscape is a ladder. Each rung adds structure to how the editor talks to a running Clojure.
| Tool | Protocol | Server-side deps |
|---|---|---|
| inf-clojure | comint over stdio (or a socket REPL) | none |
| Port | prepl (TCP) | none (built into Clojure) |
| monroe | nREPL | nREPL |
| CIDER | nREPL + cider-nrepl middleware | nREPL + cider-nrepl |
inf-clojure sits closest to the metal. It runs a Clojure (or babashka,
Planck, ...) subprocess under Emacs's comint and parses its
plain-text output. Helper commands like documentation and arglist lookup go
through predefined code snippets and screen-scraping. Universal and cheap,
but the lack of structured output means the editor can't always tell apart a
return value, stdout, and an error.
Port is one rung up. prepl is a tiny TCP protocol that ships with Clojure
itself, emitting tagged EDN messages (:ret, :out, :err, :tap,
:exception) instead of plain text. That structure is enough to build
reliable tooling on top, once you add a small bootstrap form for request /
response correlation (see doc/design.md). Helper commands
still come from sending Clojure forms, the same idea as inf-clojure, but Port
can tell which response goes with which request without scraping prose.
monroe uses nREPL, whose protocol provides sessions, ops, and request ids out of the box. The editor side stays small because nREPL gives it those primitives for free. No middleware required beyond plain nREPL.
CIDER is the maximalist rung: nREPL plus the cider-nrepl middleware suite, plus a substantial editor codebase. The middleware adds server-side support for things that aren't cheap to build on bare nREPL: debugger, inspector, test runner, profiler, structured stacktrace browser, refactoring. Heavier setup, vastly more features.
The bulk of each project lives wherever its protocol has the gaps.
inf-clojure is small because comint already exists. monroe is small because
nREPL already gives it primitives. Port has to be slightly bigger than monroe
(the two-socket model, the bootstrap) because prepl gives it less. CIDER's
bulk isn't even Emacs Lisp; it's cider-nrepl, server-side, because that's
where nREPL is meant to be extended.
Pick the rung you want: maximum power → CIDER; small structured client over prepl → Port; small client over nREPL → monroe; zero server-side requirements → inf-clojure.
Port isn't on MELPA yet; it should get there once we cut a real release.
In the meantime, the easiest way is package-vc-install on Emacs 29+:
(package-vc-install
'(port :url "https://github.com/clojure-emacs/port"
:branch "main"
:lisp-dir "lisp"))On Emacs 30+ with use-package
you can put the same form in your config and let it handle install:
(use-package port
:vc (:url "https://github.com/clojure-emacs/port" :branch "main" :lisp-dir "lisp")
:hook ((clojure-mode . port-mode)
(clojure-ts-mode . port-mode))):lisp-dir "lisp" is needed because Port's sources live under lisp/ rather
than at the repository root; without it package-vc-install won't add that
directory to load-path and byte-compilation will fail.
Port doesn't depend on any specific Clojure mode. Hook it onto whichever
one(s) you actually use (clojure-mode, clojure-ts-mode, or both).
For a manual checkout (e.g. while contributing):
(add-to-list 'load-path "/path/to/port/lisp")
(require 'port)
(add-hook 'clojure-mode-hook #'port-mode)
(add-hook 'clojure-ts-mode-hook #'port-mode)Easiest path: let Port spawn one for you. Visit a file in your project and
run M-x port. It auto-detects the project layout (deps.edn, project.clj,
or bb.edn), picks a free port in 5555-5574, starts a JVM with a prepl
server, and connects when the port comes up. The server's stdout/stderr
lands in a *port-server* buffer below the REPL.
If you'd rather run the prepl yourself (handy for embedding it in a long-running application or pre-warming the JVM), start it like this:
clojure -e '(do (clojure.core.server/start-server
{:name "port" :port 5555
:accept (quote clojure.core.server/io-prepl)})
@(promise))'
then attach from Emacs with M-x port-connect (defaults to localhost:5555).
| Binding | Command |
|---|---|
C-c C-e |
port-eval-last-sexp |
C-c C-c |
port-eval-defun-at-point |
C-c C-r |
port-eval-region |
C-c C-k |
port-eval-buffer |
C-c C-l |
port-load-file |
C-c C-d |
port-doc |
C-c C-s |
port-source |
C-c C-m |
port-macroexpand-1 |
C-c M-n |
port-set-ns |
C-c C-z |
port-switch-to-repl |
M-. |
port-find-definition |
All output, including evaluation results from source buffers, ends up in the REPL buffer.
prepl has no built-in request id, so any tooling that needs to know which response belongs to which request has to layer that on top. Port does it by opening two prepl connections per session:
- a user socket that drives the REPL buffer with raw streaming output,
- a tool socket that carries helper-command requests (doc, source,
macroexpand, ...) wrapped in a small bootstrap function that captures
*out*/*err*and returns a tagged map containing the request id.
The bootstrap (port.tooling/-eval) is sent once on connect; subsequent
helper calls go through it and dispatch back to per-request callbacks via the
request id. The user socket stays clean (no wrapping, no surprises), so
direct evals from a Clojure buffer behave like typing into the REPL.
For the deeper rationale and a walkthrough of each module, see
doc/design.md.
- No inline overlays or debugger. (The tool socket makes both tractable; they're just not implemented yet.)
Two reasons. I started this project while spending some time in Porto, and
there's already a tradition of naming Clojure-on-editor tools after drinks:
CIDER, Calva (after Calvados, the Norman apple brandy). Port wine
fit, and the protocol is prepl over a TCP port, so the pun was hard to
pass up.
Distributed under the GNU General Public License, version 3 or later.