Skip to content

Commit d3a1c7d

Browse files
authored
feat(completions): local variables (#393)
Working towards #45
1 parent f2bf792 commit d3a1c7d

File tree

5 files changed

+398
-34
lines changed

5 files changed

+398
-34
lines changed

lib/next_ls.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,6 @@ defmodule NextLS do
624624
end)
625625
|> Enum.reverse()
626626

627-
dbg(results)
628-
629627
{:reply, results, lsp}
630628
rescue
631629
e ->

lib/next_ls/autocomplete.ex

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ defmodule NextLS.Autocomplete do
2424
@alias_only_atoms ~w(alias import require)a
2525
@alias_only_charlists ~w(alias import require)c
2626

27-
def expand(code, runtime) do
27+
def expand(code, runtime, env) do
2828
case path_fragment(code) do
29-
[] -> expand_code(code, runtime)
29+
[] -> expand_code(code, runtime, env)
3030
path -> expand_path(path)
3131
end
3232
end
3333

34-
defp expand_code(code, runtime) do
34+
defp expand_code(code, runtime, env) do
3535
code = Enum.reverse(code)
3636
# helper = get_helper(code)
3737

@@ -62,13 +62,13 @@ defmodule NextLS.Autocomplete do
6262
expand_dot_call(path, List.to_atom(hint), runtime)
6363

6464
:expr ->
65-
expand_container_context(code, :expr, "", runtime) || expand_local_or_var("", "", runtime)
65+
expand_container_context(code, :expr, "", runtime) || expand_local_or_var(code, "", runtime, env)
6666

6767
{:local_or_var, local_or_var} ->
6868
hint = List.to_string(local_or_var)
6969

7070
expand_container_context(code, :expr, hint, runtime) ||
71-
expand_local_or_var(hint, List.to_string(local_or_var), runtime)
71+
expand_local_or_var(hint, List.to_string(local_or_var), runtime, env)
7272

7373
{:local_arity, local} ->
7474
expand_local(List.to_string(local), true, runtime)
@@ -77,7 +77,7 @@ defmodule NextLS.Autocomplete do
7777
expand_aliases("", runtime)
7878

7979
{:local_call, local} ->
80-
expand_local_call(List.to_atom(local), runtime)
80+
expand_local_call(List.to_atom(local), runtime, env)
8181

8282
{:operator, operator} when operator in ~w(:: -)c ->
8383
expand_container_context(code, :operator, "", runtime) ||
@@ -90,10 +90,10 @@ defmodule NextLS.Autocomplete do
9090
expand_local(List.to_string(operator), true, runtime)
9191

9292
{:operator_call, operator} when operator in ~w(|)c ->
93-
expand_container_context(code, :expr, "", runtime) || expand_local_or_var("", "", runtime)
93+
expand_container_context(code, :expr, "", runtime) || expand_local_or_var("", "", runtime, env)
9494

9595
{:operator_call, _operator} ->
96-
expand_local_or_var("", "", runtime)
96+
expand_local_or_var("", "", runtime, env)
9797

9898
{:sigil, []} ->
9999
expand_sigil(runtime)
@@ -163,12 +163,12 @@ defmodule NextLS.Autocomplete do
163163

164164
## Expand call
165165

166-
defp expand_local_call(fun, runtime) do
166+
defp expand_local_call(fun, runtime, env) do
167167
runtime
168168
|> imports_from_env()
169169
|> Enum.filter(fn {_, funs} -> List.keymember?(funs, fun, 0) end)
170170
|> Enum.flat_map(fn {module, _} -> get_signatures(fun, module) end)
171-
|> expand_signatures(runtime)
171+
|> expand_signatures(runtime, env)
172172
end
173173

174174
defp expand_dot_call(path, fun, runtime) do
@@ -192,7 +192,7 @@ defmodule NextLS.Autocomplete do
192192
yes([head])
193193
end
194194

195-
defp expand_signatures([], runtime), do: expand_local_or_var("", "", runtime)
195+
defp expand_signatures([], runtime, env), do: expand_local_or_var("", "", runtime, env)
196196

197197
## Expand dot
198198

@@ -259,8 +259,8 @@ defmodule NextLS.Autocomplete do
259259

