From 503f6c28e60bfc6656ad0e7f88aba8c42b2e501d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 18 Oct 2020 11:22:03 -0400 Subject: [PATCH 01/22] Provide code lenses for running tests (WIP) --- .../language_server/providers/code_lens.ex | 110 +++++++++++++++--- .../lib/language_server/server.ex | 19 ++- 2 files changed, 106 insertions(+), 23 deletions(-) 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..836599abc 100644 --- a/apps/language_server/lib/language_server/providers/code_lens.ex +++ b/apps/language_server/lib/language_server/providers/code_lens.ex @@ -11,6 +11,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do """ alias ElixirLS.LanguageServer.{Server, SourceFile} + alias ElixirSense.Core.Parser + alias ElixirSense.Core.State alias Erl2ex.Convert.{Context, ErlForms} alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec} import ElixirLS.LanguageServer.Protocol @@ -110,30 +112,104 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do end end - def code_lens(server_instance_id, uri, text) do + def spec_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 - } - ] + 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 + + def test_code_lens(uri, src) do + file_path = SourceFile.path_from_uri(uri) + + if imports?(src, ExUnit.Case) do + test_calls = calls_to(src, :test) + describe_calls = calls_to(src, :describe) + + calls_lenses = + for {line, _col} <- test_calls ++ describe_calls do + test_filter = "#{file_path}:#{line}" + + build_code_lens(line, "Run test", "elixir.test.run", test_filter) + end + + file_lens = build_code_lens(1, "Run test", "elixir.test.run", file_path) + + {:ok, [file_lens | calls_lenses]} + end + end + + @spec imports?(String.t(), [atom()] | atom()) :: boolean() + defp imports?(buffer, modules) do + buffer_file_metadata = + buffer + |> Parser.parse_string(true, true, 1) + + imports_set = + buffer_file_metadata.lines_to_env + |> get_imports() + |> MapSet.new() + + modules + |> List.wrap() + |> MapSet.new() + |> MapSet.subset?(imports_set) + end + + defp get_imports(lines_to_env) do + %State.Env{imports: imports} = + lines_to_env + |> Enum.max_by(fn {k, _v} -> k end) + |> elem(1) + + imports + end + + @spec calls_to(String.t(), atom() | {atom(), integer()}) :: [{pos_integer(), pos_integer()}] + defp calls_to(buffer, function) do + buffer_file_metadata = + buffer + |> Parser.parse_string(true, true, 1) + + buffer_file_metadata.calls + |> Enum.map(fn {_k, v} -> v end) + |> List.flatten() + |> Enum.filter(&is_call_to(&1, function)) + |> Enum.map(fn call -> call.position end) + end + + defp is_call_to(%State.CallInfo{} = call_info, {function, arity}) do + call_info.func == function and call_info.arity == arity + end + + defp is_call_to(%State.CallInfo{} = call_info, function) when is_atom(function) do + call_info.func == function + end + + 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/server.ex b/apps/language_server/lib/language_server/server.ex index c712f2e57..26faac8e8 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -530,13 +530,20 @@ defmodule ElixirLS.LanguageServer.Server do end defp handle_request(code_lens_req(_id, uri), state) do - if dialyzer_enabled?(state) and state.settings["suggestSpecs"] != false do - {:async, - fn -> CodeLens.code_lens(state.server_instance_id, uri, state.source_files[uri].text) end, - state} - else - {:ok, nil, state} + fun = fn -> + {:ok, spec_code_lens} = + if dialyzer_enabled?(state) and state.settings["suggestSpecs"] != false do + CodeLens.spec_code_lens(state.server_instance_id, uri, state.source_files[uri].text) + else + [] + end + + {:ok, test_code_lens} = CodeLens.test_code_lens(uri, state.source_files[uri].text) + + {:ok, spec_code_lens ++ test_code_lens} end + + {:async, fun, state} end defp handle_request(execute_command_req(_id, command, args), state) do From fd1793eba9e20394dfe462f41d808fa45ab77615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 18 Oct 2020 11:31:50 -0400 Subject: [PATCH 02/22] Remove unused function --- .../lib/language_server/providers/code_lens.ex | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) 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 836599abc..640b9a01b 100644 --- a/apps/language_server/lib/language_server/providers/code_lens.ex +++ b/apps/language_server/lib/language_server/providers/code_lens.ex @@ -190,18 +190,10 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do buffer_file_metadata.calls |> Enum.map(fn {_k, v} -> v end) |> List.flatten() - |> Enum.filter(&is_call_to(&1, function)) + |> Enum.filter(fn call_info -> call_info.func == function end) |> Enum.map(fn call -> call.position end) end - defp is_call_to(%State.CallInfo{} = call_info, {function, arity}) do - call_info.func == function and call_info.arity == arity - end - - defp is_call_to(%State.CallInfo{} = call_info, function) when is_atom(function) do - call_info.func == function - end - def build_code_lens(line, title, command, argument) do %{ "range" => range(line - 1, 0, line - 1, 0), From bd49269b25b3d61cd3e1c3242fa63b4f20b3489b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sat, 24 Oct 2020 17:33:35 -0400 Subject: [PATCH 03/22] Properly identify test modules --- .../language_server/providers/code_lens.ex | 184 +----------------- .../providers/code_lens/spec.ex | 124 ++++++++++++ .../providers/code_lens/test.ex | 91 +++++++++ 3 files changed, 219 insertions(+), 180 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/code_lens/spec.ex create mode 100644 apps/language_server/lib/language_server/providers/code_lens/test.ex 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 640b9a01b..709291e3d 100644 --- a/apps/language_server/lib/language_server/providers/code_lens.ex +++ b/apps/language_server/lib/language_server/providers/code_lens.ex @@ -10,189 +10,13 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do disable this feature. """ - alias ElixirLS.LanguageServer.{Server, SourceFile} - alias ElixirSense.Core.Parser - alias ElixirSense.Core.State - 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.Spec.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 - - 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 spec_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 - 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 - - def test_code_lens(uri, src) do - file_path = SourceFile.path_from_uri(uri) - - if imports?(src, ExUnit.Case) do - test_calls = calls_to(src, :test) - describe_calls = calls_to(src, :describe) - - calls_lenses = - for {line, _col} <- test_calls ++ describe_calls do - test_filter = "#{file_path}:#{line}" - - build_code_lens(line, "Run test", "elixir.test.run", test_filter) - end - - file_lens = build_code_lens(1, "Run test", "elixir.test.run", file_path) - - {:ok, [file_lens | calls_lenses]} - end - end - - @spec imports?(String.t(), [atom()] | atom()) :: boolean() - defp imports?(buffer, modules) do - buffer_file_metadata = - buffer - |> Parser.parse_string(true, true, 1) - - imports_set = - buffer_file_metadata.lines_to_env - |> get_imports() - |> MapSet.new() - - modules - |> List.wrap() - |> MapSet.new() - |> MapSet.subset?(imports_set) - end - - defp get_imports(lines_to_env) do - %State.Env{imports: imports} = - lines_to_env - |> Enum.max_by(fn {k, _v} -> k end) - |> elem(1) - - imports - end - - @spec calls_to(String.t(), atom() | {atom(), integer()}) :: [{pos_integer(), pos_integer()}] - defp calls_to(buffer, function) do - buffer_file_metadata = - buffer - |> Parser.parse_string(true, true, 1) - - buffer_file_metadata.calls - |> Enum.map(fn {_k, v} -> v end) - |> List.flatten() - |> Enum.filter(fn call_info -> call_info.func == function end) - |> Enum.map(fn call -> call.position end) - end + def test_code_lens(uri, text), do: CodeLens.Test.code_lens(uri, text) def build_code_lens(line, title, command, argument) do %{ diff --git a/apps/language_server/lib/language_server/providers/code_lens/spec.ex b/apps/language_server/lib/language_server/providers/code_lens/spec.ex new file mode 100644 index 000000000..c7f1c5005 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_lens/spec.ex @@ -0,0 +1,124 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.Spec do + 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/providers/code_lens/test.ex b/apps/language_server/lib/language_server/providers/code_lens/test.ex new file mode 100644 index 000000000..05ed2c668 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -0,0 +1,91 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do + alias ElixirLS.LanguageServer.Providers.CodeLens + alias ElixirLS.LanguageServer.SourceFile + alias ElixirSense.Core.Parser + alias ElixirSense.Core.Metadata + + @run_test_command "elixir.test.run" + + def code_lens(uri, text) do + buffer_file_metadata = + text + |> Parser.parse_string(true, true, 1) + + file_path = SourceFile.path_from_uri(uri) + + function_lenses = get_function_lenses(buffer_file_metadata, file_path) + module_lenses = get_module_lenses(buffer_file_metadata, file_path) + + {:ok, function_lenses ++ module_lenses} + end + + defp get_module_lenses(%Metadata{} = metadata, file_path) do + metadata + |> get_test_modules() + |> Enum.map(&build_test_module_code_lens(file_path, &1)) + 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 get_function_lenses(%Metadata{} = metadata, file_path) do + runnable_functions = [{:test, 3}, {:test, 2}, {:describe, 2}] + + for func <- runnable_functions do + for {line, _col} <- calls_to(metadata, func), + is_test_module?(metadata.lines_to_env, line) do + build_function_test_code_lens(func, file_path, line) + end + end + |> List.flatten() + 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_map(lines_to_env) do + lines_to_env + |> Map.to_list() + |> is_test_module?(line) + end + + defp is_test_module?(lines_to_env, line) when is_list(lines_to_env) do + lines_to_env + |> Enum.filter(fn {env_line, _env} -> env_line < line end) + |> List.last() + |> elem(1) + |> Map.get(:imports) + |> Enum.any?(fn module -> module == ExUnit.Case end) + end + + defp calls_to(%Metadata{} = metadata, {function, arity}) do + metadata.calls + |> Enum.map(fn {_k, v} -> v end) + |> List.flatten() + |> Enum.filter(fn call_info -> call_info.func == function and call_info.arity === arity end) + |> Enum.map(fn call -> call.position end) + end + + defp build_test_module_code_lens(file_path, {module, line}) do + CodeLens.build_code_lens(line, "Run tests in module", @run_test_command, %{ + "file_path" => file_path, + "module" => module + }) + end + + defp build_function_test_code_lens(title, file_path, line) when is_binary(title) do + CodeLens.build_code_lens(line, title, @run_test_command, %{ + "file_path" => file_path, + "line" => line + }) + end + + defp build_function_test_code_lens({:test, _arity}, file_path, line), + do: build_function_test_code_lens("Run test", file_path, line) + + defp build_function_test_code_lens({:describe, _arity}, file_path, line), + do: build_function_test_code_lens("Run tests", file_path, line) +end From b7420ea04a390ce208fe56c2c02455590bf22726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 1 Nov 2020 12:39:55 -0500 Subject: [PATCH 04/22] Handle errors in server request --- .../lib/language_server/server.ex | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 26faac8e8..9cabd4f57 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -531,16 +531,10 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_request(code_lens_req(_id, uri), state) do fun = fn -> - {:ok, spec_code_lens} = - if dialyzer_enabled?(state) and state.settings["suggestSpecs"] != false do - CodeLens.spec_code_lens(state.server_instance_id, uri, state.source_files[uri].text) - else - [] - end - - {:ok, test_code_lens} = CodeLens.test_code_lens(uri, state.source_files[uri].text) - - {:ok, spec_code_lens ++ test_code_lens} + with {:ok, spec_code_lens} <- get_spec_code_lens(state, uri), + {:ok, test_code_lens} <- CodeLens.test_code_lens(uri, state.source_files[uri].text) do + {:ok, spec_code_lens ++ test_code_lens} + end end {:async, fun, state} @@ -602,6 +596,14 @@ defmodule ElixirLS.LanguageServer.Server do } end + defp get_spec_code_lens(state, uri) do + if dialyzer_enabled?(state) and state.settings["suggestSpecs"] != false do + CodeLens.spec_code_lens(state.server_instance_id, uri, state.source_files[uri].text) + else + {:ok, []} + end + end + # Build defp trigger_build(state) do From d0b25bade42725e09750ea6a5cd055e17e92bf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 1 Nov 2020 12:58:50 -0500 Subject: [PATCH 05/22] Extract calls list --- .../lib/language_server/providers/code_lens/test.ex | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 index 05ed2c668..52eb12208 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -35,8 +35,13 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do defp get_function_lenses(%Metadata{} = metadata, file_path) do runnable_functions = [{:test, 3}, {:test, 2}, {:describe, 2}] + calls_list = + metadata.calls + |> Enum.map(fn {_k, v} -> v end) + |> List.flatten() + for func <- runnable_functions do - for {line, _col} <- calls_to(metadata, func), + for {line, _col} <- calls_to(calls_list, func), is_test_module?(metadata.lines_to_env, line) do build_function_test_code_lens(func, file_path, line) end @@ -61,10 +66,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do |> Enum.any?(fn module -> module == ExUnit.Case end) end - defp calls_to(%Metadata{} = metadata, {function, arity}) do - metadata.calls - |> Enum.map(fn {_k, v} -> v end) - |> List.flatten() + defp calls_to(calls_list, {function, arity}) do + calls_list |> Enum.filter(fn call_info -> call_info.func == function and call_info.arity === arity end) |> Enum.map(fn call -> call.position end) end From b8aeb9cbebf080172b257006a322085be0b784ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sat, 14 Nov 2020 15:11:58 -0500 Subject: [PATCH 06/22] Validate that parsing did not fail --- .../providers/code_lens/test.ex | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) 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 index 52eb12208..c6f5e050c 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -7,16 +7,14 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do @run_test_command "elixir.test.run" def code_lens(uri, text) do - buffer_file_metadata = - text - |> Parser.parse_string(true, true, 1) + with {:ok, buffer_file_metadata} <- parse_source(text) do + file_path = SourceFile.path_from_uri(uri) - file_path = SourceFile.path_from_uri(uri) + function_lenses = get_function_lenses(buffer_file_metadata, file_path) + module_lenses = get_module_lenses(buffer_file_metadata, file_path) - function_lenses = get_function_lenses(buffer_file_metadata, file_path) - module_lenses = get_module_lenses(buffer_file_metadata, file_path) - - {:ok, function_lenses ++ module_lenses} + {:ok, function_lenses ++ module_lenses} + end end defp get_module_lenses(%Metadata{} = metadata, file_path) do @@ -91,4 +89,16 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do defp build_function_test_code_lens({:describe, _arity}, file_path, line), do: build_function_test_code_lens("Run tests", file_path, line) + + 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 From 01b075867c7deef6c2e5a5be8783ac7b025b7e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sat, 14 Nov 2020 15:27:37 -0500 Subject: [PATCH 07/22] Clean up searching for test calls --- .../providers/code_lens/test.ex | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) 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 index c6f5e050c..304395310 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -38,36 +38,37 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do |> Enum.map(fn {_k, v} -> v end) |> List.flatten() - for func <- runnable_functions do - for {line, _col} <- calls_to(calls_list, func), - is_test_module?(metadata.lines_to_env, line) do - build_function_test_code_lens(func, file_path, line) - end + lines_to_env_list = Map.to_list(metadata.lines_to_env) + + for func <- runnable_functions, + {line, _col} <- calls_to(calls_list, func), + is_test_module?(lines_to_env_list, line) do + build_function_test_code_lens(func, file_path, line) end - |> List.flatten() 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_map(lines_to_env) do - lines_to_env - |> Map.to_list() - |> is_test_module?(line) - end - defp is_test_module?(lines_to_env, line) when is_list(lines_to_env) do lines_to_env - |> Enum.filter(fn {env_line, _env} -> env_line < line end) - |> List.last() + |> 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 - calls_list - |> Enum.filter(fn call_info -> call_info.func == function and call_info.arity === arity end) - |> Enum.map(fn call -> call.position end) + for call_info <- calls_list, + call_info.func == function and call_info.arity === arity do + call_info.position + end + + # calls_list + # |> Enum.filter(fn call_info -> call_info.func == function and call_info.arity === arity end) + # |> Enum.map(fn call -> call.position end) end defp build_test_module_code_lens(file_path, {module, line}) do From 03b36f6db1e57e67856b077fca575de797034b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Mon, 16 Nov 2020 22:42:16 -0500 Subject: [PATCH 08/22] Refactor provider to include test name in lenses --- .../providers/code_lens/test.ex | 198 ++++++++++++++---- 1 file changed, 152 insertions(+), 46 deletions(-) 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 index 304395310..e05847ee5 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -1,52 +1,182 @@ +defmodule 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 + ) + + %DescribeBlock{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 + +defmodule TestBlock do + @struct_keys [:name, :describe, :line] + + @enforce_keys @struct_keys + defstruct @struct_keys +end + defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do alias ElixirLS.LanguageServer.Providers.CodeLens alias ElixirLS.LanguageServer.SourceFile alias ElixirSense.Core.Parser alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State.Env @run_test_command "elixir.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) - function_lenses = get_function_lenses(buffer_file_metadata, file_path) - module_lenses = get_module_lenses(buffer_file_metadata, file_path) + 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) - {:ok, function_lenses ++ module_lenses} + module_lenses = + buffer_file_metadata + |> get_test_modules() + |> get_module_lenses(file_path) + + {:ok, test_lenses ++ describe_lenses ++ module_lenses} end end - defp get_module_lenses(%Metadata{} = metadata, file_path) do - metadata - |> get_test_modules() - |> Enum.map(&build_test_module_code_lens(file_path, &1)) + def get_test_lenses(test_blocks, file_path) do + test_blocks + |> Enum.map(fn block -> + CodeLens.build_code_lens(block.line, "Run test", @run_test_command, %{ + "filePath" => file_path, + "describe" => + if block.describe != nil do + block.describe.name + else + nil + end, + "testName" => block.name + }) + 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) + def 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 get_function_lenses(%Metadata{} = metadata, file_path) do - runnable_functions = [{:test, 3}, {:test, 2}, {:describe, 2}] - - calls_list = - metadata.calls - |> Enum.map(fn {_k, v} -> v end) - |> List.flatten() - - lines_to_env_list = Map.to_list(metadata.lines_to_env) + 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 - build_function_test_code_lens(func, file_path, line) + {_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 @@ -65,32 +195,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do call_info.func == function and call_info.arity === arity do call_info.position end - - # calls_list - # |> Enum.filter(fn call_info -> call_info.func == function and call_info.arity === arity end) - # |> Enum.map(fn call -> call.position end) - end - - defp build_test_module_code_lens(file_path, {module, line}) do - CodeLens.build_code_lens(line, "Run tests in module", @run_test_command, %{ - "file_path" => file_path, - "module" => module - }) end - defp build_function_test_code_lens(title, file_path, line) when is_binary(title) do - CodeLens.build_code_lens(line, title, @run_test_command, %{ - "file_path" => file_path, - "line" => line - }) - end - - defp build_function_test_code_lens({:test, _arity}, file_path, line), - do: build_function_test_code_lens("Run test", file_path, line) - - defp build_function_test_code_lens({:describe, _arity}, file_path, line), - do: build_function_test_code_lens("Run tests", file_path, line) - defp parse_source(text) do buffer_file_metadata = text From 642aa8bfb2d9b60a108b149d40bce4f7c3a179b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Mon, 16 Nov 2020 22:44:02 -0500 Subject: [PATCH 09/22] Rename Spec code lens provider to TypeSpec --- apps/language_server/lib/language_server/providers/code_lens.ex | 2 +- .../providers/code_lens/{spec.ex => type_spec.ex} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename apps/language_server/lib/language_server/providers/code_lens/{spec.ex => type_spec.ex} (98%) 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 709291e3d..13fee170a 100644 --- a/apps/language_server/lib/language_server/providers/code_lens.ex +++ b/apps/language_server/lib/language_server/providers/code_lens.ex @@ -14,7 +14,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do import ElixirLS.LanguageServer.Protocol def spec_code_lens(server_instance_id, uri, text), - do: CodeLens.Spec.code_lens(server_instance_id, uri, text) + do: CodeLens.TypeSpec.code_lens(server_instance_id, uri, text) def test_code_lens(uri, text), do: CodeLens.Test.code_lens(uri, text) diff --git a/apps/language_server/lib/language_server/providers/code_lens/spec.ex b/apps/language_server/lib/language_server/providers/code_lens/type_spec.ex similarity index 98% rename from apps/language_server/lib/language_server/providers/code_lens/spec.ex rename to apps/language_server/lib/language_server/providers/code_lens/type_spec.ex index c7f1c5005..f9a836945 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/spec.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/type_spec.ex @@ -1,4 +1,4 @@ -defmodule ElixirLS.LanguageServer.Providers.CodeLens.Spec do +defmodule ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec do alias ElixirLS.LanguageServer.Providers.CodeLens alias ElixirLS.LanguageServer.{Server, SourceFile} alias Erl2ex.Convert.{Context, ErlForms} From f61fdd584c4faf494a1e6c03f9ef4dba548b00fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Mon, 16 Nov 2020 22:50:56 -0500 Subject: [PATCH 10/22] Update code lenses provider documentation --- .../lib/language_server/providers/code_lens.ex | 11 ++++------- .../lib/language_server/providers/code_lens/test.ex | 9 +++++++++ .../language_server/providers/code_lens/type_spec.ex | 11 +++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) 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 13fee170a..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,13 +1,10 @@ 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.Providers.CodeLens 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 index e05847ee5..ce6469291 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -1,4 +1,13 @@ defmodule DescribeBlock 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 ElixirSense.Core.State.Env @struct_keys [:line, :name, :body_scope_id] 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 index f9a836945..ff620350f 100644 --- 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 @@ -1,4 +1,15 @@ 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} From d938be94f0547cee867bdf5e9b77c938c35c89c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Thu, 19 Nov 2020 20:33:35 -0500 Subject: [PATCH 11/22] Move doc + update command --- .../providers/code_lens/test.ex | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 index ce6469291..7b154996f 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -1,13 +1,4 @@ defmodule DescribeBlock 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 ElixirSense.Core.State.Env @struct_keys [:line, :name, :body_scope_id] @@ -68,13 +59,22 @@ defmodule TestBlock do end 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.SourceFile alias ElixirSense.Core.Parser alias ElixirSense.Core.Metadata alias ElixirSense.Core.State.Env - @run_test_command "elixir.test.run" + @run_test_command "elixir.lens.test.run" def code_lens(uri, text) do with {:ok, buffer_file_metadata} <- parse_source(text) do From 4b7990cb56e87b676b3a96692f4bcd2b5e00b95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sat, 21 Nov 2020 21:03:57 -0500 Subject: [PATCH 12/22] Improve error handling --- apps/language_server/lib/language_server/server.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index cde4726ae..c3e4255d4 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -585,6 +585,12 @@ defmodule ElixirLS.LanguageServer.Server do with {:ok, spec_code_lens} <- get_spec_code_lens(state, uri), {:ok, test_code_lens} <- CodeLens.test_code_lens(uri, state.source_files[uri].text) do {:ok, spec_code_lens ++ test_code_lens} + else + {:error, %ElixirSense.Core.Metadata{error: {line, error_msg}}} -> + {:error, :code_lens_error, "#{line}: #{error_msg}", state} + + {:error, error} -> + {:error, :code_lens_error, "Error while building code lenses: #{inspect(error)}", state} end end From 1fb2bb6b4003751b2b49b48605d0df8a06499031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 22 Nov 2020 10:30:46 -0500 Subject: [PATCH 13/22] Add code_lens_error to JsonRpc --- apps/language_server/lib/language_server/json_rpc.ex | 1 + 1 file changed, 1 insertion(+) 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 From 95f205a008a840e5e4deae560734efec0ff60ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 22 Nov 2020 10:32:26 -0500 Subject: [PATCH 14/22] Don't raise if describe not found --- .../lib/language_server/providers/code_lens/test.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 7b154996f..534213b9e 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -40,10 +40,10 @@ defmodule DescribeBlock do lines_to_env_list |> Enum.at(env_index) - with true = env_index + 1 < lines_to_env_list_length, + 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 + true <- body_scope_id != declaration_scope_id do body_scope_id else _ -> nil @@ -72,7 +72,6 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do alias ElixirLS.LanguageServer.SourceFile alias ElixirSense.Core.Parser alias ElixirSense.Core.Metadata - alias ElixirSense.Core.State.Env @run_test_command "elixir.lens.test.run" From 26fe78ef801b6f35f35bd7eff642a6870fa46d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Sun, 22 Nov 2020 21:41:30 -0500 Subject: [PATCH 15/22] Add provider unit tests --- .../providers/code_lens/test.ex | 23 ++- .../test/providers/code_lens/test_test.exs | 185 ++++++++++++++++++ 2 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 apps/language_server/test/providers/code_lens/test_test.exs 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 index 534213b9e..a3930cb95 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -105,23 +105,22 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do end end - def get_test_lenses(test_blocks, file_path) do - test_blocks - |> Enum.map(fn block -> - CodeLens.build_code_lens(block.line, "Run test", @run_test_command, %{ + defp get_test_lenses(test_blocks, file_path) do + args = fn block -> + %{ "filePath" => file_path, - "describe" => - if block.describe != nil do - block.describe.name - else - nil - end, "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 - def get_describe_lenses(describe_blocks, file_path) do + 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, %{ 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 From 185a0d98c360949789857d3a9960eea1ddff4077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Tue, 24 Nov 2020 21:33:44 -0500 Subject: [PATCH 16/22] Add server test --- .../test/fixtures/test_code_lens/mix.exs | 9 +++ .../test_code_lens/test/fixture_test.exs | 7 +++ apps/language_server/test/server_test.exs | 56 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 apps/language_server/test/fixtures/test_code_lens/mix.exs create mode 100644 apps/language_server/test/fixtures/test_code_lens/test/fixture_test.exs 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/server_test.exs b/apps/language_server/test/server_test.exs index 9e6f2b6c2..750d19e16 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -621,6 +621,62 @@ 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) + + initialize(server) + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) + + Server.receive_packet( + server, + code_lens_req(4, file_uri) + ) + + # :timer.sleep(1000) + # %{"result" => resp} = assert_received(%{"id" => 4}) + 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 + defp with_new_server(func) do server = start_supervised!({Server, nil}) packet_capture = start_supervised!({PacketCapture, self()}) From 918ada415739ccc68354e1f7db44eb8ea58e2773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Tue, 1 Dec 2020 19:34:23 -0500 Subject: [PATCH 17/22] Extract describe and test block modules --- .../providers/code_lens/test.ex | 62 +------------------ .../code_lens/test/describe_block.ex | 52 ++++++++++++++++ .../providers/code_lens/test/test_block.ex | 6 ++ 3 files changed, 60 insertions(+), 60 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/code_lens/test/describe_block.ex create mode 100644 apps/language_server/lib/language_server/providers/code_lens/test/test_block.ex 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 index a3930cb95..d8c9a25af 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -1,63 +1,3 @@ -defmodule 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 - ) - - %DescribeBlock{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 - -defmodule TestBlock do - @struct_keys [:name, :describe, :line] - - @enforce_keys @struct_keys - defstruct @struct_keys -end - defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do @moduledoc """ Identifies test execution targets and provides code lenses for automatically executing them. @@ -69,6 +9,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do """ 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 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 From 069c3239a818864abca0d0ecfb43913a156e40c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Tue, 1 Dec 2020 19:56:55 -0500 Subject: [PATCH 18/22] Handle setting for disabling test code lenses --- apps/language_server/lib/language_server/server.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index c3e4255d4..d8baa4416 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -583,7 +583,7 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_request(code_lens_req(_id, uri), state) do fun = fn -> with {:ok, spec_code_lens} <- get_spec_code_lens(state, uri), - {:ok, test_code_lens} <- CodeLens.test_code_lens(uri, state.source_files[uri].text) do + {:ok, test_code_lens} <- get_test_code_lens(state, uri) do {:ok, spec_code_lens ++ test_code_lens} else {:error, %ElixirSense.Core.Metadata{error: {line, error_msg}}} -> @@ -658,6 +658,14 @@ defmodule ElixirLS.LanguageServer.Server do end end + defp get_test_code_lens(state, uri) do + if state.settings["enableTestLenses"] == true do + CodeLens.test_code_lens(uri, state.source_files[uri].text) + else + {:ok, []} + end + end + # Build defp trigger_build(state) do From dc012a2be652730bc0d78c68c6836656d1a24517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Wed, 2 Dec 2020 20:32:27 -0500 Subject: [PATCH 19/22] Return empty list instead of error for disabled lenses --- apps/language_server/lib/language_server/server.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 54caf4f2d..31955ae21 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -783,7 +783,7 @@ defmodule ElixirLS.LanguageServer.Server do if dialyzer_enabled?(state) and !!state.settings["suggestSpecs"] do CodeLens.spec_code_lens(state.server_instance_id, uri, source_file.text) else - {:error, :invalid_request, "suggestSpecs is disabled"} + {:ok, []} end end @@ -791,7 +791,7 @@ defmodule ElixirLS.LanguageServer.Server do if state.settings["enableTestLenses"] == true do CodeLens.test_code_lens(uri, source_file.text) else - {:error, :invalid_request, "enableTestLenses is disabled"} + {:ok, []} end end From 6b616fb1135badd46ee353bdde279c48b3f3b1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Wed, 2 Dec 2020 21:03:46 -0500 Subject: [PATCH 20/22] Update tests --- apps/language_server/test/server_test.exs | 33 ++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index a809c16da..713249788 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1142,6 +1142,12 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) 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( @@ -1149,8 +1155,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do code_lens_req(4, file_uri) ) - # :timer.sleep(1000) - # %{"result" => resp} = assert_received(%{"id" => 4}) resp = assert_receive(%{"id" => 4}, 5000) assert response(4, [ @@ -1174,7 +1178,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do "command" => %{ "arguments" => [ %{ - "filePath" => file_absolute_path, + "filePath" => ^file_absolute_path, "module" => "Elixir.TestCodeLensTest" } ], @@ -1190,6 +1194,29 @@ defmodule ElixirLS.LanguageServer.ServerTest do 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) + + 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()}) From 1230ce38fb851d9e4fd9713d46390c4f10756c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20L=C3=A9vesque?= Date: Wed, 2 Dec 2020 22:10:58 -0500 Subject: [PATCH 21/22] Fix tests --- .tool-versions | 2 ++ apps/language_server/test/server_test.exs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..8f5c7e8c5 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 21.3.8.18 +elixir 1.11.2-otp-21 diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 713249788..b71818370 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1141,7 +1141,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do file_absolute_path = SourceFile.path_from_uri(file_uri) text = File.read!(file_path) - initialize(server) + fake_initialize(server) Server.receive_packet( server, @@ -1202,7 +1202,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do file_uri = SourceFile.path_to_uri(file_path) text = File.read!(file_path) - initialize(server) + fake_initialize(server) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) From f8c1c586338083829e2b937e86d587fd4a27b6eb Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Thu, 3 Dec 2020 07:34:10 -1000 Subject: [PATCH 22/22] Delete .tool-versions --- .tool-versions | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 8f5c7e8c5..000000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -erlang 21.3.8.18 -elixir 1.11.2-otp-21