Skip to content

Commit a2a1f38

Browse files
Improvements to struct field completion (#202)
* update elixir_sense * do not append : after struct field or map key in call syntax * tests added * according to LSP spec MarkupContent value should not be null * add readme section about completions * Apply suggestions from code review Co-Authored-By: Jason Axelson <[email protected]> * bump elixir_sense to resolve crash with non struct modules Co-authored-by: Jason Axelson <[email protected]>
1 parent fd1ab83 commit a2a1f38

File tree

5 files changed

+142
-8
lines changed

5 files changed

+142
-8
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ You can control which warnings are shown using the `elixirLS.dialyzerWarnOpts` s
8787

8888
ElixirLS's Dialyzer integration uses internal, undocumented Dialyzer APIs, and so it won't be robust against changes to these APIs in future Erlang versions.
8989

90+
## Code completion
91+
92+
ElixirLS bundles an advanced code completion provider. The provider builds on [Elixir Sense](https://github.com/elixir-lsp/elixir_sense) library and utilizes two main mechanisms. The first one is reflection - getting information about compiled modules from Erlang and Elixir APIs. The second one is AST analysis of the current text buffer. While reflection gives precise results, it is not well suited for on demand completion of symbols from the currently edited file. The compiled version is likely to be outdated or the file may not compile at all. AST analysis helps in that case but it has its limitations. Unfortunately it is infeasible to be 100% accurate, especially with Elixir being a metaprogramming heavy language.
93+
94+
The completions include:
95+
96+
- keywords
97+
- special form snippets
98+
- functions
99+
- macros
100+
- modules
101+
- variables
102+
- struct fields (only if the struct type is explicitly stated or can be inferred from the variable binding)
103+
- atom map keys (if map keys can be infered from variable binding)
104+
- attributes
105+
- types (in typespecs)
106+
- behaviour callbacks (inside the body of implementing module)
107+
- protocol functions (inside the body of implementing module)
108+
- keys in keyword functions arguments (if defined in spec)
109+
- function returns (if defined in spec)
110+
90111
## Workspace Symbols
91112

92113
With Dialyzer integration enabled ElixirLS will build an index of symbols (modules, functions, types and callbacks). The symbols are taken from the current workspace, all dependencies and stdlib (Elixir and erlang). This feature enables quick navigation to symbol definitions. However due to sheer number of different symbols and fuzzy search utilized by the provider, ElixirLS uses query prefixes to improve search results relevance.

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,21 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
313313
}
314314
end
315315

316-
defp from_completion_item(%{type: :field, name: name, origin: origin}, _context) do
316+
defp from_completion_item(
317+
%{type: :field, subtype: subtype, name: name, origin: origin, call?: call?},
318+
_context
319+
) do
320+
detail =
321+
case {subtype, origin} do
322+
{:map_key, _} -> "map key"
323+
{:struct_field, nil} -> "struct field"
324+
{:struct_field, module_name} -> "#{module_name} struct field"
325+
end
326+
317327
%__MODULE__{
318328
label: to_string(name),
319-
detail: "#{origin} struct field",
320-
insert_text: "#{name}: ",
329+
detail: detail,
330+
insert_text: if(call?, do: name, else: "#{name}: "),
321331
priority: 0,
322332
kind: :field,
323333
tags: []
@@ -623,7 +633,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
623633
"label" => item.label,
624634
"kind" => completion_kind(item.kind),
625635
"detail" => item.detail,
626-
"documentation" => %{"value" => item.documentation, kind: "markdown"},
636+
"documentation" => %{"value" => item.documentation || "", kind: "markdown"},
627637
"filterText" => item.filter_text,
628638
"sortText" => String.pad_leading(to_string(idx), 8, "0"),
629639
"insertText" => item.insert_text,

apps/language_server/lib/language_server/providers/definition.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do
55

66
alias ElixirLS.LanguageServer.SourceFile
77
alias ElixirLS.LanguageServer.Protocol
8-
alias ElixirSense.Providers.Definition.Location
98

109
def definition(uri, text, line, character) do
1110
case ElixirSense.definition(text, line + 1, character + 1) do
12-
%Location{found: false} ->
11+
%ElixirSense.Location{found: false} ->
1312
{:ok, []}
1413

15-
%Location{file: file, line: line, column: column} ->
14+
%ElixirSense.Location{file: file, line: line, column: column} ->
1615
line = line || 0
1716
column = column || 0
1817

apps/language_server/test/providers/completion_test.exs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,108 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
187187
assert %{"tags" => []} = get_deprecated_completion_item(tags_supported: [2])
188188
end
189189
end
190+
191+
describe "structs and maps" do
192+
test "returns struct fields in call syntax" do
193+
text = """
194+
defmodule MyModule do
195+
defstruct [some: nil, other: 1]
196+
197+
def dummy_function(var = %MyModule{}) do
198+
var.
199+
# ^
200+
end
201+
end
202+
"""
203+
204+
{line, char} = {4, 8}
205+
TestUtils.assert_has_cursor_char(text, line, char)
206+
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)
207+
208+
assert ["__struct__", "other", "some"] == items |> Enum.map(& &1["label"]) |> Enum.sort()
209+
assert (items |> hd)["detail"] == "MyModule struct field"
210+
end
211+
212+
test "returns map keys in call syntax" do
213+
text = """
214+
defmodule MyModule do
215+
def dummy_function(var = %{some: nil, other: 1}) do
216+
var.
217+
# ^
218+
end
219+
end
220+
"""
221+
222+
{line, char} = {2, 8}
223+
TestUtils.assert_has_cursor_char(text, line, char)
224+
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)
225+
226+
assert ["other", "some"] == items |> Enum.map(& &1["label"]) |> Enum.sort()
227+
assert (items |> hd)["detail"] == "map key"
228+
end
229+
230+
test "returns struct fields in update syntax" do
231+
text = """
232+
defmodule MyModule do
233+
defstruct [some: nil, other: 1]
234+
235+
def dummy_function(var = %MyModule{}) do
236+
%{var |
237+
# ^
238+
end
239+
end
240+
"""
241+
242+
{line, char} = {4, 11}
243+
TestUtils.assert_has_cursor_char(text, line, char)
244+
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)
245+
246+
assert ["__struct__", "other", "some"] ==
247+
items |> Enum.filter(&(&1["kind"] == 5)) |> Enum.map(& &1["label"]) |> Enum.sort()
248+
249+
assert (items |> hd)["detail"] == "MyModule struct field"
250+
end
251+
252+
test "returns map keys in update syntax" do
253+
text = """
254+
defmodule MyModule do
255+
def dummy_function(var = %{some: nil, other: 1}) do
256+
%{var |
257+
# ^
258+
end
259+
end
260+
"""
261+
262+
{line, char} = {2, 11}
263+
TestUtils.assert_has_cursor_char(text, line, char)
264+
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)
265+
266+
assert ["other", "some"] ==
267+
items |> Enum.filter(&(&1["kind"] == 5)) |> Enum.map(& &1["label"]) |> Enum.sort()
268+
269+
assert (items |> hd)["detail"] == "map key"
270+
end
271+
272+
test "returns struct fields in definition syntax" do
273+
text = """
274+
defmodule MyModule do
275+
defstruct [some: nil, other: 1]
276+
277+
def dummy_function() do
278+
%MyModule{}
279+
# ^
280+
end
281+
end
282+
"""
283+
284+
{line, char} = {4, 14}
285+
TestUtils.assert_has_cursor_char(text, line, char)
286+
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)
287+
288+
assert ["__struct__", "other", "some"] ==
289+
items |> Enum.filter(&(&1["kind"] == 5)) |> Enum.map(& &1["label"]) |> Enum.sort()
290+
291+
assert (items |> hd)["detail"] == "MyModule struct field"
292+
end
293+
end
190294
end

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
%{
22
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
33
"docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"},
4-
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "f89444dd713520acb5d97a98b6d24386c04b9ae8", []},
4+
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "6395dd568e542a8b05a2d3dee61f02142564f30d", []},
55
"erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []},
66
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
77
"forms": {:hex, :forms, "0.0.1", "45f3b10b6f859f95f2c2c1a1de244d63855296d55ed8e93eb0dd116b3e86c4a6", [:rebar3], [], "hexpm", "530f63ed8ed5a171f744fc75bd69cb2e36496899d19dbef48101b4636b795868"},

0 commit comments

Comments
 (0)