-
Notifications
You must be signed in to change notification settings - Fork 221
Provide test running code lens #389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
503f6c2
fd1793e
bd49269
b7420ea
d0b25ba
b8aeb9c
01b0758
03b36f6
642aa8b
f61fdd5
d938be9
395be90
4b7990c
1fb2bb6
95f205a
26fe78e
185a0d9
918ada4
069c323
2fe0e8e
dc012a2
6b616fb
1230ce3
f8c1c58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.CodeLens.Spec do | ||
Blond11516 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
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" | ||
axelson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def code_lens(uri, text) do | ||
buffer_file_metadata = | ||
Blond11516 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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}] | ||
|
||
calls_list = | ||
metadata.calls | ||
|> Enum.map(fn {_k, v} -> v end) | ||
|> List.flatten() | ||
|
||
for func <- runnable_functions do | ||
Blond11516 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
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) | ||
Blond11516 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|> List.last() | ||
|> elem(1) | ||
|> Map.get(:imports) | ||
|> Enum.any?(fn module -> module == ExUnit.Case end) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably works well, although an alternative would be to use the same configuration that ex_unit itself is using:
To be clear I don't think we should change it at this time (unless the current code results in very poor performance or some similar concern). |
||
end | ||
|
||
defp calls_to(calls_list, {function, arity}) do | ||
calls_list | ||
Blond11516 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|> 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 |
Uh oh!
There was an error while loading. Please reload this page.