Skip to content

Commit 0e4b577

Browse files
authored
Merge pull request #812 from scottming/support-go-to-definition-in-experimental
Support go to definition in the experimental project
2 parents 3f01e21 + a3ed366 commit 0e4b577

File tree

6 files changed

+278
-1
lines changed

6 files changed

+278
-1
lines changed

apps/language_server/lib/language_server/experimental/protocol/requests.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Requests do
2727
position: Types.Position
2828
end
2929

30+
defmodule GotoDefinition do
31+
use Proto
32+
33+
defrequest "textDocument/definition", :exclusive,
34+
text_document: Types.TextDocument.Identifier,
35+
position: Types.Position
36+
end
37+
3038
defmodule Formatting do
3139
use Proto
3240

apps/language_server/lib/language_server/experimental/protocol/responses.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Responses do
88
defresponse optional(list_of(Types.Location))
99
end
1010

11+
defmodule GotoDefinition do
12+
use Proto
13+
14+
defresponse optional(Types.Location)
15+
end
16+
1117
defmodule Formatting do
1218
use Proto
1319

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.Provider.Handlers.GotoDefinition do
2+
alias ElixirLS.LanguageServer.Experimental.Protocol.Requests.GotoDefinition
3+
alias ElixirLS.LanguageServer.Experimental.Protocol.Responses
4+
alias ElixirLS.LanguageServer.Experimental.SourceFile
5+
alias ElixirLS.LanguageServer.Experimental.SourceFile.Conversions
6+
require Logger
7+
8+
def handle(%GotoDefinition{} = request, _) do
9+
source_file = request.source_file
10+
pos = request.position
11+
12+
maybe_location =
13+
source_file |> SourceFile.to_string() |> ElixirSense.definition(pos.line, pos.character + 1)
14+
15+
case to_response(request.id, maybe_location, source_file) do
16+
{:ok, response} ->
17+
{:reply, response}
18+
19+
{:error, reason} ->
20+
Logger.error("GotoDefinition conversion failed: #{inspect(reason)}")
21+
{:error, Responses.GotoDefinition.error(request.id, :request_failed, inspect(reason))}
22+
end
23+
end
24+
25+
defp to_response(request_id, %ElixirSense.Location{} = location, %SourceFile{} = source_file) do
26+
with {:ok, lsp_location} <- Conversions.to_lsp(location, source_file) do
27+
{:ok, Responses.GotoDefinition.new(request_id, lsp_location)}
28+
end
29+
end
30+
31+
defp to_response(request_id, nil, _source_file) do
32+
{:ok, Responses.GotoDefinition.new(request_id, nil)}
33+
end
34+
end

apps/language_server/lib/language_server/experimental/provider/queue.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.Queue do
1515
@requests_to_handler %{
1616
Requests.FindReferences => Handlers.FindReferences,
1717
Requests.Formatting => Handlers.Formatting,
18-
Requests.CodeAction => Handlers.CodeAction
18+
Requests.CodeAction => Handlers.CodeAction,
19+
Requests.GotoDefinition => Handlers.GotoDefinition
1920
}
2021

2122
def new do

apps/language_server/lib/language_server/experimental/source_file/conversions.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
1515
alias ElixirLS.LanguageServer.Experimental.SourceFile.Position, as: ElixirPosition
1616
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Position, as: LSPosition
1717
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Range, as: LSRange
18+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Location, as: LSLocation
1819
alias ElixirLS.LanguageServer.Protocol
1920

2021
import Line
@@ -102,6 +103,16 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
102103
{:ok, range}
103104
end
104105

