From 40ddbc593f77f5deb888f44090fa5184419092ed Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Thu, 29 Sep 2022 08:39:16 +0200
Subject: [PATCH 1/7] add custom command returning tests in file

---
 apps/language_server/lib/language_server.ex   |  3 +-
 .../language_server/ex_unit_test_tracer.ex    | 90 +++++++++++++++++++
 .../providers/execute_command.ex              |  3 +-
 .../get_ex_unit_tests_in_file.ex              | 16 ++++
 4 files changed, 110 insertions(+), 2 deletions(-)
 create mode 100644 apps/language_server/lib/language_server/ex_unit_test_tracer.ex
 create mode 100644 apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex

diff --git a/apps/language_server/lib/language_server.ex b/apps/language_server/lib/language_server.ex
index 36dfd96c1..0e7dc7f18 100644
--- a/apps/language_server/lib/language_server.ex
+++ b/apps/language_server/lib/language_server.ex
@@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer do
       {ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server},
       {ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc},
       {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []},
-      {ElixirLS.LanguageServer.Tracer, []}
+      {ElixirLS.LanguageServer.Tracer, []},
+      {ElixirLS.LanguageServer.ExUnitTestTracer, []}
     ]
 
     opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0]
diff --git a/apps/language_server/lib/language_server/ex_unit_test_tracer.ex b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
new file mode 100644
index 000000000..6d0190e4a
--- /dev/null
+++ b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
@@ -0,0 +1,90 @@
+defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
+  use GenServer
+
+  @tables ~w(tests)a
+
+  for table <- @tables do
+    defp table_name(unquote(table)) do
+      :"#{__MODULE__}:#{unquote(table)}"
+    end
+  end
+
+  def start_link(args) do
+    GenServer.start_link(__MODULE__, args, name: __MODULE__)
+  end
+
+  def get_tests(path) do
+    GenServer.call(__MODULE__, {:get_tests, path}, :infinity)
+  end
+
+  @impl true
+  def init(_args) do
+    for table <- @tables do
+      table_name = table_name(table)
+
+      :ets.new(table_name, [
+        :named_table,
+        :public,
+        read_concurrency: true,
+        write_concurrency: true
+      ])
+    end
+
+    ExUnit.start(autorun: false)
+
+    {:ok, %{}}
+  end
+
+  @impl true
+  def handle_call({:get_tests, path}, _from, state) do
+    :ets.delete_all_objects(table_name(:tests))
+    tracers = Code.compiler_options()[:tracers]
+    Code.put_compiler_option(:tracers, tracers ++ [__MODULE__])
+    # TODO parallel compiler and diagnostics?
+    _ = Code.compile_file(path)
+
+    Code.put_compiler_option(:tracers, tracers)
+    result = :ets.tab2list(table_name(:tests))
+    |> Enum.map(fn {{_file, module, line}, describes} ->
+      %{
+        module: inspect(module),
+        line: line,
+        describes: describes
+      }
+    end)
+
+    {:reply, result, state}
+  end
+
+  def trace({:on_module, _, _}, %Macro.Env{} = env) do
+    test_info = Module.get_attribute(env.module, :ex_unit_tests)
+
+    if test_info != nil do
+        describe_infos =test_info
+        |> Enum.group_by(fn %ExUnit.Test{tags: tags} -> {tags.describe, tags.describe_line} end)
+        |> Enum.map(fn {{describe, describe_line}, tests} ->
+          tests = tests
+          |> Enum.map(fn %ExUnit.Test{tags: tags} = test ->
+            %{
+              name: test.name,
+              type: tags.test_type,
+              line: tags.line - 1,
+            }
+          end)
+
+          %{
+            describe: describe,
+            line: if(describe_line, do: describe_line - 1),
+            tests: tests
+          }
+        end)
+
+        :ets.insert(table_name(:tests), {{env.file, env.module, env.line - 1}, describe_infos})
+    end
+    :ok
+  end
+
+  def trace(_, %Macro.Env{} = _env) do
+    :ok
+  end
+end
diff --git a/apps/language_server/lib/language_server/providers/execute_command.ex b/apps/language_server/lib/language_server/providers/execute_command.ex
index 427521244..414759acc 100644
--- a/apps/language_server/lib/language_server/providers/execute_command.ex
+++ b/apps/language_server/lib/language_server/providers/execute_command.ex
@@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand do
     "expandMacro" => ExecuteCommand.ExpandMacro,
     "manipulatePipes" => ExecuteCommand.ManipulatePipes,
     "restart" => ExecuteCommand.Restart,
