Skip to content

Commit 704f42f

Browse files
committed
Change how requests and notfications are structured
There are a number of conversions related to document positions and document realization that need to happen when a request is serviced. Future request handlers would be made much simpler and more correct if the system would handle these conversions for writers of provider handlers. To wit, each request and notification module now has an internal `lsp` field that contains the initial request, while the surounding request represents the elixir side of the data. To convert to elixir, all one has to do is use the `to_elixir` function. The elixir struct that encloses the lsp struct has all of the same fields, and if the enclosing struct has a text document, an additional `source_file` field is added. All the fields on the elixir side are initialized to nil until `to_elixir` is called. When to_elixir is invoked, it scans the lsp document for text_documents, which are fetched from the source file store. This document is used to influence any ranges or positions and convert them into their elixir-native data structures. Finally, all of the other fields are copied into the elixir side.
1 parent dfd1af3 commit 704f42f

File tree

6 files changed

+353
-70
lines changed

6 files changed

+353
-70
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Convert do
2+
alias ElixirLS.LanguageServer.Experimental.SourceFile.Conversions
3+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types
4+
alias ElixirLS.LanguageServer.Experimental.SourceFile
5+
6+
def to_elixir(%{text_document: _} = request) do
7+
with {:ok, source_file} <- fetch_source_file(request.lsp),
8+
{:ok, updates} <- convert(request.lsp, source_file) do
9+
updated_request =
10+
request
11+
|> Map.put(:source_file, source_file)
12+
|> Map.merge(updates)
13+
14+
{:ok, updated_request}
15+
end
16+
end
17+
18+
def to_elixir(request) do
19+
request = Map.merge(request, Map.from_struct(request.lsp))
20+
21+
{:ok, request}
22+
end
23+
24+
defp fetch_source_file(%{text_document: %Types.TextDocument{} = text_document}) do
25+
with {:ok, source_file} <- SourceFile.Store.fetch(text_document.uri) do
26+
{:ok, source_file}
27+
end
28+
end
29+
30+
defp fetch_source_file(%{source_file: %SourceFile{} = source_file}) do
31+
{:ok, source_file}
32+
end
33+
34+
defp fetch_source_file(_), do: :error
35+
36+
defp convert(%{range: range}, source_file) do
37+
with {:ok, ex_range} <- Conversions.to_elixir(range, source_file) do
38+
{:ok, %{range: ex_range}}
39+
end
40+
end
41+
42+
defp convert(%{position: position}, source_file) do
43+
with {:ok, ex_pos} <- Conversions.to_elixir(position, source_file) do
44+
{:ok, %{position: ex_pos}}
45+
end
46+
end
47+
48+
defp convert(_, _) do
49+
{:ok, %{}}
50+
end
51+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.Message do
2+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.{
3+
Access,
4+
Struct,
5+
Parse,
6+
Typespec,
7+
Meta
8+
}
9+
10+
alias ElixirLS.LanguageServer.Experimental.SourceFile
11+
12+
def build(meta_type, method, types, param_names, opts \\ []) do
13+
parse_fn =
14+
if Keyword.get(opts, :include_parse?, true) do
15+
Parse.build(types)
16+
end
17+
18+
quote do
19+
unquote(Access.build())
20+
unquote(Struct.build(types))
21+
unquote(Typespec.build())
22+
unquote(parse_fn)
23+
unquote(Meta.build(types))
24+
25+
def __meta__(:method_name) do
26+
unquote(method)
27+
end
28+
29+
def __meta__(:type) do
30+
unquote(meta_type)
31+
end
32+
33+
def __meta__(:param_names) do
34+
unquote(param_names)
35+
end
36+
end
37+
end
38+
39+
def generate_elixir_types(caller_module, message_types) do
40+
message_types
41+
|> Enum.reduce(message_types, fn
42+
{:text_document, _}, types ->
43+
Keyword.put(types, :source_file, quote(do: SourceFile))
44+
45+
{:position, _}, types ->
46+
Keyword.put(types, :position, quote(do: SourceFile.Position))
47+
48+
{:range, _}, types ->
49+
Keyword.put(types, :range, quote(do: SourceFile.Range))
50+
51+
_, types ->
52+
types
53+
end)
54+
|> Keyword.put(:lsp, quote(do: unquote(caller_module).LSP))
55+
end
56+
end

apps/language_server/lib/language_server/experimental/protocol/proto/notification.ex

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Notification do
22
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata
3-
4-
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.{
5-
Access,
6-
Meta,
7-
Parse,
8-
Struct,
9-
Typespec
10-
}
3+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.Message
114

125
defmacro defnotification(method, types) do
136
CompileMetadata.add_notification_module(__CALLER__.module)
@@ -17,43 +10,40 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Notification do
1710
method: quote(do: literal(unquote(method)))
1811
]
1912

20-
all_types = Keyword.merge(jsonrpc_types, types)
13+
param_names = Keyword.keys(types)
14+
lsp_types = Keyword.merge(jsonrpc_types, types)
15+
elixir_types = Message.generate_elixir_types(__CALLER__.module, lsp_types)
2116

