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,36 @@
|
||||
defmodule Mix.Tasks.Clje.Nrepl do
|
||||
@moduledoc """
|
||||
Starts an nREPL server for CljElixir.
|
||||
|
||||
## Usage
|
||||
|
||||
mix clje.nrepl # random port
|
||||
mix clje.nrepl --port 7888 # specific port
|
||||
|
||||
Writes port to `.nrepl-port` file for editor auto-discovery.
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
@shortdoc "Start a CljElixir nREPL server"
|
||||
|
||||
@impl Mix.Task
|
||||
def run(args) do
|
||||
{opts, _, _} = OptionParser.parse(args, strict: [port: :integer])
|
||||
port = Keyword.get(opts, :port, 0)
|
||||
|
||||
Mix.Task.run("compile")
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
{:ok, server} = CljElixir.NRepl.Server.start_link(port: port)
|
||||
actual_port = CljElixir.NRepl.Server.port(server)
|
||||
|
||||
File.write!(".nrepl-port", Integer.to_string(actual_port))
|
||||
|
||||
IO.puts("nREPL server started on port #{actual_port} on host 127.0.0.1")
|
||||
IO.puts("Port written to .nrepl-port")
|
||||
IO.puts("Press Ctrl+C to stop\n")
|
||||
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,136 @@
|
||||
defmodule Mix.Tasks.Clje.Repl do
|
||||
@moduledoc """
|
||||
Starts an interactive CljElixir REPL.
|
||||
|
||||
## Usage
|
||||
|
||||
mix clje.repl
|
||||
|
||||
Features:
|
||||
- Full line editing (arrow keys, home/end, backspace)
|
||||
- Multi-line input (auto-detects unbalanced parens)
|
||||
- `pr-str` output formatting
|
||||
- Bindings persist across evaluations
|
||||
- Special commands: :quit, :history, :help
|
||||
|
||||
For readline-style history (up/down arrow recall), run with rlwrap:
|
||||
|
||||
rlwrap mix clje.repl
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
|
||||
@shortdoc "Start an interactive CljElixir REPL"
|
||||
|
||||
@impl Mix.Task
|
||||
def run(_args) do
|
||||
# Ensure the project is compiled first
|
||||
Mix.Task.run("compile")
|
||||
|
||||
# Start the application
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
IO.puts("CljElixir REPL v0.1.0")
|
||||
IO.puts("Type :help for help, :quit to exit\n")
|
||||
|
||||
state = CljElixir.REPL.new()
|
||||
loop(state)
|
||||
end
|
||||
|
||||
defp loop(state) do
|
||||
prompt = "clje:#{state.counter}> "
|
||||
|
||||
case read_input(prompt) do
|
||||
:eof ->
|
||||
IO.puts("\nBye!")
|
||||
|
||||
input ->
|
||||
input = String.trim(input)
|
||||
|
||||
case input do
|
||||
":quit" ->
|
||||
IO.puts("Bye!")
|
||||
|
||||
":history" ->
|
||||
print_history(state)
|
||||
loop(state)
|
||||
|
||||
":help" ->
|
||||
print_help()
|
||||
loop(state)
|
||||
|
||||
"" ->
|
||||
loop(state)
|
||||
|
||||
_ ->
|
||||
# Check for multi-line - read more if unbalanced
|
||||
full_input = maybe_read_more(input)
|
||||
|
||||
case CljElixir.REPL.eval(full_input, state) do
|
||||
{:ok, result_str, new_state} ->
|
||||
IO.puts(result_str)
|
||||
loop(new_state)
|
||||
|
||||
{:error, error_str, new_state} ->
|
||||
IO.puts("\e[31m#{error_str}\e[0m")
|
||||
loop(new_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp read_input(prompt) do
|
||||
case IO.gets(prompt) do
|
||||
:eof -> :eof
|
||||
{:error, _} -> :eof
|
||||
data -> data
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_read_more(input) do
|
||||
if CljElixir.REPL.balanced?(input) do
|
||||
input
|
||||
else
|
||||
read_continuation(input)
|
||||
end
|
||||
end
|
||||
|
||||
defp read_continuation(acc) do
|
||||
case IO.gets(" ") do
|
||||
:eof -> acc
|
||||
{:error, _} -> acc
|
||||
line ->
|
||||
new_acc = acc <> "\n" <> String.trim_trailing(line, "\n")
|
||||
|
||||
if CljElixir.REPL.balanced?(new_acc) do
|
||||
new_acc
|
||||
else
|
||||
read_continuation(new_acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp print_history(state) do
|
||||
state.history
|
||||
|> Enum.reverse()
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.each(fn {expr, i} ->
|
||||
IO.puts(" #{i}: #{String.replace(expr, "\n", "\n ")}")
|
||||
end)
|
||||
end
|
||||
|
||||
defp print_help do
|
||||
IO.puts("""
|
||||
CljElixir REPL Commands:
|
||||
:help - show this help
|
||||
:history - show expression history
|
||||
:quit - exit the REPL
|
||||
|
||||
Tips:
|
||||
- Multi-line input: unbalanced parens trigger continuation
|
||||
- Bindings persist: (def x 42) makes x available in later expressions
|
||||
- All CljElixir syntax is supported
|
||||
- For up/down arrow history, run: rlwrap mix clje.repl
|
||||
""")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user