Skip to content

Commit

Permalink
Implementations provider (elixir-editors#415)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lukaszsamson authored Nov 27, 2020
1 parent 99a6447 commit 434f6bc
Show file tree
Hide file tree
Showing 21 changed files with 229 additions and 107 deletions.
9 changes: 9 additions & 0 deletions apps/language_server/lib/language_server/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ defmodule ElixirLS.LanguageServer.Protocol do
end
end

defmacro implementation_req(id, uri, line, character) do
quote do
request(unquote(id), "textDocument/implementation", %{
"textDocument" => %{"uri" => unquote(uri)},
"position" => %{"line" => unquote(line), "character" => unquote(character)}
})
end
end

defmacro completion_req(id, uri, line, character) do
quote do
request(unquote(id), "textDocument/completion", %{
Expand Down
19 changes: 19 additions & 0 deletions apps/language_server/lib/language_server/protocol/location.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,23 @@ defmodule ElixirLS.LanguageServer.Protocol.Location do
"""
@derive JasonVendored.Encoder
defstruct [:uri, :range]

alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.Protocol

def new(%ElixirSense.Location{file: file, line: line, column: column}, uri) do
uri =
case file do
nil -> uri
_ -> SourceFile.path_to_uri(file)
end

%Protocol.Location{
uri: uri,
range: %{
"start" => %{"line" => line - 1, "character" => column - 1},
"end" => %{"line" => line - 1, "character" => column - 1}
}
}
end
end
45 changes: 17 additions & 28 deletions apps/language_server/lib/language_server/providers/completion.ex
Original file line number Diff line number Diff line change
Expand Up @@ -236,37 +236,26 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do

defp from_completion_item(
%{type: :module, name: name, summary: summary, subtype: subtype, metadata: metadata},
%{
def_before: nil,
prefix: prefix
},
%{def_before: nil},
_options
) do
capitalized? = String.first(name) == String.upcase(String.first(name))

if String.ends_with?(prefix, ":") and capitalized? do
nil
else
label = if capitalized?, do: name, else: ":" <> name

detail =
if subtype do
Atom.to_string(subtype)
else
"module"
end
detail =
if subtype do
Atom.to_string(subtype)
else
"module"
end

%__MODULE__{
label: label,
kind: :module,
detail: detail,
documentation: summary,
insert_text: name,
filter_text: name,
priority: 14,
tags: metadata_to_tags(metadata)
}
end
%__MODULE__{
label: name,
kind: :module,
detail: detail,
documentation: summary,
insert_text: name,
filter_text: name,
priority: 14,
tags: metadata_to_tags(metadata)
}
end

defp from_completion_item(
Expand Down
30 changes: 8 additions & 22 deletions apps/language_server/lib/language_server/providers/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,18 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do
Go-to-definition provider utilizing Elixir Sense
"""

alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.Protocol

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

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

uri =
case file do
nil -> uri
_ -> SourceFile.path_to_uri(file)
end

{:ok,
%Protocol.Location{
uri: uri,
range: %{
"start" => %{"line" => line - 1, "character" => column - 1},
"end" => %{"line" => line - 1, "character" => column - 1}
}
}}
end
{:ok, result}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do
import ElixirLS.LanguageServer.Protocol, only: [range: 4]
alias ElixirLS.LanguageServer.SourceFile

def supported? do
function_exported?(Code, :format_string!, 2)
end

def format(%SourceFile{} = source_file, uri, project_dir) do
if can_format?(uri, project_dir) do
case SourceFile.formatter_opts(uri) do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule ElixirLS.LanguageServer.Providers.Implementation do
@moduledoc """
Go-to-implementation provider utilizing Elixir Sense
"""

alias ElixirLS.LanguageServer.Protocol

def implementation(uri, text, line, character) do
locations = ElixirSense.implementations(text, line + 1, character + 1)
results = for location <- locations, do: Protocol.Location.new(location, uri)

{:ok, results}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ defmodule ElixirLS.LanguageServer.Providers.References do
end)
end

def supported? do
Mix.Tasks.Xref.__info__(:functions) |> Enum.member?({:calls, 0})
end

defp build_reference(ref, current_file_uri) do
%{
range: %{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule ElixirLS.LanguageServer.Providers.SignatureHelp do
alias ElixirLS.LanguageServer.SourceFile

def trigger_characters(), do: ["("]

def signature(%SourceFile{} = source_file, line, character) do
response =
case ElixirSense.signature(source_file.text, line + 1, character + 1) do
Expand Down
23 changes: 13 additions & 10 deletions apps/language_server/lib/language_server/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule ElixirLS.LanguageServer.Server do
Completion,
Hover,
Definition,
Implementation,
References,
Formatting,
SignatureHelp,
Expand Down Expand Up @@ -475,10 +476,6 @@ defmodule ElixirLS.LanguageServer.Server do
e in InvalidParamError ->
JsonRpc.respond_with_error(id, :invalid_params, e.message)
state

other ->
JsonRpc.respond_with_error(id, :internal_error, other.message)
state
end

defp handle_request_packet(id, _packet, state) do
Expand Down Expand Up @@ -546,6 +543,14 @@ defmodule ElixirLS.LanguageServer.Server do
{:async, fun, state}
end

defp handle_request(implementation_req(_id, uri, line, character), state) do
fun = fn ->
Implementation.implementation(uri, state.source_files[uri].text, line, character)
end

{:async, fun, state}
end

defp handle_request(references_req(_id, uri, line, character, include_declaration), state) do
source_file = get_source_file(state, uri)

Expand Down Expand Up @@ -731,9 +736,6 @@ defmodule ElixirLS.LanguageServer.Server do
rescue
e in InvalidParamError ->
{:error, :invalid_params, e.message}

other ->
{:error, :internal_error, other.message}
end

GenServer.call(parent, {:request_finished, id, result}, :infinity)
Expand All @@ -751,9 +753,10 @@ defmodule ElixirLS.LanguageServer.Server do
"hoverProvider" => true,
"completionProvider" => %{"triggerCharacters" => Completion.trigger_characters()},
"definitionProvider" => true,
"referencesProvider" => References.supported?(),
"documentFormattingProvider" => Formatting.supported?(),
"signatureHelpProvider" => %{"triggerCharacters" => ["("]},
"implementationProvider" => true,
"referencesProvider" => true,
"documentFormattingProvider" => true,
"signatureHelpProvider" => %{"triggerCharacters" => SignatureHelp.trigger_characters()},
"documentSymbolProvider" => true,
"workspaceSymbolProvider" => true,
"documentOnTypeFormattingProvider" => %{"firstTriggerCharacter" => "\n"},
Expand Down
5 changes: 3 additions & 2 deletions apps/language_server/test/providers/definition_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do
alias ElixirLS.LanguageServer.Providers.Definition
alias ElixirLS.LanguageServer.Protocol.Location
alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.Test.FixtureHelpers
require ElixirLS.Test.TextLoc

test "find definition" do
file_path = Path.join(__DIR__, "../support/references_a.ex") |> Path.expand()
file_path = FixtureHelpers.get_path("references_a.ex")
text = File.read!(file_path)
uri = SourceFile.path_to_uri(file_path)

b_file_path = Path.join(__DIR__, "../support/references_b.ex") |> Path.expand()
b_file_path = FixtureHelpers.get_path("references_b.ex")
b_uri = SourceFile.path_to_uri(b_file_path)

{line, char} = {2, 30}
Expand Down
13 changes: 7 additions & 6 deletions apps/language_server/test/providers/formatting_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
use ExUnit.Case
alias ElixirLS.LanguageServer.Providers.Formatting
alias ElixirLS.LanguageServer.SourceFile

test "Formats a file" do
uri = "file://project/file.ex"
Expand All @@ -15,7 +16,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
end
"""

source_file = %ElixirLS.LanguageServer.SourceFile{
source_file = %SourceFile{
text: text,
version: 1,
dirty?: true
Expand Down Expand Up @@ -64,7 +65,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
end
"""

source_file = %ElixirLS.LanguageServer.SourceFile{
source_file = %SourceFile{
text: text,
version: 1,
dirty?: true
Expand All @@ -83,7 +84,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
IO.puts "😀"
"""

source_file = %ElixirLS.LanguageServer.SourceFile{
source_file = %SourceFile{
text: text,
version: 1,
dirty?: true
Expand Down Expand Up @@ -118,7 +119,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
IO.puts "🏳️‍🌈"
"""

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

source_file = %ElixirLS.LanguageServer.SourceFile{
source_file = %SourceFile{
text: text,
version: 1,
dirty?: true
Expand Down Expand Up @@ -183,7 +184,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do

test "honors :inputs when deciding to format" do
file = __ENV__.file
uri = "file://" <> file
uri = SourceFile.path_to_uri(file)
project_dir = Path.dirname(file)

opts = []
Expand Down
35 changes: 35 additions & 0 deletions apps/language_server/test/providers/implementation_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule ElixirLS.LanguageServer.Providers.ImplementationTest do
use ExUnit.Case, async: true

alias ElixirLS.LanguageServer.Providers.Implementation
alias ElixirLS.LanguageServer.Protocol.Location
alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.Test.FixtureHelpers
require ElixirLS.Test.TextLoc

test "find implementations" do
# force load as currently only loaded or loadable modules that are a part
# of an application are found
Code.ensure_loaded?(ElixirLS.LanguageServer.Fixtures.ExampleBehaviourImpl)

file_path = FixtureHelpers.get_path("example_behaviour.ex")
text = File.read!(file_path)
uri = SourceFile.path_to_uri(file_path)

{line, char} = {0, 43}

ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """
defmodule ElixirLS.LanguageServer.Fixtures.ExampleBehaviour do
^
""")

assert {:ok, [%Location{uri: ^uri, range: range}]} =
Implementation.implementation(uri, text, line, char)

assert range ==
%{
"start" => %{"line" => 5, "character" => 10},
"end" => %{"line" => 5, "character" => 10}
}
end
end
9 changes: 5 additions & 4 deletions apps/language_server/test/providers/references_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do

alias ElixirLS.LanguageServer.Providers.References
alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.Test.FixtureHelpers
require ElixirLS.Test.TextLoc

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

Expand Down Expand Up @@ -39,7 +40,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do
end

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

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

test "finds references to a variable" do
file_path = Path.join(__DIR__, "../support/references_b.ex") |> Path.expand()
file_path = FixtureHelpers.get_path("references_b.ex")
text = File.read!(file_path)
uri = SourceFile.path_to_uri(file_path)
{line, char} = {4, 14}
Expand Down
Loading

0 comments on commit 434f6bc

Please sign in to comment.