Skip to content

Commit

Permalink
Merge pull request #58 from mattbaker/find-refs-in-umbrella
Browse files Browse the repository at this point in the history
Support Find References in umbrella apps
  • Loading branch information
JakeBecker authored Mar 3, 2018
2 parents 8832a28 + 891f66f commit 97c5ae4
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 54 deletions.
36 changes: 21 additions & 15 deletions apps/language_server/lib/language_server/build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ defmodule ElixirLS.LanguageServer.Build do
{nil, nil}
else
spawn_monitor(fn ->
{us, _} =
:timer.tc(fn ->
IO.puts("Compiling with Mix env #{Mix.env()}")

case reload_project() do
{:ok, mixfile_diagnostics} ->
{status, diagnostics} = compile()
Server.build_finished(parent, {status, mixfile_diagnostics ++ diagnostics})

{:error, mixfile_diagnostics} ->
Server.build_finished(parent, {:error, mixfile_diagnostics})
end
end)

JsonRpc.log_message(:info, "Compile took #{div(us, 1000)} milliseconds")
with_build_lock(fn ->
{us, _} =
:timer.tc(fn ->
IO.puts("Compiling with Mix env #{Mix.env()}")

case reload_project() do
{:ok, mixfile_diagnostics} ->
{status, diagnostics} = compile()
Server.build_finished(parent, {status, mixfile_diagnostics ++ diagnostics})

{:error, mixfile_diagnostics} ->
Server.build_finished(parent, {:error, mixfile_diagnostics})
end
end)

JsonRpc.log_message(:info, "Compile took #{div(us, 1000)} milliseconds")
end)
end)
end
end
Expand Down Expand Up @@ -93,6 +95,10 @@ defmodule ElixirLS.LanguageServer.Build do
}
end

def with_build_lock(func) do
:global.trans({__MODULE__, self()}, func)
end

defp reload_project do
mixfile = Path.absname(System.get_env("MIX_EXS") || "mix.exs")

