Skip to content

Commit f38c761

Browse files
authored
feat: server to client requests (#35)
Fixes #34
1 parent b280433 commit f38c761

File tree

13 files changed

+201
-28
lines changed

13 files changed

+201
-28
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,22 @@ jobs:
5050

5151
formatter:
5252
runs-on: ubuntu-latest
53-
name: Formatter (1.14.x.x/25.x)
53+
name: Formatter (1.15.x.x/26.x)
5454

5555
steps:
5656
- uses: actions/checkout@v2
5757
- uses: erlef/setup-beam@v1
5858
with:
59-
otp-version: 25.x
60-
elixir-version: 1.14.x
59+
otp-version: 26.x
60+
elixir-version: 1.15.x
6161
- uses: actions/cache@v3
6262
with:
6363
path: |
6464
deps
6565
_build
66-
key: ${{ runner.os }}-mix-25-1.14-${{ hashFiles('**/mix.lock') }}
66+
key: ${{ runner.os }}-mix-24-1.15-${{ hashFiles('**/mix.lock') }}
6767
restore-keys: |
68-
${{ runner.os }}-mix-25-1.14-
68+
${{ runner.os }}-mix-24-1.15-
6969
7070
- name: Install Dependencies
7171
run: mix deps.get
@@ -81,8 +81,8 @@ jobs:
8181
id: beam
8282
uses: erlef/setup-beam@v1
8383
with:
84-
otp-version: 25.x
85-
elixir-version: 1.14.x
84+
otp-version: 26.x
85+
elixir-version: 1.15.x
8686

8787
# Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
8888
# Cache key based on Elixir & Erlang version (also useful when running in matrix)

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
elixir 1.15.0
1+
elixir 1.15.2
22
erlang 26.0.2

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ GenLSP is an OTP behaviour for building processes that implement the [Language S
1414
<details>
1515
<summary><a href="https://github.com/rrrene/credo">Credo</a> language server.</summary>
1616

17-
```elixir
17+
<pre>
1818
defmodule Credo.Lsp do
1919
@moduledoc """
2020
LSP implementation for Credo.
@@ -202,7 +202,7 @@ defmodule Credo.Lsp.Cache do
202202
def category_to_severity(:consistency), do: 4
203203
def category_to_severity(:readability), do: 4
204204
end
205-
```
205+
</pre>
206206

207207
</details>
208208

lib/gen_lsp.ex

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,25 +190,42 @@ defmodule GenLSP do
190190
send(pid, {:notification, from, notification})
191191
end
192192

193-
@doc """
193+
@doc ~S'''
194194
Sends a notification to the client from the LSP process.
195195
196196
## Usage
197197
198198
```elixir
199199
GenLSP.notify(lsp, %TextDocumentPublishDiagnostics{
200200
params: %PublishDiagnosticsParams{
201-
uri: "file://#\{file}",
201+
uri: "file://#{file}",
202202
diagnostics: diagnostics
203203
}
204204
})
205205
```
206-
"""
206+
'''
207207
@spec notify(GenLSP.LSP.t(), notification :: any()) :: :ok
208208
def notify(%{buffer: buffer}, notification) do
209209
GenLSP.Buffer.outgoing(buffer, dump!(notification.__struct__.schematic(), notification))
210210
end
211211

212+
@doc ~S'''
213+
Sends a request to the client from the LSP process.
214+
215+
## Usage
216+
217+
```elixir
218+
GenLSP.request(lsp, %ClientRegisterCapability{
219+
id: System.unique_integer([:positive]),
220+
params: params
221+
})
222+
```
223+
'''
224+
@spec request(GenLSP.LSP.t(), request :: any()) :: any()
225+
def request(%{buffer: buffer}, request) do
226+
GenLSP.Buffer.outgoing_sync(buffer, dump!(request.__struct__.schematic(), request))
227+
end
228+
212229
defp write_debug(device, event, name) do
213230
IO.write(device, "#{inspect(name)} event = #{inspect(event)}")
214231
end
@@ -287,7 +304,7 @@ defmodule GenLSP do
287304
end
288305
end
289306

290-
@spec attempt(LSP.t(), String.t(), (() -> any())) :: no_return()
307+
@spec attempt(LSP.t(), String.t(), (-> any())) :: no_return()
291308
defp attempt(lsp, message, callback) do
292309
callback.()
293310
rescue

lib/gen_lsp/buffer.ex

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ defmodule GenLSP.Buffer do
4242
GenServer.cast(server, {:outgoing, packet})
4343
end
4444

45+
@doc false
46+
def outgoing_sync(server, packet) do
47+
GenServer.call(server, {:outgoing_sync, packet})
48+
end
49+
4550
@doc false
4651
def comm_state(server) do
4752
GenServer.call(server, :comm_state)
@@ -52,23 +57,38 @@ defmodule GenLSP.Buffer do
5257
{comm, comm_args} = opts[:communication]
5358
{:ok, comm_data} = comm.init(comm_args)
5459

55-
{:ok, %{comm: comm, comm_data: comm_data}}
60+
{:ok, %{comm: comm, comm_data: comm_data, awaiting_response: Map.new()}}
5661
end
5762

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

68+
def handle_call({:outgoing_sync, %{"id" => id} = packet}, from, state) do
69+
:ok = state.comm.write(Jason.encode!(packet), state.comm_data)
70+
71+
{:noreply, %{state | awaiting_response: Map.put(state.awaiting_response, id, from)}}
72+
end
73+
6374
@doc false
6475
def handle_cast({:incoming, packet}, %{lsp: lsp} = state) do
65-
case Jason.decode!(packet) do
66-
%{"id" => _} = request ->
67-
GenLSP.request_server(lsp, request)
68-
69-
notification ->
70-
GenLSP.notify_server(lsp, notification)
71-
end
76+
state =
77+
case Jason.decode!(packet) do
78+
%{"id" => id, "result" => result} when is_map_key(state.awaiting_response, id) ->
79+
{from, awaiting_response} = Map.pop(state.awaiting_response, id)
80+
GenServer.reply(from, result)
81+
82+
%{state | awaiting_response: awaiting_response}
83+
84+
%{"id" => _} = request ->
85+
GenLSP.request_server(lsp, request)
86+
state
87+
88+
notification ->
89+
GenLSP.notify_server(lsp, notification)
90+
state
91+
end
7292

7393
{:noreply, state}
7494
end

lib/gen_lsp/protocol/type_aliases/lsp_any.ex

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ defmodule GenLSP.TypeAlias.LSPAny do
1616
@doc false
1717
@spec schematic() :: Schematic.t()
1818
def schematic() do
19-
any()
19+
%Schematic{
20+
kind: "lspany",
21+
unify: fn x, dir ->
22+
case x do
23+
%mod{} ->
24+
Code.ensure_loaded(mod)
25+
26+
if function_exported?(mod, :schematic, 0) do
27+
mod.schematic().unify.(x, dir)
28+
else
29+
{:ok, x}
30+
end
31+
32+
_ ->
33+
{:ok, x}
34+
end
35+
end
36+
}
2037
end
2138
end

lib/gen_lsp/test.ex

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,57 @@ defmodule GenLSP.Test do
267267
end
268268
end
269269

270+
@doc ~S"""
271+
Assert on a request that was sent from the server.
272+
273+
## Usage
274+
275+
```elixir
276+
assert_request(client, "client/registerCapability", 1000, fn params ->
277+
assert params == %{
278+
"registrations" => [
279+
%{
280+
"id" => "file-watching",
281+
"method" => "workspace/didChangeWatchedFiles",
282+
"registerOptions" => %{
283+
"watchers" => [
284+
%{
285+
"globPattern" => "{lib|test}/**/*.{ex|exs|heex|eex|leex|surface}"
286+
}
287+
]
288+
}
289+
}
290+
]
291+
}
292+
293+
nil
294+
end)
295+
```
296+
"""
297+
defmacro assert_request(
298+
client,
299+
method,
300+
timeout \\ Application.get_env(:ex_unit, :assert_receive_timeout),
301+
callback
302+
) do
303+
quote do
304+
assert_receive %{
305+
"jsonrpc" => "2.0",
306+
"id" => id,
307+
"method" => unquote(method),
308+
"params" => params
309+
},
310+
unquote(timeout)
311+
312+
result = unquote(callback).(params)
313+
314+
GenLSP.Communication.TCP.write(
315+
Jason.encode!(%{jsonrpc: "2.0", id: id, result: result}),
316+
unquote(client)
317+
)
318+
end
319+
end
320+
270321
defp connect(port, start_time) do
271322
now = System.monotonic_time(:millisecond)
272323

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule GenLSP.MixProject do
3939
{:jason, "~> 1.3"},
4040
{:nimble_options, "~> 0.5 or ~> 1.0"},
4141
# {:schematic, path: "../schematic"},
42-
{:schematic, "~> 0.2"},
42+
{:schematic, "~> 0.2.1"},
4343
{:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}
4444
]
4545
end

mix.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%{
2-
"dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
2+
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
33
"earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"},
44
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
55
"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"},
@@ -9,7 +9,7 @@
99
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
1010
"nimble_options": {:hex, :nimble_options, "1.0.1", "b448018287b22584e91b5fd9c6c0ad717cb4bcdaa457957c8d57770f56625c43", [:mix], [], "hexpm", "078b2927cd9f84555be6386d56e849b0c555025ecccf7afee00ab6a9e6f63837"},
1111
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
12-
"schematic": {:hex, :schematic, "0.2.0", "ac710efbd98b8f4b3d137f8ebac6f9a17da917bb4d1296b487ac4157fb74c806", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4bc93bac2e7d04869fd6ced9df82c092c154fc648677512bc7c75d9a2655be3"},
12+
"schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"},
1313
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
1414
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
1515
}

test/gen_lsp/communication/stdio_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ end
3636
Main.run()'"
3737

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

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

0 commit comments

Comments
 (0)