Phase 8: REPL, printing, source maps, and nREPL server
- 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>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
defmodule CljElixir.NRepl.SessionManager do
|
||||
@moduledoc "Manages nREPL sessions with independent REPL state."
|
||||
|
||||
use GenServer
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, %{}, opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{sessions: %{}}}
|
||||
end
|
||||
|
||||
def create_session(manager) do
|
||||
GenServer.call(manager, :create_session)
|
||||
end
|
||||
|
||||
def close_session(manager, session_id) do
|
||||
GenServer.call(manager, {:close_session, session_id})
|
||||
end
|
||||
|
||||
def list_sessions(manager) do
|
||||
GenServer.call(manager, :list_sessions)
|
||||
end
|
||||
|
||||
def eval(manager, session_id, code) do
|
||||
GenServer.call(manager, {:eval, session_id, code}, :infinity)
|
||||
end
|
||||
|
||||
@doc "Eval with stdout capture. Returns {output, result} where result is {:ok, value} | {:error, error}."
|
||||
def eval_with_capture(manager, session_id, code) do
|
||||
GenServer.call(manager, {:eval_with_capture, session_id, code}, :infinity)
|
||||
end
|
||||
|
||||
# --- Callbacks ---
|
||||
|
||||
@impl true
|
||||
def handle_call(:create_session, _from, state) do
|
||||
id = generate_uuid()
|
||||
{:ok, pid} = Agent.start_link(fn -> CljElixir.REPL.new() end)
|
||||
new_sessions = Map.put(state.sessions, id, pid)
|
||||
{:reply, id, %{state | sessions: new_sessions}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:close_session, id}, _from, state) do
|
||||
case Map.pop(state.sessions, id) do
|
||||
{nil, _} ->
|
||||
{:reply, :not_found, state}
|
||||
|
||||
{pid, new_sessions} ->
|
||||
Agent.stop(pid)
|
||||
{:reply, :ok, %{state | sessions: new_sessions}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:list_sessions, _from, state) do
|
||||
{:reply, Map.keys(state.sessions), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:eval, id, code}, _from, state) do
|
||||
case Map.get(state.sessions, id) do
|
||||
nil ->
|
||||
{:reply, {:error, "unknown session"}, state}
|
||||
|
||||
pid ->
|
||||
result =
|
||||
Agent.get_and_update(
|
||||
pid,
|
||||
fn repl_state ->
|
||||
case CljElixir.REPL.eval(code, repl_state) do
|
||||
{:ok, value, new_state} -> {{:ok, value}, new_state}
|
||||
{:error, error, new_state} -> {{:error, error}, new_state}
|
||||
end
|
||||
end,
|
||||
:infinity
|
||||
)
|
||||
|
||||
{:reply, result, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:eval_with_capture, id, code}, _from, state) do
|
||||
case Map.get(state.sessions, id) do
|
||||
nil ->
|
||||
{:reply, {"", {:error, "unknown session"}}, state}
|
||||
|
||||
pid ->
|
||||
{output, result} =
|
||||
Agent.get_and_update(
|
||||
pid,
|
||||
fn repl_state ->
|
||||
{output, eval_result, new_state} = eval_capturing_output(code, repl_state)
|
||||
|
||||
{{output, eval_result}, new_state}
|
||||
end,
|
||||
:infinity
|
||||
)
|
||||
|
||||
{:reply, {output, result}, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Evaluate code while capturing stdout within the current process
|
||||
defp eval_capturing_output(code, repl_state) do
|
||||
{:ok, string_io} = StringIO.open("")
|
||||
old_gl = Process.group_leader()
|
||||
Process.group_leader(self(), string_io)
|
||||
|
||||
try do
|
||||
case CljElixir.REPL.eval(code, repl_state) do
|
||||
{:ok, value, new_state} ->
|
||||
Process.group_leader(self(), old_gl)
|
||||
{_input, output} = StringIO.contents(string_io)
|
||||
StringIO.close(string_io)
|
||||
{output, {:ok, value}, new_state}
|
||||
|
||||
{:error, error, new_state} ->
|
||||
Process.group_leader(self(), old_gl)
|
||||
{_input, output} = StringIO.contents(string_io)
|
||||
StringIO.close(string_io)
|
||||
{output, {:error, error}, new_state}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Process.group_leader(self(), old_gl)
|
||||
StringIO.close(string_io)
|
||||
{"", {:error, Exception.message(e)}, repl_state}
|
||||
end
|
||||
end
|
||||
|
||||
# Simple UUID v4 generation using :crypto (OTP built-in, no deps)
|
||||
defp generate_uuid do
|
||||
<<a::32, b::16, c::16, d::16, e::48>> = :crypto.strong_rand_bytes(16)
|
||||
|
||||
:io_lib.format("~8.16.0b-~4.16.0b-4~3.16.0b-~4.16.0b-~12.16.0b", [
|
||||
a,
|
||||
b,
|
||||
Bitwise.band(c, 0x0FFF),
|
||||
Bitwise.bor(Bitwise.band(d, 0x3FFF), 0x8000),
|
||||
e
|
||||
])
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user