diff --git a/apps/language_server/lib/language_server/json_rpc.ex b/apps/language_server/lib/language_server/json_rpc.ex index a020860d0..b60d5d1ff 100644 --- a/apps/language_server/lib/language_server/json_rpc.ex +++ b/apps/language_server/lib/language_server/json_rpc.ex @@ -203,4 +203,5 @@ defmodule ElixirLS.LanguageServer.JsonRpc do defp error_code_and_message(:request_cancelled), do: {-32800, "Request cancelled"} defp error_code_and_message(:content_modified), do: {-32801, "Content modified"} + defp error_code_and_message(:code_lens_error), do: {-32900, "Error while building code lenses"} end diff --git a/apps/language_server/lib/language_server/providers/code_lens.ex b/apps/language_server/lib/language_server/providers/code_lens.ex index 13619ffbc..353d7d866 100644 --- a/apps/language_server/lib/language_server/providers/code_lens.ex +++ b/apps/language_server/lib/language_server/providers/code_lens.ex @@ -1,139 +1,28 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do @moduledoc """ - Collects the success typings inferred by Dialyzer, translates the syntax to Elixir, and shows them - inline in the editor as @spec suggestions. + Provides different code lenses to the client. - The server, unfortunately, has no way to force the client to refresh the @spec code lenses when new - success typings, so we let this request block until we know we have up-to-date results from - Dialyzer. We rely on the client being able to await this result while still making other requests - in parallel. If the client is unable to perform requests in parallel, the client or user should - disable this feature. + Supports the following code lenses: + * Suggestions for Dialyzer @spec definitions + * Shortcuts for executing tests """ - alias ElixirLS.LanguageServer.{Server, SourceFile} - alias Erl2ex.Convert.{Context, ErlForms} - alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec} + alias ElixirLS.LanguageServer.Providers.CodeLens import ElixirLS.LanguageServer.Protocol - defmodule ContractTranslator do - def translate_contract(fun, contract, is_macro) do - # FIXME: Private module - {[%ExSpec{specs: [spec]} | _], _} = - "-spec foo#{contract}." - # FIXME: Private module - |> Parse.string() - |> hd() - |> elem(0) - # FIXME: Private module - |> ErlForms.conv_form(%Context{ - in_type_expr: true, - # FIXME: Private module - module_data: %ModuleData{} - }) + def spec_code_lens(server_instance_id, uri, text), + do: CodeLens.TypeSpec.code_lens(server_instance_id, uri, text) - spec - |> Macro.postwalk(&tweak_specs/1) - |> drop_macro_env(is_macro) - |> Macro.to_string() - |> String.replace("()", "") - |> Code.format_string!(line_length: :infinity) - |> IO.iodata_to_binary() - |> String.replace_prefix("foo", to_string(fun)) - end + def test_code_lens(uri, text), do: CodeLens.Test.code_lens(uri, text) - defp tweak_specs({:list, _meta, args}) do - case args do - [{:{}, _, [{:atom, _, []}, {wild, _, _}]}] when wild in [:_, :any] -> quote do: keyword() - list -> list - end - end - - defp tweak_specs({:nonempty_list, _meta, args}) do - case args do - [{:any, _, []}] -> quote do: [...] - _ -> args ++ quote do: [...] - end - end - - defp tweak_specs({:%{}, _meta, fields}) do - fields = - Enum.map(fields, fn - {:map_field_exact, _, [key, value]} -> {key, value} - {key, value} -> quote do: {optional(unquote(key)), unquote(value)} - field -> field - end) - |> Enum.reject(&match?({{:optional, _, [{:any, _, []}]}, {:any, _, []}}, &1)) - - fields - |> Enum.find_value(fn - {:__struct__, struct_type} when is_atom(struct_type) -> struct_type - _ -> nil - end) - |> case do - nil -> {:%{}, [], fields} - struct_type -> {{:., [], [struct_type, :t]}, [], []} - end - end - - # Undo conversion of _ to any() when inside binary spec - defp tweak_specs({:<<>>, _, children}) do - children = - Macro.postwalk(children, fn - {:any, _, []} -> quote do: _ - other -> other - end) - - {:<<>>, [], children} - end - - defp tweak_specs({:_, _, _}) do - quote do: any() - end - - defp tweak_specs({:when, [], [spec, substitutions]}) do - substitutions = Enum.reject(substitutions, &match?({:_, {:any, _, []}}, &1)) - - case substitutions do - [] -> spec - _ -> {:when, [], [spec, substitutions]} - end - end - - defp tweak_specs(node) do - node - end - - defp drop_macro_env(ast, false), do: ast - - defp drop_macro_env({:"::", [], [{:foo, [], [_env | rest]}, res]}, true) do - {:"::", [], [{:foo, [], rest}, res]} - end - end - - def code_lens(server_instance_id, uri, text) do - resp = - for {_, line, {mod, fun, arity}, contract, is_macro} <- Server.suggest_contracts(uri), - SourceFile.function_def_on_line?(text, line, fun), - spec = ContractTranslator.translate_contract(fun, contract, is_macro) do - %{ - "range" => range(line - 1, 0, line - 1, 0), - "command" => %{ - "title" => "@spec #{spec}", - "command" => "spec:#{server_instance_id}", - "arguments" => [ - %{ - "uri" => uri, - "mod" => to_string(mod), - "fun" => to_string(fun), - "arity" => arity, - "spec" => spec, - "line" => line - } - ] - } - } - end - - {:ok, resp} + def build_code_lens(line, title, command, argument) do + %{ + "range" => range(line - 1, 0, line - 1, 0), + "command" => %{ + "title" => title, + "command" => command, + "arguments" => [argument] + } + } end end diff --git a/apps/language_server/lib/language_server/providers/code_lens/test.ex b/apps/language_server/lib/language_server/providers/code_lens/test.ex new file mode 100644 index 000000000..d8c9a25af --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -0,0 +1,160 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do + @moduledoc """ + Identifies test execution targets and provides code lenses for automatically executing them. + + Supports the following execution targets: + * Test modules (any module that imports ExUnit.Case) + * Describe blocks (any call to describe/2 inside a test module) + * Test blocks (any call to test/2 or test/3 inside a test module) + """ + + alias ElixirLS.LanguageServer.Providers.CodeLens + alias ElixirLS.LanguageServer.Providers.CodeLens.Test.DescribeBlock + alias ElixirLS.LanguageServer.Providers.CodeLens.Test.TestBlock + alias ElixirLS.LanguageServer.SourceFile + alias ElixirSense.Core.Parser + alias ElixirSense.Core.Metadata + + @run_test_command "elixir.lens.test.run" + + def code_lens(uri, text) do + with {:ok, buffer_file_metadata} <- parse_source(text) do + source_lines = SourceFile.lines(text) + + file_path = SourceFile.path_from_uri(uri) + + calls_list = + buffer_file_metadata.calls + |> Enum.map(fn {_k, v} -> v end) + |> List.flatten() + + lines_to_env_list = Map.to_list(buffer_file_metadata.lines_to_env) + + describe_blocks = find_describe_blocks(lines_to_env_list, calls_list, source_lines) + describe_lenses = get_describe_lenses(describe_blocks, file_path) + + test_lenses = + lines_to_env_list + |> find_test_blocks(calls_list, describe_blocks, source_lines) + |> get_test_lenses(file_path) + + module_lenses = + buffer_file_metadata + |> get_test_modules() + |> get_module_lenses(file_path) + + {:ok, test_lenses ++ describe_lenses ++ module_lenses} + end + end + + defp get_test_lenses(test_blocks, file_path) do + args = fn block -> + %{ + "filePath" => file_path, + "testName" => block.name + } + |> Map.merge(if block.describe != nil, do: %{"describe" => block.describe.name}, else: %{}) + end + + test_blocks + |> Enum.map(fn block -> + CodeLens.build_code_lens(block.line, "Run test", @run_test_command, args.(block)) + end) + end + + defp get_describe_lenses(describe_blocks, file_path) do + describe_blocks + |> Enum.map(fn block -> + CodeLens.build_code_lens(block.line, "Run tests", @run_test_command, %{ + "filePath" => file_path, + "describe" => block.name + }) + end) + end + + defp find_test_blocks(lines_to_env_list, calls_list, describe_blocks, source_lines) do + runnable_functions = [{:test, 3}, {:test, 2}] + + for func <- runnable_functions, + {line, _col} <- calls_to(calls_list, func), + is_test_module?(lines_to_env_list, line) do + {_line, %{scope_id: scope_id}} = + Enum.find(lines_to_env_list, fn {env_line, _env} -> env_line == line end) + + describe = + describe_blocks + |> Enum.find(nil, fn describe -> + describe.body_scope_id == scope_id + end) + + %{"name" => test_name} = + ~r/^\s*test "(?.*)"(,.*)? do/ + |> Regex.named_captures(Enum.at(source_lines, line - 1)) + + %TestBlock{name: test_name, describe: describe, line: line} + end + end + + defp find_describe_blocks(lines_to_env_list, calls_list, source_lines) do + lines_to_env_list_length = length(lines_to_env_list) + + for {line, _col} <- calls_to(calls_list, {:describe, 2}), + is_test_module?(lines_to_env_list, line) do + DescribeBlock.find_block_info( + line, + lines_to_env_list, + lines_to_env_list_length, + source_lines + ) + end + end + + defp get_module_lenses(test_modules, file_path) do + test_modules + |> Enum.map(fn {module, line} -> + CodeLens.build_code_lens(line, "Run tests in module", @run_test_command, %{ + "filePath" => file_path, + "module" => module + }) + end) + end + + defp get_test_modules(%Metadata{lines_to_env: lines_to_env}) do + lines_to_env + |> Enum.group_by(fn {_line, env} -> env.module end) + |> Enum.filter(fn {_module, module_lines_to_env} -> is_test_module?(module_lines_to_env) end) + |> Enum.map(fn {module, [{line, _env} | _rest]} -> {module, line} end) + end + + defp is_test_module?(lines_to_env), do: is_test_module?(lines_to_env, :infinity) + + defp is_test_module?(lines_to_env, line) when is_list(lines_to_env) do + lines_to_env + |> Enum.max_by(fn + {env_line, _env} when env_line < line -> env_line + _ -> -1 + end) + |> elem(1) + |> Map.get(:imports) + |> Enum.any?(fn module -> module == ExUnit.Case end) + end + + defp calls_to(calls_list, {function, arity}) do + for call_info <- calls_list, + call_info.func == function and call_info.arity === arity do + call_info.position + end + end + + defp parse_source(text) do + buffer_file_metadata = + text + |> Parser.parse_string(true, true, 1) + + if buffer_file_metadata.error != nil do + {:error, buffer_file_metadata} + else + {:ok, buffer_file_metadata} + end + end +end diff --git a/apps/language_server/lib/language_server/providers/code_lens/test/describe_block.ex b/apps/language_server/lib/language_server/providers/code_lens/test/describe_block.ex new file mode 100644 index 000000000..18214a0ac --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_lens/test/describe_block.ex @@ -0,0 +1,52 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test.DescribeBlock do + alias ElixirSense.Core.State.Env + + @struct_keys [:line, :name, :body_scope_id] + + @enforce_keys @struct_keys + defstruct @struct_keys + + def find_block_info(line, lines_to_env_list, lines_to_env_list_length, source_lines) do + name = get_name(source_lines, line) + + body_scope_id = + get_body_scope_id( + line, + lines_to_env_list, + lines_to_env_list_length + ) + + %__MODULE__{line: line, body_scope_id: body_scope_id, name: name} + end + + defp get_name(source_lines, declaration_line) do + %{"name" => name} = + ~r/^\s*describe "(?.*)" do/ + |> Regex.named_captures(Enum.at(source_lines, declaration_line - 1)) + + name + end + + defp get_body_scope_id( + declaration_line, + lines_to_env_list, + lines_to_env_list_length + ) do + env_index = + lines_to_env_list + |> Enum.find_index(fn {line, _env} -> line == declaration_line end) + + {_line, %{scope_id: declaration_scope_id}} = + lines_to_env_list + |> Enum.at(env_index) + + with true <- env_index + 1 < lines_to_env_list_length, + next_env = Enum.at(lines_to_env_list, env_index + 1), + {_line, %Env{scope_id: body_scope_id}} <- next_env, + true <- body_scope_id != declaration_scope_id do + body_scope_id + else + _ -> nil + end + end +end diff --git a/apps/language_server/lib/language_server/providers/code_lens/test/test_block.ex b/apps/language_server/lib/language_server/providers/code_lens/test/test_block.ex new file mode 100644 index 000000000..5cd3b963c --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_lens/test/test_block.ex @@ -0,0 +1,6 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test.TestBlock do + @struct_keys [:name, :describe, :line] + + @enforce_keys @struct_keys + defstruct @struct_keys +end diff --git a/apps/language_server/lib/language_server/providers/code_lens/type_spec.ex b/apps/language_server/lib/language_server/providers/code_lens/type_spec.ex new file mode 100644 index 000000000..ff620350f --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_lens/type_spec.ex @@ -0,0 +1,135 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec do + @moduledoc """ + Collects the success typings inferred by Dialyzer, translates the syntax to Elixir, and shows them + inline in the editor as @spec suggestions. + + The server, unfortunately, has no way to force the client to refresh the @spec code lenses when new + success typings, so we let this request block until we know we have up-to-date results from + Dialyzer. We rely on the client being able to await this result while still making other requests + in parallel. If the client is unable to perform requests in parallel, the client or user should + disable this feature. + """ + + alias ElixirLS.LanguageServer.Providers.CodeLens + alias ElixirLS.LanguageServer.{Server, SourceFile} + alias Erl2ex.Convert.{Context, ErlForms} + alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec} + + defmodule ContractTranslator do + def translate_contract(fun, contract, is_macro) do + # FIXME: Private module + {[%ExSpec{specs: [spec]} | _], _} = + "-spec foo#{contract}." + # FIXME: Private module + |> Parse.string() + |> hd() + |> elem(0) + # FIXME: Private module + |> ErlForms.conv_form(%Context{ + in_type_expr: true, + # FIXME: Private module + module_data: %ModuleData{} + }) + + spec + |> Macro.postwalk(&tweak_specs/1) + |> drop_macro_env(is_macro) + |> Macro.to_string() + |> String.replace("()", "") + |> Code.format_string!(line_length: :infinity) + |> IO.iodata_to_binary() + |> String.replace_prefix("foo", to_string(fun)) + end + + defp tweak_specs({:list, _meta, args}) do + case args do + [{:{}, _, [{:atom, _, []}, {wild, _, _}]}] when wild in [:_, :any] -> quote do: keyword() + list -> list + end + end + + defp tweak_specs({:nonempty_list, _meta, args}) do + case args do + [{:any, _, []}] -> quote do: [...] + _ -> args ++ quote do: [...] + end + end + + defp tweak_specs({:%{}, _meta, fields}) do + fields = + Enum.map(fields, fn + {:map_field_exact, _, [key, value]} -> {key, value} + {key, value} -> quote do: {optional(unquote(key)), unquote(value)} + field -> field + end) + |> Enum.reject(&match?({{:optional, _, [{:any, _, []}]}, {:any, _, []}}, &1)) + + fields + |> Enum.find_value(fn + {:__struct__, struct_type} when is_atom(struct_type) -> struct_type + _ -> nil + end) + |> case do + nil -> {:%{}, [], fields} + struct_type -> {{:., [], [struct_type, :t]}, [], []} + end + end + + # Undo conversion of _ to any() when inside binary spec + defp tweak_specs({:<<>>, _, children}) do + children = + Macro.postwalk(children, fn + {:any, _, []} -> quote do: _ + other -> other + end) + + {:<<>>, [], children} + end + + defp tweak_specs({:_, _, _}) do + quote do: any() + end + + defp tweak_specs({:when, [], [spec, substitutions]}) do + substitutions = Enum.reject(substitutions, &match?({:_, {:any, _, []}}, &1)) + + case substitutions do + [] -> spec + _ -> {:when, [], [spec, substitutions]} + end + end + + defp tweak_specs(node) do + node + end + + defp drop_macro_env(ast, false), do: ast + + defp drop_macro_env({:"::", [], [{:foo, [], [_env | rest]}, res]}, true) do + {:"::", [], [{:foo, [], rest}, res]} + end + end + + def code_lens(server_instance_id, uri, text) do + resp = + for {_, line, {mod, fun, arity}, contract, is_macro} <- Server.suggest_contracts(uri), + SourceFile.function_def_on_line?(text, line, fun), + spec = ContractTranslator.translate_contract(fun, contract, is_macro) do + CodeLens.build_code_lens( + line, + "@spec #{spec}", + "spec:#{server_instance_id}", + %{ + "uri" => uri, + "mod" => to_string(mod), + "fun" => to_string(fun), + "arity" => arity, + "spec" => spec, + "line" => line + } + ) + end + + {:ok, resp} + end +end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index ade57eafe..31955ae21 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -689,12 +689,23 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_request(code_lens_req(_id, uri), state) do source_file = get_source_file(state, uri) - if dialyzer_enabled?(state) and !!state.settings["suggestSpecs"] do - {:async, fn -> CodeLens.code_lens(state.server_instance_id, uri, source_file.text) end, - state} - else - {:error, :invalid_request, "suggestSpecs is disabled", state} + fun = fn -> + with {:ok, spec_code_lenses} <- get_spec_code_lenses(state, uri, source_file), + {:ok, test_code_lenses} <- get_test_code_lenses(state, uri, source_file) do + {:ok, spec_code_lenses ++ test_code_lenses} + else + {:error, %ElixirSense.Core.Metadata{error: {line, error_msg}}} -> + {:error, :code_lens_error, "#{line}: #{error_msg}"} + + {:error, error} -> + {:error, :code_lens_error, "Error while building code lenses: #{inspect(error)}"} + + error -> + error + end end + + {:async, fun, state} end defp handle_request(execute_command_req(_id, command, args) = req, state) do @@ -768,6 +779,22 @@ defmodule ElixirLS.LanguageServer.Server do } end + defp get_spec_code_lenses(state, uri, source_file) do + if dialyzer_enabled?(state) and !!state.settings["suggestSpecs"] do + CodeLens.spec_code_lens(state.server_instance_id, uri, source_file.text) + else + {:ok, []} + end + end + + defp get_test_code_lenses(state, uri, source_file) do + if state.settings["enableTestLenses"] == true do + CodeLens.test_code_lens(uri, source_file.text) + else + {:ok, []} + end + end + # Build defp trigger_build(state) do diff --git a/apps/language_server/test/fixtures/test_code_lens/mix.exs b/apps/language_server/test/fixtures/test_code_lens/mix.exs new file mode 100644 index 000000000..2d4e7c168 --- /dev/null +++ b/apps/language_server/test/fixtures/test_code_lens/mix.exs @@ -0,0 +1,9 @@ +defmodule TestCodeLens.MixProject do + use Mix.Project + + def project do + [app: :references, version: "0.1.0"] + end + + def application, do: [] +end diff --git a/apps/language_server/test/fixtures/test_code_lens/test/fixture_test.exs b/apps/language_server/test/fixtures/test_code_lens/test/fixture_test.exs new file mode 100644 index 000000000..3b3b7e7bf --- /dev/null +++ b/apps/language_server/test/fixtures/test_code_lens/test/fixture_test.exs @@ -0,0 +1,7 @@ +defmodule TestCodeLensTest do + use ExUnit.Case + + test "fixture test" do + assert true + end +end diff --git a/apps/language_server/test/providers/code_lens/test_test.exs b/apps/language_server/test/providers/code_lens/test_test.exs new file mode 100644 index 000000000..b4066f957 --- /dev/null +++ b/apps/language_server/test/providers/code_lens/test_test.exs @@ -0,0 +1,185 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do + use ExUnit.Case + + alias ElixirLS.LanguageServer.Providers.CodeLens + + setup context do + ElixirLS.LanguageServer.Build.load_all_modules() + + unless context[:skip_server] do + server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() + + {:ok, %{server: server}} + else + :ok + end + end + + test "returns all module code lenses" do + uri = "file://project/file.ex" + + text = """ + defmodule MyModule do + use ExUnit.Case + end + + defmodule MyModule2 do + use ExUnit.Case + end + """ + + {:ok, lenses} = CodeLens.Test.code_lens(uri, text) + + assert lenses == + [ + build_code_lens(0, :module, "/file.ex", %{"module" => MyModule}), + build_code_lens(4, :module, "/file.ex", %{"module" => MyModule2}) + ] + end + + test "returns all nested module code lenses" do + uri = "file://project/file.ex" + + text = """ + defmodule MyModule do + use ExUnit.Case + + defmodule MyModule2 do + use ExUnit.Case + end + end + """ + + {:ok, lenses} = CodeLens.Test.code_lens(uri, text) + + assert lenses == + [ + build_code_lens(0, :module, "/file.ex", %{"module" => MyModule}), + build_code_lens(3, :module, "/file.ex", %{"module" => MyModule.MyModule2}) + ] + end + + test "does not return lenses for modules that don't import ExUnit.case" do + uri = "file://project/file.ex" + + text = """ + defmodule MyModule do + end + """ + + {:ok, lenses} = CodeLens.Test.code_lens(uri, text) + + assert lenses == [] + end + + test "returns lenses for all describe blocks" do + uri = "file://project/file.ex" + + text = """ + defmodule MyModule do + use ExUnit.Case + + describe "describe1" do + end + + describe "describe2" do + end + end + """ + + {:ok, lenses} = CodeLens.Test.code_lens(uri, text) + + assert Enum.member?( + lenses, + build_code_lens(3, :describe, "/file.ex", %{"describe" => "describe1"}) + ) + + assert Enum.member?( + lenses, + build_code_lens(6, :describe, "/file.ex", %{"describe" => "describe2"}) + ) + end + + test "returns lenses for all test blocks" do + uri = "file://project/file.ex" + + text = """ + defmodule MyModule do + use ExUnit.Case + + test "test1" do + end + + test "test2" do + end + end + """ + + {:ok, lenses} = CodeLens.Test.code_lens(uri, text) + + assert Enum.member?( + lenses, + build_code_lens(3, :test, "/file.ex", %{"testName" => "test1"}) + ) + + assert Enum.member?( + lenses, + build_code_lens(6, :test, "/file.ex", %{"testName" => "test2"}) + ) + end + + test "given test blocks inside describe blocks, should return code lenses with the test and describe name" do + uri = "file://project/file.ex" + + text = """ + defmodule MyModule do + use ExUnit.Case + + describe "describe1" do + test "test1" do + end + end + end + """ + + {:ok, lenses} = CodeLens.Test.code_lens(uri, text) + + assert Enum.member?( + lenses, + build_code_lens(4, :test, "/file.ex", %{ + "testName" => "test1", + "describe" => "describe1" + }) + ) + end + + defp build_code_lens(line, target, file_path, args) do + arguments = + %{ + "filePath" => file_path + } + |> Map.merge(args) + + %{ + "range" => %{ + "start" => %{ + "line" => line, + "character" => 0 + }, + "end" => %{ + "line" => line, + "character" => 0 + } + }, + "command" => %{ + "title" => get_lens_title(target), + "command" => "elixir.lens.test.run", + "arguments" => [arguments] + } + } + end + + defp get_lens_title(:module), do: "Run tests in module" + defp get_lens_title(:describe), do: "Run tests" + defp get_lens_title(:test), do: "Run test" +end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 5f9eda8e0..b71818370 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1134,6 +1134,89 @@ defmodule ElixirLS.LanguageServer.ServerTest do end) end + test "returns code lenses for runnable tests", %{server: server} do + in_fixture(__DIR__, "test_code_lens", fn -> + file_path = "test/fixture_test.exs" + file_uri = SourceFile.path_to_uri(file_path) + file_absolute_path = SourceFile.path_from_uri(file_uri) + text = File.read!(file_path) + + fake_initialize(server) + + Server.receive_packet( + server, + did_change_configuration(%{"elixirLS" => %{"enableTestLenses" => true}}) + ) + + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) + + Server.receive_packet( + server, + code_lens_req(4, file_uri) + ) + + resp = assert_receive(%{"id" => 4}, 5000) + + assert response(4, [ + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "testName" => "fixture test" + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run test" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 3}, + "start" => %{"character" => 0, "line" => 3} + } + }, + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "module" => "Elixir.TestCodeLensTest" + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run tests in module" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } + } + ]) = resp + end) + end + + test "does not return code lenses for runnable tests when test lenses settings is not set", %{ + server: server + } do + in_fixture(__DIR__, "test_code_lens", fn -> + file_path = "test/fixture_test.exs" + file_uri = SourceFile.path_to_uri(file_path) + text = File.read!(file_path) + + fake_initialize(server) + + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) + + Server.receive_packet( + server, + code_lens_req(4, file_uri) + ) + + resp = assert_receive(%{"id" => 4}, 5000) + + assert response(4, []) = resp + end) + end + defp with_new_server(func) do server = start_supervised!({Server, nil}) packet_capture = start_supervised!({PacketCapture, self()})