Packages

spark

2.2.9
2.7.2 2.7.1 2.7.0 2.6.1 2.6.0 2.5.0 2.4.1 2.4.0 2.3.14 2.3.13 2.3.12 2.3.11 2.3.10 2.3.9 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.69 2.2.68 2.2.67 2.2.66 2.2.65 2.2.64 2.2.63 2.2.62 2.2.61 2.2.60 2.2.59 2.2.58 2.2.57 2.2.56 2.2.55 2.2.54 2.2.53 2.2.52 2.2.51 2.2.50 2.2.49 2.2.48 2.2.47 2.2.46 2.2.45 2.2.44 2.2.43 2.2.42 2.2.41 2.2.40 2.2.39 2.2.38 2.2.37 2.2.36 2.2.35 2.2.34 2.2.33 2.2.32 2.2.31 2.2.30 2.2.29 2.2.28 2.2.27 2.2.26 2.2.25 2.2.24 2.2.23 2.2.22 2.2.21 2.2.20 2.2.19 2.2.18 2.2.17 2.2.16 2.2.15 2.2.14 2.2.13 2.2.12 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.24 2.1.23 2.1.22 2.1.21 2.1.20 2.1.19 2.1.18 2.1.17 2.1.16 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.1 2.0.0 1.1.55 1.1.54 1.1.53 1.1.52 1.1.51 1.1.50 1.1.49 1.1.48 1.1.47 1.1.46 1.1.45 1.1.44 1.1.43 1.1.42 1.1.41 1.1.40 1.1.39 1.1.38 1.1.37 1.1.36 1.1.35 1.1.34 1.1.32 1.1.31 1.1.30 1.1.29 1.1.28 1.1.27 1.1.26 1.1.25 1.1.24 1.1.22 1.1.21 1.1.20 1.1.19 1.1.18 1.1.17 1.1.16 1.1.15 1.1.14 retired 1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.4.12 0.4.11 0.4.10 0.4.9 0.4.8 0.4.7 0.4.6 0.4.5 0.4.4 0.4.3 0.4.2 0.4.1 0.3.12 0.3.11 0.3.10 0.3.9 0.3.8 0.3.7 0.3.6 0.3.5 0.3.4 0.3.3 0.3.2 0.3.1 0.3.0 0.2.18 0.2.17 0.2.16 0.2.15 0.2.14 0.2.13 0.2.12 0.2.11 0.2.10 0.2.9 0.2.8 0.2.7 0.2.6 0.2.5 0.2.4 0.2.3 0.2.2 0.2.1 0.2.0 0.1.29 0.1.28 0.1.27 0.1.26 0.1.25 0.1.24 0.1.23 0.1.22 0.1.21 0.1.20 0.1.19 0.1.18 0.1.17 0.1.16 0.1.15 0.1.14 0.1.13 0.1.12 0.1.11 0.1.10 0.1.9 0.1.8 0.1.7 0.1.6 0.1.5 0.1.4 0.1.3 0.1.2 0.1.1 0.1.0

Generic tooling for building DSLs

Current section

Files

Jump to
spark lib spark elixir_sense plugin.ex
Raw

lib/spark/elixir_sense/plugin.ex

