From cadd3742dbe45154e930c17cd626823a363b6e47 Mon Sep 17 00:00:00 2001 From: akash-akya Date: Sun, 19 Apr 2020 15:38:12 +0530 Subject: [PATCH 1/2] Load all modules after first build --- .../lib/language_server/build.ex | 35 +++++++++++-- .../lib/language_server/protocol.ex | 12 +++++ .../lib/language_server/server.ex | 13 +++-- .../umbrella/apps/app2/lib/app2/foo.ex | 5 ++ apps/language_server/test/server_test.exs | 52 +++++++++++++++++++ 5 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 apps/language_server/test/fixtures/umbrella/apps/app2/lib/app2/foo.ex diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 208bd590..ae9ebdf3 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -1,7 +1,7 @@ defmodule ElixirLS.LanguageServer.Build do alias ElixirLS.LanguageServer.{Server, JsonRpc, SourceFile} - def build(parent, root_path, fetch_deps?) do + def build(parent, root_path, opts) do if Path.absname(File.cwd!()) != Path.absname(root_path) do IO.puts("Skipping build because cwd changed from #{root_path} to #{File.cwd!()}") {nil, nil} @@ -19,10 +19,16 @@ defmodule ElixirLS.LanguageServer.Build do case reload_project() do {:ok, mixfile_diagnostics} -> # FIXME: Private API - if fetch_deps? and Mix.Dep.load_on_environment([]) != prev_deps, - do: fetch_deps() + if Keyword.get(opts, :fetch_deps?) and + Mix.Dep.load_on_environment([]) != prev_deps, + do: fetch_deps() {status, diagnostics} = compile() + + if status in [:ok, :noop] and Keyword.get(opts, :load_all_modules?) do + load_all_modules() + end + Server.build_finished(parent, {status, mixfile_diagnostics ++ diagnostics}) {:error, mixfile_diagnostics} -> @@ -160,6 +166,29 @@ defmodule ElixirLS.LanguageServer.Build do end end + defp load_all_modules do + apps = + cond do + Mix.Project.umbrella?() -> + Mix.Project.apps_paths() |> Map.keys() + + app = Keyword.get(Mix.Project.config(), :app) -> + [app] + + true -> + [] + end + + Enum.each(apps, fn app -> + true = Code.prepend_path(Path.join(Mix.Project.build_path(), "lib/#{app}/ebin")) + + case Application.load(app) do + :ok -> :ok + {:error, {:already_loaded, _}} -> :ok + end + end) + end + defp compile do case Mix.Task.run("compile", ["--return-errors", "--ignore-module-conflict"]) do {status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) -> diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index f6a6853b..45e9abd4 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -191,6 +191,18 @@ defmodule ElixirLS.LanguageServer.Protocol do end end + defmacro shutdown_req(id) do + quote do + request(unquote(id), "shutdown", %{}) + end + end + + defmacro exit_req(id) do + quote do + request(unquote(id), "exit", %{}) + end + end + # Other utilities defmacro range(start_line, start_character, end_line, end_character) do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 01692815..593b8818 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -44,6 +44,7 @@ defmodule ElixirLS.LanguageServer.Server do build_diagnostics: [], dialyzer_diagnostics: [], needs_build?: false, + load_all_modules?: false, build_running?: false, analysis_ready?: false, received_shutdown?: false, @@ -567,14 +568,20 @@ defmodule ElixirLS.LanguageServer.Server do defp trigger_build(state) do if build_enabled?(state) and not state.build_running? do fetch_deps? = Map.get(state.settings || %{}, "fetchDeps", true) - {_pid, build_ref} = Build.build(self(), state.project_dir, fetch_deps?) + + {_pid, build_ref} = + Build.build(self(), state.project_dir, + fetch_deps?: fetch_deps?, + load_all_modules?: state.load_all_modules? + ) %__MODULE__{ state | build_ref: build_ref, needs_build?: false, build_running?: true, - analysis_ready?: false + analysis_ready?: false, + load_all_modules?: false } else %__MODULE__{state | needs_build?: true, analysis_ready?: false} @@ -771,7 +778,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - put_in(state.project_dir, project_dir) + Map.merge(state, %{project_dir: project_dir, load_all_modules?: true}) prev_project_dir != project_dir -> JsonRpc.show_message( diff --git a/apps/language_server/test/fixtures/umbrella/apps/app2/lib/app2/foo.ex b/apps/language_server/test/fixtures/umbrella/apps/app2/lib/app2/foo.ex new file mode 100644 index 00000000..04f0c79c --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella/apps/app2/lib/app2/foo.ex @@ -0,0 +1,5 @@ +defmodule App2.Foo do + def hello do + :foo + end +end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 529c9180..b69d3cdf 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -336,4 +336,56 @@ defmodule ElixirLS.LanguageServer.ServerTest do ]) = resp end) end + + test "loading of umbrella app dependencies", %{server: server} do + in_fixture(__DIR__, "umbrella", fn -> + # We test this by opening the umbrella project twice. + # First to compile the applications and build the cache. + # Second time to see if loads modules + initialize(server) + + # Upon first vist, server complies and loads all umbrella applications and modules + Process.sleep(1_500) + + Server.receive_packet(server, shutdown_req(2)) + assert response(2, nil) = assert_receive(%{"id" => 2}, 5000) + Server.receive_packet(server, exit_req(3)) + + Process.sleep(500) + # unload App2.Foo + purge([App2.Foo]) + + # Upon re-visiting, server does not attempt to compile umbrella applications + initialize(server) + Process.sleep(1_500) + + file_path = "apps/app1/lib/bar.ex" + uri = SourceFile.path_to_uri(file_path) + + code = """ + defmodule Bar do + def fnuc, do: App2.Fo + #____________________^ + end + """ + + Server.receive_packet(server, did_open(uri, "elixir", 1, code)) + Server.receive_packet(server, completion_req(2, uri, 1, 23)) + + resp = assert_receive(%{"id" => 2}, 5000) + + assert response(2, %{ + "isIncomplete" => false, + "items" => [ + %{ + "detail" => "module", + "documentation" => _, + "kind" => 9, + "label" => "Foo" + } + | _ + ] + }) = resp + end) + end end From 3b77dc895de2331dc756347b6995965df917105d Mon Sep 17 00:00:00 2001 From: akash-akya Date: Wed, 29 Apr 2020 21:13:09 +0530 Subject: [PATCH 2/2] Refactor loading umbrella apps test --- .../lib/language_server/protocol.ex | 12 -- apps/language_server/test/server_test.exs | 122 +++++++++++------- 2 files changed, 77 insertions(+), 57 deletions(-) diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index 45e9abd4..f6a6853b 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -191,18 +191,6 @@ defmodule ElixirLS.LanguageServer.Protocol do end end - defmacro shutdown_req(id) do - quote do - request(unquote(id), "shutdown", %{}) - end - end - - defmacro exit_req(id) do - quote do - request(unquote(id), "exit", %{}) - end - end - # Other utilities defmacro range(start_line, start_character, end_line, end_character) do diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index b69d3cdf..b91f96fe 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -20,12 +20,15 @@ defmodule ElixirLS.LanguageServer.ServerTest do SourceFile.path_to_uri(File.cwd!()) end - setup do - {:ok, server} = Server.start_link() - {:ok, packet_capture} = PacketCapture.start_link(self()) - Process.group_leader(server, packet_capture) - - {:ok, %{server: server}} + setup context do + unless context[:skip_server] do + server = start_supervised!({Server, nil}) + packet_capture = start_supervised!({PacketCapture, self()}) + Process.group_leader(server, packet_capture) + {:ok, %{server: server}} + else + :ok + end end test "textDocument/didChange when the client hasn't claimed ownership with textDocument/didOpen", @@ -337,55 +340,84 @@ defmodule ElixirLS.LanguageServer.ServerTest do end) end - test "loading of umbrella app dependencies", %{server: server} do + @tag :skip_server + test "loading of umbrella app dependencies" do in_fixture(__DIR__, "umbrella", fn -> # We test this by opening the umbrella project twice. # First to compile the applications and build the cache. # Second time to see if loads modules - initialize(server) + with_new_server(fn server -> + initialize(server) + wait_until_compiled(server) + end) - # Upon first vist, server complies and loads all umbrella applications and modules - Process.sleep(1_500) - - Server.receive_packet(server, shutdown_req(2)) - assert response(2, nil) = assert_receive(%{"id" => 2}, 5000) - Server.receive_packet(server, exit_req(3)) - - Process.sleep(500) # unload App2.Foo purge([App2.Foo]) - # Upon re-visiting, server does not attempt to compile umbrella applications - initialize(server) - Process.sleep(1_500) + # re-visiting the same project + with_new_server(fn server -> + initialize(server) + wait_until_compiled(server) - file_path = "apps/app1/lib/bar.ex" - uri = SourceFile.path_to_uri(file_path) + file_path = "apps/app1/lib/bar.ex" + uri = SourceFile.path_to_uri(file_path) - code = """ - defmodule Bar do - def fnuc, do: App2.Fo - #____________________^ - end - """ - - Server.receive_packet(server, did_open(uri, "elixir", 1, code)) - Server.receive_packet(server, completion_req(2, uri, 1, 23)) - - resp = assert_receive(%{"id" => 2}, 5000) - - assert response(2, %{ - "isIncomplete" => false, - "items" => [ - %{ - "detail" => "module", - "documentation" => _, - "kind" => 9, - "label" => "Foo" - } - | _ - ] - }) = resp + code = """ + defmodule Bar do + def fnuc, do: App2.Fo + # ^ + end + """ + + Server.receive_packet(server, did_open(uri, "elixir", 1, code)) + Server.receive_packet(server, completion_req(3, uri, 1, 23)) + + resp = assert_receive(%{"id" => 3}, 5000) + + assert response(3, %{ + "isIncomplete" => false, + "items" => [ + %{ + "detail" => "module", + "documentation" => _, + "kind" => 9, + "label" => "Foo" + } + | _ + ] + }) = resp + end) end) end + + defp with_new_server(func) do + server = start_supervised!({Server, nil}) + packet_capture = start_supervised!({PacketCapture, self()}) + Process.group_leader(server, packet_capture) + + try do + func.(server) + after + stop_supervised(Server) + stop_supervised(PacketCapture) + flush_mailbox() + end + end + + defp flush_mailbox do + receive do + _ -> flush_mailbox() + after + 0 -> :ok + end + end + + defp wait_until_compiled(pid) do + state = :sys.get_state(pid) + + if state.build_running? do + Process.sleep(500) + wait_until_compiled(pid) + end + end end