Skip to content

Commit 614d3dc

Browse files
committed
add declaration provider
1 parent e9f764d commit 614d3dc

File tree

7 files changed

+483
-2
lines changed

7 files changed

+483
-2
lines changed

apps/language_server/lib/language_server/location.ex

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ defmodule ElixirLS.LanguageServer.Location do
1111

1212
alias ElixirSense.Core.Metadata
1313
alias ElixirSense.Core.Parser
14-
alias ElixirSense.Core.State.{ModFunInfo, TypeInfo}
14+
alias ElixirSense.Core.State.{ModFunInfo, TypeInfo, SpecInfo}
1515
alias ElixirSense.Core.Normalized.Code, as: CodeNormalized
1616
require ElixirSense.Core.Introspection, as: Introspection
1717
alias ElixirLS.LanguageServer.Location.Erl
1818

1919
@type t :: %__MODULE__{
20-
type: :module | :function | :variable | :typespec | :macro | :attribute,
20+
type: :module | :function | :variable | :typespec | :macro | :attribute | :callback,
2121
file: String.t() | nil,
2222
line: pos_integer,
2323
column: pos_integer,
@@ -50,6 +50,18 @@ defmodule ElixirLS.LanguageServer.Location do
5050
end
5151
end
5252

53+
@spec find_callback_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) ::
54+
t() | nil
55+
def find_callback_source(mod, type, arity) do
56+
case find_mod_file(mod) do
57+
file when is_binary(file) ->
58+
find_callback_position({mod, file}, type, arity)
59+
60+
_ ->
61+
nil
62+
end
63+
end
64+
5365
defp find_mod_file(Elixir), do: nil
5466

5567
defp find_mod_file(module) do
@@ -343,6 +355,19 @@ defmodule ElixirLS.LanguageServer.Location do
343355
|> min_by_line
344356
end
345357

358+
def get_callback_position_using_metadata(mod, fun, call_arity, specs) do
359+
specs
360+
|> Enum.filter(fn
361+
{{^mod, ^fun, spec_arity}, _type_info}
362+
when not is_nil(spec_arity) and Introspection.matches_arity?(spec_arity, call_arity) ->
363+
true
364+
365+
_ ->
366+
false
367+
end)
368+
|> min_by_line
369+
end
370+
346371
defp min_by_line(list) do
347372
result =
348373
list
@@ -385,4 +410,73 @@ defmodule ElixirLS.LanguageServer.Location do
385410

386411
{begin_position, end_position}
387412
end
413+
414+
defp find_callback_position({mod, file}, fun, arity) do
415+
result =
416+
if String.ends_with?(file, ".erl") do
417+
Erl.find_callback_range(file, fun)
418+
else
419+
file_metadata = Parser.parse_file(file, false, false, nil)
420+
get_callback_position(file_metadata, mod, fun, arity)
421+
end
422+
423+
case result do
424+
{{line, column}, {end_line, end_column}} ->
425+
%__MODULE__{
426+
type: :callback,
427+
file: file,
428+
line: line,
429+
column: column,
430+
end_line: end_line,
431+
end_column: end_column
432+
}
433+
434+
# TODO remove
435+
{line, column} ->
436+
%__MODULE__{
437+
type: :callback,
438+
file: file,
439+
line: line,
440+
column: column,
441+
end_line: line,
442+
end_column: column
443+
}
444+
445+
_ ->
446+
nil
447+
end
448+
end
449+
450+
defp get_callback_position(metadata, module, callback, arity) do
451+
case get_callback_position_using_metadata(module, callback, arity, metadata.specs) do
452+
nil ->
453+
get_callback_position_using_docs(module, callback, arity)
454+
455+
%SpecInfo{} = info ->
456+
info_to_range(info)
457+
end
458+
end
459+
460+
defp get_callback_position_using_docs(module, callback_name, arity) do
461+
case CodeNormalized.fetch_docs(module) do
462+
{:error, _} ->
463+
nil
464+
465+
{_, _, _, _, _, _, docs} ->
466+
docs
467+
|> Enum.filter(fn
468+
{{category, ^callback_name, doc_arity}, _line, _arg3, _arg4, _meta}
469+
when category in [:callback, :macrocallback] ->
470+
Introspection.matches_arity?(doc_arity, arity)
471+
472+
_ ->
473+
false
474+
end)
475+
|> Enum.map(fn
476+
{{_category, _function, _arity}, anno, _, _, _} ->
477+
anno_to_range(anno)
478+
end)
479+
|> Enum.min_by(fn {position, _end_position} -> position end, &<=/2, fn -> nil end)
480+
end
481+
end
388482
end

