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

Provide docs for local modules, functions and types #253

Merged
merged 9 commits into from
Aug 3, 2023
296 changes: 235 additions & 61 deletions lib/elixir_sense/core/introspection.ex

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions lib/elixir_sense/core/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,19 @@ defmodule ElixirSense.Core.State do
%__MODULE__{state | types: types}
end

defp combine_specs(nil, new), do: new

defp combine_specs(%SpecInfo{} = existing, %SpecInfo{} = new) do
%SpecInfo{
existing
| positions: [hd(new.positions) | existing.positions],
end_positions: [hd(new.end_positions) | existing.end_positions],
generated: [hd(new.generated) | existing.generated],
args: [hd(new.args) | existing.args],
specs: [hd(new.specs) | existing.specs]
}
end

def add_spec(%__MODULE__{} = state, type_name, type_args, spec, kind, pos, end_pos, options) do
arg_names =
type_args
Expand All @@ -1142,25 +1155,12 @@ defmodule ElixirSense.Core.State do
specs =
current_module_variants
|> Enum.reduce(state.specs, fn current_module, acc ->
info =
case acc[{current_module, type_name, nil}] do
nil ->
type_info

%SpecInfo{positions: positions, args: args, specs: specs} = ti ->
%SpecInfo{
ti
| positions: [pos | positions],
end_positions: [end_pos | ti.end_positions],
generated: [Keyword.get(options, :generated, false) | ti.generated],
args: [arg_names | args],
specs: [spec | specs]
}
end
nil_info = combine_specs(acc[{current_module, type_name, nil}], type_info)
arity_info = combine_specs(acc[{current_module, type_name, length(arg_names)}], type_info)

acc
|> Map.put({current_module, type_name, nil}, info)
|> Map.put({current_module, type_name, length(arg_names)}, type_info)
|> Map.put({current_module, type_name, nil}, nil_info)
|> Map.put({current_module, type_name, length(arg_names)}, arity_info)
end)

%__MODULE__{state | specs: specs}
Expand Down
8 changes: 7 additions & 1 deletion lib/elixir_sense/core/surround_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ defmodule ElixirSense.Core.SurroundContext do

def to_binding({:local_or_var, ~c"__MODULE__"}, current_module) do
if current_module not in [nil, Elixir] do
{:atom, current_module}
{{:atom, current_module}, nil}
end
end

def to_binding({:alias, {:local_or_var, ~c"__MODULE__"}, charlist}, current_module) do
if current_module not in [nil, Elixir] do
{{:atom, :"#{current_module}.#{charlist}"}, nil}
end
end

Expand Down
15 changes: 12 additions & 3 deletions lib/elixir_sense/core/type_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,19 @@ defmodule ElixirSense.Core.TypeInfo do

