Skip to content
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

Add documentation retrieval for module's built-in attributes #233

Merged
merged 2 commits into from
Jul 7, 2023
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
33 changes: 32 additions & 1 deletion lib/elixir_sense/core/builtin_attributes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule ElixirSense.Core.BuiltinAttributes do
impl
derive
enforce_keys
struct
compile
deprecated
dialyzer
Expand All @@ -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
32 changes: 19 additions & 13 deletions lib/elixir_sense/providers/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -72,8 +80,6 @@ defmodule ElixirSense.Providers.Docs do
end
end

# TODO attr

defp mod_fun_docs(
{mod, fun},
binding_env,
Expand Down
14 changes: 10 additions & 4 deletions lib/elixir_sense/providers/suggestion/complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(%{
Expand Down
3 changes: 2 additions & 1 deletion lib/elixir_sense/providers/suggestion/reducers/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 :: %{
Expand Down
25 changes: 25 additions & 0 deletions test/elixir_sense/docs_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 15 additions & 6 deletions test/elixir_sense/providers/suggestion/complete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
115 changes: 77 additions & 38 deletions test/elixir_sense/suggestions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading