Skip to content

Commit 1b7bba6

Browse files
committed
Added code generators for LSP data structures
Adding the LSP data structures by hand was growing tedious and there were a couple bugs. Doing this programatically seems like it would be safer and easier in the long run. Right now, all of the data structures are mapped, which will likely make compile times a bit longer, but mapping things as needed would add a lot of complexity to something that only needs to be done occasionally. Note: as of now, I haven't run this and checked in any artifacts. That will happen in a subsequent PR.
1 parent 29b91a6 commit 1b7bba6

File tree

19 files changed

+17493
-0
lines changed

19 files changed

+17493
-0
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,31 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
8888
end
8989
end
9090

91+
def extract({:tuple, tuple_types}, field_name, field_value) when is_list(field_value) do
92+
result =
93+
field_value
94+
|> Enum.zip(tuple_types)
95+
|> Enum.reduce_while([], fn {value, type}, acc ->
96+
case extract(type, field_name, value) do
97+
{:ok, value} -> {:cont, [value | acc]}
98+
error -> {:halt, error}
99+
end
100+
end)
101+
102+
case result do
103+
value when is_list(value) ->
104+
value_as_tuple =
105+
value
106+
|> Enum.reverse()
107+
|> List.to_tuple()
108+
109+
{:ok, value_as_tuple}
110+
111+
error ->
112+
error
113+
end
114+
end
115+
91116
def extract({:params, param_defs}, _field_name, field_value)
92117
when is_map(field_value) do
93118
result =
@@ -168,6 +193,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
168193
{:ok, float_value}
169194
end
170195

196+
def encode(:float, field_value) when is_float(field_value) do
197+
field_value
198+
end
199+
171200
def encode(:string, field_value) when is_binary(field_value) do
172201
{:ok, field_value}
173202
end
@@ -191,6 +220,13 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.Field do
191220
end
192221
end
193222

