Skip to content

Improvements to struct field completion #202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ You can control which warnings are shown using the `elixirLS.dialyzerWarnOpts` s

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.

## Code completion

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.

The completions include:

- keywords
- special form snippets
- functions
- macros
- modules
- variables
- struct fields (only if the struct type is explicitly stated or can be inferred from the variable binding)
- atom map keys (if map keys can be infered from variable binding)
- attributes
- types (in typespecs)
- behaviour callbacks (inside the body of implementing module)
- protocol functions (inside the body of implementing module)
- keys in keyword functions arguments (if defined in spec)
- function returns (if defined in spec)

## Workspace Symbols

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.
Expand Down
18 changes: 14 additions & 4 deletions apps/language_server/lib/language_server/providers/completion.ex
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,21 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
}
end

defp from_completion_item(%{type: :field, name: name, origin: origin}, _context) do
defp from_completion_item(
%{type: :field, subtype: subtype, name: name, origin: origin, call?: call?},
_context
) do
detail =
case {subtype, origin} do
{:map_key, _} -> "map key"
{:struct_field, nil} -> "struct field"
{:struct_field, module_name} -> "#{module_name} struct field"
end

%__MODULE__{
label: to_string(name),
detail: "#{origin} struct field",
insert_text: "#{name}: ",
detail: detail,
insert_text: if(call?, do: name, else: "#{name}: "),
priority: 0,
kind: :field,
tags: []
Expand Down Expand Up @@ -623,7 +633,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
"label" => item.label,
"kind" => completion_kind(item.kind),
"detail" => item.detail,
"documentation" => %{"value" => item.documentation, kind: "markdown"},
"documentation" => %{"value" => item.documentation || "", kind: "markdown"},
"filterText" => item.filter_text,
"sortText" => String.pad_leading(to_string(idx), 8, "0"),
"insertText" => item.insert_text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do

alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.Protocol
alias ElixirSense.Providers.Definition.Location

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

%Location{file: file, line: line, column: column} ->
%ElixirSense.Location{file: file, line: line, column: column} ->
line = line || 0
column = column || 0

Expand Down
104 changes: 104 additions & 0 deletions apps/language_server/test/providers/completion_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,108 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
assert %{"tags" => []} = get_deprecated_completion_item(tags_supported: [2])
end
end

describe "structs and maps" do
test "returns struct fields in call syntax" do
text = """
defmodule MyModule do
defstruct [some: nil, other: 1]

def dummy_function(var = %MyModule{}) do
var.
# ^
end
end
"""

{line, char} = {4, 8}
TestUtils.assert_has_cursor_char(text, line, char)
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)

assert ["__struct__", "other", "some"] == items |> Enum.map(& &1["label"]) |> Enum.sort()
assert (items |> hd)["detail"] == "MyModule struct field"
end

test "returns map keys in call syntax" do
text = """
defmodule MyModule do
def dummy_function(var = %{some: nil, other: 1}) do
var.
# ^
end
end
"""

{line, char} = {2, 8}
TestUtils.assert_has_cursor_char(text, line, char)
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)

assert ["other", "some"] == items |> Enum.map(& &1["label"]) |> Enum.sort()
assert (items |> hd)["detail"] == "map key"
end

test "returns struct fields in update syntax" do
text = """
defmodule MyModule do
defstruct [some: nil, other: 1]

def dummy_function(var = %MyModule{}) do
%{var |
# ^
end
end
"""

{line, char} = {4, 11}
TestUtils.assert_has_cursor_char(text, line, char)
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)

assert ["__struct__", "other", "some"] ==
items |> Enum.filter(&(&1["kind"] == 5)) |> Enum.map(& &1["label"]) |> Enum.sort()

assert (items |> hd)["detail"] == "MyModule struct field"
end

test "returns map keys in update syntax" do
text = """
defmodule MyModule do
def dummy_function(var = %{some: nil, other: 1}) do
%{var |
# ^
end
end
"""

{line, char} = {2, 11}
TestUtils.assert_has_cursor_char(text, line, char)
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)

assert ["other", "some"] ==
items |> Enum.filter(&(&1["kind"] == 5)) |> Enum.map(& &1["label"]) |> Enum.sort()

assert (items |> hd)["detail"] == "map key"
end

test "returns struct fields in definition syntax" do
text = """
defmodule MyModule do
defstruct [some: nil, other: 1]

def dummy_function() do
%MyModule{}
# ^
end
end
"""

{line, char} = {4, 14}
TestUtils.assert_has_cursor_char(text, line, char)
{:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports)

assert ["__struct__", "other", "some"] ==
items |> Enum.filter(&(&1["kind"] == 5)) |> Enum.map(& &1["label"]) |> Enum.sort()

assert (items |> hd)["detail"] == "MyModule struct field"
end
end
end
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
%{
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
"docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"},
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "f89444dd713520acb5d97a98b6d24386c04b9ae8", []},
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "6395dd568e542a8b05a2d3dee61f02142564f30d", []},
"erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"forms": {:hex, :forms, "0.0.1", "45f3b10b6f859f95f2c2c1a1de244d63855296d55ed8e93eb0dd116b3e86c4a6", [:rebar3], [], "hexpm", "530f63ed8ed5a171f744fc75bd69cb2e36496899d19dbef48101b4636b795868"},
Expand Down