Skip to content

Commit eddd6a4

Browse files
committed
return hover on specs and callbacks
1 parent 39fcde1 commit eddd6a4

File tree

5 files changed

+213
-5
lines changed

5 files changed

+213
-5
lines changed

apps/language_server/lib/language_server/providers/declaration/locator.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ defmodule ElixirLS.LanguageServer.Providers.Declaration.Locator do
77
This is effectively the reverse of the "go to implementations" provider.
88
"""
99

10-
alias ElixirSense.Core.Behaviours
11-
alias ElixirSense.Core.Binding
1210
alias ElixirSense.Core.Metadata
1311
alias ElixirSense.Core.Normalized.Code, as: NormalizedCode
1412
alias ElixirSense.Core.State
@@ -43,7 +41,7 @@ defmodule ElixirLS.LanguageServer.Providers.Declaration.Locator do
4341
end
4442

4543
@doc false
46-
def find(context, %State.Env{module: module} = env, metadata) do
44+
def find(_context, %State.Env{module: module} = env, metadata) do
4745
# Get the binding environment as in the other providers.
4846
# binding_env = Binding.from_env(env, metadata, context.begin)
4947

apps/language_server/lib/language_server/providers/hover.ex

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
9292
end
9393
end
9494

95+
defp build_callback_link(module, callback, arity) do
96+
if module != nil and ElixirSense.Core.Introspection.elixir_module?(module) do
97+
url = DocLinks.hex_docs_callback_link(module, callback, arity)
98+
99+
if url do
100+
"[View on hexdocs](#{url})\n\n"
101+
else
102+
""
103+
end
104+
else
105+
""
106+
end
107+
end
108+
95109
defp format_doc(info = %{kind: :module}) do
96110
mod_str = inspect(info.module)
97111

@@ -177,6 +191,37 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
177191
"""
178192
end
179193

194+
defp format_doc(info = %{kind: kind}) when kind in [:callback, :macrocallback] do
195+
joined = Enum.join(info.specs, "\n")
196+
formatted_spec = "```elixir\n#{joined}\n```"
197+
198+
mod_formatted =
199+
case info.module do
200+
nil -> ""
201+
atom -> inspect(atom) <> "."
202+
end
203+
204+
callback_name =
205+
"#{mod_formatted}#{info.callback}(#{Enum.join(info.args, ", ")})"
206+
|> format_header
207+
208+
"""
209+
```elixir
210+
#{callback_name}
211+
```
212+
213+
*#{kind}* #{build_callback_link(info.module, info.callback, info.arity)}
214+
215+
#{MarkdownUtils.get_metadata_md(info.metadata)}
216+
217+
### Definition
218+
219+
#{formatted_spec}
220+
221+
#{documentation_section(info.docs) |> MarkdownUtils.transform_ex_doc_links(info.module)}
222+
"""
223+
end
224+
180225
defp format_doc(info = %{kind: :variable}) do
181226
"""
182227
```elixir

apps/language_server/lib/language_server/providers/hover/docs.ex

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.Docs do
1616
alias ElixirSense.Core.ReservedWords
1717
alias ElixirSense.Core.State
1818
alias ElixirSense.Core.SurroundContext
19-
alias ElixirSense.Core.State.ModFunInfo
19+
alias ElixirSense.Core.State.{ModFunInfo, SpecInfo}
2020
alias ElixirSense.Core.TypeInfo
2121
alias ElixirSense.Core.Parser
2222

@@ -181,6 +181,47 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.Docs do
181181
)
182182

183183
case actual do
184+
{_, f, false, _} ->
185+
module = env.module
186+
{line, column} = context.end
187+
call_arity = Metadata.get_call_arity(metadata, env.module, f, line, column) || :any
188+
189+
metadata.specs
190+
|> Enum.filter(fn
191+
{{^module, ^f, a}, %SpecInfo{}} ->
192+
Introspection.matches_arity?(a, call_arity)
193+
194+
_ ->
195+
false
196+
end)
197+
|> Enum.map(fn {{_module, _f, arity}, spec_info = %SpecInfo{}} ->
198+
case spec_info do
199+
%SpecInfo{kind: :spec} ->
200+
# return def docs on on spec
201+
get_all_docs({module, fun, arity}, metadata, env, :mod_fun)
202+
203+
%SpecInfo{kind: kind} when kind in [:callback, :macrocallback] ->
204+
specs =
205+
spec_info.specs
206+
|> Enum.reject(&String.starts_with?(&1, "@spec"))
207+
|> Enum.reverse()
208+
209+
[
210+
%{
211+
kind: kind,
212+
module: module,
213+
callback: fun,
214+
arity: arity,
215+
args: spec_info.args |> List.last(),
216+
metadata: spec_info.meta,
217+
specs: specs,
218+
docs: spec_info.doc
219+
}
220+
]
221+
end
222+
end)
223+
|> List.flatten()
224+
184225
{mod, fun, true, kind} ->
185226
{line, column} = context.end
186227
call_arity = Metadata.get_call_arity(metadata, mod, fun, line, column) || :any

apps/language_server/test/providers/hover/docs_test.exs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do
531531
} = Docs.docs(buffer, 5, 6)
532532
end
533533

534-
test "find definition of local macro on definition" do
534+
test "retrieve documentation of local macro on definition" do
535535
buffer = """
536536
defmodule MyModule do
537537
defmacrop some(var), do: Macro.expand(var, __CALLER__)
@@ -547,6 +547,56 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do
547547
} = Docs.docs(buffer, 2, 14)
548548
end
549549