223+
def encode({:tuple, types}, field_value) when is_tuple(field_value) do
224+
field_value
225+
|> Tuple.to_list()
226+
|> Enum.zip(types)
227+
|> Enum.map(fn {value, type} -> encode(type, value) end)
228+
end
229+
194230
def encode({:params, param_defs}, field_value) when is_map(field_value) do
195231
param_fields =
196232
Enum.reduce_while(param_defs, [], fn {param_name, param_type}, acc ->

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Proto.TypeFunctions do
3131
{:list, type}
3232
end
3333

34+
def tuple_of(types) when is_list(types) do
35+
{:tuple, types}
36+
end
37+
3438
def map_of(type, opts \\ []) do
3539
field_name = Keyword.get(opts, :as)
3640
{:map, type, field_name}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
defmodule Mix.Tasks.Lsp.DataModel do
2+
alias Mix.Tasks.Lsp.DataModel.Enumeration
3+
alias Mix.Tasks.Lsp.DataModel.Structure
4+
alias Mix.Tasks.Lsp.DataModel.TypeAlias
5+
6+
defstruct names_to_types: %{},
7+
notifications: %{},
8+
requests: %{},
9+
structures: %{},
10+
type_aliases: %{},
11+
enumerations: %{}
12+
13+
def new do
14+
with {:ok, root_meta} <- load_meta_model() do
15+
names_to_types =
16+
root_meta
17+
|> Map.take(~w(structures enumerations typeAliases))
18+
|> Enum.flat_map(fn {type, list_of_things} ->
19+
Enum.map(list_of_things, fn %{"name" => name} -> {name, type_name(type)} end)
20+
end)
21+
|> Map.new()
22+
23+
type_aliases = load_from_meta(root_meta, "typeAliases", &TypeAlias.new/1)
24+
enumerations = load_from_meta(root_meta, "enumerations", &Enumeration.new/1)
25+
structures = load_from_meta(root_meta, "structures", &Structure.new/1)
26+
27+
data_model = %__MODULE__{
28+
names_to_types: names_to_types,
29+
enumerations: enumerations,
30+
type_aliases: type_aliases,
31+
structures: structures
32+
}
33+
34+
{:ok, data_model}
35+
end
36+
end
37+
38+
def all_types(%__MODULE__{} = data_model) do
39+
data_model.type_aliases
40+
|> Map.merge(data_model.enumerations)
41+
|> Map.merge(data_model.structures)
42+
end
43+
44+
def fetch(%__MODULE__{} = data_model, name) do
45+
field =
46+
case kind(data_model, name) do
47+
:structure -> :structures
48+
:type_alias -> :type_aliases
49+
:enumeration -> :enumerations
50+
end
51+
52+
data_model
53+
|> Map.get(field, %{})
54+
|> Map.fetch(name)
55+
|> case do
56+
{:ok, %element_module{} = element} ->
57+
{:ok, element_module.resolve(element, data_model)}
58+
59+
:error ->
60+
:error
61+
end
62+
end
63+
64+
def fetch!(%__MODULE__{} = data_model, name) do
65+
case fetch(data_model, name) do
66+
{:ok, thing} -> thing
67+
:error -> raise "Could not find type #{name}"
68+
end
69+
end
70+
71+
defp load_from_meta(root_meta, name, new_fn) do
72+
root_meta
73+
|> Map.get(name)
74+
|> Map.new(fn definition ->
75+
loaded = new_fn.(definition)
76+
{loaded.name, loaded}
77+
end)
78+
end
79+
80+
defp kind(%__MODULE__{} = data_model, name) do
81+
Map.fetch!(data_model.names_to_types, name)
82+
end
83+
84+
defp type_name("structures"), do: :structure
85+
defp type_name("enumerations"), do: :enumeration
86+
defp type_name("typeAliases"), do: :type_alias
87+
88+
@meta_model_file_name "metamodel.3.17.json"
89+
defp load_meta_model do
90+
file_name =
91+
__ENV__.file
92+
|> Path.dirname()
93+
|> Path.join([@meta_model_file_name])
94+
95+
with {:ok, file_contents} <- File.read(file_name) do
96+
JasonVendored.decode(file_contents)
97+
end
98+
end
99+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
defmodule Mix.Tasks.Lsp.DataModel.Enumeration do
2+
defmodule Value do
3+
defstruct [:name, :value, :documentation]
4+
5+
def new(%{"name" => name, "value" => value} = value_meta) do
6+
docs = value_meta["documentation"]
7+
%__MODULE__{name: name, value: value, documentation: docs}
8+
end
9+
end
10+
11+
alias Mix.Tasks.Lsp.Mappings
12+
alias Mix.Tasks.Lsp.DataModel
13+
alias Mix.Tasks.Lsp.DataModel.Type
14+
defstruct [:name, :values, :type]
15+
16+
def new(%{"name" => name, "type" => type, "values" => values}) do
17+
%__MODULE__{
18+
name: name,
19+
type: Type.new(name, type),
20+
values: Enum.map(values, &Value.new/1)
21+
}
22+
end
23+
24+
def to_protocol(%__MODULE__{} = enumeration, _, _) do
25+
module_name = Module.concat([enumeration.name])
26+
quote(do: unquote(module_name))
27+
end
28+
29+
def resolve(%__MODULE__{} = enumeration, %DataModel{} = data_model) do
30+
%__MODULE__{enumeration | type: Type.resolve(enumeration.type, data_model)}
31+
end
32+
33+
def build_definition(
34+
%__MODULE__{} = enumeration,
35+
%Mappings{} = mappings,
36+
%DataModel{}
37+
) do
38+
with {:ok, destination_module} <-
39+
Mappings.fetch_destination_module(mappings, enumeration.name) do
40+
values =
41+
Enum.map(enumeration.values, fn value ->
42+
name = value.name |> Macro.underscore() |> String.to_atom()
43+
quote(do: {unquote(name), unquote(value.value)})
44+
end)
45+
46+
ast =
47+
quote do
48+
defmodule unquote(destination_module) do
49+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto
50+
use Proto
51+
52+
defenum unquote(values)
53+
end
54+
end
55+
56+
{:ok, ast}
57+
end
58+
end
59+
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule Mix.Tasks.Lsp.DataModel.Notification do
2+
defstruct [:method, :direction, :params, :documentation]
3+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule Mix.Tasks.Lsp.DataModel.Property do
2+
alias Mix.Tasks.Lsp.DataModel
3+
alias Mix.Tasks.Lsp.Mappings
4+
alias Mix.Tasks.Lsp.DataModel.Type
5+
6+
defstruct [:name, :type, :required?, :references, :documentation]
7+
8+
def new(%{"name" => name, "type" => type} = property_meta) do
9+
required? = !Map.get(property_meta, "optional", false)
10+
11+
keys = Keyword.merge([name: name, required?: required?], type: Type.new(name, type))
12+
struct(__MODULE__, keys)
13+
end
14+
15+
def resolve(%__MODULE__{} = property, %DataModel{} = data_model) do
16+
%__MODULE__{property | type: Type.resolve(property.type, data_model)}
17+
end
18+
19+
def to_protocol(%__MODULE__{} = property, %DataModel{} = data_model, %Mappings{} = mappings) do
20+
underscored = property.name |> Macro.underscore() |> String.to_atom()
21+
type_call = Type.to_protocol(property.type, data_model, mappings)
22+
23+
if property.required? do
24+
quote(do: {unquote(underscored), unquote(type_call)})
25+
else
26+
quote(do: {unquote(underscored), optional(unquote(type_call))})
27+
end
28+
end
29+
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule Mix.Tasks.Lsp.DataModel.Request do
2+
defstruct [:method, :result, :direction, :params]
3+
end
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
defmodule Mix.Tasks.Lsp.DataModel.Structure do
2+
alias Mix.Tasks.Lsp.DataModel.Type.ObjectLiteral
3+
alias Mix.Tasks.Lsp.Mappings
4+
alias Mix.Tasks.Lsp.DataModel
5+
alias Mix.Tasks.Lsp.DataModel.Type
6+
alias Mix.Tasks.Lsp.DataModel.Property
7+
8+
defstruct name: nil, documentation: nil, properties: nil, definition: nil, module: nil
9+
10+
def new(%{"name" => name, "properties" => _} = definition) do
11+
%__MODULE__{
12+
name: name,
13+
documentation: definition[:documentation],
14+
definition: definition,
15+
module: Module.concat([name])
16+
}
17+
end
18+
19+
def to_protocol(%__MODULE__{} = structure, _, %Mappings{} = _mappings) do
20+
quote(do: unquote(structure.module))
21+
end
22+
23+
def build_definition(
24+
%__MODULE__{} = structure,
25+
%Mappings{} = mappings,
26+
%DataModel{} = data_model
27+
) do
28+
with {:ok, destination_module} <- Mappings.fetch_destination_module(mappings, structure.name) do
29+
structure = resolve(structure, data_model)
30+
object_literals = Type.collect_object_literals(structure, data_model)
31+
32+
literal_definitions =
33+
Enum.map(object_literals, &ObjectLiteral.build_definition(&1, data_model, mappings))
34+
35+
protocol_properties =
36+
Enum.map(structure.properties, &Property.to_protocol(&1, data_model, mappings))
37+
38+
ast =
39+
quote do
40+
defmodule unquote(destination_module) do
41+
alias ElixirLS.LanguageServer.Experimental.Protocol.Proto
42+
unquote_splicing(literal_definitions)
43+
44+
use Proto
45+
deftype unquote(protocol_properties)
46+
end
47+
end
48+
49+
{:ok, ast}
50+
end
51+
end
52+
53+
def resolve(%__MODULE__{properties: properties} = structure) when is_list(properties) do
54+
structure
55+
end
56+
57+
def resolve(%__MODULE__{} = structure, %DataModel{} = data_model) do
58+
%__MODULE__{structure | properties: properties(structure, data_model)}
59+
end
60+
61+
def properties(%__MODULE__{properties: properties}) when is_list(properties) do
62+
properties
63+
end
64+
65+
def properties(%__MODULE__{} = structure, %DataModel{} = data_model) do
66+
property_list(structure, data_model)
67+
end
68+
69+
defp resolve_remote_properties(%__MODULE__{} = structure, %DataModel{} = data_model) do
70+
[]
71+
|> add_extends(structure.definition)
72+
|> add_mixins(structure.definition)
73+
|> Enum.flat_map(fn %{"kind" => "reference", "name" => type_name} ->
74+
data_model
75+
|> DataModel.fetch!(type_name)
76+
|> property_list(data_model)
77+
end)
78+
end
79+
80+
defp property_list(%__MODULE__{} = structure, %DataModel{} = data_model) do
81+
base_properties =
82+
structure.definition
83+
|> Map.get("properties")
84+
|> Enum.map(&Property.new/1)
85+
86+
base_property_names = MapSet.new(base_properties, & &1.name)
87+
88+
# Note: The reject here is so that properties defined in the
89+
# current structure override those defined in mixins and extends
90+
resolved_remote_properties =
91+
structure
92+
|> resolve_remote_properties(data_model)
93+
|> Enum.reject(&(&1.name in base_property_names))
94+
95+
base_properties
96+
|> Enum.concat(resolved_remote_properties)
97+
|> Enum.sort_by(& &1.name)
98+
end
99+
100+
defp add_extends(queue, %{"extends" => extends}) do
101+
extends ++ queue
102+
end
103+
104+
defp add_extends(queue, _), do: queue
105+
106+
defp add_mixins(queue, %{"mixins" => mixins}) do
107+
mixins ++ queue
108+
end
109+
110+
defp add_mixins(queue, _), do: queue
111+
end

0 commit comments

Comments
 (0)