Skip to content

Commit

Permalink
fix(completions): imports inside blocks that generate functions (#423)
Browse files Browse the repository at this point in the history
The `test/2` macro is an example of this

Closes #420
  • Loading branch information
mhanberg authored Apr 17, 2024
1 parent d62809e commit 04d3010
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 36 deletions.
1 change: 1 addition & 0 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ defmodule NextLS do
|> then(fn
{:ok, ast} -> ast
{:error, ast, _} -> ast
{:error, :no_fuel_remaining} -> nil
end)

{:ok, {_, _, _, macro_env}} = Runtime.expand(runtime, ast, Path.basename(uri))
Expand Down
74 changes: 41 additions & 33 deletions priv/monkey/_next_ls_private_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1217,52 +1217,46 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{arg, state, env} = expand(arg, state, env)
{opts, state, env} = expand_directive_opts(opts, state, env)

case arg do
{:__aliases__, _, _} ->
# An actual compiler would raise if the alias fails.
case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do
{:ok, env} -> {arg, state, env}
{:error, _} -> {arg, state, env}
end

_ ->
{node, state, env}
if is_atom(arg) do
# An actual compiler would raise if the alias fails.
case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do
{:ok, env} -> {arg, state, env}
{:error, _} -> {arg, state, env}
end
else
{node, state, env}
end
end

defp expand({:require, meta, [arg, opts]} = node, state, env) do
{arg, state, env} = expand(arg, state, env)
{opts, state, env} = expand_directive_opts(opts, state, env)

case arg do
{:__aliases__, _, _} ->
# An actual compiler would raise if the module is not defined or if the require fails.
case Macro.Env.define_require(env, meta, arg, [trace: false] ++ opts) do
{:ok, env} -> {arg, state, env}
{:error, _} -> {arg, state, env}
end

_ ->
{node, state, env}
if is_atom(arg) do
# An actual compiler would raise if the module is not defined or if the require fails.
case Macro.Env.define_require(env, meta, arg, [trace: false] ++ opts) do
{:ok, env} -> {arg, state, env}
{:error, _} -> {arg, state, env}
end
else
{node, state, env}
end
end

defp expand({:import, meta, [arg, opts]} = node, state, env) do
{arg, state, env} = expand(arg, state, env)
{opts, state, env} = expand_directive_opts(opts, state, env)

case arg do
{:__aliases__, _, _} ->
# An actual compiler would raise if the module is not defined or if the import fails.
with true <- is_atom(arg) and Code.ensure_loaded?(arg),
{:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do
{arg, state, env}
else
_ -> {arg, state, env}
end

_ ->
{node, state, env}
if is_atom(arg) do
# An actual compiler would raise if the module is not defined or if the import fails.
with true <- is_atom(arg) and Code.ensure_loaded?(arg),
{:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do
{arg, state, env}
else
_ -> {arg, state, env}
end
else
{node, state, env}
end
end

Expand Down Expand Up @@ -1444,6 +1438,20 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{Enum.reverse(blocks), put_in(state.functions, functions), env}
end

defp expand_macro(_meta, Kernel, type, [{_name, _, params}, blocks], _callback, state, env)
when type in [:def, :defp] and is_list(params) and is_list(blocks) do
{blocks, state} =
for {type, block} <- blocks, reduce: {[], state} do
{acc, state} ->
{res, state, _env} = expand(block, state, env)
{[{type, res} | acc], state}
end

arity = length(List.wrap(params))

{Enum.reverse(blocks), state, env}
end

defp expand_macro(meta, Kernel, :@, [{name, _, p}] = args, callback, state, env) when is_list(p) do
state = update_in(state.attrs, &[to_string(name) | &1])
expand_macro_callback(meta, Kernel, :@, args, callback, state, env)
Expand Down Expand Up @@ -1527,7 +1535,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do
{Enum.reverse(acc), state, env}
end

defp expand_list([h | t], state, env, acc) do
defp expand_list([h | t] = list, state, env, acc) do
{h, state, env} = expand(h, state, env)
expand_list(t, state, env, [h | acc])
end
Expand Down
95 changes: 92 additions & 3 deletions test/next_ls/completions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ defmodule NextLS.CompletionsTest do
import GenLSP.Test
import NextLS.Support.Utils

defmacrop assert_match({:in, _, [left, right]}) do
quote do
assert Enum.any?(unquote(right), fn x ->
match?(unquote(left), x)
end),
"""
failed to find a match inside of list
left: #{unquote(Macro.to_string(left))}
right: #{inspect(unquote(right), pretty: true)}
"""
end
end

@moduletag tmp_dir: true, root_paths: ["my_proj"]

setup %{tmp_dir: tmp_dir} do
Expand Down Expand Up @@ -34,7 +48,7 @@ defmodule NextLS.CompletionsTest do
baz = Path.join(cwd, "my_proj/lib/baz.ex")

File.write!(baz, """
defmodule Foo.Bar.Baz do
defmodule Foo.Bing.Baz do
def run() do
:ok
end
Expand Down Expand Up @@ -361,12 +375,47 @@ defmodule NextLS.CompletionsTest do
]
end

test "aliases in document", %{client: client, foo: foo} do
uri = uri(foo)

did_open(client, foo, """
defmodule Foo do
alias Foo.Bing
def run() do
B
end
end
""")

request client, %{
method: "textDocument/completion",
id: 2,
jsonrpc: "2.0",
params: %{
textDocument: %{
uri: uri
},
position: %{
line: 4,
character: 5
}
}
}

assert_result 2, results

assert_match(
%{"data" => _, "documentation" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} in results
)
end

test "inside alias special form", %{client: client, foo: foo} do
uri = uri(foo)

did_open(client, foo, """
defmodule Foo do
alias Foo.Bar.
alias Foo.Bing.
def run() do
:ok
Expand All @@ -390,7 +439,47 @@ defmodule NextLS.CompletionsTest do
}

assert_result 2, [
%{"data" => _, "documentation" => _, "insertText" => "Baz", "kind" => 9, "label" => "Baz"}
%{"data" => _, "documentation" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"}
]
end

test "import functions appear", %{client: client, foo: foo} do
uri = uri(foo)

did_open(client, foo, """
defmodule Foo do
use ExUnit.Case
import ExUnit.CaptureLog
test "foo" do
cap
end
end
""")

request client, %{
method: "textDocument/completion",
id: 2,
jsonrpc: "2.0",
params: %{
textDocument: %{
uri: uri
},
position: %{
line: 5,
character: 7
}
}
}

assert_result 2, results

assert_match(
%{"data" => _, "documentation" => _, "insertText" => "capture_log", "kind" => 3, "label" => "capture_log/1"} in results
)

assert_match(
%{"data" => _, "documentation" => _, "insertText" => "capture_log", "kind" => 3, "label" => "capture_log/2"} in results
)
end
end

0 comments on commit 04d3010

Please sign in to comment.