Skip to content

Commit

Permalink
Merge pull request #812 from scottming/support-go-to-definition-in-ex…
Browse files Browse the repository at this point in the history
…perimental

Support go to definition in the experimental project
  • Loading branch information
scohen committed Mar 7, 2023
2 parents 3f01e21 + a3ed366 commit 0e4b577
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 1 deletion.
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

0 comments on commit 0e4b577

Please sign in to comment.