Skip to content

Commit

Permalink
feat: server to client requests (#35)
Browse files Browse the repository at this point in the history
Fixes #34
  • Loading branch information
mhanberg authored Jul 14, 2023
1 parent b280433 commit f38c761
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 28 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,22 @@ jobs:

formatter:
runs-on: ubuntu-latest
name: Formatter (1.14.x.x/25.x)
name: Formatter (1.15.x.x/26.x)

steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: 25.x
elixir-version: 1.14.x
otp-version: 26.x
elixir-version: 1.15.x
- uses: actions/cache@v3
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-25-1.14-${{ hashFiles('**/mix.lock') }}
key: ${{ runner.os }}-mix-24-1.15-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-25-1.14-
${{ runner.os }}-mix-24-1.15-
- name: Install Dependencies
run: mix deps.get
Expand All @@ -81,8 +81,8 @@ jobs:
id: beam
uses: erlef/setup-beam@v1
with:
otp-version: 25.x
elixir-version: 1.14.x
otp-version: 26.x
elixir-version: 1.15.x

# Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
# Cache key based on Elixir & Erlang version (also useful when running in matrix)
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.15.0
elixir 1.15.2
erlang 26.0.2
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ GenLSP is an OTP behaviour for building processes that implement the [Language S
<details>
<summary><a href="https://github.com/rrrene/credo">Credo</a> language server.</summary>

```elixir
<pre>
defmodule Credo.Lsp do
@moduledoc """
LSP implementation for Credo.
Expand Down Expand Up @@ -202,7 +202,7 @@ defmodule Credo.Lsp.Cache do
def category_to_severity(:consistency), do: 4
def category_to_severity(:readability), do: 4
end
```
</pre>

</details>

Expand Down
25 changes: 21 additions & 4 deletions lib/gen_lsp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,25 +190,42 @@ defmodule GenLSP do
send(pid, {:notification, from, notification})
end

@doc """
@doc ~S'''
Sends a notification to the client from the LSP process.
## Usage
```elixir
GenLSP.notify(lsp, %TextDocumentPublishDiagnostics{
params: %PublishDiagnosticsParams{
uri: "file://#\{file}",
uri: "file://#{file}",
diagnostics: diagnostics
}
})
```
"""
'''
@spec notify(GenLSP.LSP.t(), notification :: any()) :: :ok
def notify(%{buffer: buffer}, notification) do
GenLSP.Buffer.outgoing(buffer, dump!(notification.__struct__.schematic(), notification))
end

@doc ~S'''
Sends a request to the client from the LSP process.
## Usage
```elixir
GenLSP.request(lsp, %ClientRegisterCapability{
id: System.unique_integer([:positive]),
params: params
})
```
'''
@spec request(GenLSP.LSP.t(), request :: any()) :: any()
def request(%{buffer: buffer}, request) do
GenLSP.Buffer.outgoing_sync(buffer, dump!(request.__struct__.schematic(), request))
end

defp write_debug(device, event, name) do
IO.write(device, "#{inspect(name)} event = #{inspect(event)}")
end
Expand Down Expand Up @@ -287,7 +304,7 @@ defmodule GenLSP do
end
end

@spec attempt(LSP.t(), String.t(), (() -> any())) :: no_return()
@spec attempt(LSP.t(), String.t(), (-> any())) :: no_return()
defp attempt(lsp, message, callback) do
callback.()
rescue
Expand Down
36 changes: 28 additions & 8 deletions lib/gen_lsp/buffer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ defmodule GenLSP.Buffer do
GenServer.cast(server, {:outgoing, packet})
end

@doc false
def outgoing_sync(server, packet) do
GenServer.call(server, {:outgoing_sync, packet})
end

@doc false
def comm_state(server) do
GenServer.call(server, :comm_state)
Expand All @@ -52,23 +57,38 @@ defmodule GenLSP.Buffer do
{comm, comm_args} = opts[:communication]
{:ok, comm_data} = comm.init(comm_args)

{:ok, %{comm: comm, comm_data: comm_data}}
{:ok, %{comm: comm, comm_data: comm_data, awaiting_response: Map.new()}}
end

@doc false
def handle_call(:comm_state, _from, %{comm_data: comm_data} = state) do
{:reply, comm_data, state}
end

def handle_call({:outgoing_sync, %{"id" => id} = packet}, from, state) do
:ok = state.comm.write(Jason.encode!(packet), state.comm_data)

{:noreply, %{state | awaiting_response: Map.put(state.awaiting_response, id, from)}}
end

@doc false
def handle_cast({:incoming, packet}, %{lsp: lsp} = state) do
case Jason.decode!(packet) do
%{"id" => _} = request ->
GenLSP.request_server(lsp, request)

notification ->
GenLSP.notify_server(lsp, notification)
end
state =
case Jason.decode!(packet) do
%{"id" => id, "result" => result} when is_map_key(state.awaiting_response, id) ->
{from, awaiting_response} = Map.pop(state.awaiting_response, id)
GenServer.reply(from, result)

%{state | awaiting_response: awaiting_response}

%{"id" => _} = request ->
GenLSP.request_server(lsp, request)
state

notification ->
GenLSP.notify_server(lsp, notification)
state
end

{:noreply, state}
end
Expand Down
19 changes: 18 additions & 1 deletion lib/gen_lsp/protocol/type_aliases/lsp_any.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ defmodule GenLSP.TypeAlias.LSPAny do
@doc false
@spec schematic() :: Schematic.t()
def schematic() do
any()
%Schematic{
kind: "lspany",
unify: fn x, dir ->
case x do
%mod{} ->
Code.ensure_loaded(mod)

if function_exported?(mod, :schematic, 0) do
mod.schematic().unify.(x, dir)
else
{:ok, x}
end

_ ->
{:ok, x}
end
end
}
end
end
51 changes: 51 additions & 0 deletions lib/gen_lsp/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,57 @@ defmodule GenLSP.Test do
end
end

@doc ~S"""
Assert on a request that was sent from the server.
## Usage
```elixir
assert_request(client, "client/registerCapability", 1000, fn params ->
assert params == %{
"registrations" => [
%{
"id" => "file-watching",
"method" => "workspace/didChangeWatchedFiles",
"registerOptions" => %{
"watchers" => [
%{
"globPattern" => "{lib|test}/**/*.{ex|exs|heex|eex|leex|surface}"
}
]
}
}
]
}
nil
end)
```
"""
defmacro assert_request(
client,
method,
timeout \\ Application.get_env(:ex_unit, :assert_receive_timeout),
callback
) do
quote do
assert_receive %{
"jsonrpc" => "2.0",
"id" => id,
"method" => unquote(method),
"params" => params
},
unquote(timeout)

result = unquote(callback).(params)

GenLSP.Communication.TCP.write(
Jason.encode!(%{jsonrpc: "2.0", id: id, result: result}),
unquote(client)
)
end
end

defp connect(port, start_time) do
now = System.monotonic_time(:millisecond)

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ defmodule GenLSP.MixProject do
{:jason, "~> 1.3"},
{:nimble_options, "~> 0.5 or ~> 1.0"},
# {:schematic, path: "../schematic"},
{:schematic, "~> 0.2"},
{:schematic, "~> 0.2.1"},
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}
]
end
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
%{
"dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"},
Expand All @@ -9,7 +9,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"nimble_options": {:hex, :nimble_options, "1.0.1", "b448018287b22584e91b5fd9c6c0ad717cb4bcdaa457957c8d57770f56625c43", [:mix], [], "hexpm", "078b2927cd9f84555be6386d56e849b0c555025ecccf7afee00ab6a9e6f63837"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"schematic": {:hex, :schematic, "0.2.0", "ac710efbd98b8f4b3d137f8ebac6f9a17da917bb4d1296b487ac4157fb74c806", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4bc93bac2e7d04869fd6ced9df82c092c154fc648677512bc7c75d9a2655be3"},
"schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
}
2 changes: 1 addition & 1 deletion test/gen_lsp/communication/stdio_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ end
Main.run()'"

test "can read and write through stdio" do
port = Port.open({:spawn, @command}, [:binary, env: [{'MIX_ENV', 'test'}]])
port = Port.open({:spawn, @command}, [:binary, env: [{~c"MIX_ENV", ~c"test"}]])

expected_message = "Content-Length: #{@length}\r\n\r\n#{@string}"

Expand Down
2 changes: 1 addition & 1 deletion test/gen_lsp/communication/tcp_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ defmodule GenLSP.Communication.TCPTest do
defp connect(start_time) do
now = System.monotonic_time(:millisecond)

case :gen_tcp.connect('localhost', @port, @connect_opts) do
case :gen_tcp.connect(~c"localhost", @port, @connect_opts) do
{:error, :econnrefused} when now - start_time > 5000 ->
connect(start_time)

Expand Down
43 changes: 43 additions & 0 deletions test/gen_lsp_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,49 @@ defmodule GenLSPTest do
500
end

test "can send a request from the server to the client", %{client: client} do
id = System.unique_integer([:positive])

assert :ok ==
request(client, %{
"jsonrpc" => "2.0",
"method" => "initialize",
"params" => %{"capabilities" => %{}},
"id" => id
})

assert_result ^id, %{"capabilities" => %{}, "serverInfo" => %{"name" => "Test LSP"}}, 500

assert :ok ==
notify(client, %{
method: "initialized",
jsonrpc: "2.0",
params: %{}
})

assert_request(client, "client/registerCapability", 1000, fn params ->
assert params == %{
"registrations" => [
%{
"id" => "file-watching",
"method" => "workspace/didChangeWatchedFiles",
"registerOptions" => %{
"watchers" => [
%{
"globPattern" => "{lib|test}/**/*.{ex|exs|heex|eex|leex|surface}"
}
]
}
}
]
}

nil
end)

assert_notification "window/logMessage", %{"message" => "done initializing"}, 500
end

test "the server can receive a notification", %{client: client} do
assert :ok ==
notify(client, %{
Expand Down
25 changes: 25 additions & 0 deletions test/support/example_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ defmodule GenLSPTest.ExampleServer do
}, lsp}
end

def handle_notification(%Notifications.Initialized{}, lsp) do
GenLSP.request(lsp, %GenLSP.Requests.ClientRegisterCapability{
id: System.unique_integer([:positive]),
params: %GenLSP.Structures.RegistrationParams{
registrations: [
%GenLSP.Structures.Registration{
id: "file-watching",
method: "workspace/didChangeWatchedFiles",
register_options: %GenLSP.Structures.DidChangeWatchedFilesRegistrationOptions{
watchers: [
%GenLSP.Structures.FileSystemWatcher{
glob_pattern: "{lib|test}/**/*.{ex|exs|heex|eex|leex|surface}"
}
]
}
}
]
}
})

GenLSP.log(lsp, "done initializing")

{:noreply, lsp}
end

@impl true
def handle_notification(%Notifications.TextDocumentDidOpen{} = notification, lsp) do
send(lsp.assigns.test_pid, {:callback, notification})
Expand Down

0 comments on commit f38c761

Please sign in to comment.