defmodule Spark.ElixirSense.Plugin do
@moduledoc false
Module.register_attribute(__MODULE__, :is_elixir_sense_plugin, persist: true)
@is_elixir_sense_plugin true
@generic_reducer nil
@matcher ElixirSense.Providers.Suggestion.Matcher
case Code.ensure_compiled(ElixirSense.Plugin) do
{:module, _} ->
@behaviour ElixirSense.Plugin
@behaviour ElixirSense.Providers.Suggestion.GenericReducer
@generic_reducer ElixirSense.Providers.Suggestion.GenericReducer
_ ->
:ok
end
case Code.ensure_compiled(ElixirLS.LanguageServer.Plugin) do
{:module, _} ->
@behaviour ElixirLS.LanguageServer.Plugin
@behaviour ElixirLS.LanguageServer.Providers.Completion.GenericReducer
@generic_reducer ElixirLS.LanguageServer.Providers.Completion.GenericReducer
@matcher ElixirLS.Utils.Matcher
_ ->
:ok
end
if @generic_reducer do
def reduce(hint, env, buffer_metadata, cursor_context, acc) do
apply(@generic_reducer, :reduce, [
__MODULE__,
hint,
env,
buffer_metadata,
cursor_context,
acc
])
end
end
alias Spark.ElixirSense.Entity
alias Spark.ElixirSense.Types
def suggestions(hint, _, _chain, opts) do
case suggestions(hint, opts) do
:ignore ->
case func_call_chain(opts.cursor_context.text_before, opts.env) do
[%{candidate: {module, function_call}, npar: arg_index} = info | _] ->
autocomplete_spark_options(hint, opts, {module, function_call, arg_index, info}, opts)
_ ->
:ignore
end
suggestions ->
suggestions
end
rescue
_ ->
:ignore
end
def suggestions(hint, opts) do
is_spark_entity? = do_dsl?(opts.env) || fragment?(opts.env)
if is_spark_entity? do
case func_call_chain(opts.cursor_context.text_before, opts.env) do
[%{candidate: {Elixir, :use}, params: [mod | _]} = info | _] ->
{using, _} = get_mod(mod, opts.env)
if Code.ensure_loaded?(using) && Spark.implements_behaviour?(using, Spark.Dsl) do
opt_schema = using.opt_schema()
if opt_schema do
case autocomplete_schema(opt_schema, hint, info.value_type_path, opts) do
[] ->
:ignore
completions ->
{:override, Enum.uniq(completions)}
end
else
:ignore
end
else
:ignore
end
[%{candidate: {module, function_call}, npar: arg_index} = info | _] = chain ->
chain
|> Enum.split_while(fn
%{candidate: {Elixir, :defmodule}} -> false
_ -> true
end)
|> case do
{_, []} ->
autocomplete_spark_options(
hint,
opts,
{module, function_call, arg_index, info},
opts
)
{path_items, _} ->
scope_path =
path_items
|> Enum.map(fn %{candidate: {_, function_call}} -> function_call end)
|> Enum.reverse()
get_suggestions(hint, opts, scope_path, info.value_type_path, arg_index)
end
_ ->
:ignore
end
else
case func_call_chain(opts.cursor_context.text_before, opts.env) do
[%{candidate: {module, function_call}, npar: arg_index} = info | _] ->
autocomplete_spark_options(hint, opts, {module, function_call, arg_index, info}, opts)
_ ->
:ignore
end
end
rescue
_e ->
:ignore
end
defp autocomplete_schema(
schema,
hint,
[{:keyword_key, key, _rest_keyword} | value_type_path],
opts
) do
case Keyword.fetch(schema, key) do
:error ->
[]
{:ok, config} ->
option_values(key, config, hint, opts, value_type_path)
end
end
defp autocomplete_schema(schema, hint, [{:making_keyword_key, other_opts}], _opts) do
other_keys =
other_opts
|> Enum.reject(&cursor?/1)
|> Enum.map(&elem(&1, 0))
schema
|> Keyword.drop(other_keys)
|> Enum.filter(fn {key, _} ->
apply(@matcher, :match?, [to_string(key), hint])
end)
|> Enum.map(fn {key, config} ->
option_suggestions(key, config, :option)
end)
end
defp autocomplete_schema(schema, hint, [], _opts) do
schema
|> Enum.filter(fn {key, _} ->
apply(@matcher, :match?, [to_string(key), hint])
end)
|> Enum.map(fn {key, config} ->
option_suggestions(key, config, :option)
end)
end
defp autocomplete_schema(_schema, _hint, _value_type_path, _opts) do
[]
end
defp func_call_chain(code, env) do
case Code.Fragment.container_cursor_to_quoted(code, columns: true) do
{:ok, container} ->
container
|> cursor_path()
|> Enum.reverse()
|> rewrite_pipes()
|> collapse_do_blocks()
|> collapse_lists()
|> Enum.reverse()
|> to_partials()
|> path_to_mod_fun_arg_info(env)
|> Enum.reverse()
|> handle_dangling_do_blocks()
_ ->
[]
end
end
defp rewrite_pipes([{:|>, _, [params_1, {call, meta, params_rest}]}, _ | rest]) do
rewrite_pipes([{call, meta, [params_1 | params_rest || []]} | rest])
end
defp rewrite_pipes([first | rest]) do
[first | rewrite_pipes(rest)]
end
defp rewrite_pipes([]), do: []
defp handle_dangling_do_blocks([%{value_type_path: [{:keyword_key, :do, []}]} = info | rest]) do
[%{info | value_type_path: [:do_block]} | rest]
end
defp handle_dangling_do_blocks(other), do: other
defp collapse_do_blocks([first | rest]) do
case first do
{_func, _, args} ->
case List.last(args) do
[{:do, _block}] ->
[first | collapse_do_blocks(Enum.drop(rest, 2))]
_ ->
[first | collapse_do_blocks(rest)]
end
_ ->
[first, collapse_do_blocks(rest)]
end
end
defp collapse_do_blocks([]), do: []
defp collapse_lists([first, _second | rest]) when is_list(first) do
[first | collapse_lists(rest)]
end
defp collapse_lists([first | rest]), do: [first | collapse_lists(rest)]
defp collapse_lists([]), do: []
defp path_to_mod_fun_arg_info(path, env) do
path
|> Enum.reduce_while({[], []}, fn
{:__block__, _, _items}, {acc, stack} ->
{:cont, {acc, [:block | stack]}}
item, {acc, stack} ->
case to_mod_fun_arg_info(item, env) do
{:ok, item} ->
item = %{item | value_type_path: merge_do_and_blocks(stack ++ item.value_type_path)}
{:cont, {[item | acc], []}}
_ ->
case cursor_at_value_type_path(item) do
nil ->
{:cont, {acc, stack}}
path ->
{:cont, {acc, stack ++ path}}
end
end
end)
|> elem(0)
end
defp merge_do_and_blocks([]), do: []
defp merge_do_and_blocks([:block, {:keyword_key, :do, _} | rest]) do
merge_do_and_blocks([:do_block | rest])
end
defp merge_do_and_blocks([first | rest]) do
[first | merge_do_and_blocks(rest)]
end
defp to_partials(items, last \\ :___none___)
defp to_partials([item], :__none__), do: [item]
defp to_partials([item], last) do
[replace_with_cursor(item, last)]
end
defp to_partials([], _), do: []
defp to_partials([first, second | rest], :__none__) do
[replace_with_cursor(second, first) | to_partials(rest, second)]
end
defp to_partials([first | rest], last) do
[replace_with_cursor(first, last) | to_partials(rest, first)]
end
defp replace_with_cursor(inside, replace) do
Macro.prewalk(inside, fn
^replace ->
{:__cursor__, [], []}
other ->
other
end)
end
@excluded_funs [:__block__]
defp to_mod_fun_arg_info(item, binding_env) do
with {_, {:ok, call_info}} <-
Macro.prewalk(item, nil, &find_call_pre/2),
{{m, elixir_prefix}, f} when f not in @excluded_funs <-
get_mod_fun(
call_info.call,
binding_env
) do
{:ok,
%{
candidate: {m, f},
elixir_prefix: elixir_prefix,
params: call_info.params,
npar: call_info.npar,
pos: {{call_info.meta[:line], call_info.meta[:column]}, {call_info.meta[:line], nil}},
value_type_path: call_info[:value_type_path] || [],
cursor_at_option: call_info.cursor_at_option,
options_so_far: call_info.options,
option: call_info.option
}}
else
_other ->
:error
end
end
defp cursor_path(container) do
Macro.path(container, fn
{:__cursor__, _, _} ->
true
_ ->
false
end)
end
defp find_call_pre(ast, {:ok, call_info}),
do: {ast, {:ok, call_info}}
# transform `a |> b(c)` calls into `b(a, c)`
defp find_call_pre({:|>, _, [params_1, {call, meta, params_rest}]}, state) do
params = [params_1 | params_rest || []]
find_call_pre({call, meta, params}, state)
end
defp find_call_pre({{:., meta, call}, _, params} = ast, _state) when is_list(params) do
{ast, find_cursor_in_params(params, call, meta)}
end
defp find_call_pre({atom, meta, params} = ast, _state)
when is_atom(atom) and is_list(params) and atom not in [:{}, :%{}] do
{ast, find_cursor_in_params(params, atom, meta)}
end
defp find_call_pre({atom, meta, params} = ast, _state)
when is_atom(atom) and is_list(params) and atom not in [:{}, :%{}] do
{ast, find_cursor_in_params(params, atom, meta)}
end
defp find_call_pre(ast, state), do: {ast, state}
defp get_mod_fun(atom, _binding_env) when is_atom(atom), do: {{Elixir, atom}, atom}
defp get_mod_fun([{:__aliases__, _, list}, fun], binding_env) do
mod = get_mod(list, binding_env)
if mod do
{mod, fun}
end
end
defp get_mod_fun([{:__MODULE__, _, nil}, fun], binding_env) do
if binding_env.current_module not in [nil, Elixir] do
{{binding_env.current_module, false}, fun}
end
end
defp get_mod_fun([{:@, _, [{name, _, nil}]}, fun], binding_env) when is_atom(name) do
case apply(ElixirSense.Core.Binding, :expand, [binding_env, {:attribute, name}]) do
{:atom, atom} ->
{{atom, false}, fun}
_ ->
nil
end
end
defp get_mod_fun([{name, _, nil}, fun], binding_env) when is_atom(name) do
case apply(ElixirSense.Core.Binding, :expand, [binding_env, {:variable, name}]) do
{:atom, atom} ->
{{atom, false}, fun}
_ ->
nil
end
end
defp get_mod_fun([atom, fun], _binding_env) when is_atom(atom), do: {{atom, false}, fun}
defp get_mod_fun(_, _binding_env), do: nil
defp get_mod({:__aliases__, _, list}, binding_env) do
get_mod(list, binding_env)
end
defp get_mod([{:__aliases__, _, list} | _rest], binding_env) do
get_mod(list, binding_env)
end
defp get_mod([{:__MODULE__, _, nil} | rest], binding_env) do
if binding_env.current_module not in [nil, Elixir] do
mod =
binding_env.current_module
|> Module.split()
|> Kernel.++(rest)
|> Module.concat()
{mod, false}
end
end
defp get_mod([{:@, _, [{name, _, nil}]} | rest], binding_env) when is_atom(name) do
case apply(ElixirSense.Core.Binding, :expand, [binding_env, {:attribute, name}]) do
{:atom, atom} ->
if apply(ElixirSense.Core.Introspection, :elixir_module?, [atom]) do
mod =
atom
|> Module.split()
|> Kernel.++(rest)
|> Module.concat()
{mod, false}
else
nil
end
_ ->
nil
end
end
defp get_mod([head | _rest] = list, _binding_env) when is_atom(head) do
{Module.concat(list), head == Elixir}
end
defp get_mod(_list, _binding_env), do: nil
defp find_cursor_in_params(params, call, meta) do
params
|> Enum.with_index()
|> Enum.find_value(fn {param, index} ->
case cursor_at_value_type_path(param) do
nil ->
nil
path ->
{:ok,
%{
call: call,
params: params,
npar: index,
meta: meta,
value_type_path: path,
options: nil,
cursor_at_option: false,
option: nil
}}
end
end)
end
defp cursor?({:__cursor__, _, _}), do: true
defp cursor?(_), do: false
defp cursor_at_value_type_path([{:do, _, block}]) do
Enum.find_value(List.wrap(block), fn item ->
case cursor_at_value_type_path(item) do
nil ->
nil
path ->
[:do_block | path]
end
end)
end
defp cursor_at_value_type_path([{:__cursor__, _, _}]) do
[{:list_index, 0, []}]
end
defp cursor_at_value_type_path(other) when is_list(other) do
if Keyword.keyword?(Enum.reject(other, &cursor?/1)) do
Enum.find_value(other, fn
{:__cursor__, _, _} = cursor ->
[
{:making_keyword_key, Enum.reject(other, &(&1 == cursor))}
]
{key, value} ->
case cursor_at_value_type_path(value) do
nil ->
nil
path ->
[{:keyword_key, key, Keyword.delete(other, key)} | path]
end
end)
else
other
|> Enum.with_index()
|> Enum.find_value(fn {value, index} ->
case cursor_at_value_type_path(value) do
nil ->
nil
path ->
[{:list_index, index} | path]
end
end)
|> case do
[{:list_index, index} | path] ->
[{:list_index, index, List.delete_at(path, index)} | path]
nil ->
nil
end
end
end
defp cursor_at_value_type_path({a, b}) do
cursor_at_value_type_path({:{}, [], [a, b]})
end
defp cursor_at_value_type_path({:{}, _, elems}) do
elems
|> Enum.with_index()
|> Enum.find_value(fn
{{:__cursor__, _, _}, index} ->
[{:making_tuple_elem, index, List.delete_at(elems, index)}]
{value, index} ->
case cursor_at_value_type_path(value) do
nil ->
nil
path ->
[{:tuple_index, index, List.delete_at(elems, index)} | path]
end
end)
end
defp cursor_at_value_type_path({:%{}, _, keys}) do
Enum.find_value(keys, fn
{:__cursor__, _, _} = cursor ->
[{:making_map_key, Enum.reject(keys, &(&1 == cursor))}]
{key, value} ->
case cursor_at_value_type_path(value) do
nil ->
nil
path ->
[
{:map_key, key, Enum.reject(keys, fn {other_key, _val} -> other_key == key end)}
| path
]
end
end)
end
defp cursor_at_value_type_path({:__cursor__, _, []}), do: []
defp cursor_at_value_type_path(_), do: nil
defp autocomplete_spark_options(hint, _, {module, function_call, arg_index, info}, opts) do
case Code.fetch_docs(module) do
{:docs_v1, _a, :elixir, _b, _c, _d, functions} ->
schema =
Enum.find_value(functions, fn
{{_, ^function_call, _}, _, _, _, %{spark_opts: spark_opts}} ->
Enum.find_value(spark_opts, fn {index, schema} ->
if index == arg_index do
schema
end
end)
_ ->
nil
end)
if schema do
{:override, Enum.uniq(autocomplete_schema(schema, hint, info.value_type_path, opts))}
else
:ignore
end
_ ->
:ignore
end
end
defp get_suggestions(hint, opts, scope_path, value_type_path, arg_index) do
dsl_mod = get_dsl_mod(opts)
{type, _value_type_path} =
case value_type_path do
[:do_block | rest] ->
{:builder, rest}
_ ->
{:option, value_type_path}
end
if dsl_mod do
extension_kinds =
List.flatten(dsl_mod.module_info()[:attributes][:spark_extension_kinds] || [])
extensions =
default_extensions(dsl_mod)
|> Enum.concat(parse_extensions(opts, extension_kinds))
|> Enum.flat_map(fn extension ->
[extension | extension.add_extensions()]
end)
case get_constructors(extensions, scope_path, hint, arg_index) do
[] ->
:ignore
constructors ->
suggestions =
Enum.flat_map(constructors, fn
{:value, key, config} ->
List.wrap(option_values(key, config, hint, opts, value_type_path))
{key, config} ->
List.wrap(option_suggestions(key, config, type))
%{__struct__: Spark.Dsl.Entity} = entity ->
List.wrap(entity_suggestions(entity))
%{__struct__: Spark.Dsl.Section} = section ->
List.wrap(section_suggestions(section))
end)
|> filter_matches(hint)
{:override, Enum.uniq(List.flatten(suggestions))}
end
else
:ignore
end
end
defp filter_matches(hints, match) do
if match do
Enum.filter(hints, fn
%{label: label} ->
apply(@matcher, :match?, [label, match])
%{name: name} ->
apply(@matcher, :match?, [name, match])
end)
else
hints
end
end
defp do_dsl?(env) do
Enum.any?(env.attributes, &(&1.name == :spark_is)) ||
Enum.any?(env.requires, fn module ->
try do
module.module_info(:attributes)
|> Enum.any?(fn
{:spark_dsl, [true]} ->
true
_ ->
false
end)
rescue
_ -> false
end
end)
end
defp fragment?(env) do
Enum.any?(env.attributes, &(&1.name == :spark_fragment_of)) ||
Spark.Dsl.Fragment in env.requires
end
defp get_dsl_mod(opts) do
if Spark.Dsl.Fragment in opts.env.requires do
parse_fragment(opts)
else
Enum.find(opts.env.requires, &spark_extension?/1)
end
end
defp parse_fragment(opts) do
case Regex.named_captures(
~r/of:\s+?(?<of>|([^\s]*))?[\,\s$]/,
opts.cursor_context.text_before
) do
%{"of" => dsl} when dsl != "" ->
module = Module.concat([dsl])
if Code.ensure_loaded?(module) do
module
end
_ ->
nil
end
rescue
_ -> false
end
defp section_suggestions(section) do
%{
type: :generic,
kind: :function,
label: to_string(section.name),
snippet: """
#{section.name} do
#{snippet_or_default(section.snippet, "$0")}
end
""",
detail: "Dsl Section",
documentation: Map.get(section, :docs) || ""
}
end
defp entity_suggestions(entity) do
%{
type: :generic,
kind: :function,
label: to_string(entity.name),
snippet: snippet_or_default(entity.snippet, "#{entity.name} #{args(entity)}"),
detail: "Dsl Entity",
documentation: Map.get(entity, :docs) || ""
}
end
defp option_suggestions(key, config, type) do
snippet = snippet_or_default(config[:snippet], default_snippet(config))
snippet =
if type == :option do
"#{key}: #{snippet}"
else
"#{key} #{snippet}"
end
config = Spark.Options.update_key_docs(config)
%{
type: :generic,
kind: :function,
label: to_string(key),
snippet: snippet,
detail: "Option",
documentation: config[:doc]
}
end
defp option_values(key, config, hint, opts, value_type_path) do
case config[:type] do
:boolean ->
enum_suggestion([true, false], hint, "boolean")
{:one_of, values} ->
enum_suggestion(values, hint, to_string(key))
{:in, values} ->
enum_suggestion(values, hint, to_string(key))
{:spark_type, module, func} ->
Types.find_builtin_types(module, func, hint, opts.cursor_context) ++
Types.find_custom_types(module, func, hint, opts.module_store)
{:spark_type, module, func, templates} ->
Types.find_builtin_types(module, func, hint, opts.cursor_context, templates) ++
Types.find_custom_types(module, func, hint, opts.module_store)
{:spark, type} ->
Entity.find_entities(type, hint)
{:spark_behaviour, behaviour, builtins} ->
Entity.find_spark_behaviour_impls(behaviour, builtins, hint, opts.module_store)
{:spark_function_behaviour, behaviour, builtins, _} ->
Entity.find_spark_behaviour_impls(behaviour, builtins, hint, opts.module_store)
{:behaviour, behaviour} ->
Entity.find_behaviour_impls(behaviour, hint, opts.module_store)
{:spark_behaviour, behaviour} ->
Entity.find_spark_behaviour_impls(behaviour, nil, hint, opts.module_store)
{:spark_function_behaviour, behaviour, _} ->
Entity.find_spark_behaviour_impls(behaviour, nil, hint, opts.module_store)
{:or, subtypes} ->
Enum.flat_map(subtypes, fn subtype ->
option_values(key, Keyword.put(config, :type, subtype), hint, opts, value_type_path)
end)
{:list, subtype} ->
case value_type_path do
[{:list_index, _, _} | value_type_path] ->
option_values(key, Keyword.put(config, :type, subtype), hint, opts, value_type_path)
_ ->
[
%{
type: :generic,
kind: :class,
snippet: inspect("[:$0]"),
insert_text: "",
detail: "list",
documentation: "[]"
}
]
end
{:wrap_list, subtype} ->
case value_type_path do
[{:list_index, _, _} | value_type_path] ->
option_values(key, Keyword.put(config, :type, subtype), hint, opts, value_type_path)
_ ->
[
%{
type: :generic,
kind: :class,
snippet: inspect("[:$0]"),
insert_text: "",
detail: "list",
documentation: "[]"
}
| option_values(
key,
Keyword.put(config, :type, subtype),
hint,
opts,
value_type_path
)
]
end
_ ->
[]
end
end
defp enum_suggestion(values, hint, label) do
Enum.flat_map(values, fn v ->
inspected = inspect(v)
if apply(@matcher, :match?, [inspected, hint]) do
[
%{
type: :generic,
kind: :type_parameter,
label: label,
insert_text: inspect(v),
snippet: inspect(v),
documentation: inspect(v),
priority: 0
}
]
else
[]
end
end)
end
defp default_snippet(config) do
cond do
config[:type] == :boolean && config[:default] in [true, false] ->
"#{to_string(!config[:default])}"
config[:type] == :string ->
"\"$0\""
match?({:list, _}, config[:type]) ->
{_, inner_type} = config[:type]
"[#{default_snippet(type: inner_type)}]"
match?({:wrap_list, _}, config[:type]) ->
{_, inner_type} = config[:type]
default_snippet(type: inner_type)
match?({in_type, _} when in_type in [:in, :one_of], config[:type]) ->
{_, list} = config[:type]
if is_nil(config[:default]) do
{_, list} = config[:type]
inspect(Enum.at(list, 0))
else
list |> Enum.reject(&(&1 == config[:default])) |> Enum.at(0) |> inspect()
end
match?({:tagged_tuple, _, _}, config[:type]) ->
{:tagged_tuple, tag, inner_type} = config[:type]
"{:#{tag}, #{default_snippet(inner_type)}}"
config[:type] == :atom ->
":$0"
match?({:mfa_or_fun, _}, config[:type]) ->
{:mfa_or_fun, arity} = config[:type]
default_snippet(type: {:or, [{:fun, arity}, :mfa]})
match?({:literal, _}, config[:type]) ->
{:literal, value} = config[:type]
inspect(value)
match?({:fun, 0}, config[:type]) ->
"""
fn ->
${0:body}
end
"""
match?({:fun, _}, config[:type]) ->
{:fun, arity} = config[:type]
args = Enum.map_join(0..(arity - 1), ", ", &"arg#{&1 + 1}")
"""
fn #{args} ->
${#{arity}:body}
end
"""
match?(:map, config[:type]) || match?({:map, _, _}, config[:type]) ->
"%{${1:key} => ${2:value}}"
match?(:keyword_list, config[:type]) ->
"[${1:key} => ${2:value}]"
config[:type] == :mfa ->
"{${1:module}, :${2:function}, [${3:args}]}"
config[:type] == :keyword_list ->
"[$0]"
true ->
"$0"
end
end
defp args(entity) do
case Spark.Dsl.Entity.arg_names(entity) do
[] ->
""
args ->
args
|> Enum.with_index()
|> Enum.map(fn {arg, index} ->
"${#{index + 1}:#{arg}}"
end)
|> Enum.intersperse(", ")
|> Enum.join()
end
end
defp get_constructors(extensions, [], hint, arg_index) do
Enum.flat_map(
extensions,
fn extension ->
try do
top_level_constructors =
extension
|> sections()
|> Enum.filter(& &1.top_level?)
|> Enum.flat_map(fn section ->
do_find_constructors(section, [], hint, arg_index)
end)
extension.sections()
|> apply_dsl_patches(extensions)
|> Enum.filter(fn section ->
!section.top_level? &&
apply(@matcher, :match?, [to_string(section.name), hint])
end)
|> Enum.concat(top_level_constructors)
rescue
_e ->
[]
end
end
)
end
defp get_constructors(extensions, [first | rest], hint, arg_index) do
extensions
|> Enum.flat_map(&sections/1)
|> Enum.filter(& &1.top_level?)
|> Enum.find(fn section ->
Enum.any?(section.sections, &(&1.name == first)) ||
Enum.any?(section.entities, &(&1.name == first))
end)
|> case do
nil ->
Enum.flat_map(
extensions,
fn extension ->
try do
extension
|> sections()
|> apply_dsl_patches(extensions)
|> Enum.flat_map(fn section ->
if section.name == first do
do_find_constructors(section, rest, hint, arg_index)
else
[]
end
end)
rescue
_e ->
[]
end
end
)
top_level_section ->
do_find_constructors(top_level_section, [first | rest], hint, arg_index)
end
end
defp apply_dsl_patches(sections_or_entities, extensions, path \\ [])
defp apply_dsl_patches(sections_or_entities, extensions, path)
when is_list(sections_or_entities) do
Enum.map(sections_or_entities, &apply_dsl_patches(&1, extensions, path))
end
defp apply_dsl_patches(%Spark.Dsl.Entity{} = entity, _, _), do: entity
defp apply_dsl_patches(%Spark.Dsl.Section{} = section, extensions, path) do
section_path_matcher = path ++ [section.name]
new_entities =
Enum.flat_map(extensions, fn extension ->
extension
|> dsl_patches()
|> Enum.flat_map(fn
%Spark.Dsl.Patch.AddEntity{section_path: ^section_path_matcher, entity: entity} ->
[entity]
_ ->
[]
end)
end)
%{
section
| sections:
Enum.map(section.sections, &apply_dsl_patches(&1, extensions, path ++ [section.name])),
entities: section.entities ++ new_entities
}
end
defp do_find_constructors(entity_or_section, path, hint, arg_index, recursives \\ [])
defp do_find_constructors(
%{__struct__: Spark.Dsl.Entity} = entity,
[],
hint,
arg_index,
recursives
) do
case argument_or_option(entity, arg_index) do
{:argument, key, config} ->
[{:value, key, config}]
:option ->
find_opt_hints(entity, hint) ++
find_entity_hints(entity, hint, recursives)
{:both, key, config} ->
[{:value, key, config}] ++
find_opt_hints(entity, hint) ++
find_entity_hints(entity, hint, recursives)
end
end
defp do_find_constructors(section, [], hint, _arg_index, recursives) do
find_opt_hints(section, hint) ++
find_entity_hints(section, hint, []) ++
Enum.filter(section.sections, fn section ->
apply(@matcher, :match?, [to_string(section.name), hint])
end) ++ recursives
end
defp do_find_constructors(
%{__struct__: Spark.Dsl.Entity} = entity,
[next | rest],
hint,
arg_index,
recursives
) do
entity.entities
|> Kernel.||([])
|> Enum.flat_map(&elem(&1, 1))
|> Enum.concat(recursives)
|> Enum.concat(List.wrap(recursive_for(entity)))
|> Enum.filter(&(&1.name == next))
|> Enum.flat_map(
&do_find_constructors(
&1,
rest,
hint,
arg_index,
Enum.uniq(recursives ++ List.wrap(recursive_for(entity)))
)
)
|> Enum.uniq()
end
defp do_find_constructors(section, [first | rest], hint, arg_index, recursives) do
Enum.flat_map(section.entities, fn entity ->
if entity.name == first do
do_find_constructors(entity, rest, hint, arg_index)
else
[]
end
end) ++
Enum.flat_map(section.sections, fn section ->
if section.name == first do
do_find_constructors(section, rest, hint, arg_index)
else
[]
end
end) ++ recursives
end
defp argument_or_option(entity, arg_index) do
case Enum.at(entity.args || [], arg_index) do
nil ->
:option
{:optional, key} ->
{:both, key, entity.schema[key]}
{:optional, key, _value} ->
{:both, key, entity.schema[key]}
key when is_atom(key) ->
{:argument, key, entity.schema[key]}
end
end
defp recursive_for(entity) do
if entity.recursive_as do
entity
end
end
defp find_opt_hints(%{__struct__: Spark.Dsl.Entity} = entity, hint) do
entity.schema
|> Enum.reject(&(elem(&1, 0) in entity.args))
|> Enum.flat_map(fn {key, value} ->
if apply(@matcher, :match?, [to_string(key), hint]) do
arg_index = Enum.find_index(Spark.Dsl.Entity.arg_names(entity), &(&1 == key))
if arg_index do
[{key, Keyword.put(value, :arg_index, arg_index)}]
else
[{key, value}]
end
else
[]
end
end)
end
defp find_opt_hints(section, hint) do
Enum.filter(section.schema, fn {key, _value} ->
apply(@matcher, :match?, [to_string(key), hint])
end)
end
defp find_entity_hints(%{__struct__: Spark.Dsl.Entity} = entity, hint, recursives) do
entity.entities
|> Keyword.values()
|> Enum.concat(recursives)
|> Enum.concat(List.wrap(recursive_for(entity)))
|> List.flatten()
|> Enum.filter(fn entity ->
apply(@matcher, :match?, [to_string(entity.name), hint])
end)
end
defp find_entity_hints(section, hint, _recursives) do
Enum.filter(section.entities, fn entity ->
apply(@matcher, :match?, [to_string(entity.name), hint])
end)
end
defp spark_extension?(module) do
true in List.wrap(module.module_info()[:attributes][:spark_dsl])
rescue
_ -> false
end
defp default_extensions(dsl_mod) do
dsl_mod.module_info()[:attributes][:spark_default_extensions]
|> List.wrap()
|> List.flatten()
end
defp parse_extensions(opts, extension_kinds) do
Enum.flat_map([:extensions | extension_kinds], fn extension_kind ->
case Regex.named_captures(
~r/#{extension_kind}:\s+?(?<extensions>(\[[^\]]*\])|([^\s]*))?[\,\s$]/,
opts.cursor_context.text_before
) do
%{"extensions" => extensions} when extensions != "" ->
extensions
|> String.replace("[", "")
|> String.replace("]", "")
|> String.split(",")
|> Enum.map(&String.trim/1)
_ ->
[]
end
end)
|> Enum.uniq()
|> Enum.map(&Module.concat([&1]))
|> Enum.filter(fn module ->
try do
Code.ensure_loaded?(module)
rescue
_ ->
false
end
end)
end
defp sections(extension) do
extension.sections()
rescue
_ ->
[]
end
defp dsl_patches(extension) do
extension.dsl_patches()
rescue
_ ->
[]
end
defp snippet_or_default(nil, default), do: default
defp snippet_or_default("", default), do: default
defp snippet_or_default(snippet, _default), do: snippet
end