Skip to content

Commit 434f6bc

Browse files
authored
Implementations provider (elixir-editors#415)
* move to provider * no need to feature check as formatter and references are available since elixir 1.7 and we require 1.8 * elixir_sense suggestions for erlang modules now properly include : no need to patch * elixir_sense definitions now return nil when not found extract location related code to a new module * add implementations provider * update elixir_sense * do not format test/tmp fixtures * move fixtures to common dir * do not build filesystem URIs by string concat as it will break on Windows * add tests * fix invalid uris * Revert "do not format test/tmp fixtures" This reverts commit 5012101bc4ba31052d26fbb4e184a624a75a6c76. * Revert "fix invalid uris" This reverts commit 38eeb67c129384aa4343e5a546d7a7c0fe159779. * run formatter * increase timeout * bump elixir_sense * don't catch everyting * bump elixir_sense fix tests on elixir < 1.11
1 parent 99a6447 commit 434f6bc

21 files changed

+229
-107
lines changed

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 implementation_req(id, uri, line, character) do
105+
quote do
106+
request(unquote(id), "textDocument/implementation", %{
107+
"textDocument" => %{"uri" => unquote(uri)},
108+
"position" => %{"line" => unquote(line), "character" => unquote(character)}
109+
})
110+
end
111+
end
112+
104113
defmacro completion_req(id, uri, line, character) do
105114
quote do
106115
request(unquote(id), "textDocument/completion", %{

apps/language_server/lib/language_server/protocol/location.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,23 @@ defmodule ElixirLS.LanguageServer.Protocol.Location do
66
"""
77
@derive JasonVendored.Encoder
88
defstruct [:uri, :range]
9+
10+
alias ElixirLS.LanguageServer.SourceFile
11+
alias ElixirLS.LanguageServer.Protocol
12+
13+
def new(%ElixirSense.Location{file: file, line: line, column: column}, uri) do
14+
uri =
15+
case file do
16+
nil -> uri
17+
_ -> SourceFile.path_to_uri(file)
18+
end
19+
20+
%Protocol.Location{
21+
uri: uri,
22+
range: %{
23+
"start" => %{"line" => line - 1, "character" => column - 1},
24+
"end" => %{"line" => line - 1, "character" => column - 1}
25+
}
26+
}
27+
end
928
end

apps/language_server/lib/language_server/providers/completion.ex

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -236,37 +236,26 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
236236

237237
defp from_completion_item(
238238
%{type: :module, name: name, summary: summary, subtype: subtype, metadata: metadata},
239-
%{
240-
def_before: nil,
241-
prefix: prefix
242-
},
239+
%{def_before: nil},
243240
_options
244241
) do
245-
capitalized? = String.first(name) == String.upcase(String.first(name))
246-
247-
if String.ends_with?(prefix, ":") and capitalized? do
248-
nil
249-
else
250-
label = if capitalized?, do: name, else: ":" <> name
251-
252-
detail =
253-
if subtype do
254-
Atom.to_string(subtype)
255-
else
256-
"module"
257-
end
242+
detail =
243+
if subtype do
244+
Atom.to_string(subtype)
245+
else
246+
"module"
247+
end
258248

259-
%__MODULE__{
260-
label: label,
261-
kind: :module,
262-
detail: detail,
263-
documentation: summary,
264-
insert_text: name,
265-
filter_text: name,
266-
priority: 14,
267-
tags: metadata_to_tags(metadata)
268-
}
269-
end
249+
%__MODULE__{
250+
label: name,
251+
kind: :module,
252+
detail: detail,
253+
documentation: summary,
254+
insert_text: name,
255+
filter_text: name,
256+
priority: 14,
257+
tags: metadata_to_tags(metadata)
258+
}
270259
end
271260

272261
defp from_completion_item(

apps/language_server/lib/language_server/providers/definition.ex

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,18 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do
33
Go-to-definition provider utilizing Elixir Sense
44
"""
55

6-
alias ElixirLS.LanguageServer.SourceFile
76
alias ElixirLS.LanguageServer.Protocol
87

98
def definition(uri, text, line, character) do
10-
case ElixirSense.definition(text, line + 1, character + 1) do
11-
%ElixirSense.Location{found: false} ->
12-
{:ok, []}
9+
result =
10+
case ElixirSense.definition(text, line + 1, character + 1) do
11+
nil ->
12+
nil
1313

14-
%ElixirSense.Location{file: file, line: line, column: column} ->
15-
line = line || 0
16-
column = column || 0
14+
%ElixirSense.Location{} = location ->
15+
Protocol.Location.new(location, uri)
16+
end
1717

18-
uri =
19-
case file do
20-
nil -> uri
21-
_ -> SourceFile.path_to_uri(file)
22-
end
23-
24-
{:ok,
25-
%Protocol.Location{
26-
uri: uri,
27-
range: %{
28-
"start" => %{"line" => line - 1, "character" => column - 1},
29-
"end" => %{"line" => line - 1, "character" => column - 1}
30-
}
31-
}}
32-
end
18+
{:ok, result}
3319
end
3420
end

apps/language_server/lib/language_server/providers/formatting.ex

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do
22
import ElixirLS.LanguageServer.Protocol, only: [range: 4]
33
alias ElixirLS.LanguageServer.SourceFile
44

5-
def supported? do
6-
function_exported?(Code, :format_string!, 2)
7-
end
8-
95
def format(%SourceFile{} = source_file, uri, project_dir) do
106
if can_format?(uri, project_dir) do
117
case SourceFile.formatter_opts(uri) do
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule ElixirLS.LanguageServer.Providers.Implementation do
2+
@moduledoc """
3+
Go-to-implementation provider utilizing Elixir Sense
4+
"""
5+
6+
alias ElixirLS.LanguageServer.Protocol
7+
8+
def implementation(uri, text, line, character) do
9+
locations = ElixirSense.implementations(text, line + 1, character + 1)
10+
results = for location <- locations, do: Protocol.Location.new(location, uri)
11+
12+
{:ok, results}
13+
end
14+
end

apps/language_server/lib/language_server/providers/references.ex

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ defmodule ElixirLS.LanguageServer.Providers.References do
2424
end)
2525
end
2626

27-
def supported? do
28-
Mix.Tasks.Xref.__info__(:functions) |> Enum.member?({:calls, 0})
29-
end
30-
3127
defp build_reference(ref, current_file_uri) do
3228
%{
3329
range: %{

apps/language_server/lib/language_server/providers/signature_help.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule ElixirLS.LanguageServer.Providers.SignatureHelp do
22
alias ElixirLS.LanguageServer.SourceFile
33

4+
def trigger_characters(), do: ["("]
5+
46
def signature(%SourceFile{} = source_file, line, character) do
57
response =
68
case ElixirSense.signature(source_file.text, line + 1, character + 1) do

apps/language_server/lib/language_server/server.ex

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule ElixirLS.LanguageServer.Server do
2222
Completion,
2323
Hover,
2424
Definition,
25+
Implementation,
2526
References,
2627
Formatting,
2728
SignatureHelp,
@@ -475,10 +476,6 @@ defmodule ElixirLS.LanguageServer.Server do
475476
e in InvalidParamError ->
476477
JsonRpc.respond_with_error(id, :invalid_params, e.message)
477478
state
478-
479-
other ->
480-
JsonRpc.respond_with_error(id, :internal_error, other.message)
481-
state
482479
end
483480

484481
defp handle_request_packet(id, _packet, state) do
@@ -546,6 +543,14 @@ defmodule ElixirLS.LanguageServer.Server do
546543
{:async, fun, state}
547544
end
548545

546+
defp handle_request(implementation_req(_id, uri, line, character), state) do
547+
fun = fn ->
548+
Implementation.implementation(uri, state.source_files[uri].text, line, character)
549+
end
550+
551+
{:async, fun, state}
552+
end
553+
549554
defp handle_request(references_req(_id, uri, line, character, include_declaration), state) do
550555
source_file = get_source_file(state, uri)
551556

@@ -731,9 +736,6 @@ defmodule ElixirLS.LanguageServer.Server do
731736
rescue
732737
e in InvalidParamError ->
733738
{:error, :invalid_params, e.message}
734-
735-
other ->
736-
{:error, :internal_error, other.message}
737739
end
738740

739741
GenServer.call(parent, {:request_finished, id, result}, :infinity)
@@ -751,9 +753,10 @@ defmodule ElixirLS.LanguageServer.Server do
751753
"hoverProvider" => true,
752754
"completionProvider" => %{"triggerCharacters" => Completion.trigger_characters()},
753755
"definitionProvider" => true,
754-
"referencesProvider" => References.supported?(),
755-
"documentFormattingProvider" => Formatting.supported?(),
756-
"signatureHelpProvider" => %{"triggerCharacters" => ["("]},
756+
"implementationProvider" => true,
757+
"referencesProvider" => true,
758+
"documentFormattingProvider" => true,
759+
"signatureHelpProvider" => %{"triggerCharacters" => SignatureHelp.trigger_characters()},
757760
"documentSymbolProvider" => true,
758761
"workspaceSymbolProvider" => true,
759762
"documentOnTypeFormattingProvider" => %{"firstTriggerCharacter" => "\n"},

apps/language_server/test/providers/definition_test.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do
44
alias ElixirLS.LanguageServer.Providers.Definition
55
alias ElixirLS.LanguageServer.Protocol.Location
66
alias ElixirLS.LanguageServer.SourceFile
7+
alias ElixirLS.LanguageServer.Test.FixtureHelpers
78
require ElixirLS.Test.TextLoc
89

910
test "find definition" do
10-
file_path = Path.join(__DIR__, "../support/references_a.ex") |> Path.expand()
11+
file_path = FixtureHelpers.get_path("references_a.ex")
1112
text = File.read!(file_path)
1213
uri = SourceFile.path_to_uri(file_path)
1314

14-
b_file_path = Path.join(__DIR__, "../support/references_b.ex") |> Path.expand()
15+
b_file_path = FixtureHelpers.get_path("references_b.ex")
1516
b_uri = SourceFile.path_to_uri(b_file_path)
1617

1718
{line, char} = {2, 30}

apps/language_server/test/providers/formatting_test.exs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
22
use ExUnit.Case
33
alias ElixirLS.LanguageServer.Providers.Formatting
4+
alias ElixirLS.LanguageServer.SourceFile
45

56
test "Formats a file" do
67
uri = "file://project/file.ex"
@@ -15,7 +16,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
1516
end
1617
"""
1718

18-
source_file = %ElixirLS.LanguageServer.SourceFile{
19+
source_file = %SourceFile{
1920
text: text,
2021
version: 1,
2122
dirty?: true
@@ -64,7 +65,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
6465
end
6566
"""
6667

67-
source_file = %ElixirLS.LanguageServer.SourceFile{
68+
source_file = %SourceFile{
6869
text: text,
6970
version: 1,
7071
dirty?: true
@@ -83,7 +84,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
8384
IO.puts "😀"
8485
"""
8586

86-
source_file = %ElixirLS.LanguageServer.SourceFile{
87+
source_file = %SourceFile{
8788
text: text,
8889
version: 1,
8990
dirty?: true
@@ -118,7 +119,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
118119
IO.puts "🏳️‍🌈"
119120
"""
120121

121-
source_file = %ElixirLS.LanguageServer.SourceFile{
122+
source_file = %SourceFile{
122123
text: text,
123124
version: 1,
124125
dirty?: true
@@ -153,7 +154,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
153154
IO.puts "ẕ̸͇̞̲͇͕̹̙̄͆̇͂̏̊͒̒̈́́̕͘͠͝à̵̢̛̟̞͚̟͖̻̹̮̘͚̻͍̇͂̂̅́̎̉͗́́̃̒l̴̻̳͉̖̗͖̰̠̗̃̈́̓̓̍̅͝͝͝g̷̢͚̠̜̿̊́̋͗̔ȍ̶̹̙̅̽̌̒͌͋̓̈́͑̏͑͊͛͘ ̸̨͙̦̫̪͓̠̺̫̖͙̫̏͂̒̽́̿̂̊́͂͋͜͠͝͝ṭ̴̜͎̮͉̙͍͔̜̾͋͒̓̏̉̄͘͠͝ͅę̷̡̭̹̰̺̩̠͓͌̃̕͜͝ͅͅx̵̧͍̦͈͍̝͖͙̘͎̥͕̾̾̍̀̿̔̄̑̈͝t̸̛͇̀̕"
154155
"""
155156

156-
source_file = %ElixirLS.LanguageServer.SourceFile{
157+
source_file = %SourceFile{
157158
text: text,
158159
version: 1,
159160
dirty?: true
@@ -183,7 +184,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
183184

184185
test "honors :inputs when deciding to format" do
185186
file = __ENV__.file
186-
uri = "file://" <> file
187+
uri = SourceFile.path_to_uri(file)
187188
project_dir = Path.dirname(file)
188189

189190
opts = []
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
defmodule ElixirLS.LanguageServer.Providers.ImplementationTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ElixirLS.LanguageServer.Providers.Implementation
5+
alias ElixirLS.LanguageServer.Protocol.Location
6+
alias ElixirLS.LanguageServer.SourceFile
7+
alias ElixirLS.LanguageServer.Test.FixtureHelpers
8+
require ElixirLS.Test.TextLoc
9+
10+
test "find implementations" do
11+
# force load as currently only loaded or loadable modules that are a part
12+
# of an application are found
13+
Code.ensure_loaded?(ElixirLS.LanguageServer.Fixtures.ExampleBehaviourImpl)
14+
15+
file_path = FixtureHelpers.get_path("example_behaviour.ex")
16+
text = File.read!(file_path)
17+
uri = SourceFile.path_to_uri(file_path)
18+
19+
{line, char} = {0, 43}
20+
21+
ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """
22+
defmodule ElixirLS.LanguageServer.Fixtures.ExampleBehaviour do
23+
^
24+
""")
25+
26+
assert {:ok, [%Location{uri: ^uri, range: range}]} =
27+
Implementation.implementation(uri, text, line, char)
28+
29+
assert range ==
30+
%{
31+
"start" => %{"line" => 5, "character" => 10},
32+
"end" => %{"line" => 5, "character" => 10}
33+
}
34+
end
35+
end

apps/language_server/test/providers/references_test.exs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do
33

44
alias ElixirLS.LanguageServer.Providers.References
55
alias ElixirLS.LanguageServer.SourceFile
6+
alias ElixirLS.LanguageServer.Test.FixtureHelpers
67
require ElixirLS.Test.TextLoc
78

89
test "finds references to a function" do
9-
file_path = Path.join(__DIR__, "../support/references_b.ex") |> Path.expand()
10+
file_path = FixtureHelpers.get_path("references_b.ex")
1011
text = File.read!(file_path)
1112
uri = SourceFile.path_to_uri(file_path)
1213

@@ -39,7 +40,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do
3940
end
4041

4142
test "cannot find a references to a macro generated function call" do
42-
file_path = Path.join(__DIR__, "../support/uses_macro_a.ex") |> Path.expand()
43+
file_path = FixtureHelpers.get_path("uses_macro_a.ex")
4344
text = File.read!(file_path)
4445
uri = SourceFile.path_to_uri(file_path)
4546
{line, char} = {6, 13}
@@ -53,7 +54,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do
5354
end
5455

5556
test "finds a references to a macro imported function call" do
56-
file_path = Path.join(__DIR__, "../support/uses_macro_a.ex") |> Path.expand()
57+
file_path = FixtureHelpers.get_path("uses_macro_a.ex")
5758
text = File.read!(file_path)
5859
uri = SourceFile.path_to_uri(file_path)
5960
{line, char} = {10, 4}
@@ -75,7 +76,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do
7576
end
7677

7778
test "finds references to a variable" do
78-
file_path = Path.join(__DIR__, "../support/references_b.ex") |> Path.expand()
79+
file_path = FixtureHelpers.get_path("references_b.ex")
7980
text = File.read!(file_path)
8081
uri = SourceFile.path_to_uri(file_path)
8182
{line, char} = {4, 14}

0 commit comments

Comments
 (0)