-    "mixClean" => ExecuteCommand.MixClean
+    "mixClean" => ExecuteCommand.MixClean,
+    "getExUnitTestsInFile" => ExecuteCommand.GetExUnitTestsInFile
   }
 
   @callback execute([any], %ElixirLS.LanguageServer.Server{}) ::
diff --git a/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
new file mode 100644
index 000000000..3c2100ea3
--- /dev/null
+++ b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
@@ -0,0 +1,16 @@
+defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile do
+  alias ElixirLS.LanguageServer.{SourceFile, ExUnitTestTracer}
+  @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand
+
+  @impl ElixirLS.LanguageServer.Providers.ExecuteCommand
+  def execute([uri], _state) do
+    path = SourceFile.Path.from_uri(uri)
+    # with {:ok, _} = Code.compile_file(path) do
+      tests = ExUnitTestTracer.get_tests(path)
+      {:ok, tests}
+    # else
+    #   {:error, reason} ->
+    #     {:error, :server_error, inspect(reason)}
+    # end
+  end
+end

From 2f601d62fc9e8995bf860211324b0460f559741c Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Sat, 22 Oct 2022 13:44:01 +0200
Subject: [PATCH 2/7] add tests

---
 apps/language_server/.formatter.exs           |  5 +-
 .../language_server/ex_unit_test_tracer.ex    | 68 +++++++++++++------
 .../get_ex_unit_tests_in_file.ex              | 12 ++--
 .../test/fixtures/project_with_tests/mix.exs  |  9 +++
 .../project_with_tests/test/error_test.exs    | 21 ++++++
 .../project_with_tests/test/fixture_test.exs  | 21 ++++++
 .../get_ex_unit_tests_in_file_test.exs        | 53 +++++++++++++++
 7 files changed, 159 insertions(+), 30 deletions(-)
 create mode 100644 apps/language_server/test/fixtures/project_with_tests/mix.exs
 create mode 100644 apps/language_server/test/fixtures/project_with_tests/test/error_test.exs
 create mode 100644 apps/language_server/test/fixtures/project_with_tests/test/fixture_test.exs
 create mode 100644 apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs

