Skip to content

Commit 1a2c2dc

Browse files
myronmarstonJosé Valim
authored andcommitted
Improve IEx autocomplete to support navigating map atom keys (#5488)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
1 parent 03b6c07 commit 1a2c2dc

File tree

5 files changed

+223
-37
lines changed

5 files changed

+223
-37
lines changed

lib/iex/lib/iex/autocomplete.ex

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
defmodule IEx.Autocomplete do
22
@moduledoc false
33

4-
def expand('') do
4+
def expand(expr, server \\ IEx.Server)
5+
6+
def expand('', _server) do
57
expand_import("")
68
end
79

8-
def expand([h | t]=expr) do
10+
def expand([h | t]=expr, server) do
911
cond do
1012
h === ?. and t != [] ->
11-
expand_dot(reduce(t))
13+
expand_dot(reduce(t), server)
1214
h === ?: and t == [] ->
1315
expand_erlang_modules()
1416
identifier?(h) ->
15-
expand_expr(reduce(expr))
17+
expand_expr(reduce(expr), server)
1618
(h == ?/) and t != [] and identifier?(hd(t)) ->
17-
expand_expr(reduce(t))
19+
expand_expr(reduce(t), server)
1820
h in '([{' ->
1921
expand('')
2022
true ->
@@ -26,31 +28,33 @@ defmodule IEx.Autocomplete do
2628
(h in ?a..?z) or (h in ?A..?Z) or (h in ?0..?9) or h in [?_, ??, ?!]
2729
end
2830

29-
defp expand_dot(expr) do
31+
defp expand_dot(expr, server) do
3032
case Code.string_to_quoted expr do
3133
{:ok, atom} when is_atom(atom) ->
32-
expand_call(atom, "")
34+
expand_call(atom, "", server)
3335
{:ok, {:__aliases__, _, list}} ->
34-
expand_elixir_modules(list, "")
36+
expand_elixir_modules(list, "", server)
37+
{:ok, {_, _, _} = ast_node} ->
38+
expand_call(ast_node, "", server)
3539
_ ->
3640
no()
3741
end
3842
end
3943

40-
defp expand_expr(expr) do
44+
defp expand_expr(expr, server) do
4145
case Code.string_to_quoted expr do
4246
{:ok, atom} when is_atom(atom) ->
4347
expand_erlang_modules(Atom.to_string(atom))
4448
{:ok, {atom, _, nil}} when is_atom(atom) ->
4549
expand_import(Atom.to_string(atom))
4650
{:ok, {:__aliases__, _, [root]}} ->
47-
expand_elixir_modules([], Atom.to_string(root))
51+
expand_elixir_modules([], Atom.to_string(root), server)
4852
{:ok, {:__aliases__, _, [h | _] = list}} when is_atom(h) ->
4953
hint = Atom.to_string(List.last(list))
5054
list = Enum.take(list, length(list) - 1)
51-
expand_elixir_modules(list, hint)
52-
{:ok, {{:., _, [mod, fun]}, _, []}} when is_atom(fun) ->
53-
expand_call(mod, Atom.to_string(fun))
55+
expand_elixir_modules(list, hint, server)
56+
{:ok, {{:., _, [ast_node, fun]}, _, []}} when is_atom(fun) ->
57+
expand_call(ast_node, Atom.to_string(fun), server)
5458
_ ->
5559
no()
5660
end
@@ -105,21 +109,37 @@ defmodule IEx.Autocomplete do
105109
## Expand calls
106110

107111
# :atom.fun
108-
defp expand_call(mod, hint) when is_atom(mod) do
112+
defp expand_call(mod, hint, _server) when is_atom(mod) do
109113
expand_require(mod, hint)
110114
end
111115

112116
# Elixir.fun
113-
defp expand_call({:__aliases__, _, list}, hint) do
114-
expand_alias(list)
117+
defp expand_call({:__aliases__, _, list}, hint, server) do
118+
expand_alias(list, server)
115119
|> normalize_module
116120
|> expand_require(hint)
117121
end
118122

119-
defp expand_call(_, _) do
123+
# variable.fun_or_key
124+
defp expand_call({_, _, _} = ast_node, hint, server) do
125+
case value_from_binding(ast_node, server) do
126+
{:ok, mod} when is_atom(mod) -> expand_call(mod, hint, server)
127+
{:ok, map} when is_map(map) -> expand_map_field_access(map, hint)
128+
_otherwise -> no()
129+
end
130+
end
131+
132+
defp expand_call(_, _, _) do
120133
no()
121134
end
122135

136+
defp expand_map_field_access(map, hint) do
137+
case match_map_fields(map, hint) do
138+
[%{kind: :map_key, name: name, value_is_map: false}] when name == hint -> no()
139+
map_fields when is_list(map_fields) -> format_expansion(map_fields, hint)
140+
end
141+
end
142+
123143
defp expand_require(mod, hint) do
124144
format_expansion match_module_funs(mod, hint), hint
125145
end
@@ -145,26 +165,27 @@ defmodule IEx.Autocomplete do
145165

146166
## Elixir modules
147167

148-
defp expand_elixir_modules([], hint) do
149-
expand_elixir_modules(Elixir, hint, match_aliases(hint))
168+
defp expand_elixir_modules([], hint, server) do
169+
aliases = match_aliases(hint, server)
170+
expand_elixir_modules_from_aliases(Elixir, hint, aliases)
150171
end
151172

152-
defp expand_elixir_modules(list, hint) do
153-
expand_alias(list)
173+
defp expand_elixir_modules(list, hint, server) do
174+
expand_alias(list, server)
154175
|> normalize_module
155-
|> expand_elixir_modules(hint, [])
176+
|> expand_elixir_modules_from_aliases(hint, [])
156177
end
157178

158-
defp expand_elixir_modules(mod, hint, aliases) do
179+
defp expand_elixir_modules_from_aliases(mod, hint, aliases) do
159180
aliases
160181
|> Kernel.++(match_elixir_modules(mod, hint))
161182
|> Kernel.++(match_module_funs(mod, hint))
162183
|> format_expansion(hint)
163184
end
164185

165-
defp expand_alias([name | rest] = list) do
186+
defp expand_alias([name | rest] = list, server) do
166187
module = Module.concat(Elixir, name)
167-
Enum.find_value env_aliases(), list, fn {alias, mod} ->
188+
Enum.find_value env_aliases(server), list, fn {alias, mod} ->
168189
if alias === module do
169190
case Atom.to_string(mod) do
170191
"Elixir." <> mod ->
@@ -176,12 +197,12 @@ defmodule IEx.Autocomplete do
176197
end
177198
end
178199

179-
defp env_aliases do
180-
Application.get_env(:iex, :autocomplete_server).current_env.aliases
181-
end
200+
defp env_aliases(server), do: server.current_env.aliases
201+
202+
defp get_evaluator(server), do: server.evaluator
182203

183-
defp match_aliases(hint) do
184-
for {alias, _mod} <- env_aliases(),
204+
defp match_aliases(hint, server) do
205+
for {alias, _mod} <- env_aliases(server),
185206
[name] = Module.split(alias),
186207
starts_with?(name, hint) do
187208
%{kind: :module, type: :alias, name: name}
@@ -269,6 +290,14 @@ defmodule IEx.Autocomplete do
269290
end
270291
end
271292

293+
defp match_map_fields(map, hint) do
294+
for {key, value} <- map,
295+
is_atom(key),
296+
key = to_string(key),
297+
String.starts_with?(key, hint),
298+
do: %{kind: :map_key, name: key, value_is_map: is_map(value)}
299+
end
300+
272301
defp get_module_funs(mod) do
273302
docs = Code.get_docs(mod, :docs) || []
274303
module_info_funs(mod) |> Enum.reject(&hidden_fun?(&1, docs))
@@ -316,6 +345,10 @@ defmodule IEx.Autocomplete do
316345
for a <- :lists.sort(arities), do: "#{name}/#{a}"
317346
end
318347

348+
defp to_entries(%{kind: :map_key, name: name}) do
349+
[name]
350+
end
351+
319352
defp to_uniq_entries(%{kind: :module}) do
320353
[]
321354
end
@@ -324,6 +357,10 @@ defmodule IEx.Autocomplete do
324357
to_entries(fun)
325358
end
326359

360+
defp to_uniq_entries(%{kind: :map_key}) do
361+
[]
362+
end
363+
327364
defp to_hint(%{kind: :module, name: name}, hint) when name == hint do
328365
format_hint(name, name) <> "."
329366
end
@@ -336,8 +373,41 @@ defmodule IEx.Autocomplete do
336373
format_hint(name, hint)
337374
end
338375

376+
defp to_hint(%{kind: :map_key, name: name, value_is_map: true}, hint) when name == hint do
377+
format_hint(name, hint) <> "."
378+
end
379+
380+
defp to_hint(%{kind: :map_key, name: name}, hint) do
381+
format_hint(name, hint)
382+
end
383+
339384
defp format_hint(name, hint) do
340385
hint_size = byte_size(hint)
341386
:binary.part(name, hint_size, byte_size(name) - hint_size)
342387
end
388+
389+
defp value_from_binding(ast_node, server) do
390+
with evaluator when is_pid(evaluator) <- get_evaluator(server),
391+
{var, map_key_path} <- extract_from_ast(ast_node, []) do
392+
IEx.Evaluator.value_from_binding(evaluator, var, map_key_path)
393+
else
394+
_ -> :error
395+
end
396+
end
397+
398+
defp extract_from_ast(var_name, acc) when is_atom(var_name) do
399+
{var_name, acc}
400+
end
401+
402+
defp extract_from_ast({var_name, _, nil}, acc) when is_atom(var_name) do
403+
{var_name, acc}
404+
end
405+
406+
defp extract_from_ast({{:., _, [ast_node, fun]}, _, []}, acc) when is_atom(fun) do
407+
extract_from_ast(ast_node, [fun | acc])
408+
end
409+
410+
defp extract_from_ast(_ast_node, _acc) do
411+
:error
412+
end
343413
end

lib/iex/lib/iex/evaluator.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ defmodule IEx.Evaluator do
2424
end
2525
end
2626

27+
@doc """
28+
Gets a value out of the binding, using the provided
29+
variable name and map key path.
30+
"""
31+
@spec value_from_binding(pid, atom, [atom]) :: {:ok, any} | :error
32+
def value_from_binding(evaluator, var_name, map_key_path) do
33+
ref = make_ref()
34+
send evaluator, {:value_from_binding, ref, self(), var_name, map_key_path}
35+
36+
receive do
37+
{^ref, result} -> result
38+
after
39+
5000 -> :error
40+
end
41+
end
42+
2743
defp loop(server, history, state) do
2844
receive do
2945
{:eval, ^server, code, iex_state} ->
@@ -33,11 +49,24 @@ defmodule IEx.Evaluator do
3349
{:peek_env, receiver} ->
3450
send receiver, {:peek_env, state.env}
3551
loop(server, history, state)
52+
{:value_from_binding, ref, receiver, var_name, map_key_path} ->
53+
value = traverse_binding(state.binding, var_name, map_key_path)
54+
send receiver, {ref, value}
55+
loop(server, history, state)
3656
{:done, ^server} ->
3757
:ok
3858
end
3959
end
4060

61+
defp traverse_binding(binding, var_name, map_key_path) do
62+
accumulator = Keyword.fetch(binding, var_name)
63+
64+
Enum.reduce map_key_path, accumulator, fn
65+
key, {:ok, map} when is_map(map) -> Map.fetch(map, key)
66+
_key, _acc -> :error
67+
end
68+
end
69+
4170
defp loop_state(opts) do
4271
env =
4372
if env = opts[:env] do

lib/iex/lib/iex/server.ex

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ defmodule IEx.Server do
4242
end
4343
end
4444

45+
@doc """
46+
Returns the PID of the IEx evaluator process if it exists.
47+
"""
48+
@spec evaluator :: pid | nil
49+
def evaluator() do
50+
case IEx.Server.local do
51+
nil -> nil
52+
pid ->
53+
{:dictionary, dictionary} = Process.info(pid, :dictionary)
54+
dictionary[:evaluator]
55+
end
56+
end
57+
4558
@doc """
4659
Returns the current session environment if a session exists.
4760
"""
@@ -144,7 +157,11 @@ defmodule IEx.Server do
144157
loop(run_state(opts), evaluator, Process.monitor(evaluator))
145158
end
146159

147-
defp start_evaluator(opts) do
160+
@doc """
161+
Starst an evaluator using the provided options.
162+
"""
163+
@spec start_evaluator(Keyword.t) :: pid
164+
def start_evaluator(opts) do
148165
self_pid = self()
149166
self_leader = Process.group_leader
150167
evaluator = opts[:evaluator] ||

lib/iex/mix.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ defmodule IEx.Mixfile do
1111
[registered: [IEx.Supervisor, IEx.Config],
1212
mod: {IEx.App, []},
1313
env: [
14-
autocomplete_server: IEx.Server,
1514
colors: [],
1615
inspect: [pretty: true],
1716
history_size: 20,

0 commit comments

Comments
 (0)