Skip to content

Commit

Permalink
feat(completions): local variables (#393)
Browse files Browse the repository at this point in the history
Working towards #45
  • Loading branch information
mhanberg authored Mar 10, 2024
1 parent f2bf792 commit d3a1c7d
Show file tree
Hide file tree
Showing 5 changed files with 398 additions and 34 deletions.
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

0 comments on commit d3a1c7d

Please sign in to comment.