Expand Down
42 changes: 32 additions & 10 deletions apps/language_server/lib/language_server/providers/references.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ defmodule ElixirLS.LanguageServer.Providers.References do
any function or module identified at the provided location.
"""

alias ElixirLS.LanguageServer.SourceFile
alias ElixirLS.LanguageServer.{SourceFile, Build}
alias ElixirSense.Core.{Metadata, Parser, Source, Introspection}

def references(text, line, character, _include_declaration) do
xref_at_cursor(text, line, character)
|> Enum.filter(fn %{line: line} -> is_integer(line) end)
|> Enum.map(&build_location/1)
Build.with_build_lock(fn ->
xref_at_cursor(text, line, character)
|> Enum.filter(fn %{line: line} -> is_integer(line) end)
|> Enum.map(&build_location/1)
end)
end

def supported? do
Expand Down Expand Up @@ -48,19 +50,39 @@ defmodule ElixirLS.LanguageServer.Providers.References do
defp add_arity({mod, fun}, %{scope: {fun, arity}, module: mod}), do: {mod, fun, arity}
defp add_arity({mod, fun}, _env), do: {mod, fun, nil}

def callers(nil), do: []
def callers(mfa), do: Mix.Tasks.Xref.calls() |> Enum.filter(caller_filter(mfa))
defp callers(mfa) do
if Mix.Project.umbrella?() do
umbrella_calls()
else
Mix.Tasks.Xref.calls()
end
|> Enum.filter(caller_filter(mfa))
end

def umbrella_calls() do
build_dir = Path.expand(Mix.Project.config()[:build_path])

Mix.Project.apps_paths()
|> Enum.flat_map(fn {app, path} ->
Mix.Project.in_project(app, path, [build_path: build_dir], fn _ ->
Mix.Tasks.Xref.calls()
|> Enum.map(fn %{file: file} = call ->
Map.put(call, :file, Path.expand(file))
end)
end)
end)
end

defp caller_filter({module, nil, nil}), do: &match?(%{callee: {^module, _, _}}, &1)
defp caller_filter({module, func, nil}), do: &match?(%{callee: {^module, ^func, _}}, &1)
defp caller_filter({module, func, arity}), do: &match?(%{callee: {^module, ^func, ^arity}}, &1)

defp build_location(call) do
defp build_location(%{file: file, line: line}) do
%{
"uri" => SourceFile.path_to_uri(call.file),
"uri" => SourceFile.path_to_uri(file),
"range" => %{
"start" => %{"line" => call.line - 1, "character" => 0},
"end" => %{"line" => call.line - 1, "character" => 0}
"start" => %{"line" => line - 1, "character" => 0},
"end" => %{"line" => line - 1, "character" => 0}
}
}
end
Expand Down
11 changes: 7 additions & 4 deletions apps/language_server/lib/language_server/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,13 @@ defmodule ElixirLS.LanguageServer.Server do

defp handle_request(references_req(_id, uri, line, character, include_declaration), state) do
fun = fn ->
{
:ok,
References.references(state.source_files[uri].text, line, character, include_declaration)
}
{:ok,
References.references(
state.source_files[uri].text,
line,
character,
include_declaration
)}
end

{:async, fun, state}
Expand Down
5 changes: 5 additions & 0 deletions apps/language_server/test/fixtures/references/lib/a.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule A do
def fun do
B.fun()
end
end
5 changes: 5 additions & 0 deletions apps/language_server/test/fixtures/references/lib/b.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule B do
def fun do
:ok
end
end
11 changes: 11 additions & 0 deletions apps/language_server/test/fixtures/references/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule References.MixProject do
use Mix.Project

def project do
[app: :references, version: "0.1.0"]
end

def application do
[]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule App1 do
def hello() do
App2.hello()
end
end
15 changes: 15 additions & 0 deletions apps/language_server/test/fixtures/umbrella/apps/app1/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule App1.Mixfile do
use Mix.Project

def project do
[app: :app1, version: "0.1.0", deps: deps()]
end

def application do
[]
end

defp deps do
[{:app2, in_umbrella: true}]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule App2 do
def hello do
:app2
end
end
11 changes: 11 additions & 0 deletions apps/language_server/test/fixtures/umbrella/apps/app2/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule App2.Mixfile do
use Mix.Project

def project do
[app: :app2, version: "0.1.0"]
end

def application do
[]
end
end
7 changes: 7 additions & 0 deletions apps/language_server/test/fixtures/umbrella/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Umbrella.Mixfile do
use Mix.Project

def project do
[apps_path: "apps"]
end
end
97 changes: 72 additions & 25 deletions apps/language_server/test/server_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ defmodule ElixirLS.LanguageServer.ServerTest do

doctest(Server)

defp initialize(server) do
Server.receive_packet(server, initialize_req(1, root_uri(), %{}))
Server.receive_packet(server, notification("initialized"))

Server.receive_packet(
server,
did_change_configuration(%{"elixirLS" => %{"dialyzerEnabled" => false}})
)
end

defp root_uri do
SourceFile.path_to_uri(File.cwd!())
end

setup do
{:ok, server} = Server.start_link()
{:ok, packet_capture} = PacketCapture.start_link(self())
Expand Down Expand Up @@ -105,16 +119,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do

test "formatter", %{server: server} do
in_fixture(__DIR__, "formatter", fn ->
root_uri = SourceFile.path_to_uri(File.cwd!())
Server.receive_packet(server, initialize_req(1, root_uri, %{}))
Server.receive_packet(server, notification("initialized"))

Server.receive_packet(
server,
did_change_configuration(%{"elixirLS" => %{"dialyzerEnabled" => false}})
)

uri = Path.join([root_uri, "file.ex"])
uri = Path.join([root_uri(), "file.ex"])
code = ~S(
defmodule MyModule do
def my_fn do
Expand All @@ -123,6 +128,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do
end
)

initialize(server)
Server.receive_packet(server, did_open(uri, "elixir", 1, code))
Server.receive_packet(server, formatting_req(1, uri, %{}))

Expand Down Expand Up @@ -168,16 +174,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do

test "reports build diagnostics", %{server: server} do
in_fixture(__DIR__, "build_errors", fn ->
root_uri = SourceFile.path_to_uri(File.cwd!())
error_file = SourceFile.path_to_uri("lib/has_error.ex")

Server.receive_packet(server, initialize_req(1, root_uri, %{}))
Server.receive_packet(server, notification("initialized"))

Server.receive_packet(
server,
did_change_configuration(%{"elixirLS" => %{"dialyzerEnabled" => false}})
)
initialize(server)

assert_receive notification("textDocument/publishDiagnostics", %{
"uri" => ^error_file,
Expand All @@ -196,15 +195,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do

test "reports error if no mixfile", %{server: server} do
in_fixture(__DIR__, "no_mixfile", fn ->
root_uri = SourceFile.path_to_uri(File.cwd!())
mixfile_uri = SourceFile.path_to_uri("mix.exs")
Server.receive_packet(server, initialize_req(1, root_uri, %{}))
Server.receive_packet(server, notification("initialized"))

Server.receive_packet(
server,
did_change_configuration(%{"elixirLS" => %{"dialyzerEnabled" => false}})
)
initialize(server)

assert_receive notification("textDocument/publishDiagnostics", %{
"uri" => ^mixfile_uri,
Expand All @@ -218,4 +211,58 @@ defmodule ElixirLS.LanguageServer.ServerTest do
5000
end)
end

test "finds references in non-umbrella project", %{server: server} do
in_fixture(__DIR__, "references", fn ->
file_path = "lib/b.ex"
file_uri = SourceFile.path_to_uri(file_path)
text = File.read!(file_path)
reference_uri = SourceFile.path_to_uri("lib/a.ex")

initialize(server)
Server.receive_packet(server, did_open(file_uri, "elixir", 1, text))

Server.receive_packet(
server,
references_req(4, file_uri, 1, 8, true)
)

assert_receive(
response(4, [
%{
"range" => %{"start" => %{"line" => 2}, "end" => %{"line" => 2}},
"uri" => ^reference_uri
}
]),
5000
)
end)
end

test "finds references in umbrella project", %{server: server} do
in_fixture(__DIR__, "umbrella", fn ->
file_path = "apps/app2/lib/app2.ex"
file_uri = SourceFile.path_to_uri(file_path)
text = File.read!(file_path)
reference_uri = SourceFile.path_to_uri("apps/app1/lib/app1.ex")

initialize(server)
Server.receive_packet(server, did_open(file_uri, "elixir", 1, text))

Server.receive_packet(
server,
references_req(4, file_uri, 1, 9, true)
)

assert_receive(
response(4, [
%{
"range" => %{"start" => %{"line" => 2}, "end" => %{"line" => 2}},
"uri" => ^reference_uri
}
]),
5000
)
end)
end
end

0 comments on commit 97c5ae4

Please sign in to comment.