diff --git a/lib/gen_lsp/communication/stdio.ex b/lib/gen_lsp/communication/stdio.ex index 62c1601..233f222 100644 --- a/lib/gen_lsp/communication/stdio.ex +++ b/lib/gen_lsp/communication/stdio.ex @@ -9,10 +9,21 @@ defmodule GenLSP.Communication.Stdio do @separator "\r\n\r\n" @impl true - def init(_) do - :ok = :io.setopts(encoding: :latin1, binary: true) + @doc """ + ## Options - {:ok, nil} + * `:device` - the IO device to read from and write to. Defaults to `:stdio`. + """ + def init(opts) do + opts = Keyword.validate!(opts, device: :stdio) + device = opts[:device] + + case device do + :stdio -> :io.setopts(:standard_io, encoding: :latin1, binary: true) + _ -> :io.setopts(device, encoding: :latin1, binary: true) + end + + {:ok, %{device: device}} end @impl true @@ -21,21 +32,20 @@ defmodule GenLSP.Communication.Stdio do end @impl true - def write(body, _) do + def write(body, %{device: device}) do content_length = body |> IO.iodata_length() |> Integer.to_string() - IO.binwrite( - :stdio, - IO.iodata_to_binary(["Content-Length: ", content_length, @separator, body]) - ) + data = IO.iodata_to_binary(["Content-Length: ", content_length, @separator, body]) + + IO.binwrite(device, data) end @impl true - def read(_, _) do - headers = read_header(%{}) + def read(%{device: device}, _) do + headers = read_header(device, %{}) case headers do :eof -> @@ -45,25 +55,20 @@ defmodule GenLSP.Communication.Stdio do {:error, error} headers -> - body = + content_length = headers |> Map.fetch!("Content-Length") |> String.to_integer() - |> read_body() + + body = read_body(device, content_length) {:ok, body, ""} end end - defp read_header(headers) do - case IO.read(:stdio, :line) do - :eof -> - :eof - - {:error, error} -> - {:error, error} - - line -> + defp read_header(device, headers) do + case IO.read(device, :line) do + line when is_binary(line) -> line = String.trim(line) case line do @@ -71,25 +76,28 @@ defmodule GenLSP.Communication.Stdio do headers "" -> - read_header(headers) + read_header(device, headers) line -> [k, v] = String.split(line, ":", parts: 2) - read_header(Map.put(headers, String.trim(k), String.trim(v))) + headers = Map.put(headers, String.trim(k), String.trim(v)) + + read_header(device, headers) end - end - end - defp read_body(length) do - case IO.binread(:stdio, length) do :eof -> :eof {:error, error} -> {:error, error} + end + end - payload -> - payload + defp read_body(device, length) when is_integer(length) do + case IO.binread(device, length) do + payload when is_binary(payload) -> payload + :eof -> :eof + {:error, error} -> {:error, error} end end end diff --git a/test/gen_lsp/communication/stdio_test.exs b/test/gen_lsp/communication/stdio_test.exs index fa1b0cb..26be889 100644 --- a/test/gen_lsp/communication/stdio_test.exs +++ b/test/gen_lsp/communication/stdio_test.exs @@ -7,8 +7,8 @@ defmodule GenLSP.Communication.StdioTest do @command "elixir --erl '-kernel standard_io_encoding latin1' -S mix run -e ' defmodule GenLSP.Support.Buffer do - def loop do - case GenLSP.Communication.Stdio.read([], nil) do + def loop(state) do + case GenLSP.Communication.Stdio.read(state, nil) do :eof -> :eof @@ -16,20 +16,20 @@ defmodule GenLSP.Support.Buffer do body |> Jason.decode!() |> Jason.encode!() - |> GenLSP.Communication.Stdio.write([]) + |> GenLSP.Communication.Stdio.write(state) - loop() + loop(state) end end end defmodule Main do def run() do - GenLSP.Communication.Stdio.init([]) + {:ok, state} = GenLSP.Communication.Stdio.init([]) # the following match ensures that the script completes and does # not raise after stdin is closed. - :eof = GenLSP.Support.Buffer.loop() + :eof = GenLSP.Support.Buffer.loop(state) end end @@ -49,4 +49,19 @@ Main.run()'" # assert the message is echoed back assert_receive {^port, {:data, ^expected_message}}, 2000 end + + test "reads from and writes to an explicit :device instead of :stdio" do + {:ok, source} = StringIO.open(~s(Content-Length: 9\r\n\r\n{"a":"b"})) + {:ok, read_state} = GenLSP.Communication.Stdio.init(device: source) + assert {:ok, ~s({"a":"b"}), ""} = GenLSP.Communication.Stdio.read(read_state, "") + + {:ok, sink} = StringIO.open("") + {:ok, write_state} = GenLSP.Communication.Stdio.init(device: sink) + assert :ok = GenLSP.Communication.Stdio.write(~s({"a":1}), write_state) + assert {"", ~s(Content-Length: 7\r\n\r\n{"a":1})} = StringIO.contents(sink) + end + + test "defaults to :stdio when no device is given" do + assert {:ok, %{device: :stdio}} = GenLSP.Communication.Stdio.init([]) + end end