- IPrintWithWriter protocol + CljElixir.Printer module with pr-str, print-str, pr, prn for all BEAM types (EDN-like output) - Source-mapped error messages: line/col metadata from reader now propagated through transformer into Elixir AST for accurate error locations in .clje files - Interactive REPL (mix clje.repl) with multi-line input detection, history, bindings persistence, and pr-str formatted output - nREPL server (mix clje.nrepl) with TCP transport, Bencode wire protocol, session management, and core operations (clone, close, eval, describe, ls-sessions, load-file, interrupt, completions). Writes .nrepl-port for editor auto-discovery. 92 new tests (699 total, 0 failures). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
3.5 KiB
Elixir
151 lines
3.5 KiB
Elixir
defmodule CljElixir.NRepl.Server do
|
|
@moduledoc """
|
|
nREPL TCP server for CljElixir.
|
|
|
|
Listens on a configurable port and handles nREPL protocol messages
|
|
over TCP using Bencode encoding.
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
alias CljElixir.NRepl.{Bencode, Handler, SessionManager}
|
|
|
|
defstruct [:listen_socket, :port, :session_manager]
|
|
|
|
def start_link(opts \\ []) do
|
|
port = Keyword.get(opts, :port, 0)
|
|
GenServer.start_link(__MODULE__, port, opts)
|
|
end
|
|
|
|
def port(server) do
|
|
GenServer.call(server, :port)
|
|
end
|
|
|
|
# --- Callbacks ---
|
|
|
|
@impl true
|
|
def init(port) do
|
|
{:ok, manager} = SessionManager.start_link()
|
|
|
|
# Open TCP listener
|
|
opts = [:binary, packet: :raw, active: false, reuseaddr: true]
|
|
|
|
case :gen_tcp.listen(port, opts) do
|
|
{:ok, listen_socket} ->
|
|
# Get actual port (important when port=0)
|
|
{:ok, actual_port} = :inet.port(listen_socket)
|
|
|
|
state = %__MODULE__{
|
|
listen_socket: listen_socket,
|
|
port: actual_port,
|
|
session_manager: manager
|
|
}
|
|
|
|
# Start accepting connections in a separate process
|
|
spawn_link(fn -> accept_loop(listen_socket, manager) end)
|
|
|
|
Logger.info("nREPL server started on port #{actual_port}")
|
|
{:ok, state}
|
|
|
|
{:error, reason} ->
|
|
{:stop, reason}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:port, _from, state) do
|
|
{:reply, state.port, state}
|
|
end
|
|
|
|
@impl true
|
|
def terminate(_reason, state) do
|
|
if state.listen_socket do
|
|
:gen_tcp.close(state.listen_socket)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
# --- Connection handling ---
|
|
|
|
defp accept_loop(listen_socket, manager) do
|
|
case :gen_tcp.accept(listen_socket) do
|
|
{:ok, client_socket} ->
|
|
spawn(fn -> handle_connection(client_socket, manager) end)
|
|
accept_loop(listen_socket, manager)
|
|
|
|
{:error, :closed} ->
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Accept error: #{inspect(reason)}")
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp handle_connection(socket, manager) do
|
|
case read_message(socket, "") do
|
|
{:ok, msg, rest} ->
|
|
responses = Handler.handle(msg, manager)
|
|
|
|
Enum.each(responses, fn response ->
|
|
encoded = Bencode.encode(response)
|
|
:gen_tcp.send(socket, encoded)
|
|
end)
|
|
|
|
handle_connection_with_buffer(socket, manager, rest)
|
|
|
|
{:error, _reason} ->
|
|
:gen_tcp.close(socket)
|
|
end
|
|
end
|
|
|
|
defp handle_connection_with_buffer(socket, manager, buffer) do
|
|
case try_decode(buffer) do
|
|
{:ok, msg, rest} ->
|
|
responses = Handler.handle(msg, manager)
|
|
|
|
Enum.each(responses, fn response ->
|
|
encoded = Bencode.encode(response)
|
|
:gen_tcp.send(socket, encoded)
|
|
end)
|
|
|
|
handle_connection_with_buffer(socket, manager, rest)
|
|
|
|
:incomplete ->
|
|
case :gen_tcp.recv(socket, 0) do
|
|
{:ok, data} ->
|
|
handle_connection_with_buffer(socket, manager, buffer <> data)
|
|
|
|
{:error, _} ->
|
|
:gen_tcp.close(socket)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp read_message(socket, buffer) do
|
|
case try_decode(buffer) do
|
|
{:ok, msg, rest} ->
|
|
{:ok, msg, rest}
|
|
|
|
:incomplete ->
|
|
case :gen_tcp.recv(socket, 0) do
|
|
{:ok, data} -> read_message(socket, buffer <> data)
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp try_decode(data) when byte_size(data) == 0, do: :incomplete
|
|
|
|
defp try_decode(data) do
|
|
try do
|
|
{msg, rest} = Bencode.decode_one(data)
|
|
{:ok, msg, rest}
|
|
rescue
|
|
_ -> :incomplete
|
|
end
|
|
end
|
|
end
|