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 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..9cfc75ec8
--- /dev/null
+++ b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex
@@ -0,0 +1,115 @@
+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]
+    # TODO build lock?
+    Code.put_compiler_option(:tracers, [__MODULE__])
+
+    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
+
+  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 ->
+              # 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,
+            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..dfc167e7b
--- /dev/null
+++ b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex
@@ -0,0 +1,18 @@
+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
+    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)}
+      end
+    else
+      {:error, :server_error, "This feature requires elixir >= 1.13"}
+    end
+  end
+end
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..ebdb8c156 100644
--- a/apps/language_server/lib/language_server/tracer.ex
+++ b/apps/language_server/lib/language_server/tracer.ex
@@ -206,14 +206,22 @@ defmodule ElixirLS.LanguageServer.Tracer do
 
   defp build_module_info(module, file, line) do
     defs =
-      for {name, arity} <- Module.definitions_in(module) do
-        def_info = Module.get_definition(module, {name, arity})
-        {{name, arity}, build_def_info(def_info)}
+      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
 
     attributes =
-      for name <- Module.attributes_in(module) do
-        {name, Module.get_attribute(module, name)}
+      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
 
     %{
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..688cc9f6a
--- /dev/null
+++ b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs
@@ -0,0 +1,55 @@
+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
+
+  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
+
+    @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)
+    end
+  end
+end
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,
diff --git a/apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs
new file mode 100644
index 000000000..71a23daa8
--- /dev/null
+++ b/apps/language_server/test_fixtures/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/fixtures/project_with_tests/test/error_test.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs
new file mode 100644
index 000000000..2ef75b8c6
--- /dev/null
+++ b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs
@@ -0,0 +1,21 @@
+defmodule ErrorTest 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/fixtures/project_with_tests/test/fixture_test.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs
new file mode 100644
index 000000000..dd215cb29
--- /dev/null
+++ b/apps/language_server/test_fixtures/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