260260
## Expand local or var
261261

262-
defp expand_local_or_var(code, hint, runtime) do
263-
format_expansion(match_var(code, hint, runtime) ++ match_local(code, false, runtime))
262+
defp expand_local_or_var(code, hint, runtime, env) do
263+
format_expansion(match_var(code, hint, runtime, env) ++ match_local(hint, false, runtime))
264264
end
265265

266266
defp expand_local(hint, exact?, runtime) do
@@ -286,9 +286,9 @@ defmodule NextLS.Autocomplete do
286286
match_module_funs(runtime, nil, imports, hint, exact?)
287287
end
288288

289-
defp match_var(code, hint, runtime) do
289+
defp match_var(code, hint, _runtime, env) do
290290
code
291-
|> variables_from_binding(runtime)
291+
|> variables_from_binding(env)
292292
|> Enum.filter(&String.starts_with?(&1, hint))
293293
|> Enum.sort()
294294
|> Enum.map(&%{kind: :variable, name: &1})
@@ -774,13 +774,8 @@ defmodule NextLS.Autocomplete do
774774
[]
775775
end
776776

777-
defp variables_from_binding(_hint, _runtime) do
778-
# {:ok, ast} = Code.Fragment.container_cursor_to_quoted(hint, columns: true)
779-
780-
# ast |> Macro.to_string() |> IO.puts()
781-
782-
# NextLS.ASTHelpers.Variables.collect(ast)
783-
[]
777+
defp variables_from_binding(_hint, env) do
778+
env.variables
784779
end
785780

786781
defp value_from_binding([_var | _path], _runtime) do
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
defmodule NextLS.ASTHelpers.Env do
2+
@moduledoc false
3+
alias Sourceror.Zipper
4+
5+
defp inside?(range, position) do
6+
Sourceror.compare_positions(range.start, position) == :lt && Sourceror.compare_positions(range.end, position) == :gt
7+
end
8+
9+
def build(ast) do
10+
cursor =
11+
ast
12+
|> Zipper.zip()
13+
|> Zipper.find(fn
14+
{:__cursor__, _, _} -> true
15+
_ -> false
16+
end)
17+
18+
position = cursor |> Zipper.node() |> Sourceror.get_range() |> Map.get(:start)
19+
zipper = Zipper.prev(cursor)
20+
21+
env =
22+
ascend(zipper, %{variables: []}, fn node, zipper, acc ->
23+
is_inside =
24+
with {_, _, _} <- node,
25+
range when not is_nil(range) <- Sourceror.get_range(node) do
26+
inside?(range, position)
27+
else
28+
_ ->
29+
false
30+
end
31+
32+
case node do
33+
{match_op, _, [pm | _]} when match_op in [:=] and not is_inside ->
34+
{_, vars} =
35+
Macro.prewalk(pm, [], fn node, acc ->
36+
case node do
37+
{name, _, nil} ->
38+
{node, [to_string(name) | acc]}
39+
40+
_ ->
41+
{node, acc}
42+
end
43+
end)
44+
45+
Map.update!(acc, :variables, &(vars ++ &1))
46+
47+
{match_op, _, [pm | _]} when match_op in [:<-] ->
48+
up_node = zipper |> Zipper.up() |> Zipper.node()
49+
50+
# in_match operator comes with for and with normally, so we need to
51+
# check if we are inside the parent node, which is the for/with
52+
is_inside =
53+
with {_, _, _} <- up_node,
54+
range when not is_nil(range) <- Sourceror.get_range(up_node) do
55+
inside?(range, position)
56+
else
57+
_ ->
58+
false
59+
end
60+
61+
if is_inside do
62+
{_, vars} =
63+
Macro.prewalk(pm, [], fn node, acc ->
64+
case node do
65+
{name, _, nil} ->
66+
{node, [to_string(name) | acc]}
67+
68+
_ ->
69+
{node, acc}
70+
end
71+
end)
72+
73+
Map.update!(acc, :variables, &(vars ++ &1))
74+
else
75+
acc
76+
end
77+
78+
{def, _, [{_, _, args} | _]} when def in [:def, :defp, :defmacro, :defmacrop] and args != [] and is_inside ->
79+
{_, vars} =
80+
Macro.prewalk(args, [], fn node, acc ->
81+
case node do
82+
{name, _, nil} ->
83+
{node, [to_string(name) | acc]}
84+
85+
_ ->
86+
{node, acc}
87+
end
88+
end)
89+
90+
Map.update!(acc, :variables, &(vars ++ &1))
91+
92+
{:->, _, [args | _]} when args != [] ->
93+
{_, vars} =
94+
Macro.prewalk(args, [], fn node, acc ->
95+
case node do
96+
{name, _, nil} ->
97+
{node, [to_string(name) | acc]}
98+
99+
_ ->
100+
{node, acc}
101+
end
102+
end)
103+
104+
Map.update!(acc, :variables, &(vars ++ &1))
105+
106+
_ ->
107+
acc
108+
end
109+
end)
110+
111+
%{
112+
variables: Enum.uniq(env.variables)
113+
}
114+
end
115+
116+
def ascend(%Zipper{path: nil} = zipper, acc, callback), do: callback.(Zipper.node(zipper), zipper, acc)
117+
118+
def ascend(zipper, acc, callback) do
119+
node = Zipper.node(zipper)
120+
acc = callback.(node, zipper, acc)
121+
122+
zipper =
123+
cond do
124+
match?({:->, _, _}, node) ->
125+
Zipper.up(zipper)
126+
127+
true ->
128+
left = Zipper.left(zipper)
129+
if left, do: left, else: Zipper.up(zipper)
130+
end
131+
132+
ascend(zipper, acc, callback)
133+
end
134+
end

