Skip to content

Commit 25614e0

Browse files
committed
Add struct field autocompletion to IEx
1 parent 4fc23d0 commit 25614e0

File tree

2 files changed

+71
-15
lines changed

2 files changed

+71
-15
lines changed

lib/iex/lib/iex/autocomplete.ex

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ defmodule IEx.Autocomplete do
5454
expand_dot_call(path, List.to_atom(hint), shell)
5555

5656
:expr ->
57-
expand_local_or_var("", shell)
57+
expand_struct_fields_or_local_or_var(code, "", shell)
5858

5959
{:local_or_var, local_or_var} ->
60-
expand_local_or_var(List.to_string(local_or_var), shell)
60+
expand_struct_fields_or_local_or_var(code, List.to_string(local_or_var), shell)
6161

6262
{:local_arity, local} ->
6363
expand_local(List.to_string(local), true, shell)
@@ -267,31 +267,71 @@ defmodule IEx.Autocomplete do
267267
end
268268
end
269269

270-
## Elixir modules
270+
## Structs
271271

272272
defp expand_structs(hint, shell) do
273273
aliases =
274274
for {alias, mod} <- aliases_from_env(shell),
275275
[name] = Module.split(alias),
276276
String.starts_with?(name, hint),
277-
has_struct?(mod),
277+
struct?(mod) and not function_exported?(mod, :exception, 1),
278278
do: %{kind: :struct, name: name}
279279

280280
modules =
281281
for "Elixir." <> name = full_name <- match_modules("Elixir." <> hint, true),
282282
String.starts_with?(name, hint),
283283
mod = String.to_atom(full_name),
284-
has_struct?(mod),
284+
struct?(mod) and not function_exported?(mod, :exception, 1),
285285
do: %{kind: :struct, name: name}
286286

287287
format_expansion(aliases ++ modules, hint)
288288
end
289289

290-
defp has_struct?(mod) do
291-
Code.ensure_loaded?(mod) and function_exported?(mod, :__struct__, 1) and
292-
not function_exported?(mod, :exception, 1)
290+
defp struct?(mod) do
291+
Code.ensure_loaded?(mod) and function_exported?(mod, :__struct__, 1)
292+
end
293+
294+
defp expand_struct_fields_or_local_or_var(code, hint, shell) do
295+
with {:ok, quoted} <- Code.Fragment.container_cursor_to_quoted(code),
296+
{aliases, pairs} <- find_struct_fields(quoted),
297+
{:ok, alias} <- value_from_alias(aliases, shell),
298+
true <- struct?(alias) do
299+
pairs =
300+
Enum.reduce(pairs, Map.from_struct(alias.__struct__), fn {key, _}, map ->
301+
Map.delete(map, key)
302+
end)
303+
304+
entries =
305+
for {key, _value} <- pairs,
306+
name = Atom.to_string(key),
307+
if(hint == "",
308+
do: not String.starts_with?(name, "_"),
309+
else: String.starts_with?(name, hint)
310+
),
311+
do: %{kind: :keyword, name: name}
312+
313+
format_expansion(entries, hint)
314+
else
315+
_ -> expand_local_or_var(hint, shell)
316+
end
317+
end
318+
319+
defp find_struct_fields(ast) do
320+
ast
321+
|> Macro.prewalker()
322+
|> Enum.find_value(fn node ->
323+
with {:%, _, [{:__aliases__, _, aliases}, {:%{}, _, pairs}]} <- node,
324+
{pairs, [{:__cursor__, _, []}]} <- Enum.split(pairs, -1),
325+
true <- Keyword.keyword?(pairs) do
326+
{aliases, pairs}
327+
else
328+
_ -> nil
329+
end
330+
end)
293331
end
294332

333+
## Aliases and modules
334+
295335
defp expand_aliases(all, shell) do
296336
case String.split(all, ".") do
297337
[hint] ->
@@ -309,20 +349,14 @@ defmodule IEx.Autocomplete do
309349
end
310350
end
311351

312-
defp value_from_alias([name | rest], shell) when is_binary(name) do
313-
name = String.to_atom(name)
314-
352+
defp value_from_alias([name | rest], shell) do
315353
case Keyword.fetch(aliases_from_env(shell), Module.concat(Elixir, name)) do
316354
{:ok, name} when rest == [] -> {:ok, name}
317355
{:ok, name} -> {:ok, Module.concat([name | rest])}
318356
:error -> {:ok, Module.concat([name | rest])}
319357
end
320358
end
321359

322-
defp value_from_alias([_ | _], _) do
323-
:error
324-
end
325-
326360
defp match_aliases(hint, shell) do
327361
for {alias, module} <- aliases_from_env(shell),
328362
[name] = Module.split(alias),
@@ -552,6 +586,10 @@ defmodule IEx.Autocomplete do
552586
["~#{name} (sigil_#{name})"]
553587
end
554588

589+
defp to_entries(%{kind: :keyword, name: name}) do
590+
["#{name}:"]
591+
end
592+
555593
defp to_entries(%{kind: _, name: name}) do
556594
[name]
557595
end
@@ -578,6 +616,10 @@ defmodule IEx.Autocomplete do
578616
format_hint(name, hint) <> "{"
579617
end
580618

619+
defp to_hint(%{kind: :keyword, name: name}, hint) do
620+
format_hint(name, hint) <> ": "
621+
end
622+
581623
defp to_hint(%{kind: _, name: name}, hint) do
582624
format_hint(name, hint)
583625
end

lib/iex/test/iex/autocomplete_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,20 @@ defmodule IEx.AutocompleteTest do
394394
end
395395

396396
test "completion for struct keys" do
397+
assert {:yes, '', entries} = expand('%URI{')
398+
assert 'path:' in entries
399+
assert 'query:' in entries
400+
401+
assert {:yes, '', entries} = expand('%URI{path: "foo",')
402+
assert 'path:' not in entries
403+
assert 'query:' in entries
404+
405+
assert {:yes, 'ry: ', []} = expand('%URI{path: "foo", que')
406+
assert {:no, [], []} = expand('%URI{path: "foo", unkno')
407+
assert {:no, [], []} = expand('%Unkown{path: "foo", unkno')
408+
end
409+
410+
test "completion for struct var keys" do
397411
eval("struct = %IEx.AutocompleteTest.MyStruct{}")
398412
assert expand('struct.my') == {:yes, '_val', []}
399413
end

0 commit comments

Comments
 (0)