Port

May 7, 2026 · View on GitHub

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.

Why prepl?

prepl (clojure.core.server/io-prepl) is a streaming text protocol that ships with Clojure itself. There is no bencode, no middleware, no sessions — just send a Clojure form and 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 follows monroe's lead and implements such commands by simply evaluating a Clojure form (e.g. (clojure.repl/doc foo)) and printing the result into the REPL buffer.

How does it compare to other Clojure tools for Emacs?

The Clojure-on-Emacs landscape can be thought of as a ladder where each rung adds structure to how the editor talks to a running Clojure. Roughly:

ToolProtocolServer-side deps
inf-clojurecomint over stdio (or a socket REPL)none
Portprepl (TCP)none — built into Clojure
monroenREPLnREPL
CIDERnREPL + cider-nrepl middlewarenREPL + 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 are implemented by sending predefined code snippets to the REPL and reading what the REPL prints back. 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; instead of plain text it emits tagged EDN messages (:ret, :out, :err, :tap, :exception). 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 are still implemented by sending Clojure forms — same idea as inf-clojure — but Port can tell which response goes with which request without scraping prose.

monroe is another rung up. It 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 provides server-side support for features that aren't cheap to build on bare nREPL — debugger, inspector, test runner, profiler, structured stacktrace browser, refactoring. Heavier setup, vastly more features.

A useful way to read the ladder: 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 mostly 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 actually want: maximum power → CIDER; small structured client over prepl → Port; small client over nREPL → monroe; zero server-side requirements → inf-clojure.

Architecture

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.

Starting a prepl

The simplest path is to 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 or project.clj), picks a free port in 5555-5574, starts a JVM with a prepl server on that port, and connects when it's ready. 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))'

and then M-x port-connect to attach.

Installation

Port isn't on MELPA yet (it might be once it's stable enough for 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"))

If you use use-package on Emacs 30+ you can put the same thing in your config and let it handle install:

(use-package port
  :vc (:url "https://github.com/clojure-emacs/port" :branch "main")
  :hook (clojure-mode . port-mode))

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)

Connecting from Emacs

For most projects M-x port is all you need — it starts a prepl and connects. Use M-x port-connect (default localhost:5555) when you've started a prepl yourself or want to attach to one running elsewhere.

Key bindings (in port-mode)

BindingCommand
C-c C-eport-eval-last-sexp
C-c C-cport-eval-defun-at-point
C-c C-rport-eval-region
C-c C-kport-eval-buffer
C-c C-lport-load-file
C-c C-dport-doc
C-c C-sport-source
C-c C-mport-macroexpand-1
C-c M-nport-set-ns
C-c C-zport-switch-to-repl
M-.port-find-definition

All output, including evaluation results from source buffers, is written to the REPL buffer.

Limitations (today)

  • No port-jack-in; you start the prepl yourself.
  • No inline overlays or debugger. (The tool socket makes these tractable; they're just not implemented yet.)
  • No structured error rendering; stacktraces print as the server emits them.
  • No persistent input history.

Design

For the architecture, the rationale behind the two-socket model, and a walkthrough of how each piece fits together, see doc/design.md.

License

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