Skip to content

Support go to definition in the experimental project #812

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

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Requests do
position: Types.Position
end

defmodule GotoDefinition do
use Proto

defrequest "textDocument/definition", :exclusive,
text_document: Types.TextDocument.Identifier,
position: Types.Position
end

defmodule Formatting do
use Proto

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ defmodule ElixirLS.LanguageServer.Experimental.Protocol.Responses do
defresponse optional(list_of(Types.Location))
end

defmodule GotoDefinition do
use Proto

defresponse optional(Types.Location)
end

defmodule Formatting do
use Proto

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule ElixirLS.LanguageServer.Experimental.Provider.Handlers.GotoDefinition do
alias ElixirLS.LanguageServer.Experimental.Protocol.Requests.GotoDefinition
alias ElixirLS.LanguageServer.Experimental.Protocol.Responses
alias ElixirLS.LanguageServer.Experimental.SourceFile
alias ElixirLS.LanguageServer.Experimental.SourceFile.Conversions
require Logger

def handle(%GotoDefinition{} = request, _) do
source_file = request.source_file
pos = request.position

maybe_location =
source_file |> SourceFile.to_string() |> ElixirSense.definition(pos.line, pos.character + 1)

case to_response(request.id, maybe_location, source_file) do
{:ok, response} ->
{:reply, response}

{:error, reason} ->
Logger.error("GotoDefinition conversion failed: #{inspect(reason)}")
{:error, Responses.GotoDefinition.error(request.id, :request_failed, inspect(reason))}
end
end

defp to_response(request_id, %ElixirSense.Location{} = location, %SourceFile{} = source_file) do
with {:ok, lsp_location} <- Conversions.to_lsp(location, source_file) do
{:ok, Responses.GotoDefinition.new(request_id, lsp_location)}
end
end

defp to_response(request_id, nil, _source_file) do
{:ok, Responses.GotoDefinition.new(request_id, nil)}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.Queue do
@requests_to_handler %{
Requests.FindReferences => Handlers.FindReferences,
Requests.Formatting => Handlers.Formatting,
Requests.CodeAction => Handlers.CodeAction
Requests.CodeAction => Handlers.CodeAction,
Requests.GotoDefinition => Handlers.GotoDefinition
}

def new do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
alias ElixirLS.LanguageServer.Experimental.SourceFile.Position, as: ElixirPosition
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Position, as: LSPosition
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Range, as: LSRange
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Location, as: LSLocation
alias ElixirLS.LanguageServer.Protocol

import Line
Expand Down Expand Up @@ -102,6 +103,16 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
{:ok, range}
end

def to_lsp(%ElixirSense.Location{} = location, %SourceFile{} = source_file) do
position = SourceFile.Position.new(location.line, location.column - 1)

with {:ok, source_file} <- fetch_source_file(location, source_file),
{:ok, ls_position} <- to_lsp(position, source_file) do
ls_range = %LSRange{start: ls_position, end: ls_position}
{:ok, LSLocation.new(uri: source_file.uri, range: ls_range)}
end
end

def to_lsp(%ElixirRange{} = ex_range, %SourceFile{} = source) do
with {:ok, start_pos} <- to_lsp(ex_range.start, source.document),
{:ok, end_pos} <- to_lsp(ex_range.end, source.document) do
Expand All @@ -128,6 +139,13 @@ defmodule ElixirLS.LanguageServer.Experimental.SourceFile.Conversions do
end

# Private
defp fetch_source_file(%{file: nil}, source_file) do
{:ok, source_file}
end

defp fetch_source_file(%{file: path}, _) do
SourceFile.Store.open_temporary(path)
end

defp extract_lsp_character(%ElixirPosition{} = position, line(ascii?: true, text: text)) do
character = min(position.character, byte_size(text))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
defmodule ElixirLS.Experimental.Provider.Handlers.GotoDefinitionTest do
use ExUnit.Case, async: true

alias ElixirLS.LanguageServer.Experimental.Protocol.Requests.GotoDefinition
alias ElixirLS.LanguageServer.Experimental.Protocol.Responses
alias ElixirLS.LanguageServer.Experimental.Provider.Env
alias ElixirLS.LanguageServer.Experimental.Provider.Handlers
alias ElixirLS.LanguageServer.Experimental.SourceFile
alias ElixirLS.LanguageServer.Experimental.SourceFile.Conversions

alias ElixirLS.LanguageServer.Fixtures.LspProtocol
alias ElixirLS.LanguageServer.Test.FixtureHelpers

import LspProtocol
import ElixirLS.Test.TextLoc, only: [annotate_assert: 4]

setup do
{:ok, _} = start_supervised(SourceFile.Store)
:ok
end

def request(file_path, line, char) do
uri = Conversions.ensure_uri(file_path)

params = [
text_document: [uri: uri],
position: [line: line, character: char]
]

with {:ok, contents} <- File.read(file_path),
:ok <- SourceFile.Store.open(uri, contents, 1),
{:ok, req} <- build(GotoDefinition, params) do
GotoDefinition.to_elixir(req)
end
end

def handle(request) do
Handlers.GotoDefinition.handle(request, Env.new())
end

def with_referenced_file(_) do
path = FixtureHelpers.get_path("references_referenced.ex")
uri = Conversions.ensure_uri(path)
{:ok, file_uri: uri, file_path: path}
end

describe "when a file contains references" do
setup [:with_referenced_file]

test "find definition remote function call", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_remote.ex")
{line, char} = {4, 28}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
ReferencesReferenced.referenced_fun()
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 1
assert definition.range.start.character == 6
assert definition.range.end.line == 1
assert definition.range.end.character == 6
end

test "find definition remote macro call", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_remote.ex")
{line, char} = {8, 28}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
ReferencesReferenced.referenced_macro a do
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 8
assert definition.range.start.character == 11
assert definition.range.end.line == 8
assert definition.range.end.character == 11
end

test "find definition imported function call", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_imported.ex")
{line, char} = {4, 5}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
referenced_fun()
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 1
assert definition.range.start.character == 6
assert definition.range.end.line == 1
assert definition.range.end.character == 6
end

test "find definition imported macro call", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_imported.ex")
{line, char} = {8, 5}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
referenced_macro a do
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 8
assert definition.range.start.character == 11
assert definition.range.end.line == 8
assert definition.range.end.character == 11
end

test "find definition local function call", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_referenced.ex")
{line, char} = {15, 5}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
referenced_fun()
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 1
assert definition.range.start.character == 6
assert definition.range.end.line == 1
assert definition.range.end.character == 6
end

test "find definition local macro call", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_referenced.ex")
{line, char} = {19, 5}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
referenced_macro a do
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 8
assert definition.range.start.character == 11
assert definition.range.end.line == 8
assert definition.range.end.character == 11
end

test "find definition variable", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_referenced.ex")
{line, char} = {4, 13}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
IO.puts(referenced_variable + 1)
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 2
assert definition.range.start.character == 4
assert definition.range.end.line == 2
assert definition.range.end.character == 4
end

test "find definition attribute", %{file_uri: uri} do
file_path = FixtureHelpers.get_path("references_referenced.ex")
{line, char} = {27, 5}

{:ok, request} = request(file_path, line, char)

annotate_assert(file_path, line, char, """
@referenced_attribute
^
""")

{:reply, %Responses.GotoDefinition{result: definition}} = handle(request)

assert definition.uri == uri
assert definition.range.start.line == 24
assert definition.range.start.character == 2
assert definition.range.end.line == 24
assert definition.range.end.character == 2
end
end
end