Current section
Files
Jump to
Current section
Files
lib/fsharpy.ex
defmodule Fsharpy do
@moduledoc """
Access F# interactive (FSI) from Elixir.
"""
use GenServer
require Logger
alias Fsharpy.FromFsharp
@port_line_max 1024 * 8
@timeout 10 * 1000
# Client API
@doc """
Starts an F# session
"""
def start_link do
GenServer.start_link(__MODULE__, :ok, [])
end
@doc """
Quits an F# session
"""
def quit(server) do
GenServer.cast(server, :quit)
end
def get_raw(server, expression) do
GenServer.call(server, {:eval, expression})
end
@doc """
Sends a string of F# code to be evaluated and returns the output of the
expression as an unformatted string
"""
def eval(server, expression) do
result = get_raw(server, expression)
FromFsharp.get_vals(result)
end
@doc """
Sends a string of F# code to be evaluated and displays the output of the
expression.
"""
def print(server, expression) do
result = get_raw(server, expression)
result
|> String.trim_leading
|> String.trim_trailing
|> add_gutter
|> IO.puts
end
## Server Callbacks
def init(:ok) do
Process.flag(:trap_exit, true)
port = Port.open({:spawn_executable, find_fsi_path},
[:binary, :exit_status,
{:args, ["--gui-", "--nologo", "--utf8output"]}])
{:ok, %{port: port}}
end
def handle_call({:eval, "#quit"}, _from, state) do
{:stop, :normal, :ok, state}
end
def handle_call({:eval, expression}, _from, %{port: port} = state) do
send_data_to_fsi(port, expression)
case collect_responses(port) do
{:response, response} ->
{:reply, response, state}
:timeout ->
{:stop, :port_timeout, state}
end
end
def handle_cast(:quit, _state) do
exit(:normal)
end
def handle_info({_port, {:exit_status, 0}}, _state) do
exit(:normal)
end
def handle_info({:EXIT, port, reason}, %{port: port} = state) do
{:stop, {:port_terminated, reason}, state}
end
def handle_info(unexpected, state) do
Logger.debug "Ignoring: #{inspect unexpected}"
{:noreply, state}
end
def terminate(_reason, %{port: port}) do
if Port.info port != nil do
Logger.debug "Closing port [#{inspect port}]."
Port.close port
end
end
## Helper functions
defp add_gutter(multi_line_text) do
prefixed = "\n" <> multi_line_text
gutter = "\n#{IO.ANSI.cyan_background}#{IO.ANSI.white}F#:"
<> "#{IO.ANSI.default_background}#{IO.ANSI.default_color} "
String.replace(prefixed, "\n", gutter)
end
defp collect_responses(port) do
collect_responses(port, "")
end
defp collect_responses(_port, accumulator) do
receive do
{_port, {:exit_status, 0}} ->
exit(:normal)
{port, {:data, data}} ->
Logger.debug "#{inspect {port, {:data, data}}}"
combined = accumulator <> data
if String.ends_with?(data, "> ") do
response = combined
|> String.trim_trailing("> ")
|> String.trim
{:response, response}
else
collect_responses(port, combined)
end
after @timeout ->
:timeout
end
end
defp find_fsi_path do
fsi_names =
case Application.get_env(:fsharpy, :fsi_names) do
nil ->
["fsi", "fsharpi"]
config ->
config
end
fsi = fsi_names
|> Enum.find_value(&System.find_executable/1)
if fsi == nil do
raise "Could not locate the interactive in the path."
<> " Searched for #{inspect fsi_names}."
else
fsi
end
end
defp send_data_to_fsi(port, msg_to_fsi) do
cmd = (msg_to_fsi
|> String.trim_trailing
|> String.trim_trailing(";;"))
<> ";;\n"
Port.command(port, cmd)
end
end