test/next_ls/autocomplete_test.exs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ defmodule NextLS.AutocompleteTest do
8181
[runtime: pid]
8282
end
8383

84-
defp expand(runtime, expr) do
85-
NextLS.Autocomplete.expand(Enum.reverse(expr), runtime)
84+
defp expand(runtime, expr, env \\ %{variables: []}) do
85+
NextLS.Autocomplete.expand(Enum.reverse(expr), runtime, env)
8686
end
8787

8888
test "Erlang module completion", %{runtime: runtime} do
@@ -414,14 +414,42 @@ defmodule NextLS.AutocompleteTest do
414414
]} = expand(runtime, ~c"put_")
415415
end
416416

417-
# TODO: this only partially works, will not say we support for now
418-
# test "variable name completion", %{runtime: runtime} do
419-
# prev = "numeral = 3; number = 3; nothing = nil"
420-
# assert expand(runtime, ~c"#{prev}\nnumb") == {:yes, ~c"er", []}
421-
# assert expand(runtime, ~c"#{prev}\nnum") == {:yes, ~c"", [~c"number", ~c"numeral"]}
422-
# # FIXME: variables + local functions
423-
# # assert expand(runtime, ~c"#{prev}\nno") == {:yes, ~c"", [~c"nothing", ~c"node/0", ~c"node/1", ~c"not/1"]}
424-
# end
417+
test "variable name completion", %{runtime: runtime} do
418+
prev = "numeral = 3; number = 3; nothing = nil"
419+
env = %{variables: ["numeral", "number", "nothing"]}
420+
assert expand(runtime, ~c"#{prev}\nnumb", env) == {:yes, [%{name: "number", kind: :variable}]}
421+
422+
assert expand(runtime, ~c"#{prev}\nnum", env) ==
423+
{:yes, [%{name: "number", kind: :variable}, %{name: "numeral", kind: :variable}]}
424+
425+
assert expand(runtime, ~c"#{prev}\nno", env) == {
426+
:yes,
427+
[
428+
%{name: "nothing", kind: :variable},
429+
%{
430+
arity: 0,
431+
name: "node",
432+
docs:
433+
"## 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",
434+
kind: :function
435+
},
436+
%{
437+
arity: 1,
438+
name: "node",
439+
docs:
440+
"## 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",
441+
kind: :function
442+
},
443+
%{
444+
arity: 1,
445+
name: "not",
446+
docs:
447+
"## 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",
448+
kind: :function
449+
}
450+
]
451+
}
452+
end
425453

426454
# TODO: locals
427455
# test "completion of manually imported functions and macros", %{runtime: runtime} do

0 commit comments

Comments
 (0)