apps/language_server/lib/language_server/location/erl.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@ defmodule ElixirLS.LanguageServer.Location.Erl do
3030
find_range_by_regex(file, regex)
3131
end
3232

33+
@doc """
34+
Finds the position range of a callback definition in an Erlang `.erl` file.
35+
36+
## Parameters
37+
38+
- `file`: The path to the Erlang file.
39+
- `name`: The name of the type (as an atom).
40+
41+
## Returns
42+
43+
- `{{line, start_column}, {line, end_column}}` if found.
44+
- `nil` if not found.
45+
"""
46+
def find_callback_range(file, name) do
47+
escaped =
48+
name
49+
|> Atom.to_string()
50+
|> Regex.escape()
51+
52+
regex = ~r/^-callback\s+(?<name>#{escaped})\b/u
53+
54+
find_range_by_regex(file, regex)
55+
end
56+
3357
@doc """
3458
Finds the position range of a function definition in an Erlang `.erl` file.
3559

apps/language_server/lib/language_server/protocol.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ defmodule ElixirLS.LanguageServer.Protocol do
101101
end
102102
end
103103

104+
defmacro declaration_req(id, uri, line, character) do
105+
quote do
106+
request(unquote(id), "textDocument/declaration", %{
107+
"textDocument" => %{"uri" => unquote(uri)},
108+
"position" => %{"line" => unquote(line), "character" => unquote(character)}
109+
})
110+
end
111+
end
112+
104113
defmacro implementation_req(id, uri, line, character) do
105114
quote do
106115
request(unquote(id), "textDocument/implementation", %{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule ElixirLS.LanguageServer.Providers.Declaration do
2+
@moduledoc """
3+
textDocument/declaration provider utilizing Elixir Sense
4+
"""
5+
6+
alias ElixirLS.LanguageServer.{Protocol, Parser}
7+
alias ElixirLS.LanguageServer.Providers.Declaration.Locator
8+
9+
def declaration(
10+
uri,
11+
%Parser.Context{source_file: source_file, metadata: metadata},
12+
line,
13+
character,
14+
project_dir
15+
) do
16+
result =
17+
case Locator.declaration(source_file.text, line, character, metadata: metadata) do
18+
nil ->
19+
nil
20+
21+
%ElixirLS.LanguageServer.Location{} = location ->
22+
Protocol.Location.new(location, uri, source_file.text, project_dir)
23+
end
24+
25+
{:ok, result}
26+
end
27+
end
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
defmodule ElixirLS.LanguageServer.Providers.Declaration.Locator do
2+
@moduledoc """
3+
Provides a function to find the declaration of a callback or protocol function,
4+
that is, the place where a behaviour or protocol defines the callback that is being
5+
implemented.
6+
7+
This is effectively the reverse of the "go to implementations" provider.
8+
"""
9+
10+
alias ElixirSense.Core.Behaviours
11+
alias ElixirSense.Core.Binding
12+
alias ElixirSense.Core.Metadata
13+
alias ElixirSense.Core.Normalized.Code, as: NormalizedCode
14+
alias ElixirSense.Core.State
15+
alias ElixirLS.LanguageServer.Location
16+
alias ElixirSense.Core.Parser
17+
18+
require ElixirSense.Core.Introspection, as: Introspection
19+
20+
@doc """
21+
Finds the declaration (callback or protocol definition) for the function under the cursor.
22+
23+
It parses the code, determines the environment and then checks if the current function
24+
is an implementation of any behaviour (or protocol). For each matching behaviour,
25+
it returns the location where the callback is declared.
26+
27+
Returns either a single `%Location{}` or a list of locations if multiple declarations are found.
28+
"""
29+
def declaration(code, line, column, options \\ []) do
30+
case NormalizedCode.Fragment.surround_context(code, {line, column}) do
31+
:none ->
32+
nil
33+
34+
context ->
35+
metadata =
36+
Keyword.get_lazy(options, :metadata, fn ->
37+
Parser.parse_string(code, true, false, {line, column})
38+
end)
39+
40+
env = Metadata.get_cursor_env(metadata, {line, column}, {context.begin, context.end})
41+
find(context, env, metadata)
42+
end
43+
end
44+
45+
@doc false
46+
def find(context, %State.Env{module: module} = env, metadata) do
47+
# Get the binding environment as in the other providers.
48+
# binding_env = Binding.from_env(env, metadata, context.begin)
49+
50+
case env.function do
51+
nil ->
52+
nil
53+
54+
{fun, arity} ->
55+
# Get the behaviours (and possibly protocols) declared for the current module.
56+
behaviours = Metadata.get_module_behaviours(metadata, env, module)
57+
58+
# For each behaviour, if the current function is a callback for it,
59+
# try to find the callback’s declaration.
60+
locations =
61+
for behaviour <- behaviours,
62+
Introspection.is_callback(behaviour, fun, arity, metadata),
63+
location = get_callback_location(behaviour, fun, arity, metadata),
64+
location != nil do
65+
location
66+
end
67+
68+
case locations do
69+
[] -> nil
70+
[single] -> single
71+
multiple -> multiple
72+
end
73+
end
74+
end
75+
76+
# Attempts to find the callback declaration in the behaviour (or protocol) module.
77+
# First it checks for a callback spec in the metadata; if none is found, it falls back
78+
# to trying to locate the source code.
79+
defp get_callback_location(behaviour, fun, arity, metadata) do
80+
case Enum.find(metadata.specs, fn
81+
{{^behaviour, ^fun, a}, _spec_info} ->
82+
Introspection.matches_arity?(a, arity)
83+
84+
_ ->
85+
false
86+
end) do
87+
nil ->
88+
# Fallback: try to locate the function in the behaviour module’s source.
89+
Location.find_callback_source(behaviour, fun, arity)
90+
91+
{{^behaviour, ^fun, _a}, spec_info} ->
92+
{{line, column}, {end_line, end_column}} = Location.info_to_range(spec_info)
93+
94+
%Location{
95+
file: nil,
96+
type: :callback,
97+
line: line,
98+
column: column,
99+
end_line: end_line,
100+
end_column: end_column
101+
}
102+
end
103+
end
104+
end

apps/language_server/lib/language_server/server.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ defmodule ElixirLS.LanguageServer.Server do
3434
Completion,
3535
Hover,
3636
Definition,
37+
Declaration,
3738
Implementation,
3839
References,
3940
Formatting,
@@ -979,6 +980,18 @@ defmodule ElixirLS.LanguageServer.Server do
979980
{:async, fun, state}
980981
end
981982

983+
defp handle_request(declaration_req(_id, uri, line, character), state = %__MODULE__{}) do
984+
source_file = get_source_file(state, uri)
985+
986+
fun = fn ->
987+
{line, character} = SourceFile.lsp_position_to_elixir(source_file.text, {line, character})
988+
parser_context = Parser.parse_immediate(uri, source_file, {line, character})
989+
Declaration.declaration(uri, parser_context, line, character, state.project_dir)
990+
end
991+
992+
{:async, fun, state}
993+
end
994+
982995
defp handle_request(implementation_req(_id, uri, line, character), state = %__MODULE__{}) do
983996
source_file = get_source_file(state, uri)
984997

@@ -1291,6 +1304,7 @@ defmodule ElixirLS.LanguageServer.Server do
12911304
"hoverProvider" => true,
12921305
"completionProvider" => %{"triggerCharacters" => Completion.trigger_characters()},
12931306
"definitionProvider" => true,
1307+
"declarationProvider" => true,
12941308
"implementationProvider" => true,
12951309
"referencesProvider" => true,
12961310
"documentFormattingProvider" => true,

0 commit comments

Comments
 (0)