diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 7abeb685..833120e4 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -624,8 +624,6 @@ defmodule NextLS do end) |> Enum.reverse() - dbg(results) - {:reply, results, lsp} rescue e -> diff --git a/lib/next_ls/autocomplete.ex b/lib/next_ls/autocomplete.ex index 6867bb08..040a5985 100644 --- a/lib/next_ls/autocomplete.ex +++ b/lib/next_ls/autocomplete.ex @@ -24,14 +24,14 @@ defmodule NextLS.Autocomplete do @alias_only_atoms ~w(alias import require)a @alias_only_charlists ~w(alias import require)c - def expand(code, runtime) do + def expand(code, runtime, env) do case path_fragment(code) do - [] -> expand_code(code, runtime) + [] -> expand_code(code, runtime, env) path -> expand_path(path) end end - defp expand_code(code, runtime) do + defp expand_code(code, runtime, env) do code = Enum.reverse(code) # helper = get_helper(code) @@ -62,13 +62,13 @@ defmodule NextLS.Autocomplete do expand_dot_call(path, List.to_atom(hint), runtime) :expr -> - expand_container_context(code, :expr, "", runtime) || expand_local_or_var("", "", runtime) + expand_container_context(code, :expr, "", runtime) || expand_local_or_var(code, "", runtime, env) {:local_or_var, local_or_var} -> hint = List.to_string(local_or_var) expand_container_context(code, :expr, hint, runtime) || - expand_local_or_var(hint, List.to_string(local_or_var), runtime) + expand_local_or_var(hint, List.to_string(local_or_var), runtime, env) {:local_arity, local} -> expand_local(List.to_string(local), true, runtime) @@ -77,7 +77,7 @@ defmodule NextLS.Autocomplete do expand_aliases("", runtime) {:local_call, local} -> - expand_local_call(List.to_atom(local), runtime) + expand_local_call(List.to_atom(local), runtime, env) {:operator, operator} when operator in ~w(:: -)c -> expand_container_context(code, :operator, "", runtime) || @@ -90,10 +90,10 @@ defmodule NextLS.Autocomplete do expand_local(List.to_string(operator), true, runtime) {:operator_call, operator} when operator in ~w(|)c -> - expand_container_context(code, :expr, "", runtime) || expand_local_or_var("", "", runtime) + expand_container_context(code, :expr, "", runtime) || expand_local_or_var("", "", runtime, env) {:operator_call, _operator} -> - expand_local_or_var("", "", runtime) + expand_local_or_var("", "", runtime, env) {:sigil, []} -> expand_sigil(runtime) @@ -163,12 +163,12 @@ defmodule NextLS.Autocomplete do ## Expand call - defp expand_local_call(fun, runtime) do + defp expand_local_call(fun, runtime, env) do runtime |> imports_from_env() |> Enum.filter(fn {_, funs} -> List.keymember?(funs, fun, 0) end) |> Enum.flat_map(fn {module, _} -> get_signatures(fun, module) end) - |> expand_signatures(runtime) + |> expand_signatures(runtime, env) end defp expand_dot_call(path, fun, runtime) do @@ -192,7 +192,7 @@ defmodule NextLS.Autocomplete do yes([head]) end - defp expand_signatures([], runtime), do: expand_local_or_var("", "", runtime) + defp expand_signatures([], runtime, env), do: expand_local_or_var("", "", runtime, env) ## Expand dot @@ -259,8 +259,8 @@ defmodule NextLS.Autocomplete do ## Expand local or var - defp expand_local_or_var(code, hint, runtime) do - format_expansion(match_var(code, hint, runtime) ++ match_local(code, false, runtime)) + defp expand_local_or_var(code, hint, runtime, env) do + format_expansion(match_var(code, hint, runtime, env) ++ match_local(hint, false, runtime)) end defp expand_local(hint, exact?, runtime) do @@ -286,9 +286,9 @@ defmodule NextLS.Autocomplete do match_module_funs(runtime, nil, imports, hint, exact?) end - defp match_var(code, hint, runtime) do + defp match_var(code, hint, _runtime, env) do code - |> variables_from_binding(runtime) + |> variables_from_binding(env) |> Enum.filter(&String.starts_with?(&1, hint)) |> Enum.sort() |> Enum.map(&%{kind: :variable, name: &1}) @@ -774,13 +774,8 @@ defmodule NextLS.Autocomplete do [] end - defp variables_from_binding(_hint, _runtime) do - # {:ok, ast} = Code.Fragment.container_cursor_to_quoted(hint, columns: true) - - # ast |> Macro.to_string() |> IO.puts() - - # NextLS.ASTHelpers.Variables.collect(ast) - [] + defp variables_from_binding(_hint, env) do + env.variables end defp value_from_binding([_var | _path], _runtime) do diff --git a/lib/next_ls/helpers/ast_helpers/env.ex b/lib/next_ls/helpers/ast_helpers/env.ex new file mode 100644 index 00000000..345da2ef --- /dev/null +++ b/lib/next_ls/helpers/ast_helpers/env.ex @@ -0,0 +1,134 @@ +defmodule NextLS.ASTHelpers.Env do + @moduledoc false + alias Sourceror.Zipper + + defp inside?(range, position) do + Sourceror.compare_positions(range.start, position) == :lt && Sourceror.compare_positions(range.end, position) == :gt + end + + def build(ast) do + cursor = + ast + |> Zipper.zip() + |> Zipper.find(fn + {:__cursor__, _, _} -> true + _ -> false + end) + + position = cursor |> Zipper.node() |> Sourceror.get_range() |> Map.get(:start) + zipper = Zipper.prev(cursor) + + env = + ascend(zipper, %{variables: []}, fn node, zipper, acc -> + is_inside = + with {_, _, _} <- node, + range when not is_nil(range) <- Sourceror.get_range(node) do + inside?(range, position) + else + _ -> + false + end + + case node do + {match_op, _, [pm | _]} when match_op in [:=] and not is_inside -> + {_, vars} = + Macro.prewalk(pm, [], fn node, acc -> + case node do + {name, _, nil} -> + {node, [to_string(name) | acc]} + + _ -> + {node, acc} + end + end) + + Map.update!(acc, :variables, &(vars ++ &1)) + + {match_op, _, [pm | _]} when match_op in [:<-] -> + up_node = zipper |> Zipper.up() |> Zipper.node() + + # in_match operator comes with for and with normally, so we need to + # check if we are inside the parent node, which is the for/with + is_inside = + with {_, _, _} <- up_node, + range when not is_nil(range) <- Sourceror.get_range(up_node) do + inside?(range, position) + else + _ -> + false + end + + if is_inside do + {_, vars} = + Macro.prewalk(pm, [], fn node, acc -> + case node do + {name, _, nil} -> + {node, [to_string(name) | acc]} + + _ -> + {node, acc} + end + end) + + Map.update!(acc, :variables, &(vars ++ &1)) + else + acc + end + + {def, _, [{_, _, args} | _]} when def in [:def, :defp, :defmacro, :defmacrop] and args != [] and is_inside -> + {_, vars} = + Macro.prewalk(args, [], fn node, acc -> + case node do + {name, _, nil} -> + {node, [to_string(name) | acc]} + + _ -> + {node, acc} + end + end) + + Map.update!(acc, :variables, &(vars ++ &1)) + + {:->, _, [args | _]} when args != [] -> + {_, vars} = + Macro.prewalk(args, [], fn node, acc -> + case node do + {name, _, nil} -> + {node, [to_string(name) | acc]} + + _ -> + {node, acc} + end + end) + + Map.update!(acc, :variables, &(vars ++ &1)) + + _ -> + acc + end + end) + + %{ + variables: Enum.uniq(env.variables) + } + end + + def ascend(%Zipper{path: nil} = zipper, acc, callback), do: callback.(Zipper.node(zipper), zipper, acc) + + def ascend(zipper, acc, callback) do + node = Zipper.node(zipper) + acc = callback.(node, zipper, acc) + + zipper = + cond do + match?({:->, _, _}, node) -> + Zipper.up(zipper) + + true -> + left = Zipper.left(zipper) + if left, do: left, else: Zipper.up(zipper) + end + + ascend(zipper, acc, callback) + end +end diff --git a/test/next_ls/autocomplete_test.exs b/test/next_ls/autocomplete_test.exs index 4acef04f..2736f3df 100644 --- a/test/next_ls/autocomplete_test.exs +++ b/test/next_ls/autocomplete_test.exs @@ -81,8 +81,8 @@ defmodule NextLS.AutocompleteTest do [runtime: pid] end - defp expand(runtime, expr) do - NextLS.Autocomplete.expand(Enum.reverse(expr), runtime) + defp expand(runtime, expr, env \\ %{variables: []}) do + NextLS.Autocomplete.expand(Enum.reverse(expr), runtime, env) end test "Erlang module completion", %{runtime: runtime} do @@ -414,14 +414,42 @@ defmodule NextLS.AutocompleteTest do ]} = expand(runtime, ~c"put_") end - # TODO: this only partially works, will not say we support for now - # test "variable name completion", %{runtime: runtime} do - # prev = "numeral = 3; number = 3; nothing = nil" - # assert expand(runtime, ~c"#{prev}\nnumb") == {:yes, ~c"er", []} - # assert expand(runtime, ~c"#{prev}\nnum") == {:yes, ~c"", [~c"number", ~c"numeral"]} - # # FIXME: variables + local functions - # # assert expand(runtime, ~c"#{prev}\nno") == {:yes, ~c"", [~c"nothing", ~c"node/0", ~c"node/1", ~c"not/1"]} - # end + test "variable name completion", %{runtime: runtime} do + prev = "numeral = 3; number = 3; nothing = nil" + env = %{variables: ["numeral", "number", "nothing"]} + assert expand(runtime, ~c"#{prev}\nnumb", env) == {:yes, [%{name: "number", kind: :variable}]} + + assert expand(runtime, ~c"#{prev}\nnum", env) == + {:yes, [%{name: "number", kind: :variable}, %{name: "numeral", kind: :variable}]} + + assert expand(runtime, ~c"#{prev}\nno", env) == { + :yes, + [ + %{name: "nothing", kind: :variable}, + %{ + arity: 0, + name: "node", + docs: + "## Kernel.node/0\n\nReturns an atom representing the name of the local node.\nIf the node is not alive, `:nonode@nohost` is returned instead.\n\nAllowed in guard tests. Inlined by the compiler.\n\n", + kind: :function + }, + %{ + arity: 1, + name: "node", + docs: + "## Kernel.node/1\n\nReturns an atom representing the name of the local node.\nIf the node is not alive, `:nonode@nohost` is returned instead.\n\nAllowed in guard tests. Inlined by the compiler.\n\n", + kind: :function + }, + %{ + arity: 1, + name: "not", + docs: + "## Kernel.not/1\n\nStrictly boolean \"not\" operator.\n\n`value` must be a boolean; if it's not, an `ArgumentError` exception is raised.\n\nAllowed in guard tests. Inlined by the compiler.\n\n## Examples\n\n iex> not false\n true\n\n\n", + kind: :function + } + ] + } + end # TODO: locals # test "completion of manually imported functions and macros", %{runtime: runtime} do diff --git a/test/next_ls/helpers/ast_helpers/env_test.exs b/test/next_ls/helpers/ast_helpers/env_test.exs new file mode 100644 index 00000000..473f8e07 --- /dev/null +++ b/test/next_ls/helpers/ast_helpers/env_test.exs @@ -0,0 +1,209 @@ +defmodule NextLS.ASTHelpers.EnvTest do + use ExUnit.Case, async: true + + describe "build/2" do + test "collects simple variables" do + code = """ + defmodule Foo do + def one do + foo = :bar + + Enum.map([foo], fn -> + bar = x + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["foo", "bar"] + end + + test "collects variables from patterns" do + code = """ + defmodule Foo do + def one() do + %{bar: [one, %{baz: two}]} = Some.thing() + + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["two", "one"] + end + + test "collects variables from 'formal' parameters" do + code = """ + defmodule Foo do + def zero(notme) do + :error + end + + def one(foo, bar, baz) do + + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["baz", "bar", "foo"] + end + + test "collects variables from stab parameters" do + code = """ + defmodule Foo do + def one() do + Enum.map(Some.thing(), fn + four -> + :ok + + one, two, three -> + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["three", "two", "one"] + end + + test "collects variables from left stab" do + code = """ + defmodule Foo do + def one() do + with [foo] <- thing(), + bar <- thang() do + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["foo", "bar"] + end + + test "scopes variables lexically" do + code = """ + defmodule Foo do + def one() do + baz = Some.thing() + foo = Enum.map(two(), fn bar -> + big_bar = bar * 2 + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["baz", "bar", "big_bar"] + end + + test "comprehension and with parameters do not leak" do + code = """ + defmodule Foo do + def one(entries) do + with {:ok, entry} <- entries do + :ok + end + + for entry <- entries do + :ok + end + + __cursor__() + end + + def two do + baz = :bar + end + end + """ + + actual = + code + |> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}}) + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + end) + |> NextLS.ASTHelpers.Env.build() + + assert actual.variables == ["entries"] + end + end +end