Skip to content

Commit

Permalink
find elixir modules that require adding an alias
Browse files Browse the repository at this point in the history
  • Loading branch information
ajayvigneshk committed Sep 21, 2022
1 parent 85d4a87 commit 5e02409
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 39 deletions.
4 changes: 2 additions & 2 deletions lib/elixir_sense.ex
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ defmodule ElixirSense do
spec: "@spec insert_at(list, integer, any) :: list", summary: "Returns a list with `value` inserted at the specified `index`."}]
"""
@spec suggestions(String.t(), pos_integer, pos_integer) :: [Suggestion.suggestion()]
def suggestions(buffer, line, column) do
def suggestions(buffer, line, column, opts \\ []) do
hint = Source.prefix(buffer, line, column)
buffer_file_metadata = Parser.parse_string(buffer, true, true, line)
{text_before, text_after} = Source.split_at(buffer, line, column)
Expand All @@ -220,7 +220,7 @@ defmodule ElixirSense do
at_module_body?: Metadata.at_module_body?(buffer_file_metadata, env)
}

Suggestion.find(hint, env, buffer_file_metadata, cursor_context, module_store)
Suggestion.find(hint, env, buffer_file_metadata, cursor_context, module_store, opts)
end

@doc """
Expand Down
21 changes: 19 additions & 2 deletions lib/elixir_sense/providers/suggestion.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ defmodule ElixirSense.Providers.Suggestion do
overridable: &Reducers.Overridable.add_overridable/5,
param_options: &Reducers.Params.add_options/5,
typespecs: &Reducers.TypeSpecs.add_types/5,
populate_common: &Reducers.Common.populate/5,
populate_common: &Reducers.Common.populate/6,
variables: &Reducers.Common.add_variables/5,
modules: &Reducers.Common.add_modules/5,
functions: &Reducers.Common.add_functions/5,
Expand All @@ -99,12 +99,14 @@ defmodule ElixirSense.Providers.Suggestion do
bitstring_options: &Reducers.Bitstring.add_bitstring_options/5
]

@add_opts_for [:populate_common]

@doc """
Finds all suggestions for a hint based on context information.
"""
@spec find(String.t(), State.Env.t(), Metadata.t(), cursor_context, ModuleStore.t()) ::
[suggestion()]
def find(hint, env, buffer_metadata, cursor_context, module_store) do
def find(hint, env, buffer_metadata, cursor_context, module_store, opts \\ []) do
plugins = module_store.by_behaviour[ElixirSense.Plugin] || []

reducers =
Expand All @@ -114,6 +116,7 @@ defmodule ElixirSense.Providers.Suggestion do
{module, &module.reduce/5}
end)
|> Enum.concat(@reducers)
|> maybe_add_opts(opts)

context =
plugins
Expand All @@ -139,4 +142,18 @@ defmodule ElixirSense.Providers.Suggestion do
|> Enum.reduce(item, fn module, item -> module.decorate(item) end)
end
end

defp maybe_add_opts(reducers, opts) do
Enum.map(reducers, fn {name, reducer} ->
if name in @add_opts_for do
{name, reducer_with_opts(reducer, opts)}
else
{name, reducer}
end
end)
end

defp reducer_with_opts(fun, opts) do
fn a, b, c, d, e -> fun.(a, b, c, d, e, opts) end
end
end
132 changes: 103 additions & 29 deletions lib/elixir_sense/providers/suggestion/complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,19 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
behaviours: []
end

@spec complete(String.t(), Env.t()) ::
@spec complete(String.t(), Env.t(), list(keyword())) ::
[
ElixirSense.Providers.Suggestion.Reducers.Common.func()
| ElixirSense.Providers.Suggestion.Reducers.Common.mod()
| ElixirSense.Providers.Suggestion.Reducers.Common.variable()
| ElixirSense.Providers.Suggestion.Reducers.Common.attribute()
| ElixirSense.Providers.Suggestion.Reducers.Struct.field()
]
def complete(hint, %Env{} = env) do
do_expand(hint |> String.to_charlist(), env)
def complete(hint, %Env{} = env, opts \\ []) do
do_expand(hint |> String.to_charlist(), env, opts)
end

def do_expand(code, env) do
def do_expand(code, env, opts \\ []) do
# TODO remove when we require elixir 1.13
only_structs =
case code do
Expand All @@ -110,7 +110,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do

case NormalizedCode.CursorContext.cursor_context(code) do
{:alias, hint} when is_list(hint) ->
expand_aliases(List.to_string(hint), env, false)
expand_aliases(List.to_string(hint), env, false, opts)

{:alias, prefix, hint} ->
expand_prefixed_aliases(prefix, hint, env, false)
Expand All @@ -119,26 +119,26 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
expand_erlang_modules(List.to_string(unquoted_atom), env)

{:dot, path, hint} ->
expand_dot(path, List.to_string(hint), false, env, only_structs)
expand_dot(path, List.to_string(hint), false, env, only_structs, opts)

{:dot_arity, path, hint} ->
expand_dot(path, List.to_string(hint), true, env, only_structs)
expand_dot(path, List.to_string(hint), true, env, only_structs, opts)

{:dot_call, _path, _hint} ->
# no need to expand signatures here, we have signatures provider
# IEx calls
# expand_dot_call(path, List.to_atom(hint), env)
# to provide signatures and falls back to expand_local_or_var
expand_expr(env)
expand_expr(env, opts)

:expr ->
# IEx calls expand_local_or_var("", env)
# we choose to retun more and handle some special cases
# TODO expand_expr(env) after we require elixir 1.13
case code do
[?^] -> expand_var("", env)
[?%] -> expand_aliases("", env, true)
_ -> expand_expr(env)
[?%] -> expand_aliases("", env, true, opts)
_ -> expand_expr(env, opts)
end

{:local_or_var, local_or_var} ->
Expand All @@ -155,13 +155,13 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
# IEx calls
# expand_dot_call(path, List.to_atom(hint), env)
# to provide signatures and falls back to expand_local_or_var
expand_expr(env)
expand_expr(env, opts)

# elixir >= 1.13
{:operator, operator} ->
case operator do
[?^] -> expand_var("", env)
[?&] -> expand_expr(env)
[?&] -> expand_expr(env, opts)
_ -> expand_local(List.to_string(operator), false, env)
end

Expand All @@ -185,15 +185,15 @@ defmodule ElixirSense.Providers.Suggestion.Complete do

# elixir >= 1.13
{:struct, struct} when is_list(struct) ->
expand_aliases(List.to_string(struct), env, true)
expand_aliases(List.to_string(struct), env, true, opts)

# elixir >= 1.14
{:struct, {:alias, prefix, hint}} ->
expand_prefixed_aliases(prefix, hint, env, true)

# elixir >= 1.14
{:struct, {:dot, path, hint}} ->
expand_dot(path, List.to_string(hint), false, env, true)
expand_dot(path, List.to_string(hint), false, env, true, opts)

# elixir >= 1.14
{:struct, {:module_attribute, attribute}} ->
Expand All @@ -213,12 +213,12 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
end
end

defp expand_dot(path, hint, exact?, env, only_structs) do
defp expand_dot(path, hint, exact?, env, only_structs, opts) do
filter = struct_module_filter(only_structs, env)

case expand_dot_path(path, env) do
{:ok, {:atom, mod}} when hint == "" ->
expand_aliases(mod, "", [], not only_structs, env, filter)
expand_aliases(mod, "", [], not only_structs, env, filter, opts)

{:ok, {:atom, mod}} ->
expand_require(mod, hint, exact?, env)
Expand Down Expand Up @@ -315,10 +315,10 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
end
end

defp expand_expr(env) do
defp expand_expr(env, opts) do
local_or_var = expand_local_or_var("", env)
erlang_modules = expand_erlang_modules("", env)
elixir_modules = expand_aliases("", env, false)
elixir_modules = expand_aliases("", env, false, opts)
attributes = expand_attribute("", env)

local_or_var ++ erlang_modules ++ elixir_modules ++ attributes
Expand Down Expand Up @@ -461,35 +461,35 @@ defmodule ElixirSense.Providers.Suggestion.Complete do

## Elixir modules

defp expand_aliases(all, env, only_structs) do
defp expand_aliases(all, env, only_structs, opts) do
filter = struct_module_filter(only_structs, env)

case String.split(all, ".") do
[hint] ->
aliases = match_aliases(hint, env)
expand_aliases(Elixir, hint, aliases, false, env, filter)
expand_aliases(Elixir, hint, aliases, false, env, filter, opts)

parts ->
hint = List.last(parts)
list = Enum.take(parts, length(parts) - 1)

case value_from_alias(list, env) do
{:ok, alias} -> expand_aliases(alias, hint, [], false, env, filter)
{:ok, alias} -> expand_aliases(alias, hint, [], false, env, filter, opts)
:error -> no()
end
end
end

defp expand_aliases(mod, hint, aliases, include_funs, env, filter) do
defp expand_aliases(mod, hint, aliases, include_funs, env, filter, opts) do
aliases
|> Kernel.++(match_elixir_modules(mod, hint, env, filter))
|> Kernel.++(match_elixir_modules(mod, hint, env, filter, opts))
|> Kernel.++(if include_funs, do: match_module_funs(mod, hint, false, true, env), else: [])
|> format_expansion()
end

