Skip to content
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

feat(completions): local variables #393

Merged
merged 1 commit into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,6 @@ defmodule NextLS do
end)
|> Enum.reverse()

dbg(results)

{:reply, results, lsp}
rescue
e ->
Expand Down
39 changes: 17 additions & 22 deletions lib/next_ls/autocomplete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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) ||
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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})
Expand Down Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions lib/next_ls/helpers/ast_helpers/env.ex
Original file line number Diff line number Diff line change
@@ -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
48 changes: 38 additions & 10 deletions test/next_ls/autocomplete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading