diff --git a/lib/elixir_sense/core/builtin_attributes.ex b/lib/elixir_sense/core/builtin_attributes.ex index db955cc5..dbb77331 100644 --- a/lib/elixir_sense/core/builtin_attributes.ex +++ b/lib/elixir_sense/core/builtin_attributes.ex @@ -6,7 +6,6 @@ defmodule ElixirSense.Core.BuiltinAttributes do impl derive enforce_keys - struct compile deprecated dialyzer @@ -28,7 +27,39 @@ defmodule ElixirSense.Core.BuiltinAttributes do typedoc doc moduledoc + for + protocol )a def all, do: @list + + def docs(attribute) when attribute in @list do + case Module.reserved_attributes() do + %{^attribute => %{doc: doc}} -> + doc + + _ -> + # Older Elixir versions haven't document these attributes + # Reference: https://github.com/elixir-lang/elixir/commit/14c55d15afbf08b0d8289a4399a15b4109b6ac5a + case attribute do + :enforce_keys -> + "Ensures the given keys are always set when building the struct defined in the current module." + + :fallback_to_any -> + "If set to `true` generates a default protocol implementation " <> + "for all types (inside `defprotocol`)." + + :for -> + "The current module/type a protocol implementation is being defined for (inside `defimpl`)." + + :protocol -> + "The current protocol being implemented (inside `defimpl`)." + + _ -> + nil + end + end + end + + def docs(_), do: nil end diff --git a/lib/elixir_sense/providers/docs.ex b/lib/elixir_sense/providers/docs.ex index 17b84cdf..410b8684 100644 --- a/lib/elixir_sense/providers/docs.ex +++ b/lib/elixir_sense/providers/docs.ex @@ -3,6 +3,7 @@ defmodule ElixirSense.Providers.Docs do Doc Provider """ alias ElixirSense.Core.Binding + alias ElixirSense.Core.BuiltinAttributes alias ElixirSense.Core.Introspection alias ElixirSense.Core.State alias ElixirSense.Core.SurroundContext @@ -31,17 +32,24 @@ defmodule ElixirSense.Providers.Docs do type = SurroundContext.to_binding(context, module) - mod_fun_docs( - type, - binding_env, - imports, - requires, - aliases, - module, - mods_funs, - metadata_types, - scope - ) + case type do + {:attribute, attribute} -> + docs = BuiltinAttributes.docs(attribute) + if docs, do: {"@" <> Atom.to_string(attribute), %{docs: docs}} + + _ -> + mod_fun_docs( + type, + binding_env, + imports, + requires, + aliases, + module, + mods_funs, + metadata_types, + scope + ) + end end defp mod_fun_docs( @@ -72,8 +80,6 @@ defmodule ElixirSense.Providers.Docs do end end - # TODO attr - defp mod_fun_docs( {mod, fun}, binding_env, diff --git a/lib/elixir_sense/providers/suggestion/complete.ex b/lib/elixir_sense/providers/suggestion/complete.ex index 90328939..e58c277a 100644 --- a/lib/elixir_sense/providers/suggestion/complete.ex +++ b/lib/elixir_sense/providers/suggestion/complete.ex @@ -412,10 +412,16 @@ defmodule ElixirSense.Providers.Suggestion.Complete do attribute_name when is_atom(attribute_name) <- attribute_names, name = Atom.to_string(attribute_name), Matcher.match?(name, hint), - do: name + do: attribute_name ) |> Enum.sort() - |> Enum.map(&%{kind: :attribute, name: &1}) + |> Enum.map( + &%{ + kind: :attribute, + name: Atom.to_string(&1), + summary: BuiltinAttributes.docs(&1) + } + ) |> format_expansion() end @@ -1042,8 +1048,8 @@ defmodule ElixirSense.Providers.Suggestion.Complete do [%{type: :variable, name: name}] end - defp to_entries(%{kind: :attribute, name: name}) do - [%{type: :attribute, name: "@" <> name}] + defp to_entries(%{kind: :attribute, name: name, summary: summary}) do + [%{type: :attribute, name: "@" <> name, summary: summary}] end defp to_entries(%{ diff --git a/lib/elixir_sense/providers/suggestion/reducers/common.ex b/lib/elixir_sense/providers/suggestion/reducers/common.ex index 91d8081b..a1124d8d 100644 --- a/lib/elixir_sense/providers/suggestion/reducers/common.ex +++ b/lib/elixir_sense/providers/suggestion/reducers/common.ex @@ -9,7 +9,8 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do @type attribute :: %{ type: :attribute, - name: String.t() + name: String.t(), + summary: String.t() } @type variable :: %{ diff --git a/test/elixir_sense/docs_test.exs b/test/elixir_sense/docs_test.exs index a4c6b33b..557af9f3 100644 --- a/test/elixir_sense/docs_test.exs +++ b/test/elixir_sense/docs_test.exs @@ -1016,4 +1016,29 @@ defmodule ElixirSense.DocsTest do Flattens the given `list` of nested lists. """ end + + test "retrieve reserved module attributes documentation" do + buffer = """ + defmodule MyModule do + @on_load :on_load + + def on_load(), do: :ok + end + """ + + assert %{ + actual_subject: "@on_load", + docs: %{docs: "A hook that will be invoked whenever the module is loaded."} + } = ElixirSense.docs(buffer, 2, 6) + end + + test "retrieve unreserved module attributes documentation" do + buffer = """ + defmodule MyModule do + @my_attribute nil + end + """ + + refute ElixirSense.docs(buffer, 2, 6) + end end diff --git a/test/elixir_sense/providers/suggestion/complete_test.exs b/test/elixir_sense/providers/suggestion/complete_test.exs index 6ed764ec..c75e5c60 100644 --- a/test/elixir_sense/providers/suggestion/complete_test.exs +++ b/test/elixir_sense/providers/suggestion/complete_test.exs @@ -1036,16 +1036,19 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do scope: {:some, 0} } - assert expand(~c"@numb", env) == [%{type: :attribute, name: "@number"}] + assert expand(~c"@numb", env) == [%{type: :attribute, name: "@number", summary: nil}] assert expand(~c"@num", env) == - [%{type: :attribute, name: "@number"}, %{type: :attribute, name: "@numeral"}] + [ + %{type: :attribute, name: "@number", summary: nil}, + %{type: :attribute, name: "@numeral", summary: nil} + ] assert expand(~c"@", env) == [ - %{name: "@nothing", type: :attribute}, - %{type: :attribute, name: "@number"}, - %{type: :attribute, name: "@numeral"} + %{name: "@nothing", type: :attribute, summary: nil}, + %{type: :attribute, name: "@number", summary: nil}, + %{type: :attribute, name: "@numeral", summary: nil} ] end @@ -1069,7 +1072,13 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do assert expand(~c"@befo", env_outside_module) == [] assert expand(~c"@befo", env_module) == - [%{type: :attribute, name: "@before_compile"}] + [ + %{ + type: :attribute, + name: "@before_compile", + summary: "A hook that will be invoked before the module is compiled." + } + ] end test "kernel special form completion" do diff --git a/test/elixir_sense/suggestions_test.exs b/test/elixir_sense/suggestions_test.exs index 8714cc54..70533225 100644 --- a/test/elixir_sense/suggestions_test.exs +++ b/test/elixir_sense/suggestions_test.exs @@ -1513,53 +1513,92 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 5, 9) end - test "lists attributes" do - buffer = """ - defmodule MyModule do - @my_attribute1 true - @my_attribute2 false - @ - end - """ + describe "suggestions for module attributes" do + test "lists attributes" do + buffer = """ + defmodule MyModule do + @my_attribute1 true + @my_attribute2 false + @ + end + """ - list = - ElixirSense.suggestions(buffer, 4, 4) - |> Enum.filter(fn s -> s.type == :attribute and s.name |> String.starts_with?("@my") end) + list = + ElixirSense.suggestions(buffer, 4, 4) + |> Enum.filter(fn s -> s.type == :attribute and s.name |> String.starts_with?("@my") end) + |> Enum.map(fn %{name: name} -> name end) - assert list == [ - %{name: "@my_attribute1", type: :attribute}, - %{name: "@my_attribute2", type: :attribute} - ] - end + assert list == ["@my_attribute1", "@my_attribute2"] + end - test "lists module attributes in module scope" do - buffer = """ - defmodule MyModule do - @myattr "asd" - @moduledoc "asdf" - def some do - @m + test "lists module attributes in module scope" do + buffer = """ + defmodule MyModule do + @myattr "asd" + @moduledoc "asdf" + def some do + @m + end end + """ + + list = + ElixirSense.suggestions(buffer, 2, 5) + |> Enum.filter(fn s -> s.type == :attribute end) + |> Enum.map(fn %{name: name} -> name end) + + assert list == ["@macrocallback", "@moduledoc", "@myattr"] + + list = + ElixirSense.suggestions(buffer, 5, 7) + |> Enum.filter(fn s -> s.type == :attribute end) + |> Enum.map(fn %{name: name} -> name end) + + assert list == ["@myattr"] end - """ - list = - ElixirSense.suggestions(buffer, 2, 5) - |> Enum.filter(fn s -> s.type == :attribute end) + test "built-in attributes should include documentation" do + buffer = """ + defmodule MyModule do + @call + @enfor + end + """ - assert list == [ - %{name: "@macrocallback", type: :attribute}, - %{name: "@moduledoc", type: :attribute}, - %{name: "@myattr", type: :attribute} - ] + list = + ElixirSense.suggestions(buffer, 2, 7) + |> Enum.filter(fn s -> s.type == :attribute end) - list = - ElixirSense.suggestions(buffer, 5, 7) - |> Enum.filter(fn s -> s.type == :attribute end) + assert [%{summary: "Provides a specification for a behaviour callback."}] = list - assert list == [ - %{name: "@myattr", type: :attribute} - ] + list = + ElixirSense.suggestions(buffer, 3, 8) + |> Enum.filter(fn s -> s.type == :attribute end) + + assert [ + %{ + summary: + "Ensures the given keys are always set when building the struct defined in the current module." + } + ] = list + end + + test "non built-in attributes should not include documentation" do + buffer = """ + defmodule MyModule do + @myattr "asd" + def some do + @m + end + end + """ + + list = + ElixirSense.suggestions(buffer, 4, 6) + |> Enum.filter(fn s -> s.type == :attribute end) + + assert [%{summary: nil}] = list + end end test "lists builtin module attributes on incomplete code" do