if Code.ensure_loaded?(ElixirSense.Plugin) do
defmodule Ash.ElixirSense.Plugin do
@moduledoc false
@behaviour ElixirSense.Plugin
use ElixirSense.Providers.Suggestion.GenericReducer
alias Ash.ElixirSense.Resource
alias Ash.ElixirSense.Types
alias ElixirSense.Providers.Suggestion.Matcher
def suggestions(hint, {_, function_call, arg_index, info}, _chain, opts) do
opts = add_module_store(opts)
option = info.option || get_option(opts.cursor_context.text_before)
if option do
get_suggestions(hint, opts, [function_call], {:value, option})
else
get_suggestions(hint, opts, [function_call], {:arg, arg_index})
end
end
def suggestions(hint, opts) do
opts = add_module_store(opts)
option = get_section_option(opts.cursor_context.text_before)
if option do
get_suggestions(hint, opts, [], {:value, option})
else
get_suggestions(hint, opts)
end
end
# For some reason, the module store does not change when modules are defined
# so we are building our own fresh copy here. This is definitely a performance
# hit
defp add_module_store(opts) do
Map.put(opts, :module_store, ElixirSense.Core.ModuleStore.build(all_loaded()))
end
defp all_loaded do
:code.all_loaded()
|> Enum.filter(fn
{mod, _} when is_atom(mod) -> true
_ -> false
end)
|> Enum.map(&elem(&1, 0))
end
def get_suggestions(hint, opts, opt_path \\ [], type \\ nil) do
with true <- Enum.any?(opts.env.attributes, &(&1.name == :ash_is)),
dsl_mod when not is_nil(dsl_mod) <-
Enum.find(opts.env.requires, &ash_extension?/1) do
extension_kinds =
List.flatten(dsl_mod.module_info[:attributes][:ash_extension_kinds] || [])
extensions = default_extensions(dsl_mod) ++ parse_extensions(opts, extension_kinds)
scopes_to_lines =
Enum.reduce(opts.buffer_metadata.lines_to_env, %{}, fn {line, env}, acc ->
line = line - 1
Map.update(acc, env.scope_id, line, fn existing ->
if existing < line do
existing
else
line
end
end)
end)
scope_path = get_scope_path(opts, scopes_to_lines, nil, opt_path)
case get_constructors(extensions, scope_path, hint, type) do
[] ->
:ignore
constructors ->
suggestions =
case find_option(constructors, type) do
{key, config} ->
option_values(key, config, hint, opts)
_ ->
# Check for an edge case where we are editing the first argument of a constructor
with {:value, option} <- type,
entity when not is_nil(entity) <-
find_building_entity(constructors, option),
[arg | _] <- entity.args,
config when not is_nil(config) <- entity.schema[arg] do
option_values(arg, config, hint, opts)
else
_ ->
hint =
case type do
{:value, val} when not is_nil(val) ->
to_string(val)
_ ->
nil
end
Enum.map(constructors, fn
{key, config} ->
option_suggestions(key, config, type)
%{__struct__: Ash.Dsl.Entity} = entity ->
entity_suggestions(entity)
%{__struct__: Ash.Dsl.Section} = section ->
section_suggestions(section)
end)
|> filter_matches(hint)
end
end
{:override, List.flatten(suggestions)}
end
else
_ ->
:ignore
end
end
defp filter_matches(hints, match) do
if match do
Enum.filter(hints, fn %{label: label} ->
Matcher.match?(label, match)
end)
else
hints
end
end
defp find_building_entity(constructors, option) do
Enum.find(constructors, fn
%{__struct__: Ash.Dsl.Entity, name: ^option} ->
true
_ ->
false
end)
end
defp find_option(constructors, {:value, option}) do
Enum.find_value(constructors, fn
{^option, _} = opt ->
opt
%{__struct__: Ash.Dsl.Entity, name: ^option, args: [arg | _], schema: schema} ->
{arg, schema[arg]}
_ ->
false
end)
end
defp find_option(constructors, {:arg, arg_index}) do
Enum.find(constructors, fn
{_option, config} ->
config[:arg_index] == arg_index
_ ->
false
end)
end
defp find_option(_, _), do: nil
defp section_suggestions(section) do
snippet = Map.get(section, :snippet)
snippet =
if snippet && snippet != "" do
snippet
else
"$0"
end
%{
type: :generic,
kind: :function,
label: to_string(section.name),
snippet: "#{section.name} do\n #{snippet}\nend",
detail: "Dsl Section",
documentation: Map.get(section, :docs) || ""
}
end
defp entity_suggestions(entity) do
snippet = Map.get(entity, :snippet)
snippet =
if snippet && snippet != "" do
snippet
else
"$0"
end
%{
type: :generic,
kind: :function,
label: to_string(entity.name),
snippet: "#{entity.name} #{args(entity)}do\n #{snippet}\nend",
detail: "Dsl Entity",
documentation: Map.get(entity, :docs) || ""
}
end
defp option_suggestions(key, config, type) do
snippet =
if config[:snippet] && config[:snippet] != "" do
config[:snippet]
else
default_snippet(config)
end
snippet =
case type do
:option ->
"#{key}: #{snippet}"
{:arg, _} ->
"#{key}: #{snippet}"
_ ->
"#{key} #{snippet}"
end
%{
type: :generic,
kind: :function,
label: to_string(key),
snippet: snippet,
detail: "Option",
documentation: config[:doc]
}
end
defp get_option(text) when is_binary(text) do
case Regex.named_captures(~r/\s(?