164 lines
3.7 KiB
Elixir
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
|