106+
def to_lsp(%ElixirSense.Location{} = location, %SourceFile{} = source_file) do
107+
position = SourceFile.Position.new(location.line, location.column - 1)
108+
109+
with {:ok, source_file} <- fetch_source_file(location, source_file),
110+
{:ok, ls_position} <- to_lsp(position, source_file) do
111+
ls_range = %LSRange{start: ls_position, end: ls_position}
112+
{:ok, LSLocation.new(uri: source_file.uri, range: ls_range)}
113+
end
114+
end
115+
105116
def to_lsp(%ElixirRange{} = ex_range, %SourceFile{} = source) do
106117
with {:ok, start_pos} <- to_lsp(ex_range.start, source.document),
107118
{:ok, end_pos} <- to_lsp(ex_range.end, source.document) do
@@ -128,6 +139,13 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
128139
end
129140

130141
# Private
142+
defp fetch_source_file(%{file: nil}, source_file) do
143+
{:ok, source_file}
144+
end
145+
146+
defp fetch_source_file(%{file: path}, _) do
147+
SourceFile.Store.open_temporary(path)
148+
end
131149

132150
defp extract_lsp_character(%ElixirPosition{} = position, line(ascii?: true, text: text)) do
133151
character = min(position.character, byte_size(text))
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
defmodule ElixirLS.Experimental.Provider.Handlers.GotoDefinitionTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ElixirLS.LanguageServer.Experimental.Protocol.Requests.GotoDefinition
5+
alias ElixirLS.LanguageServer.Experimental.Protocol.Responses
6+
alias ElixirLS.LanguageServer.Experimental.Provider.Env
7+
alias ElixirLS.LanguageServer.Experimental.Provider.Handlers
8+
alias ElixirLS.LanguageServer.Experimental.SourceFile
9+
alias ElixirLS.LanguageServer.Experimental.SourceFile.Conversions
10+
11+
alias ElixirLS.LanguageServer.Fixtures.LspProtocol
12+
alias ElixirLS.LanguageServer.Test.FixtureHelpers
13+
14+
import LspProtocol
15+
import ElixirLS.Test.TextLoc, only: [annotate_assert: 4]
16+
17+
setup do
18+
{:ok, _} = start_supervised(SourceFile.Store)
19+
:ok
20+
end
21+
22+
def request(file_path, line, char) do
23+
uri = Conversions.ensure_uri(file_path)
24+
25+
params = [
26+
text_document: [uri: uri],
27+
position: [line: line, character: char]
28+
]
29+
30+
with {:ok, contents} <- File.read(file_path),
31+
:ok <- SourceFile.Store.open(uri, contents, 1),
32+
{:ok, req} <- build(GotoDefinition, params) do
33+
GotoDefinition.to_elixir(req)
34+
end
35+
end
36+
37+
def handle(request) do
38+
Handlers.GotoDefinition.handle(request, Env.new())
39+
end
40+
41+
def with_referenced_file(_) do
42+
path = FixtureHelpers.get_path("references_referenced.ex")
43+
uri = Conversions.ensure_uri(path)
44+
{:ok, file_uri: uri, file_path: path}
45+
end
46+
47+
describe "when a file contains references" do
48+
setup [:with_referenced_file]
49+
50+
test "find definition remote function call", %{file_uri: uri} do
51+
file_path = FixtureHelpers.get_path("references_remote.ex")
52+
{line, char} = {4, 28}
53+
54+
{:ok, request} = request(file_path, line, char)
55+
56+
annotate_assert(file_path, line, char, """
57+
ReferencesReferenced.referenced_fun()
58+
^
59+
""")
60+
61+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
62+
63+
assert definition.uri == uri
64+
assert definition.range.start.line == 1
65+
assert definition.range.start.character == 6
66+
assert definition.range.end.line == 1
67+
assert definition.range.end.character == 6
68+
end
69+
70+
test "find definition remote macro call", %{file_uri: uri} do
71+
file_path = FixtureHelpers.get_path("references_remote.ex")
72+
{line, char} = {8, 28}
73+
74+
{:ok, request} = request(file_path, line, char)
75+
76+
annotate_assert(file_path, line, char, """
77+
ReferencesReferenced.referenced_macro a do
78+
^
79+
""")
80+
81+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
82+
83+
assert definition.uri == uri
84+
assert definition.range.start.line == 8
85+
assert definition.range.start.character == 11
86+
assert definition.range.end.line == 8
87+
assert definition.range.end.character == 11
88+
end
89+
90+
test "find definition imported function call", %{file_uri: uri} do
91+
file_path = FixtureHelpers.get_path("references_imported.ex")
92+
{line, char} = {4, 5}
93+
94+
{:ok, request} = request(file_path, line, char)
95+
96+
annotate_assert(file_path, line, char, """
97+
referenced_fun()
98+
^
99+
""")
100+
101+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
102+
103+
assert definition.uri == uri
104+
assert definition.range.start.line == 1
105+
assert definition.range.start.character == 6
106+
assert definition.range.end.line == 1
107+
assert definition.range.end.character == 6
108+
end
109+
110+
test "find definition imported macro call", %{file_uri: uri} do
111+
file_path = FixtureHelpers.get_path("references_imported.ex")
112+
{line, char} = {8, 5}
113+
114+
{:ok, request} = request(file_path, line, char)
115+
116+
annotate_assert(file_path, line, char, """
117+
referenced_macro a do
118+
^
119+
""")
120+
121+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
122+
123+
assert definition.uri == uri
124+
assert definition.range.start.line == 8
125+
assert definition.range.start.character == 11
126+
assert definition.range.end.line == 8
127+
assert definition.range.end.character == 11
128+
end
129+
130+
test "find definition local function call", %{file_uri: uri} do
131+
file_path = FixtureHelpers.get_path("references_referenced.ex")
132+
{line, char} = {15, 5}
133+
134+
{:ok, request} = request(file_path, line, char)
135+
136+
annotate_assert(file_path, line, char, """
137+
referenced_fun()
138+
^
139+
""")
140+
141+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
142+
143+
assert definition.uri == uri
144+
assert definition.range.start.line == 1
145+
assert definition.range.start.character == 6
146+
assert definition.range.end.line == 1
147+
assert definition.range.end.character == 6
148+
end
149+
150+
test "find definition local macro call", %{file_uri: uri} do
151+
file_path = FixtureHelpers.get_path("references_referenced.ex")
152+
{line, char} = {19, 5}
153+
154+
{:ok, request} = request(file_path, line, char)
155+
156+
annotate_assert(file_path, line, char, """
157+
referenced_macro a do
158+
^
159+
""")
160+
161+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
162+
163+
assert definition.uri == uri
164+
assert definition.range.start.line == 8
165+
assert definition.range.start.character == 11
166+
assert definition.range.end.line == 8
167+
assert definition.range.end.character == 11
168+
end
169+
170+
test "find definition variable", %{file_uri: uri} do
171+
file_path = FixtureHelpers.get_path("references_referenced.ex")
172+
{line, char} = {4, 13}
173+
174+
{:ok, request} = request(file_path, line, char)
175+
176+
annotate_assert(file_path, line, char, """
177+
IO.puts(referenced_variable + 1)
178+
^
179+
""")
180+
181+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
182+
183+
assert definition.uri == uri
184+
assert definition.range.start.line == 2
185+
assert definition.range.start.character == 4
186+
assert definition.range.end.line == 2
187+
assert definition.range.end.character == 4
188+
end
189+
190+
test "find definition attribute", %{file_uri: uri} do
191+
file_path = FixtureHelpers.get_path("references_referenced.ex")
192+
{line, char} = {27, 5}
193+
194+
{:ok, request} = request(file_path, line, char)
195+
196+
annotate_assert(file_path, line, char, """
197+
@referenced_attribute
198+
^
199+
""")
200+
201+
{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)
202+
203+
assert definition.uri == uri
204+
assert definition.range.start.line == 24
205+
assert definition.range.start.character == 2
206+
assert definition.range.end.line == 24
207+
assert definition.range.end.character == 2
208+
end
209+
end
210+
end

0 commit comments

Comments
 (0)