550+
test "retrieve documentation of local macro on spec" do
551+
buffer = """
552+
defmodule MyModule do
553+
@doc "Some macro"
554+
@spec some(integer()) :: Macro.t()
555+
defmacro some(var), do: Macro.expand(var, __CALLER__)
556+
end
557+
"""
558+
559+
assert %{
560+
docs: [
561+
%{
562+
args: ["var"],
563+
arity: 1,
564+
function: :some,
565+
module: MyModule,
566+
metadata: %{},
567+
specs: ["@spec some(integer()) :: Macro.t()"],
568+
docs: "Some macro",
569+
kind: :macro
570+
}
571+
]
572+
} = Docs.docs(buffer, 3, 10)
573+
end
574+
575+
test "retrieve documentation of local function on spec" do
576+
buffer = """
577+
defmodule MyModule do
578+
@doc "Some fun"
579+
@spec some(integer()) :: atom()
580+
def some(var), do: Macro.expand(var, __CALLER__)
581+
end
582+
"""
583+
584+
assert %{
585+
docs: [
586+
%{
587+
args: ["var"],
588+
arity: 1,
589+
function: :some,
590+
module: MyModule,
591+
metadata: %{},
592+
specs: ["@spec some(integer()) :: atom()"],
593+
docs: "Some fun",
594+
kind: :function
595+
}
596+
]
597+
} = Docs.docs(buffer, 3, 10)
598+
end
599+
550600
test "does not find definition of local macro if it's defined after the cursor" do
551601
buffer = """
552602
defmodule MyModule do
@@ -1361,6 +1411,58 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do
13611411
end
13621412
end
13631413

1414+
describe "callbacks" do
1415+
test "retrieve documentation of local callback" do
1416+
buffer = """
1417+
defmodule MyModule do
1418+
@doc "Some callback"
1419+
@doc since: "2.3"
1420+
@callback some(integer()) :: atom()
1421+
end
1422+
"""
1423+
1424+
assert %{
1425+
docs: [
1426+
%{
1427+
args: ["integer()"],
1428+
arity: 1,
1429+
module: MyModule,
1430+
callback: :some,
1431+
metadata: %{since: "2.3"},
1432+
specs: ["@callback some(integer()) :: atom()"],
1433+
docs: "Some callback",
1434+
kind: :callback
1435+
}
1436+
]
1437+
} = Docs.docs(buffer, 4, 14)
1438+
end
1439+
1440+
test "retrieve documentation of local macrocallback" do
1441+
buffer = """
1442+
defmodule MyModule do
1443+
@doc "Some macrocallback"
1444+
@doc since: "2.3"
1445+
@macrocallback some(integer()) :: Macro.t()
1446+
end
1447+
"""
1448+
1449+
assert %{
1450+
docs: [
1451+
%{
1452+
args: ["integer()"],
1453+
arity: 1,
1454+
module: MyModule,
1455+
callback: :some,
1456+
metadata: %{since: "2.3"},
1457+
specs: ["@macrocallback some(integer()) :: Macro.t()"],
1458+
docs: "Some macrocallback",
1459+
kind: :macrocallback
1460+
}
1461+
]
1462+
} = Docs.docs(buffer, 4, 19)
1463+
end
1464+
end
1465+
13641466
describe "types" do
13651467
test "type with @typedoc false" do
13661468
buffer = """

apps/language_server/test/providers/hover_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ defmodule ElixirLS.LanguageServer.Providers.HoverTest do
9999
)
100100
end
101101

102+
test "callback hover" do
103+
text = """
104+
defmodule MyModule do
105+
@callback some(integer()) :: atom()
106+
end
107+
"""
108+
109+
{line, char} = {1, 13}
110+
parser_context = ParserContextBuilder.from_string(text)
111+
112+
{line, char} =
113+
SourceFile.lsp_position_to_elixir(parser_context.source_file.text, {line, char})
114+
115+
assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} =
116+
Hover.hover(parser_context, line, char)
117+
118+
assert String.starts_with?(
119+
v,
120+
"```elixir\nMyModule.some(integer())\n```\n\n*callback*"
121+
)
122+
end
123+
102124
test "elixir type hover" do
103125
text = """
104126
defmodule MyModule do

0 commit comments

Comments
 (0)