- 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>
137 lines
3.0 KiB
Elixir
137 lines
3.0 KiB
Elixir
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
|