diff --git a/apps/language_server/.formatter.exs b/apps/language_server/.formatter.exs
index 4629fcb03..cfcc89ae3 100644
--- a/apps/language_server/.formatter.exs
+++ b/apps/language_server/.formatter.exs
@@ -1,4 +1,7 @@
-impossible_to_format = ["test/fixtures/token_missing_error/lib/has_error.ex"]
+impossible_to_format = [
+  "test/fixtures/token_missing_error/lib/has_error.ex",
+  "test/fixtures/project_with_tests/test/error_test.exs"
+]
 
 [
   inputs:
diff --git a/apps/language_server/lib/language_server/ex_unit_test_tracer.ex b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
index 6d0190e4a..49af37ae0 100644
--- a/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
+++ b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
@@ -40,18 +40,29 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
     :ets.delete_all_objects(table_name(:tests))
     tracers = Code.compiler_options()[:tracers]
     Code.put_compiler_option(:tracers, tracers ++ [__MODULE__])
-    # TODO parallel compiler and diagnostics?
-    _ = Code.compile_file(path)
-
-    Code.put_compiler_option(:tracers, tracers)
-    result = :ets.tab2list(table_name(:tests))
-    |> Enum.map(fn {{_file, module, line}, describes} ->
-      %{
-        module: inspect(module),
-        line: line,
-        describes: describes
-      }
-    end)
+
+    result =
+      try do
+        # TODO parallel compiler and diagnostics?
+        _ = Code.compile_file(path)
+
+        result =
+          :ets.tab2list(table_name(:tests))
+          |> Enum.map(fn {{_file, module, line}, describes} ->
+            %{
+              module: inspect(module),
+              line: line,
+              describes: describes
+            }
+          end)
+
+        {:ok, result}
+      rescue
+        e ->
+          {:error, e}
+      after
+        Code.put_compiler_option(:tracers, tracers)
+      end
 
     {:reply, result, state}
   end
@@ -60,17 +71,29 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
     test_info = Module.get_attribute(env.module, :ex_unit_tests)
 
     if test_info != nil do
-        describe_infos =test_info
+      describe_infos =
+        test_info
         |> Enum.group_by(fn %ExUnit.Test{tags: tags} -> {tags.describe, tags.describe_line} end)
         |> Enum.map(fn {{describe, describe_line}, tests} ->
-          tests = tests
-          |> Enum.map(fn %ExUnit.Test{tags: tags} = test ->
-            %{
-              name: test.name,
-              type: tags.test_type,
-              line: tags.line - 1,
-            }
-          end)
+          tests =
+            tests
+            |> Enum.map(fn %ExUnit.Test{tags: tags} = test ->
+              # drop test prefix
+              "test " <> test_name = Atom.to_string(test.name)
+
+              test_name =
+                if describe != nil do
+                  test_name |> String.replace_prefix(describe <> " ", "")
+                else
+                  test_name
+                end
+
+              %{
+                name: test_name,
+                type: tags.test_type,
+                line: tags.line - 1
+              }
+            end)
 
           %{
             describe: describe,
@@ -79,8 +102,9 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
           }
         end)
 
-        :ets.insert(table_name(:tests), {{env.file, env.module, env.line - 1}, describe_infos})
+      :ets.insert(table_name(:tests), {{env.file, env.module, env.line - 1}, describe_infos})
     end
+
     :ok
   end
 
diff --git a/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
index 3c2100ea3..37acc2314 100644
--- a/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
+++ b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
@@ -5,12 +5,10 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile
   @impl ElixirLS.LanguageServer.Providers.ExecuteCommand
   def execute([uri], _state) do
     path = SourceFile.Path.from_uri(uri)
-    # with {:ok, _} = Code.compile_file(path) do
-      tests = ExUnitTestTracer.get_tests(path)
-      {:ok, tests}
-    # else
-    #   {:error, reason} ->
-    #     {:error, :server_error, inspect(reason)}
-    # end
+
+    case ExUnitTestTracer.get_tests(path) do
+      {:ok, tests} -> {:ok, tests}
+      {:error, reason} -> {:error, :server_error, inspect(reason)}
+    end
   end
 end
diff --git a/apps/language_server/test/fixtures/project_with_tests/mix.exs b/apps/language_server/test/fixtures/project_with_tests/mix.exs
new file mode 100644
index 000000000..71a23daa8
--- /dev/null
+++ b/apps/language_server/test/fixtures/project_with_tests/mix.exs
@@ -0,0 +1,9 @@
+defmodule ProjectWithTests.MixProject do
+  use Mix.Project
+
+  def project do
+    [app: :project_with_tests, version: "0.1.0"]
+  end
+
+  def application, do: []
+end
diff --git a/apps/language_server/test/fixtures/project_with_tests/test/error_test.exs b/apps/language_server/test/fixtures/project_with_tests/test/error_test.exs
new file mode 100644
index 000000000..12be9dae8
--- /dev/null
+++ b/apps/language_server/test/fixtures/project_with_tests/test/error_test.exs
@@ -0,0 +1,21 @@
+defmodule FixtureTest do
+  use ExUnit.Case
+
+  defmodule ModuleWithoutTests do
+  end
+
+  test "fixture test" do
+    assert true
+  end
+
+  describe "describe with test" do
+    test "fixture test" do
+      assert true
+    end
+  end
+
+  describe "describe without test" do
+  end
+
+  test "this will be a test in future" do
+end
diff --git a/apps/language_server/test/fixtures/project_with_tests/test/fixture_test.exs b/apps/language_server/test/fixtures/project_with_tests/test/fixture_test.exs
new file mode 100644
index 000000000..dd215cb29
--- /dev/null
+++ b/apps/language_server/test/fixtures/project_with_tests/test/fixture_test.exs
@@ -0,0 +1,21 @@
+defmodule FixtureTest do
+  use ExUnit.Case
+
+  defmodule ModuleWithoutTests do
+  end
+
+  test "fixture test" do
+    assert true
+  end
+
+  describe "describe with test" do
+    test "fixture test" do
+      assert true
+    end
+  end
+
+  describe "describe without test" do
+  end
+
+  test "this will be a test in future"
+end
diff --git a/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
new file mode 100644
index 000000000..6e0cf3fa3
--- /dev/null
+++ b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
@@ -0,0 +1,53 @@
+defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFileTest do
+  alias ElixirLS.LanguageServer.{ExUnitTestTracer, SourceFile}
+  alias ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile
+  use ElixirLS.Utils.MixTest.Case, async: false
+
+  setup do
+    {:ok, _} = start_supervised(ExUnitTestTracer)
+
+    {:ok, %{}}
+  end
+
+  @tag fixture: true
+  test "return tests" do
+    in_fixture(Path.join(__DIR__, "../.."), "project_with_tests", fn ->
+      uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/fixture_test.exs"))
+
+      assert {:ok,
+              [
+                %{
+                  describes: [
+                    %{
+                      describe: nil,
+                      line: nil,
+                      tests: [
+                        %{line: 19, name: "this will be a test in future", type: :test},
+                        %{line: 6, name: "fixture test", type: :test}
+                      ]
+                    },
+                    %{
+                      describe: "describe with test",
+                      line: 10,
+                      tests: [
+                        %{line: 11, name: "fixture test", type: :test}
+                      ]
+                    }
+                  ],
+                  line: 0,
+                  module: "FixtureTest"
+                }
+              ]} = GetExUnitTestsInFile.execute([uri], nil)
+    end)
+  end
+
+  @tag fixture: true
+  test "return error when file fails to compile" do
+    in_fixture(Path.join(__DIR__, "../.."), "project_with_tests", fn ->
+      uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/error_test.exs"))
+
+      assert {:error, :server_error, "%TokenMissingError" <> _} =
+               GetExUnitTestsInFile.execute([uri], nil)
+    end)
+  end
+end

From c60f4e040e057cdf3d671c9e640a0a6d4d692be2 Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Sat, 22 Oct 2022 18:30:02 +0200
Subject: [PATCH 3/7] move outside of tests to prevent mix from running tests
 in fixture

---
 .../execute_command/get_ex_unit_tests_in_file_test.exs        | 4 ++--
 .../fixtures/project_with_tests/mix.exs                       | 0
 .../fixtures/project_with_tests/test/error_test.exs           | 2 +-
 .../fixtures/project_with_tests/test/fixture_test.exs         | 0
 4 files changed, 3 insertions(+), 3 deletions(-)
 rename apps/language_server/{test => test_fixtures}/fixtures/project_with_tests/mix.exs (100%)
 rename apps/language_server/{test => test_fixtures}/fixtures/project_with_tests/test/error_test.exs (92%)
 rename apps/language_server/{test => test_fixtures}/fixtures/project_with_tests/test/fixture_test.exs (100%)

diff --git a/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
index 6e0cf3fa3..21e5e2501 100644
--- a/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
+++ b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
@@ -11,7 +11,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFileT
 
   @tag fixture: true
   test "return tests" do
-    in_fixture(Path.join(__DIR__, "../.."), "project_with_tests", fn ->
+    in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
       uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/fixture_test.exs"))
 
       assert {:ok,
@@ -43,7 +43,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFileT
 
   @tag fixture: true
   test "return error when file fails to compile" do
-    in_fixture(Path.join(__DIR__, "../.."), "project_with_tests", fn ->
+    in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
       uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/error_test.exs"))
 
       assert {:error, :server_error, "%TokenMissingError" <> _} =
diff --git a/apps/language_server/test/fixtures/project_with_tests/mix.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs
similarity index 100%
rename from apps/language_server/test/fixtures/project_with_tests/mix.exs
rename to apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs
diff --git a/apps/language_server/test/fixtures/project_with_tests/test/error_test.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs
similarity index 92%
rename from apps/language_server/test/fixtures/project_with_tests/test/error_test.exs
rename to apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs
index 12be9dae8..2ef75b8c6 100644
--- a/apps/language_server/test/fixtures/project_with_tests/test/error_test.exs
+++ b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs
@@ -1,4 +1,4 @@
-defmodule FixtureTest do
+defmodule ErrorTest do
   use ExUnit.Case
 
   defmodule ModuleWithoutTests do
diff --git a/apps/language_server/test/fixtures/project_with_tests/test/fixture_test.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs
similarity index 100%
rename from apps/language_server/test/fixtures/project_with_tests/test/fixture_test.exs
rename to apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs

From 7b7d02ab1328f59ed4e5f7dd3384f3f34751aecb Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Sat, 22 Oct 2022 18:30:38 +0200
Subject: [PATCH 4/7] disable other tracers

---
 .../language_server/lib/language_server/ex_unit_test_tracer.ex | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/apps/language_server/lib/language_server/ex_unit_test_tracer.ex b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
index 49af37ae0..9cfc75ec8 100644
--- a/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
+++ b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
@@ -39,7 +39,8 @@ defmodule ElixirLS.LanguageServer.ExUnitTestTracer do
   def handle_call({:get_tests, path}, _from, state) do
     :ets.delete_all_objects(table_name(:tests))
     tracers = Code.compiler_options()[:tracers]
-    Code.put_compiler_option(:tracers, tracers ++ [__MODULE__])
+    # TODO build lock?
+    Code.put_compiler_option(:tracers, [__MODULE__])
 
     result =
       try do

From 5447a278301c9c5c635c3f61c148b85da5d991bc Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Sat, 22 Oct 2022 18:30:49 +0200
Subject: [PATCH 5/7] increase timeout

---
 apps/language_server/test/server_test.exs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs
index bb49655f7..ca8b3f453 100644
--- a/apps/language_server/test/server_test.exs
+++ b/apps/language_server/test/server_test.exs
@@ -726,7 +726,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do
     Server.receive_packet(server, did_open(uri, "elixir", 1, code))
     Server.receive_packet(server, completion_req(1, uri, 2, 25))
 
-    resp = assert_receive(%{"id" => 1}, 1000)
+    resp = assert_receive(%{"id" => 1}, 5000)
 
     assert response(1, %{
              "isIncomplete" => true,

From 6dc2eb959b23ba2fe3bf9b08f1d924512f0d866a Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Sat, 22 Oct 2022 21:23:42 +0200
Subject: [PATCH 6/7] improve compatibility with elixir 1.11 and 1.12

---
 .../lib/language_server/source_file.ex             |  6 +++---
 apps/language_server/lib/language_server/tracer.ex | 14 ++++++++++----
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex
index e79feab77..ce7cec7e5 100644
--- a/apps/language_server/lib/language_server/source_file.ex
+++ b/apps/language_server/lib/language_server/source_file.ex
@@ -240,10 +240,10 @@ defmodule ElixirLS.LanguageServer.SourceFile do
     try do
       true = Code.ensure_loaded?(Mix.Tasks.Format)
 
-      if function_exported?(Mix.Tasks.Format, :formatter_for_file, 1) do
-        {:ok, Mix.Tasks.Format.formatter_for_file(path)}
+      if Version.match?(System.version(), ">= 1.13.0") do
+        {:ok, apply(Mix.Tasks.Format, :formatter_for_file, [path])}
       else
-        {:ok, {nil, Mix.Tasks.Format.formatter_opts_for_file(path)}}
+        {:ok, {nil, apply(Mix.Tasks.Format, :formatter_opts_for_file, [path])}}
       end
     rescue
       e ->
diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex
index 3b7d7023c..0d4065c30 100644
--- a/apps/language_server/lib/language_server/tracer.ex
+++ b/apps/language_server/lib/language_server/tracer.ex
@@ -205,16 +205,22 @@ defmodule ElixirLS.LanguageServer.Tracer do
   end
 
   defp build_module_info(module, file, line) do
-    defs =
+    defs = if Version.match?(System.version(), ">= 1.12.0") do
       for {name, arity} <- Module.definitions_in(module) do
-        def_info = Module.get_definition(module, {name, arity})
+        def_info = apply(Module, :get_definition, [module, {name, arity}])
         {{name, arity}, build_def_info(def_info)}
       end
+    else
+      []
+    end
 
-    attributes =
-      for name <- Module.attributes_in(module) do
+    attributes = if Version.match?(System.version(), ">= 1.13.0") do
+      for name <- apply(Module, :attributes_in, [module]) do
         {name, Module.get_attribute(module, name)}
       end
+    else
+      []
+    end
 
     %{
       defs: defs,

From 0df887f99cb31776ad408424288b464ce55e67cd Mon Sep 17 00:00:00 2001
From: Lukasz Samson <lukaszsamson@gmail.com>
Date: Sat, 22 Oct 2022 23:32:43 +0200
Subject: [PATCH 7/7] on_module is only available since 1.13

---
 .../get_ex_unit_tests_in_file.ex              | 12 ++-
 .../lib/language_server/tracer.ex             | 28 +++----
 .../get_ex_unit_tests_in_file_test.exs        | 76 ++++++++++---------
 3 files changed, 62 insertions(+), 54 deletions(-)

diff --git a/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
index 37acc2314..dfc167e7b 100644
--- a/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
+++ b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
@@ -4,11 +4,15 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile
 
   @impl ElixirLS.LanguageServer.Providers.ExecuteCommand
   def execute([uri], _state) do
-    path = SourceFile.Path.from_uri(uri)
+    if Version.match?(System.version(), ">= 1.13.0") do
+      path = SourceFile.Path.from_uri(uri)
 
-    case ExUnitTestTracer.get_tests(path) do
-      {:ok, tests} -> {:ok, tests}
-      {:error, reason} -> {:error, :server_error, inspect(reason)}
+      case ExUnitTestTracer.get_tests(path) do
+        {:ok, tests} -> {:ok, tests}
+        {:error, reason} -> {:error, :server_error, inspect(reason)}
+      end
+    else
+      {:error, :server_error, "This feature requires elixir >= 1.13"}
     end
   end
 end
diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex
index 0d4065c30..ebdb8c156 100644
--- a/apps/language_server/lib/language_server/tracer.ex
+++ b/apps/language_server/lib/language_server/tracer.ex
@@ -205,22 +205,24 @@ defmodule ElixirLS.LanguageServer.Tracer do
   end
 
   defp build_module_info(module, file, line) do
-    defs = if Version.match?(System.version(), ">= 1.12.0") do
-      for {name, arity} <- Module.definitions_in(module) do
-        def_info = apply(Module, :get_definition, [module, {name, arity}])
-        {{name, arity}, build_def_info(def_info)}
+    defs =
+      if Version.match?(System.version(), ">= 1.12.0") do
+        for {name, arity} <- Module.definitions_in(module) do
+          def_info = apply(Module, :get_definition, [module, {name, arity}])
+          {{name, arity}, build_def_info(def_info)}
+        end
+      else
+        []
       end
-    else
-      []
-    end
 
-    attributes = if Version.match?(System.version(), ">= 1.13.0") do
-      for name <- apply(Module, :attributes_in, [module]) do
-        {name, Module.get_attribute(module, name)}
+    attributes =
+      if Version.match?(System.version(), ">= 1.13.0") do
+        for name <- apply(Module, :attributes_in, [module]) do
+          {name, Module.get_attribute(module, name)}
+        end
+      else
+        []
       end
-    else
-      []
-    end
 
     %{
       defs: defs,
diff --git a/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
index 21e5e2501..688cc9f6a 100644
--- a/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
+++ b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
@@ -9,45 +9,47 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFileT
     {:ok, %{}}
   end
 
-  @tag fixture: true
-  test "return tests" do
-    in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
-      uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/fixture_test.exs"))
+  if Version.match?(System.version(), ">= 1.13.0") do
+    @tag fixture: true
+    test "return tests" do
+      in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
+        uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/fixture_test.exs"))
 
-      assert {:ok,
-              [
-                %{
-                  describes: [
-                    %{
-                      describe: nil,
-                      line: nil,
-                      tests: [
-                        %{line: 19, name: "this will be a test in future", type: :test},
-                        %{line: 6, name: "fixture test", type: :test}
-                      ]
-                    },
-                    %{
-                      describe: "describe with test",
-                      line: 10,
-                      tests: [
-                        %{line: 11, name: "fixture test", type: :test}
-                      ]
-                    }
-                  ],
-                  line: 0,
-                  module: "FixtureTest"
-                }
-              ]} = GetExUnitTestsInFile.execute([uri], nil)
-    end)
-  end
+        assert {:ok,
+                [
+                  %{
+                    describes: [
+                      %{
+                        describe: nil,
+                        line: nil,
+                        tests: [
+                          %{line: 19, name: "this will be a test in future", type: :test},
+                          %{line: 6, name: "fixture test", type: :test}
+                        ]
+                      },
+                      %{
+                        describe: "describe with test",
+                        line: 10,
+                        tests: [
+                          %{line: 11, name: "fixture test", type: :test}
+                        ]
+                      }
+                    ],
+                    line: 0,
+                    module: "FixtureTest"
+                  }
+                ]} = GetExUnitTestsInFile.execute([uri], nil)
+      end)
+    end
 
-  @tag fixture: true
-  test "return error when file fails to compile" do
-    in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
-      uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/error_test.exs"))
+    @tag fixture: true
+    test "return error when file fails to compile" do
+      in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn ->
+        uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/error_test.exs"))
 
-      assert {:error, :server_error, "%TokenMissingError" <> _} =
-               GetExUnitTestsInFile.execute([uri], nil)
-    end)
+        assert {:error, :server_error, "%TokenMissingError" <> _} =
+                 GetExUnitTestsInFile.execute([uri], nil)
+      end)
+    end
   end
 end