2217
quote location: :keep do
23-
unquote(Access.build())
24-
unquote(Struct.build(all_types))
25-
unquote(Typespec.build())
26-
unquote(build_notification_parse_function(method))
27-
unquote(Parse.build(types))
28-
unquote(Meta.build(all_types))
29-
30-
def __meta__(:method_name) do
31-
unquote(method)
18+
defmodule LSP do
19+
unquote(Message.build(:notification, method, lsp_types, param_names))
3220
end
3321

34-
def __meta__(:type) do
35-
:notification
36-
end
22+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Convert
3723

38-
def __meta__(:param_names) do
39-
unquote(Keyword.keys(types))
24+
unquote(
25+
Message.build(:notification, method, elixir_types, param_names, include_parse?: false)
26+
)
27+
28+
unquote(build_parse(method))
29+
30+
def to_elixir(%__MODULE__{} = request) do
31+
Convert.to_elixir(request)
4032
end
4133
end
4234
end
4335

44-
defp build_notification_parse_function(method) do
36+
defp build_parse(method) do
4537
quote do
4638
def parse(%{"method" => unquote(method), "jsonrpc" => "2.0"} = request) do
4739
params = Map.get(request, "params", %{})
40+
flattened_notificaiton = Map.merge(request, params)
4841

49-
case parse(params) do
50-
{:ok, result} ->
51-
result =
52-
result
53-
|> Map.put(:method, unquote(method))
54-
|> Map.put(:jsonrpc, "2.0")
55-
56-
{:ok, result}
42+
case LSP.parse(flattened_notificaiton) do
43+
{:ok, raw_lsp} ->
44+
struct_opts = [method: unquote(method), jsonrpc: "2.0", lsp: raw_lsp]
45+
notification = struct(__MODULE__, struct_opts)
46+
{:ok, notification}
5747

5848
error ->
5949
error

apps/language_server/lib/language_server/experimental/protocol/proto/request.ex

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
11
defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Request do
22
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.CompileMetadata
3+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.Message
4+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions
35

4-
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Macros.{
5-
Access,
6-
Struct,
7-
Parse,
8-
Typespec,
9-
Meta
10-
}
6+
import TypeFunctions, only: [optional: 1, integer: 0, literal: 1]
117

128
defmacro defrequest(method, types) do
13-
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions
14-
import TypeFunctions, only: [optional: 1, integer: 0, literal: 1]
15-
169
CompileMetadata.add_request_module(__CALLER__.module)
17-
1810
# id is optional so we can resuse the parse function. If it's required,
1911
# it will go in the pattern match for the params, which won't work.
2012

@@ -24,43 +16,38 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Request do
2416
method: quote(do: literal(unquote(method)))
2517
]
2618

27-
all_types = Keyword.merge(jsonrpc_types, types)
19+
lsp_types = Keyword.merge(jsonrpc_types, types)
20+
elixir_types = Message.generate_elixir_types(__CALLER__.module, lsp_types)
21+
param_names = Keyword.keys(types)
2822

2923
quote location: :keep do
30-
unquote(Access.build())
31-
unquote(Struct.build(all_types))
32-
unquote(Typespec.build())
33-
unquote(build_request_parse_function(method))
34-
unquote(Parse.build(types))
35-
unquote(Meta.build(all_types))
36-
37-
def __meta__(:method_name) do
38-
unquote(method)
24+
defmodule LSP do
25+
unquote(Message.build(:request, method, lsp_types, param_names))
3926
end
4027

41-
def __meta__(:type) do
42-
:request
43-
end
28+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Convert
29+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types
4430

45-
def __meta__(:param_names) do
46-
unquote(Keyword.keys(types))
31+
unquote(Message.build(:request, method, elixir_types, param_names, include_parse?: false))
32+
33+
unquote(build_parse(method))
34+
35+
def to_elixir(%__MODULE__{} = request) do
36+
Convert.to_elixir(request)
4737
end
4838
end
4939
end
5040

51-
defp build_request_parse_function(method) do
41+
defp build_parse(method) do
5242
quote do
5343
def parse(%{"method" => unquote(method), "id" => id, "jsonrpc" => "2.0"} = request) do
5444
params = Map.get(request, "params", %{})
45+
flattened_request = Map.merge(request, params)
5546

56-
case parse(params) do
57-
{:ok, request} ->
58-
request =
59-
request
60-
|> Map.put(:id, id)
61-
|> Map.put(:method, unquote(method))
62-
|> Map.put(:jsonrpc, "2.0")
63-
47+
case LSP.parse(flattened_request) do
48+
{:ok, raw_lsp} ->
49+
struct_opts = [id: id, method: unquote(method), jsonrpc: "2.0", lsp: raw_lsp]
50+
request = struct(__MODULE__, struct_opts)
6451
{:ok, request}
6552

6653
error ->

apps/language_server/lib/language_server/experimental/source_file/conversions.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
4040
to_elixir(ls_range, source)
4141
end
4242

43+
def to_elixir(%LSPosition{} = position, %SourceFile{} = source_file) do
44+
to_elixir(position, source_file.document)
45+
end
46+
4347
def to_elixir(%ElixirPosition{} = position, _) do
4448
position
4549
end

0 commit comments

Comments
 (0)