Skip to content

Commit f4e2311

Browse files
committed
complete record fields
1 parent 607bae6 commit f4e2311

File tree

6 files changed

+241
-2
lines changed

6 files changed

+241
-2
lines changed

apps/language_server/lib/language_server/providers/completion.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
723723
{:map_key, _} -> "map key"
724724
{:struct_field, nil} -> "struct field"
725725
{:struct_field, module_name} -> "#{module_name} struct field"
726+
{:record_field, module_and_record} -> "#{module_and_record} record field"
726727
end
727728

728729
formatted_spec =
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Record do
2+
@moduledoc false
3+
4+
alias ElixirSense.Core.Introspection
5+
alias ElixirSense.Core.Metadata
6+
alias ElixirSense.Core.Source
7+
alias ElixirSense.Core.State
8+
alias ElixirSense.Core.State.{RecordInfo, TypeInfo}
9+
alias ElixirLS.Utils.Matcher
10+
11+
@type field :: %{
12+
type: :field,
13+
subtype: :record_field,
14+
name: String.t(),
15+
origin: String.t() | nil,
16+
call?: boolean,
17+
type_spec: String.t() | nil
18+
}
19+
20+
@doc """
21+
A reducer that adds suggestions of record fields.
22+
"""
23+
def add_fields(hint, env, buffer_metadata, context, acc) do
24+
text_before = context.text_before
25+
26+
case find_record_fields(hint, text_before, env, buffer_metadata, context.cursor_position) do
27+
{[], _} ->
28+
{:cont, acc}
29+
30+
{fields, nil} ->
31+
{:halt, %{acc | result: fields}}
32+
33+
{fields, :maybe_record_update} ->
34+
reducers = [
35+
:populate_complete_engine,
36+
:modules,
37+
:functions,
38+
:macros,
39+
:variables,
40+
:attributes
41+
]
42+
43+
{:cont, %{acc | result: fields, reducers: reducers}}
44+
end
45+
end
46+
47+
defp find_record_fields(hint, text_before, env, metadata, cursor_position) do
48+
%State.Env{
49+
module: module,
50+
vars: vars,
51+
attributes: attributes
52+
} = env
53+
54+
%Metadata{
55+
structs: structs,
56+
records: records,
57+
mods_funs_to_positions: mods_funs,
58+
types: metadata_types,
59+
specs: specs
60+
} = metadata
61+
62+
binding_env = %ElixirSense.Core.Binding{
63+
attributes: attributes,
64+
variables: vars,
65+
structs: structs,
66+
functions: env.functions,
67+
macros: env.macros,
68+
current_module: module,
69+
specs: specs,
70+
types: metadata_types,
71+
mods_funs: mods_funs
72+
}
73+
74+
# check if we are inside local or remote call arguments and parameter is 0, 1 or 2
75+
# record fields can specified on 0, 1 and 2 position in the argument list
76+
# TODO implement retrieval from docs chunks on 1.18
77+
# right now only local buffer records are supported as there is no suitable API for introspection
78+
# @__records__ is compile time only attribute and accessing it would require a tracer
79+
with %{
80+
candidate: {m, f},
81+
npar: npar,
82+
elixir_prefix: elixir_prefix,
83+
options_so_far: options_so_far,
84+
option: nil,
85+
cursor_at_option: cursor_at_option
86+
}
87+
when npar < 2 <-
88+
Source.which_func(text_before, binding_env),
89+
{mod, fun, true, :mod_fun} <-
90+
Introspection.actual_mod_fun(
91+
{m, f},
92+
env,
93+
metadata.mods_funs_to_positions,
94+
metadata.types,
95+
cursor_position,
96+
not elixir_prefix
97+
),
98+
%RecordInfo{} = info <- records[{mod, fun}] do
99+
fields = get_fields(hint, mod, fun, info.fields, options_so_far, metadata_types)
100+
101+
{fields, if(npar == 0 and cursor_at_option in [false, :maybe], do: :maybe_record_update)}
102+
else
103+
_o ->
104+
{[], nil}
105+
end
106+
end
107+
108+
defp get_fields(hint, module, record_name, fields, fields_so_far, types) do
109+
field_types = get_field_types(types, module, record_name)
110+
111+
for {key, _value} when is_atom(key) <- fields,
112+
key not in fields_so_far,
113+
key_str = Atom.to_string(key),
114+
Matcher.match?(key_str, hint) do
115+
type_spec =
116+
case Keyword.get(field_types, key, nil) do
117+
nil -> nil
118+
some -> Introspection.to_string_with_parens(some)
119+
end
120+
121+
%{
122+
type: :field,
123+
name: key_str,
124+
subtype: :record_field,
125+
origin: "#{inspect(module)}.#{record_name}",
126+
type_spec: type_spec,
127+
call?: false
128+
}
129+
end
130+
|> Enum.sort_by(& &1.name)
131+
end
132+
133+
defp get_field_types(types, module, record) do
134+
# assume there is a type record_name or record_name_t or t
135+
with %TypeInfo{specs: [spec | _]} <-
136+
types[{module, record, 0}] || types[{module, :"#{record}_t", 0}] ||
137+
types[{module, :t, 0}],
138+
{:ok, ast} <- Code.string_to_quoted(spec),
139+
{:@, _,
140+
[
141+
{:type, _,
142+
[
143+
{:"::", _,
144+
[
145+
{_type_name, _, []},
146+
{:record, _,
147+
[
148+
_tag,
149+
field_types
150+
]}
151+
]}
152+
]}
153+
]} <- ast do
154+
field_types
155+
else
156+
_ -> []
157+
end
158+
end
159+
end