defp expand_prefixed_aliases({:local_or_var, '__MODULE__'}, hint, env, only_structs) do
if env.scope_module != nil and Introspection.elixir_module?(env.scope_module) do
expand_aliases("#{env.scope_module}.#{hint}", env, only_structs)
expand_aliases("#{env.scope_module}.#{hint}", env, only_structs, [])
else
no()
end
Expand All @@ -499,7 +499,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
case value_from_binding({:attribute, List.to_atom(attribute)}, env) do
{:ok, {:atom, atom}} ->
if Introspection.elixir_module?(atom) do
expand_aliases("#{atom}.#{hint}", env, only_structs)
expand_aliases("#{atom}.#{hint}", env, only_structs, [])
else
no()
end
Expand Down Expand Up @@ -528,7 +528,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
end
end

defp match_elixir_modules(module, hint, env, filter) do
defp match_elixir_modules(module, hint, env, filter, opts) do
name = Atom.to_string(module)
depth = length(String.split(name, ".")) + 1
base = name <> "." <> hint
Expand All @@ -542,13 +542,28 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
valid_alias_piece?("." <> name),
concatted = parts |> Enum.take(depth) |> Module.concat(),
filter.(concatted) do
{name, concatted}
{name, concatted, false}
end
|> Kernel.++(match_elixir_modules_that_require_alias(module, hint, env, filter, opts))
|> Enum.reject(fn
{_, concatted, true} ->
Enum.find(env.aliases, fn {_as, module} ->
concatted == module
end)

_rest ->
false
end)
|> Enum.uniq_by(&elem(&1, 1))
|> Enum.map(fn {name, module} ->
|> Enum.map(fn {name, module, required_alias?} ->
{desc, meta} = Introspection.get_module_docs_summary(module)
subtype = Introspection.get_module_subtype(module)
%{kind: :module, type: :elixir, name: name, desc: {desc, meta}, subtype: subtype}
result = %{kind: :module, type: :elixir, name: name, desc: {desc, meta}, subtype: subtype}

cond do
required_alias? -> Map.put(result, :required_alias, module)
true -> result
end
end)
end

Expand Down Expand Up @@ -586,6 +601,46 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
end
end

defp match_elixir_modules_that_require_alias(Elixir, hint, env, filter, opts) do
if Keyword.get(opts, :required_alias) do
for {suggestion, required_alias} <-
find_elixir_modules_that_require_alias(Elixir, hint, env),
mod_as_atom = required_alias |> String.to_atom(),
filter.(mod_as_atom),
required_alias_mod = required_alias |> String.split(".") |> Module.concat() do
{suggestion, required_alias_mod, true}
end
else
[]
end
end

defp match_elixir_modules_that_require_alias(_module, _hint, _env, _filter, _opts), do: []

def find_elixir_modules_that_require_alias(Elixir, hint, env) do
get_modules(true, env)
|> Enum.sort()
|> Enum.dedup()
|> Enum.reduce([], fn module, acc ->
module_parts = module |> String.split(".")

maybe_index =
Enum.find_index(module_parts, fn module_part -> Matcher.match?(module_part, hint) end)

case maybe_index do
nil ->
acc

index ->
required_alias = Enum.slice(module_parts, 0..index)
[suggestion | _] = Enum.reverse(required_alias)
required_alias = required_alias |> Module.concat() |> Atom.to_string()
[{suggestion, required_alias} | acc]
end
end)
|> Enum.filter(fn {suggestion, _required_alias} -> valid_alias_piece?("." <> suggestion) end)
end

defp match_modules(hint, root, env) do
hint_parts = hint |> String.split(".")
hint_parts_length = length(hint_parts)
Expand Down Expand Up @@ -894,6 +949,25 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
[%{type: :field, name: name, subtype: subtype, origin: origin, call?: true}]
end

defp to_entries(%{
kind: :module,
name: name,
required_alias: module,
desc: {desc, metadata},
subtype: subtype
}) do
[
%{
type: :module,
name: name,
required_alias: module,
subtype: subtype,
summary: desc,
metadata: metadata
}
]
end

defp to_entries(%{kind: :module, name: name, desc: {desc, metadata}, subtype: subtype}) do
[%{type: :module, name: name, subtype: subtype, summary: desc, metadata: metadata}]
end
Expand Down
10 changes: 6 additions & 4 deletions lib/elixir_sense/providers/suggestion/reducers/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do
* Variable fields
"""
def populate(hint, env, buffer_metadata, context, acc) do
def populate(hint, env, buffer_metadata, context, acc, opts \\ []) do
text_before = context.text_before

%Metadata{
Expand All @@ -76,7 +76,8 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do
metadata_specs,
metadata_types,
structs,
text_before
text_before,
opts
)

suggestions_by_type = Enum.group_by(suggestions, & &1.type)
Expand Down Expand Up @@ -159,7 +160,8 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do
metadata_specs,
metadata_types,
structs,
text_before
text_before,
opts
) do
env = %Complete.Env{
aliases: aliases,
Expand Down Expand Up @@ -194,6 +196,6 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do
hint
end

Complete.complete(hint, env)
Complete.complete(hint, env, opts)
end
end
Loading

0 comments on commit 5e02409

Please sign in to comment.