defmodule Alchemist.Completer do
|
|
@moduledoc false
|
|
|
|
def run(exp) do
|
|
code = case is_bitstring(exp) do
|
|
true -> exp |> String.to_char_list
|
|
_ -> exp
|
|
end
|
|
|
|
{status, result, list } = expand(code |> Enum.reverse)
|
|
|
|
case { status, result, list } do
|
|
{ :no, _, _ } -> ''
|
|
{ :yes, [], _ } -> List.insert_at(list, 0, exp)
|
|
{ :yes, _, _ } -> run(code ++ result)
|
|
end
|
|
end
|
|
|
|
def expand('') do
|
|
expand_import("")
|
|
end
|
|
|
|
def expand([h|t]=expr) do
|
|
cond do
|
|
h === ?. and t != []->
|
|
expand_dot(reduce(t))
|
|
h === ?: ->
|
|
expand_erlang_modules()
|
|
identifier?(h) ->
|
|
expand_expr(reduce(expr))
|
|
(h == ?/) and t != [] and identifier?(hd(t)) ->
|
|
expand_expr(reduce(t))
|
|
h in '([{' ->
|
|
expand('')
|
|
true ->
|
|
no()
|
|
end
|
|
end
|
|
|
|
defp identifier?(h) do
|
|
(h in ?a..?z) or (h in ?A..?Z) or (h in ?0..?9) or h in [?_, ??, ?!]
|
|
end
|
|
|
|
defp expand_dot(expr) do
|
|
case Code.string_to_quoted expr do
|
|
{:ok, atom} when is_atom(atom) ->
|
|
expand_call(atom, "")
|
|
{:ok, {:__aliases__, _, list}} ->
|
|
expand_elixir_modules(list, "")
|
|
_ ->
|
|
no()
|
|
end
|
|
end
|
|
|
|
defp expand_expr(expr) do
|
|
case Code.string_to_quoted expr 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))
|
|
{:ok, {:__aliases__, _, [root]}} ->
|
|
expand_elixir_modules([], Atom.to_string(root))
|
|
{:ok, {:__aliases__, _, [h|_] = list}} when is_atom(h) ->
|
|
hint = Atom.to_string(List.last(list))
|
|
list = Enum.take(list, length(list) - 1)
|
|
expand_elixir_modules(list, hint)
|
|
{:ok, {{:., _, [mod, fun]}, _, []}} when is_atom(fun) ->
|
|
expand_call(mod, Atom.to_string(fun))
|
|
_ ->
|
|
no()
|
|
end
|
|
end
|
|
|
|
defp reduce(expr) do
|
|
Enum.reverse Enum.reduce ' ([{', expr, fn token, acc ->
|
|
hd(:string.tokens(acc, [token]))
|
|
end
|
|
end
|
|
|
|
defp yes(hint, entries) do
|
|
{:yes, String.to_char_list(hint), Enum.map(entries, &String.to_char_list/1)}
|
|
end
|
|
|
|
defp no do
|
|
{:no, '', []}
|
|
end
|
|
|
|
## Formatting
|
|
|
|
defp format_expansion([], _) do
|
|
no()
|
|
end
|
|
|
|
defp format_expansion([uniq], hint) do
|
|
case to_hint(uniq, hint) do
|
|
"" -> yes("", to_uniq_entries(uniq))
|
|
hint -> yes(hint, [])
|
|
end
|
|
end
|
|
|
|
defp format_expansion([first|_]=entries, hint) do
|
|
binary = Enum.map(entries, &(&1.name))
|
|
length = byte_size(hint)
|
|
prefix = :binary.longest_common_prefix(binary)
|
|
if prefix in [0, length] do
|
|
yes("", Enum.flat_map(entries, &to_entries/1))
|
|
else
|
|
yes(:binary.part(first.name, prefix, length-prefix), [])
|
|
end
|
|
end
|
|
|
|
## Expand calls
|
|
|
|
# :atom.fun
|
|
defp expand_call(mod, hint) when is_atom(mod) do
|
|
expand_require(mod, hint)
|
|
end
|
|
|
|
# Elixir.fun
|
|
defp expand_call({:__aliases__, _, list}, hint) do
|
|
expand_alias(list)
|
|
|> normalize_module
|
|
|> expand_require(hint)
|
|
end
|
|
|
|
defp expand_call(_, _) do
|
|
no()
|
|
end
|
|
|
|
defp expand_require(mod, hint) 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
|
|
end
|
|
|
|
## Erlang modules
|
|
|
|
defp expand_erlang_modules(hint \\ "") do
|
|
format_expansion match_erlang_modules(hint), hint
|
|
end
|
|
|
|
defp match_erlang_modules(hint) do
|
|
for mod <- match_modules(hint, true) do
|
|
%{kind: :module, name: mod, type: :erlang}
|
|
end
|
|
end
|
|
|
|
## Elixir modules
|
|
|
|
defp expand_elixir_modules([], hint) do
|
|
expand_elixir_modules(Elixir, hint, match_aliases(hint))
|
|
end
|
|
|
|
defp expand_elixir_modules(list, hint) do
|
|
expand_alias(list)
|
|
|> normalize_module
|
|
|> expand_elixir_modules(hint, [])
|
|
end
|
|
|
|
defp expand_elixir_modules(mod, hint, aliases) do
|
|
aliases
|
|
|> Kernel.++(match_elixir_modules(mod, hint))
|
|
|> Kernel.++(match_module_funs(mod, hint))
|
|
|> format_expansion(hint)
|
|
end
|
|
|
|
defp expand_alias([name | rest] = list) do
|
|
module = Module.concat(Elixir, name)
|
|
Enum.find_value env_aliases(), list, fn {alias, mod} ->
|
|
if alias === module do
|
|
case Atom.to_string(mod) do
|
|
"Elixir." <> mod ->
|
|
Module.concat [mod|rest]
|
|
_ ->
|
|
mod
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp env_aliases() do
|
|
:ets.lookup(:alchemist, "aliases")
|
|
|> format_ets_aliases
|
|
end
|
|
|
|
defp format_ets_aliases([{"aliases", []}]) do
|
|
[]
|
|
end
|
|
|
|
defp format_ets_aliases(list) do
|
|
list
|
|
|> List.first
|
|
|> Tuple.to_list
|
|
|> List.last
|
|
end
|
|
|
|
defp match_aliases(hint) do
|
|
for {alias, _mod} <- env_aliases(),
|
|
[name] = Module.split(alias),
|
|
starts_with?(name, hint) do
|
|
%{kind: :module, type: :alias, name: name}
|
|
end
|
|
end
|
|
|
|
defp match_elixir_modules(module, hint) do
|
|
name = Atom.to_string(module)
|
|
depth = length(String.split(name, ".")) + 1
|
|
base = name <> "." <> hint
|
|
|
|
for mod <- match_modules(base, module === Elixir),
|
|
parts = String.split(mod, "."),
|
|
depth <= length(parts) do
|
|
%{kind: :module, type: :elixir, name: Enum.at(parts, depth-1)}
|
|
end
|
|
|> Enum.uniq
|
|
end
|
|
|
|
## Helpers
|
|
|
|
defp normalize_module(mod) do
|
|
if is_list(mod) do
|
|
Module.concat(mod)
|
|
else
|
|
mod
|
|
end
|
|
end
|
|
|
|
defp match_modules(hint, root) do
|
|
get_modules(root)
|
|
|> :lists.usort()
|
|
|> Enum.drop_while(& not starts_with?(&1, hint))
|
|
|> Enum.take_while(& starts_with?(&1, hint))
|
|
end
|
|
|
|
defp get_modules(true) do
|
|
["Elixir.Elixir"] ++ get_modules(false)
|
|
end
|
|
|
|
defp get_modules(false) do
|
|
modules = Enum.map(:code.all_loaded(), &Atom.to_string(elem(&1, 0)))
|
|
case :code.get_mode() do
|
|
:interactive -> modules ++ get_modules_from_applications()
|
|
_otherwise -> modules
|
|
end
|
|
end
|
|
|
|
defp get_modules_from_applications do
|
|
for [app] <- loaded_applications(),
|
|
{:ok, modules} = :application.get_key(app, :modules),
|
|
module <- modules do
|
|
Atom.to_string(module)
|
|
end
|
|
end
|
|
|
|
defp loaded_applications do
|
|
# If we invoke :application.loaded_applications/0,
|
|
# it can error if we don't call safe_fixtable before.
|
|
# Since in both cases we are reaching over the
|
|
# application controller internals, we choose to match
|
|
# for performance.
|
|
:ets.match(:ac_tab, {{:loaded, :"$1"}, :_})
|
|
end
|
|
|
|
defp match_module_funs(mod, hint) do
|
|
case ensure_loaded(mod) do
|
|
{:module, _} ->
|
|
falist = get_module_funs(mod)
|
|
|
|
list = Enum.reduce falist, [], fn {f, a}, acc ->
|
|
case :lists.keyfind(f, 1, acc) do
|
|
{f, aa} -> :lists.keyreplace(f, 1, acc, {f, [a|aa]})
|
|
false -> [{f, [a]}|acc]
|
|
end
|
|
end
|
|
|
|
for {fun, arities} <- list,
|
|
name = Atom.to_string(fun),
|
|
starts_with?(name, hint) do
|
|
%{kind: :function, name: name, arities: arities}
|
|
end |> :lists.sort()
|
|
|
|
_otherwise -> []
|
|
end
|
|
end
|
|
|
|
defp get_module_funs(mod) do
|
|
if function_exported?(mod, :__info__, 1) do
|
|
if docs = Code.get_docs(mod, :docs) do
|
|
for {tuple, _line, _kind, _sign, doc} <- docs, doc != false, do: tuple
|
|
else
|
|
mod.__info__(:macros) ++ (mod.__info__(:functions) -- [__info__: 1])
|
|
end
|
|
else
|
|
mod.module_info(:exports)
|
|
end
|
|
end
|
|
|
|
defp ensure_loaded(Elixir), do: {:error, :nofile}
|
|
defp ensure_loaded(mod),
|
|
do: Code.ensure_compiled(mod)
|
|
|
|
defp starts_with?(_string, ""), do: true
|
|
defp starts_with?(string, hint), do: String.starts_with?(string, hint)
|
|
|
|
## Ad-hoc conversions
|
|
|
|
defp to_entries(%{kind: :module, name: name}) do
|
|
[name]
|
|
end
|
|
|
|
defp to_entries(%{kind: :function, name: name, arities: arities}) do
|
|
for a <- :lists.sort(arities), do: "#{name}/#{a}"
|
|
end
|
|
|
|
defp to_uniq_entries(%{kind: :module}) do
|
|
[]
|
|
end
|
|
|
|
defp to_uniq_entries(%{kind: :function} = fun) do
|
|
to_entries(fun)
|
|
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 format_hint(name, hint) do
|
|
hint_size = byte_size(hint)
|
|
:binary.part(name, hint_size, byte_size(name) - hint_size)
|
|
end
|
|
end
|