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() <> 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