Skip to content
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
54 changes: 27 additions & 27 deletions lib/iex/lib/iex/autocomplete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ defmodule IEx.Autocomplete do

def expand(expr, server \\ IEx.Server)

def expand('', _server) do
expand_import("")
def expand('', server) do
expand_variable_or_import("", server)
end

def expand([h | t]=expr, server) do
Expand Down Expand Up @@ -46,7 +46,7 @@ defmodule IEx.Autocomplete do
{:ok, atom} when is_atom(atom) ->
expand_erlang_modules(Atom.to_string(atom))
{:ok, {atom, _, nil}} when is_atom(atom) ->
expand_import(Atom.to_string(atom))
expand_variable_or_import(Atom.to_string(atom), server)
{:ok, {:__aliases__, _, [root]}} ->
expand_elixir_modules([], Atom.to_string(root), server)
{:ok, {:__aliases__, _, [h | _] = list}} when is_atom(h) ->
Expand Down Expand Up @@ -144,11 +144,14 @@ defmodule IEx.Autocomplete do
format_expansion match_module_funs(mod, hint), hint
end

defp expand_import(hint) do
funs = match_module_funs(IEx.Helpers, hint) ++
match_module_funs(Kernel, hint) ++
match_module_funs(Kernel.SpecialForms, hint)
format_expansion funs, hint
defp expand_variable_or_import(hint, server) do
Enum.concat([
match_variables(server, hint),
match_module_funs(IEx.Helpers, hint),
match_module_funs(Kernel, hint),
match_module_funs(Kernel.SpecialForms, hint),
])
|> format_expansion(hint)
end

## Erlang modules
Expand Down Expand Up @@ -286,6 +289,16 @@ defmodule IEx.Autocomplete do
end
end

defp match_variables(server, hint) do
with evaluator when is_pid(evaluator) <- server.evaluator() do
IEx.Evaluator.variables_from_binding(evaluator, hint)
|> Stream.map(&%{kind: :variable, name: &1})
|> Enum.sort_by(&(&1.name))
else
_ -> []
end
end

defp match_map_fields(map, hint) do
for {key, value} <- map,
is_atom(key),
Expand Down Expand Up @@ -333,47 +346,34 @@ defmodule IEx.Autocomplete do

## Ad-hoc conversions

defp to_entries(%{kind: :module, name: name}) do
defp to_entries(%{kind: kind, name: name}) when
kind in [:map_key, :module, :variable] do
[name]
end

defp to_entries(%{kind: :function, name: name, arities: arities}) do
for a <- :lists.sort(arities), do: "#{name}/#{a}"
end

defp to_entries(%{kind: :map_key, name: name}) do
[name]
end

defp to_uniq_entries(%{kind: :module}) do
defp to_uniq_entries(%{kind: kind}) when
kind in [:map_key, :module, :variable] do
[]
end

defp to_uniq_entries(%{kind: :function} = fun) do
to_entries(fun)
end

defp to_uniq_entries(%{kind: :map_key}) do
[]
end

defp to_hint(%{kind: :module, name: name}, hint) when name == hint do
format_hint(name, name) <> "."
end

defp to_hint(%{kind: :module, name: name}, hint) do
format_hint(name, hint)
end

defp to_hint(%{kind: :function, name: name}, hint) do
format_hint(name, hint)
end

defp to_hint(%{kind: :map_key, name: name, value_is_map: true}, hint) when name == hint do
format_hint(name, hint) <> "."
end

defp to_hint(%{kind: :map_key, name: name}, hint) do
defp to_hint(%{kind: kind, name: name}, hint) when
kind in [:function, :map_key, :module, :variable] do
format_hint(name, hint)
end

Expand Down
28 changes: 28 additions & 0 deletions lib/iex/lib/iex/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ defmodule IEx.Evaluator do
end
end

@doc """
Gets a list of variables out of the binding that match the passed
variable prefix.
"""
@spec variables_from_binding(pid, String.t) :: [String.t]
def variables_from_binding(evaluator, variable_prefix) do
ref = make_ref()
send evaluator, {:variables_from_binding, ref, self(), variable_prefix}

receive do
{^ref, result} -> result
after
5000 -> []
end
end

@doc """
Returns the current session environment if a session exists.
"""
Expand Down Expand Up @@ -68,6 +84,10 @@ defmodule IEx.Evaluator do
value = traverse_binding(state.binding, var_name, map_key_path)
send receiver, {ref, value}
loop(server, history, state)
{:variables_from_binding, ref, receiver, var_prefix} ->
value = find_matched_variables(state.binding, var_prefix)
send receiver, {ref, value}
loop(server, history, state)
{:done, ^server} ->
:ok
end
Expand All @@ -82,6 +102,14 @@ defmodule IEx.Evaluator do
end
end

defp find_matched_variables(binding, var_prefix) do
for {var_name, _value} <- binding,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that variable names in bindings are atoms, they are not always atoms in bindings because of hygiene, and then use Atom.to_string/1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know -- I had no idea! Is it worth adding test coverage for this since it's a simple mistake to make? What's an example of an expression that would result in a variable name not being an atom?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any expression that defines a variable inside a quote:

iex(1)> Code.eval_quoted (quote do var = 1 end), []
{1, [{{:var, Elixir}, 1}]}

One suggestion is to define a macro inside the test module and invoke that macro on @tag previous_line: "Mod.macro".

is_atom(var_name),
var_name = Atom.to_string(var_name),
String.starts_with?(var_name, var_prefix),
do: var_name
end

defp loop_state(opts) do
env =
if env = opts[:env] do
Expand Down
16 changes: 16 additions & 0 deletions lib/iex/test/iex/autocomplete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ defmodule IEx.AutocompleteTest do
assert expand('put_') == {:yes, '', ['put_elem/3', 'put_in/2', 'put_in/3']}
end

@tag previous_line: "numeral = 3; number = 3; nothing = nil"
test "variable name completion" do
assert expand('numb') == {:yes, 'er', []}
assert expand('num') == {:yes, '', ['number', 'numeral']}
assert expand('no') == {:yes, '', ['nothing', 'not/1', 'node/0', 'node/1']}
end

defmacro define_var do
quote do: var!(my_var_1, Elixir) = 1
end

@tag previous_line: "require #{__MODULE__}; #{__MODULE__}.define_var(); my_var_2 = 2"
test "ignores quoted variables when performing variable completion" do
assert expand('my_var') == {:yes, '_2', []}
end

test "kernel special form completion" do
assert expand('unquote_spl') == {:yes, 'icing', []}
end
Expand Down