Files
CljElixir/lib/clj_elixir/nrepl/handler.ex
Adam 7e82efd7ec 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>
2026-03-08 11:03:10 -04:00

111 lines
3.4 KiB
Elixir

defmodule CljElixir.NRepl.Handler do
@moduledoc "Handles nREPL protocol messages."
alias CljElixir.NRepl.SessionManager
def handle(msg, session_manager) do
op = Map.get(msg, "op")
id = Map.get(msg, "id", "unknown")
session = Map.get(msg, "session")
case op do
"clone" -> handle_clone(id, session, session_manager)
"close" -> handle_close(id, session, session_manager)
"eval" -> handle_eval(msg, id, session, session_manager)
"describe" -> handle_describe(id, session)
"ls-sessions" -> handle_ls_sessions(id, session_manager)
"load-file" -> handle_load_file(msg, id, session, session_manager)
"interrupt" -> handle_interrupt(id, session)
"completions" -> handle_completions(msg, id, session)
_ -> [%{"id" => id, "session" => session || "", "status" => ["done", "error", "unknown-op"]}]
end
end
defp handle_clone(id, _session, manager) do
new_id = SessionManager.create_session(manager)
[%{"id" => id, "new-session" => new_id, "status" => ["done"]}]
end
defp handle_close(id, session, manager) do
SessionManager.close_session(manager, session)
[%{"id" => id, "session" => session, "status" => ["done"]}]
end
defp handle_eval(msg, id, session, manager) do
code = Map.get(msg, "code", "")
# Capture stdout inside the Agent process where eval actually runs
{output, result} = SessionManager.eval_with_capture(manager, session, code)
responses = []
# Send stdout if any
responses =
if output != "" do
responses ++ [%{"id" => id, "session" => session, "out" => output}]
else
responses
end
# Send value or error
responses =
case result do
{:ok, value} ->
responses ++ [%{"id" => id, "session" => session, "value" => value, "ns" => "user"}]
{:error, error} ->
responses ++
[%{"id" => id, "session" => session, "err" => error, "status" => ["eval-error"]}]
end
# Send done
responses ++ [%{"id" => id, "session" => session, "status" => ["done"]}]
end
defp handle_describe(id, session) do
[
%{
"id" => id,
"session" => session || "",
"status" => ["done"],
"ops" => %{
"clone" => %{},
"close" => %{},
"eval" => %{},
"describe" => %{},
"ls-sessions" => %{},
"load-file" => %{},
"interrupt" => %{},
"completions" => %{}
},
"versions" => %{
"clj-elixir" => %{"major" => 0, "minor" => 1, "incremental" => 0},
"nrepl" => %{"major" => 1, "minor" => 0, "incremental" => 0}
}
}
]
end
defp handle_ls_sessions(id, manager) do
sessions = SessionManager.list_sessions(manager)
[%{"id" => id, "sessions" => sessions, "status" => ["done"]}]
end
defp handle_load_file(msg, id, session, manager) do
code = Map.get(msg, "file", "")
handle_eval(Map.put(msg, "code", code), id, session, manager)
end
defp handle_interrupt(id, session) do
# Not truly implemented -- just acknowledge
[%{"id" => id, "session" => session, "status" => ["done"]}]
end
defp handle_completions(msg, id, session) do
_prefix = Map.get(msg, "prefix", "")
# Basic completions -- return empty for now, can be extended later
[%{"id" => id, "session" => session || "", "completions" => [], "status" => ["done"]}]
end
end