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+?(?|([^\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(§ions/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+?(?(\[[^\]]*\])|([^\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