if Code.ensure_loaded?(ElixirSense.Plugin) do defmodule Spark.ElixirSense.Plugin do @moduledoc false @behaviour ElixirSense.Plugin use ElixirSense.Providers.Suggestion.GenericReducer alias ElixirSense.Providers.Suggestion.Matcher alias Spark.ElixirSense.Entity alias Spark.ElixirSense.Types 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 == :spark_is)), dsl_mod when not is_nil(dsl_mod) <- Enum.find(opts.env.requires, &spark_extension?/1) do extension_kinds = List.flatten(dsl_mod.module_info[:attributes][:spark_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__: Spark.Dsl.Entity} = entity -> entity_suggestions(entity) %{__struct__: Spark.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__: Spark.Dsl.Entity, name: ^option} -> true _ -> false end) end defp find_option(constructors, {:value, option}) do Enum.find_value(constructors, fn {^option, _} = opt -> opt %{__struct__: Spark.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(?