Files
2026-03-09 23:09:46 -04:00

164 lines
3.7 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
ns = CljElixir.REPL.current_ns(state)
prompt = "#{ns}:#{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
IO.write(prompt)
read_line()
end
# Read a line character-by-character, treating both \r and \n as line terminators.
# This avoids IO.gets hanging when the terminal sends \r without \n.
defp read_line, do: read_line([])
defp read_line(acc) do
case IO.getn("", 1) do
:eof ->
if acc == [], do: :eof, else: acc |> Enum.reverse() |> IO.iodata_to_binary()
{:error, _} ->
if acc == [], do: :eof, else: acc |> Enum.reverse() |> IO.iodata_to_binary()
<<c>> when c in [?\r, ?\n] ->
IO.write("\n")
acc |> Enum.reverse() |> IO.iodata_to_binary()
char ->
read_line([char | acc])
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
IO.write(" ")
case read_line() do
:eof -> acc
line ->
trimmed = String.trim(line)
if trimmed == "" do
# Empty Enter in continuation mode: submit what we have
acc
else
new_acc = acc <> "\n" <> trimmed
if CljElixir.REPL.balanced?(new_acc) do
new_acc
else
read_continuation(new_acc)
end
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