apps/language_server/lib/language_server/providers/completion/suggestion.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Suggestion do
7171
generic()
7272
| Reducers.CompleteEngine.t()
7373
| Reducers.Struct.field()
74+
| Reducers.Record.field()
7475
| Reducers.Returns.return()
7576
| Reducers.Callbacks.callback()
7677
| Reducers.Protocol.protocol_function()
@@ -88,6 +89,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Suggestion do
8889

8990
@reducers [
9091
structs_fields: &Reducers.Struct.add_fields/5,
92+
record_fields: &Reducers.Record.add_fields/5,
9193
returns: &Reducers.Returns.add_returns/5,
9294
callbacks: &Reducers.Callbacks.add_callbacks/5,
9395
protocol_functions: &Reducers.Protocol.add_functions/5,

apps/language_server/test/providers/completion/suggestions_test.exs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4800,6 +4800,83 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.SuggestionTest do
48004800
] = suggestions |> Enum.filter(&(&1.name == "user"))
48014801
end
48024802

4803+
test "records from metadata fields" do
4804+
buffer = """
4805+
defmodule SomeSchema do
4806+
require Record
4807+
Record.defrecord(:user, name: "john", age: 25)
4808+
@type user :: record(:user, name: String.t(), age: integer)
4809+
4810+
def d() do
4811+
w = user()
4812+
w = user(n)
4813+
user(w, n)
4814+
user(w, name: "1", a)
4815+
end
4816+
end
4817+
"""
4818+
4819+
suggestions = Suggestion.suggestions(buffer, 7, 14)
4820+
4821+
assert [
4822+
%{
4823+
name: "age",
4824+
origin: "SomeSchema.user",
4825+
type: :field,
4826+
call?: false,
4827+
subtype: :record_field,
4828+
type_spec: "integer()"
4829+
},
4830+
%{
4831+
name: "name",
4832+
origin: "SomeSchema.user",
4833+
type: :field,
4834+
call?: false,
4835+
subtype: :record_field,
4836+
type_spec: "String.t()"
4837+
}
4838+
] = suggestions |> Enum.filter(&(&1.type == :field))
4839+
4840+
suggestions = Suggestion.suggestions(buffer, 8, 15)
4841+
4842+
assert [
4843+
%{
4844+
name: "name",
4845+
origin: "SomeSchema.user",
4846+
type: :field,
4847+
call?: false,
4848+
subtype: :record_field,
4849+
type_spec: "String.t()"
4850+
}
4851+
] = suggestions |> Enum.filter(&(&1.type == :field))
4852+
4853+
suggestions = Suggestion.suggestions(buffer, 9, 14)
4854+
4855+
assert [
4856+
%{
4857+
name: "name",
4858+
origin: "SomeSchema.user",
4859+
type: :field,
4860+
call?: false,
4861+
subtype: :record_field,
4862+
type_spec: "String.t()"
4863+
}
4864+
] = suggestions |> Enum.filter(&(&1.type == :field))
4865+
4866+
suggestions = Suggestion.suggestions(buffer, 10, 25)
4867+
4868+
assert [
4869+
%{
4870+
name: "age",
4871+
origin: "SomeSchema.user",
4872+
type: :field,
4873+
call?: false,
4874+
subtype: :record_field,
4875+
type_spec: "integer()"
4876+
}
4877+
] = suggestions |> Enum.filter(&(&1.type == :field))
4878+
end
4879+
48034880
defp suggestions_by_type(type, buffer) do
48044881
{line, column} = get_last_line_and_column(buffer)
48054882
suggestions_by_type(type, buffer, line, column)

dep_versions.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[
2-
elixir_sense: "ac76304976b22efe39d8d0d55e26797dd114a9f7",
2+
elixir_sense: "6c293f27ea3afc4f5913a7ed03a07db0fa15a0a2",
33
dialyxir_vendored: "f8f64cfb6797c518294687e7c03ae817bacbc6ee",
44
jason_v: "f1c10fa9c445cb9f300266122ef18671054b2330",
55
erl2ex_vendored: "073ac6b9a44282e718b6050c7b27cedf9217a12a",

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
33
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
44
"dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "f8f64cfb6797c518294687e7c03ae817bacbc6ee", [ref: "f8f64cfb6797c518294687e7c03ae817bacbc6ee"]},
5-
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "ac76304976b22efe39d8d0d55e26797dd114a9f7", [ref: "ac76304976b22efe39d8d0d55e26797dd114a9f7"]},
5+
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "6c293f27ea3afc4f5913a7ed03a07db0fa15a0a2", [ref: "6c293f27ea3afc4f5913a7ed03a07db0fa15a0a2"]},
66
"erl2ex_vendored": {:git, "https://github.com/elixir-lsp/erl2ex.git", "073ac6b9a44282e718b6050c7b27cedf9217a12a", [ref: "073ac6b9a44282e718b6050c7b27cedf9217a12a"]},
77
"erlex_vendored": {:git, "https://github.com/elixir-lsp/erlex.git", "c0e448db27bcbb3f369861d13e3b0607ed37048d", [ref: "c0e448db27bcbb3f369861d13e3b0607ed37048d"]},
88
"jason_v": {:git, "https://github.com/elixir-lsp/jason.git", "f1c10fa9c445cb9f300266122ef18671054b2330", [ref: "f1c10fa9c445cb9f300266122ef18671054b2330"]},

0 commit comments

Comments
 (0)