From 70d0220b3e5b6f8ece449e5e9725f1497dab5f56 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Sun, 30 Mar 2025 15:30:10 -0400 Subject: [PATCH] feat: opaque assigns structure This hides the implementation of assigns behind a function `assigns/1`. This also implements a backend for this new opaque structure that is backed by another process, in this case an Agent (not tied on agent, but was the closest thing to what currently exists. --- lib/gen_lsp.ex | 9 +++++++-- lib/gen_lsp/assigns.ex | 22 ++++++++++++++++++++++ lib/gen_lsp/lsp.ex | 19 ++++++++++++++++--- lib/gen_lsp/test.ex | 18 ++++++++++++++++-- test/gen_lsp/communication/tcp_test.exs | 9 ++++----- test/gen_lsp_test.exs | 8 +------- test/support/example_server.ex | 8 ++++---- 7 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 lib/gen_lsp/assigns.ex diff --git a/lib/gen_lsp.ex b/lib/gen_lsp.ex index 7bdbcb8..6c0a361 100644 --- a/lib/gen_lsp.ex +++ b/lib/gen_lsp.ex @@ -170,6 +170,10 @@ defmodule GenLSP do type: {:or, [:pid, :atom]}, doc: "The `t:pid/0` or name of the `GenLSP.Buffer` process." ], + assigns: [ + type: {:or, [:pid, :atom]}, + doc: "The `t:pid/0` or name of the `GenLSP.Assigns` process." + ], name: [ type: :atom, doc: @@ -188,7 +192,7 @@ defmodule GenLSP do opts = NimbleOptions.validate!(opts, @options_schema) :proc_lib.start_link(__MODULE__, :init, [ - {module, init_args, Keyword.take(opts, [:name, :buffer]), self()} + {module, init_args, Keyword.take(opts, [:name, :buffer, :assigns]), self()} ]) end @@ -196,7 +200,8 @@ defmodule GenLSP do def init({module, init_args, opts, parent}) do me = self() buffer = opts[:buffer] - lsp = %LSP{mod: module, pid: me, buffer: buffer} + assigns = opts[:assigns] + lsp = %LSP{mod: module, pid: me, buffer: buffer, assigns: assigns} case module.init(lsp, init_args) do {:ok, %LSP{} = lsp} -> diff --git a/lib/gen_lsp/assigns.ex b/lib/gen_lsp/assigns.ex new file mode 100644 index 0000000..48b97f6 --- /dev/null +++ b/lib/gen_lsp/assigns.ex @@ -0,0 +1,22 @@ +defmodule GenLSP.Assigns do + use Agent + + def start_link(opts \\ []) do + Agent.start_link(fn -> Map.new() end, Keyword.take(opts, [:name])) + end + + def get(agent) do + Agent.get(agent, & &1) + end + + def merge(agent, new_assigns) do + Agent.update(agent, &Map.merge(&1, Map.new(new_assigns))) + end + + def update(agent, callback) do + Agent.update(agent, fn assigns -> + new_assigns = callback.(assigns) + Map.merge(assigns, Map.new(new_assigns)) + end) + end +end diff --git a/lib/gen_lsp/lsp.ex b/lib/gen_lsp/lsp.ex index e229fdb..100f549 100644 --- a/lib/gen_lsp/lsp.ex +++ b/lib/gen_lsp/lsp.ex @@ -6,13 +6,26 @@ defmodule GenLSP.LSP do """ typedstruct do field :mod, atom(), enforce: true - field :assigns, map(), default: Map.new() field :buffer, atom() | pid() + field :assigns, atom() | pid() field :pid, pid() end - @spec assign(t(), Keyword.t()) :: t() + @spec assign(t(), Keyword.t() | (map() -> keyword())) :: t() def assign(%__MODULE__{assigns: assigns} = lsp, new_assigns) when is_list(new_assigns) do - %{lsp | assigns: Map.merge(assigns, Map.new(new_assigns))} + GenLSP.Assigns.merge(assigns, new_assigns) + + lsp + end + + def assign(%__MODULE__{assigns: assigns} = lsp, callback) when is_function(callback, 1) do + GenLSP.Assigns.update(assigns, callback) + + lsp + end + + @spec assigns(t()) :: map() + def assigns(%__MODULE__{assigns: assigns}) do + GenLSP.Assigns.get(assigns) end end diff --git a/lib/gen_lsp/test.ex b/lib/gen_lsp/test.ex index 9582ed5..a7f0b55 100644 --- a/lib/gen_lsp/test.ex +++ b/lib/gen_lsp/test.ex @@ -38,6 +38,7 @@ defmodule GenLSP.Test do @spec server(mod :: atom(), opts :: Keyword.t()) :: server() def server(mod, opts \\ []) do buffer_id = Keyword.get(opts, :buffer_id, :buffer) + assigns_id = Keyword.get(opts, :assigns_id, :assigns) lsp_id = Keyword.get(opts, :lsp_id, :lsp) buffer = @@ -45,11 +46,24 @@ defmodule GenLSP.Test do id: buffer_id ) + assigns = + start_supervised!(GenLSP.Assigns, id: assigns_id) + {:ok, port} = :inet.port(GenLSP.Buffer.comm_state(buffer).lsocket) - lsp = start_supervised!({mod, Keyword.merge([buffer: buffer], opts)}, id: lsp_id) + lsp = + start_supervised!({mod, Keyword.merge([buffer: buffer, assigns: assigns], opts)}, + id: lsp_id + ) - %{lsp: lsp, buffer: buffer, port: port, buffer_id: buffer_id, lsp_id: lsp_id} + %{ + lsp: lsp, + buffer: buffer, + assigns: assigns, + port: port, + buffer_id: buffer_id, + lsp_id: lsp_id + } end @doc """ diff --git a/test/gen_lsp/communication/tcp_test.exs b/test/gen_lsp/communication/tcp_test.exs index 6ba216a..ae1cff9 100644 --- a/test/gen_lsp/communication/tcp_test.exs +++ b/test/gen_lsp/communication/tcp_test.exs @@ -26,7 +26,7 @@ defmodule GenLSP.Communication.TCPTest do @string ~s|{"a":"‘","b":"#{String.duplicate("hello world! ", 5000)}"}| @length byte_size(@string) - @port 9000 + @port 5000 @connect_opts [:binary, packet: :raw, active: false] @@ -35,11 +35,10 @@ defmodule GenLSP.Communication.TCPTest do # the following match ensures that the script completes and does # not raise after stdin is closed. Task.start_link(fn -> - {:ok, args} = GenLSP.Communication.TCP.init(port: @port) - {:ok, args} = GenLSP.Communication.TCP.listen(args) - send(me, {:done, args}) + {:ok, tcp} = GenLSP.Communication.TCP.init(port: @port) + {:ok, tcp} = GenLSP.Communication.TCP.listen(tcp) - assert :eof = GenLSP.Support.Buffer.loop(args, me, "") + assert :eof = GenLSP.Support.Buffer.loop(tcp, me, "") end) assert {:ok, socket} = connect() diff --git a/test/gen_lsp_test.exs b/test/gen_lsp_test.exs index cae960a..1a24cdf 100644 --- a/test/gen_lsp_test.exs +++ b/test/gen_lsp_test.exs @@ -16,13 +16,7 @@ defmodule GenLSPTest do test "stores the user state and internal state", %{server: server} do assert alive?(server) - assert %GenLSP.LSP{ - assigns: %{foo: :bar, test_pid: self()}, - buffer: server.buffer, - pid: server.lsp, - mod: GenLSPTest.ExampleServer - } == - :sys.get_state(server.lsp) + assert %{foo: :bar, test_pid: self()} == :sys.get_state(server.assigns) end test "can receive and reply to a request", %{client: client} do diff --git a/test/support/example_server.ex b/test/support/example_server.ex index c67daba..f89744b 100644 --- a/test/support/example_server.ex +++ b/test/support/example_server.ex @@ -78,7 +78,7 @@ defmodule GenLSPTest.ExampleServer do } }) - send(lsp.assigns.test_pid, result) + send(assigns(lsp).test_pid, result) GenLSP.log(lsp, "done initializing") @@ -87,7 +87,7 @@ defmodule GenLSPTest.ExampleServer do @impl true def handle_notification(%Notifications.TextDocumentDidOpen{} = notification, lsp) do - send(lsp.assigns.test_pid, {:callback, notification}) + send(assigns(lsp).test_pid, {:callback, notification}) {:noreply, lsp} end @@ -98,7 +98,7 @@ defmodule GenLSPTest.ExampleServer do } = notification, lsp ) do - send(lsp.assigns.test_pid, {:callback, notification}) + send(assigns(lsp).test_pid, {:callback, notification}) GenLSP.notify(lsp, %Notifications.TextDocumentPublishDiagnostics{ params: %Structures.PublishDiagnosticsParams{ @@ -126,7 +126,7 @@ defmodule GenLSPTest.ExampleServer do end def handle_info(_message, lsp) do - send(lsp.assigns.test_pid, {:info, :ack}) + send(assigns(lsp).test_pid, {:info, :ack}) {:noreply, lsp} end end