for(
{key, value} <- BuiltinTypes.all(),
type_ast <- [value[:spec]],
spec <- [format_type_spec_ast(type_ast, :type, line_length: @param_option_spec_line_length)],
spec =
case value do
%{spec: spec} ->
format_type_spec_ast(spec, :type, line_length: @param_option_spec_line_length)

%{signature: signature} ->
"@type #{signature}"

_ ->
"@type #{key}()"
end,
signature <- [
value[:signature] || TypeAst.extract_signature(type_ast) || "#{key}()"
value[:signature] || TypeAst.extract_signature(value[:spec]) || "#{key}()"
],
{name, arity} = extract_name_and_arity.(key),
doc = value[:doc] || "",
Expand Down
24 changes: 1 addition & 23 deletions lib/elixir_sense/providers/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -103,28 +103,6 @@ defmodule ElixirSense.Providers.Docs do
end
end

defp mod_fun_docs(
{:variable, name} = type,
context,
binding_env,
env,
metadata
) do
case Binding.expand(binding_env, type) do
:none ->
mod_fun_docs(
{nil, name},
context,
binding_env,
env,
metadata
)

_ ->
nil
end
end

defp mod_fun_docs(
{mod, fun},
context,
Expand All @@ -149,7 +127,7 @@ defmodule ElixirSense.Providers.Docs do
{mod, fun, true, kind} ->
{line, column} = context.end
call_arity = Metadata.get_call_arity(metadata, mod, fun, line, column) || :any
markdown = Introspection.get_all_docs({mod, fun, call_arity}, kind)
markdown = Introspection.get_all_docs({mod, fun, call_arity}, metadata, kind)

if markdown do
{mod_fun_to_string({mod, fun}), markdown}
Expand Down
2 changes: 2 additions & 0 deletions lib/elixir_sense/providers/suggestion/reducers/type_specs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.TypeSpecs do
defp find_builtin_types({nil, hint}) do
TypeInfo.find_all_builtin(&Matcher.match?("#{&1.name}", hint))
|> Enum.map(&type_info_to_suggestion(&1, nil))
|> Enum.sort_by(fn %{name: name, arity: arity} -> {name, arity} end)
end

defp find_builtin_types({_mod, _hint}), do: []
Expand All @@ -113,6 +114,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.TypeSpecs do
|> Kernel.++(TypeInfo.find_all(actual_mod, &Matcher.match?("#{&1.name}", hint)))
|> Enum.map(&type_info_to_suggestion(&1, actual_mod))
|> Enum.uniq_by(fn %{name: name, arity: arity} -> {name, arity} end)
|> Enum.sort_by(fn %{name: name, arity: arity} -> {name, arity} end)
end

defp find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) do
Expand Down
20 changes: 17 additions & 3 deletions test/elixir_sense/core/introspection_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule ElixirSense.Core.IntrospectionTest do
use ExUnit.Case, async: true
doctest ElixirSense.Core.Introspection
alias ElixirSense.Core.Metadata
alias ElixirSense.Core.TypeInfo
import ElixirSense.Core.Introspection

Expand Down Expand Up @@ -763,6 +764,7 @@ defmodule ElixirSense.Core.IntrospectionTest do
assert docs =
get_all_docs(
{ElixirSenseExample.ModuleWithDelegates, :delegated_fun, 2},
%Metadata{},
:mod_fun
)

Expand All @@ -778,7 +780,12 @@ defmodule ElixirSense.Core.IntrospectionTest do
end

test "returns since metadata on functions" do
assert docs = get_all_docs({ElixirSenseExample.ModuleWithDocs, :some_fun, 2}, :mod_fun)
assert docs =
get_all_docs(
{ElixirSenseExample.ModuleWithDocs, :some_fun, 2},
%Metadata{},
:mod_fun
)

assert docs == """
> ElixirSenseExample.ModuleWithDocs.some_fun(a, b \\\\\\\\ nil)
Expand All @@ -795,6 +802,7 @@ defmodule ElixirSense.Core.IntrospectionTest do
assert docs =
get_all_docs(
{ElixirSenseExample.ModuleWithDocs, :soft_deprecated_fun, 1},
%Metadata{},
:mod_fun
)

Expand All @@ -810,7 +818,12 @@ defmodule ElixirSense.Core.IntrospectionTest do
end

test "returns since metadata on types" do
assert docs = get_all_docs({ElixirSenseExample.ModuleWithDocs, :some_type, 0}, :type)
assert docs =
get_all_docs(
{ElixirSenseExample.ModuleWithDocs, :some_type, 0},
%Metadata{},
:type
)

assert docs == """
> ElixirSenseExample.ModuleWithDocs.some_type()
Expand All @@ -830,7 +843,8 @@ defmodule ElixirSense.Core.IntrospectionTest do
end

test "returns since metadata on modules" do
assert docs = get_all_docs({ElixirSenseExample.ModuleWithDocs, nil, :any}, :mod_fun)
assert docs =
get_all_docs({ElixirSenseExample.ModuleWithDocs, nil, :any}, %Metadata{}, :mod_fun)

assert docs == """
> ElixirSenseExample.ModuleWithDocs
Expand Down
22 changes: 12 additions & 10 deletions test/elixir_sense/core/metadata_builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2863,15 +2863,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do

assert state.specs == %{
{Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{
args: [["t", "boolean"]],
args: [["t", "boolean"], ["t", "integer"]],
kind: :callback,
name: :with_spec,
positions: [{3, 3}],
end_positions: [{3, 40}],
generated: [false],
positions: [{3, 3}, {2, 3}],
end_positions: [{3, 40}, {2, 42}],
generated: [false, false],
specs: [
"@callback with_spec(t, boolean) :: number",
"@spec with_spec(t, boolean) :: number"
"@callback with_spec(t, integer) :: String.t",
"@spec with_spec(t, boolean) :: number",
"@spec with_spec(t, integer) :: String.t"
]
},
{Proto, :with_spec, nil} => %ElixirSense.Core.State.SpecInfo{
Expand Down Expand Up @@ -4674,13 +4676,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do

assert state.specs == %{
{Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{
args: [[]],
args: [[], []],
kind: :spec,
name: :abc,
positions: [{3, 3}],
end_positions: [{3, 25}],
generated: [false],
specs: ["@spec abc :: reference"]
positions: [{3, 3}, {2, 3}],
end_positions: [{3, 25}, {2, 30}],
generated: [false, false],
specs: ["@spec abc :: reference", "@spec abc :: atom | integer"]
},
{Proto, :abc, nil} => %ElixirSense.Core.State.SpecInfo{
kind: :spec,
Expand Down
43 changes: 43 additions & 0 deletions test/elixir_sense/definition_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,26 @@ defmodule ElixirSense.Providers.DefinitionTest do
}
end

test "find definition of local __MODULE__" do
buffer = """
defmodule MyModule do
def my_fun(), do: :ok

def a do
my_fun1 = 1
__MODULE__.my_fun()
end
end
"""

assert ElixirSense.definition(buffer, 6, 6) == %Location{
type: :module,
file: nil,
line: 1,
column: 1
}
end

test "find definition of local functions with __MODULE__" do
buffer = """
defmodule MyModule do
Expand Down Expand Up @@ -1089,6 +1109,29 @@ defmodule ElixirSense.Providers.DefinitionTest do
}
end

@tag requires_elixir_1_14: true
test "find definition of local __MODULE__ submodule" do
buffer = """
defmodule MyModule do
defmodule Sub do
def my_fun(), do: :ok
end

def a do
my_fun1 = 1
__MODULE__.Sub.my_fun()
end
end
"""

assert ElixirSense.definition(buffer, 8, 17) == %Location{
type: :module,
file: nil,
line: 2,
column: 3
}
end

@tag requires_elixir_1_14: true
test "find definition of local functions with @attr" do
buffer = """
Expand Down
Loading
Loading