diff --git a/lib/elixir_sense.ex b/lib/elixir_sense.ex index eb05ffbd..82fc937c 100644 --- a/lib/elixir_sense.ex +++ b/lib/elixir_sense.ex @@ -152,8 +152,7 @@ defmodule ElixirSense do ...> end ...> ''' iex> ElixirSense.suggestions(code, 3, 12) - [%{type: :hint, value: "MyList.insert_at"}, - %{origin: "List", type: :function, args: "list, index, value", arity: 3, name: "insert_at", metadata: %{}, + [%{origin: "List", type: :function, args: "list, index, value", arity: 3, name: "insert_at", metadata: %{}, 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()] @@ -164,15 +163,7 @@ defmodule ElixirSense do env = Metadata.get_env(buffer_file_metadata, line) - Suggestion.find( - hint, - env, - buffer_file_metadata.structs, - buffer_file_metadata.mods_funs_to_positions, - buffer_file_metadata.types, - buffer_file_metadata.specs, - text_before - ) + Suggestion.find(hint, text_before, env, buffer_file_metadata) end @doc """ diff --git a/lib/elixir_sense/providers/suggestion.ex b/lib/elixir_sense/providers/suggestion.ex index b14373ed..11f6c290 100644 --- a/lib/elixir_sense/providers/suggestion.ex +++ b/lib/elixir_sense/providers/suggestion.ex @@ -1,794 +1,96 @@ defmodule ElixirSense.Providers.Suggestion do @moduledoc """ - Provider responsible for finding suggestions for auto-completing - """ + Provider responsible for finding suggestions for auto-completing. - alias ElixirSense.Core.Introspection - alias ElixirSense.Core.Source - alias ElixirSense.Core.State - alias ElixirSense.Core.Struct - alias ElixirSense.Core.TypeInfo - alias ElixirSense.Providers.Suggestion.Complete - alias ElixirSense.Providers.Suggestion.Hint + It provides suggestions based on a list of pre-defined reducers. - @type attribute :: %{ - type: :attribute, - name: String.t() - } + ## Reducers - @type variable :: %{ - type: :variable, - name: String.t() - } + A reducer is a function with the following spec: - @type field :: %{ - type: :field, - subtype: :struct_field | :map_key, - name: String.t(), - origin: String.t() | nil, - call?: boolean - } + @spec reducer( + String.t(), + String.t(), + State.Env.t(), + Metadata.t(), + acc() + ) :: {:cont | :halt, acc()} - @type return :: %{ - type: :return, - description: String.t(), - spec: String.t(), - snippet: String.t() - } + ## Examples - @type callback :: %{ - type: :callback, - name: String.t(), - arity: non_neg_integer, - args: String.t(), - origin: String.t(), - summary: String.t(), - spec: String.t(), - metadata: map - } + Adding suggestions: - @type protocol_function :: %{ - type: :protocol_function, - name: String.t(), - arity: non_neg_integer, - args: String.t(), - origin: String.t(), - summary: String.t(), - spec: String.t(), - metadata: map - } + def my_reducer(hint, prefix, env, buffer_metadata, acc) do + suggestions = ... + {:cont, %{acc | result: acc.result ++ suggestions}} + end - @type func :: %{ - type: :function | :macro, - name: String.t(), - arity: non_neg_integer, - args: String.t(), - origin: String.t(), - summary: String.t(), - spec: String.t(), - metadata: map - } + Defining the only set of suggestions to be provided: - @type mod :: %{ - type: :module, - name: String.t(), - subtype: String.t(), - summary: String.t(), - metadata: map - } + def my_reducer(hint, prefix, env, buffer_metadata, acc) do + suggestions = ... + {:halt, %{acc | result: suggestions}} + end - @type param_option :: %{ - type: :param_option, - name: String.t(), - origin: String.t(), - type_spec: String.t(), - doc: String.t(), - expanded_spec: String.t() - } + Defining a list of suggestions to be provided and allow an extra + limited set of additional reducers to run next: - @type type_spec :: %{ - type: :type_spec, - name: String.t(), - arity: non_neg_integer, - origin: String.t(), - spec: String.t(), - doc: String.t(), - signature: String.t(), - metadata: map - } + def my_reducer(hint, prefix, env, buffer_metadata, acc) do + suggestions = ... + {:cont, %{acc | result: fields, reducers: [:populate_common, :variables]}} + end + """ - @type hint :: %{ - type: :hint, - value: String.t() - } + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State + alias ElixirSense.Providers.Suggestion.Reducers @type suggestion :: - attribute - | variable - | field - | return - | callback - | protocol_function - | func - | mod - | hint - | param_option - | type_spec + Reducers.Common.attribute() + | Reducers.Common.variable() + | Reducers.Struct.field() + | Reducers.Returns.return() + | Reducers.Callbacks.callback() + | Reducers.Protocol.protocol_function() + | Reducers.Common.func() + | Reducers.Common.mod() + | Reducers.Params.param_option() + | Reducers.TypeSpecs.type_spec() + + @type acc :: %{result: [suggestion], reducers: [atom], context: map} + + @reducers [ + structs_fields: &Reducers.Struct.add_fields/5, + returns: &Reducers.Returns.add_returns/5, + callbacks: &Reducers.Callbacks.add_callbacks/5, + protocol_functions: &Reducers.Protocol.add_functions/5, + param_options: &Reducers.Params.add_options/5, + typespecs: &Reducers.TypeSpecs.add_types/5, + populate_common: &Reducers.Common.populate/5, + variables: &Reducers.Common.add_variables/5, + modules: &Reducers.Common.add_modules/5, + functions: &Reducers.Common.add_functions/5, + macros: &Reducers.Common.add_macros/5, + variable_fields: &Reducers.Common.add_fields/5, + attributes: &Reducers.Common.add_attributes/5 + ] @doc """ Finds all suggestions for a hint based on context information. """ - @spec find( - String.t(), - State.Env.t(), - State.structs_t(), - State.mods_funs_to_positions_t(), - State.types_t(), - State.specs_t(), - String.t() - ) :: [suggestion] - def find( - hint, - %State.Env{} = env, - structs, - mods_and_funs, - metadata_types, - metadata_specs, - text_before - ) do - case find_struct_fields( - hint, - text_before, - env, - structs, - mods_and_funs, - metadata_types - ) do - {[], _} -> - find_all_except_struct_fields( - hint, - env, - mods_and_funs, - metadata_types, - metadata_specs, - structs, - text_before - ) - - {fields, nil} -> - Hint.get(hint, fields) - - {fields, :maybe_struct_update} -> - mods_funcs_suggestions = - find_hint_mods_funcs( - hint, - env, - mods_and_funs, - metadata_specs, - structs, - text_before - ) - - Hint.combine(mods_funcs_suggestions, fields) - end - end - - @spec find_all_except_struct_fields( - String.t(), - State.Env.t(), - State.mods_funs_to_positions_t(), - State.types_t(), - State.specs_t(), - State.structs_t(), - String.t() - ) :: [suggestion] - defp find_all_except_struct_fields( - hint, - %State.Env{ - imports: imports, - aliases: aliases, - behaviours: behaviours, - scope: scope, - module: module, - protocol: protocol - } = env, - mods_and_funs, - metadata_types, - metadata_specs, - structs, - text_before - ) do - mods_and_funcs = - find_hint_mods_funcs( - hint, - env, - mods_and_funs, - metadata_specs, - structs, - text_before - ) - - callbacks_or_returns = - case scope do - {_f, _a} -> - find_returns(behaviours, protocol, metadata_specs, hint, module, scope) - - _mod -> - find_callbacks(behaviours, protocol, hint) ++ find_protocol_functions(protocol, hint) - end - - param_options = - find_param_options( - text_before, - hint, - imports, - aliases, - module, - mods_and_funs, - metadata_types - ) - - typespecs = find_typespecs(hint, aliases, module, scope, mods_and_funs, metadata_types) - - Hint.combine(mods_and_funcs, param_options ++ typespecs ++ callbacks_or_returns) - end - - defp expand_current_module(:__MODULE__, current_module), do: current_module - defp expand_current_module(module, _current_module), do: module - - @spec find_struct_fields( - String.t(), - String.t(), - State.Env.t(), - State.structs_t(), - State.mods_funs_to_positions_t(), - State.types_t() - ) :: {[suggestion], nil | :maybe_struct_update} - defp find_struct_fields( - hint, - text_before, - %State.Env{ - module: module, - vars: vars, - attributes: attributes - } = env, - structs, - mods_funs, - metadata_types - ) do - case Source.which_struct(text_before, module) do - {{:attribute, attr}, fields_so_far, _elixir_prefix, _var} = struct -> - mod = - case Enum.find(attributes, fn %_{name: name} -> name == attr end) do - %_{type: {:atom, mod}} -> mod - _ -> nil - end - - result = - get_fields( - env, - mods_funs, - metadata_types, - structs, - hint, - struct |> put_elem(0, mod) - ) - - {result, if(fields_so_far == [], do: :maybe_struct_update)} - - {mod, fields_so_far, _elixir_prefix, _var} = struct when is_atom(mod) and mod != :_ -> - result = - get_fields( - env, - mods_funs, - metadata_types, - structs, - hint, - struct - ) - - {result, if(fields_so_far == [], do: :maybe_struct_update)} - - {:_, fields_so_far, false, _var} when is_list(fields_so_far) -> - result = - [:__struct__] - |> Kernel.--(fields_so_far) - |> Enum.filter(fn field -> String.starts_with?("#{field}", hint) end) - |> Enum.map(fn field -> - %{ - type: :field, - subtype: :struct_field, - name: Atom.to_string(field), - origin: nil, - call?: false - } - end) - - {result, if(fields_so_far == [], do: :maybe_struct_update)} - - {:map, fields_so_far, var_or_attr} when not is_nil(var_or_attr) -> - result = - case var_or_attr do - {:variable, var} -> - get_fields_from_var_or_attr(vars, structs, var, fields_so_far, hint) - - {:attribute, attr} -> - get_fields_from_var_or_attr(attributes, structs, attr, fields_so_far, hint) - end - - {result, if(fields_so_far == [], do: :maybe_struct_update)} - - _ -> - {[], nil} - end - end - - defp get_fields( - %State.Env{ - imports: imports, - aliases: aliases, - module: module, - vars: vars, - attributes: attributes - }, - mods_funs, - metadata_types, - structs, - hint, - {mod, fields_so_far, elixir_prefix, var_or_attr} - ) do - with {actual_mod, _, true} <- - Introspection.actual_mod_fun( - {expand_current_module(mod, module), nil}, - imports, - if(elixir_prefix, do: [], else: aliases), - module, - mods_funs, - metadata_types - ), - true <- Struct.is_struct(actual_mod, structs) do - fields = Struct.get_fields(actual_mod, structs) - - fields - |> Kernel.--(fields_so_far) - |> Enum.filter(fn field -> String.starts_with?("#{field}", hint) end) - |> Enum.map(fn field -> - %{ - type: :field, - subtype: :struct_field, - name: Atom.to_string(field), - call?: false, - origin: inspect(actual_mod) - } - end) - else - _ -> - case var_or_attr do - nil -> - [] - - {:variable, var} -> - get_fields_from_var_or_attr(vars, structs, var, fields_so_far, hint) - - {:attribute, attr} -> - get_fields_from_var_or_attr(attributes, structs, attr, fields_so_far, hint) - end - end - end - - defp get_fields_from_var_or_attr(vars, structs, var, fields_so_far, hint) do - case Enum.find(vars, fn %_{name: name} -> name == var end) do - %_{type: {:map, fields}} -> - for {field, _} <- fields, - field not in fields_so_far, - String.starts_with?("#{field}", hint) do - %{ - type: :field, - subtype: :map_key, - name: Atom.to_string(field), - origin: nil, - call?: false - } - end - - %_{type: {:struct, fields, nil}} -> - for {field, _} <- fields |> Keyword.put_new(:__struct__, nil), - field not in fields_so_far, - String.starts_with?("#{field}", hint) do - %{ - type: :field, - subtype: :struct_field, - name: Atom.to_string(field), - origin: nil, - call?: false - } - end - - %_{type: {:struct, _fields, module}} -> - if Struct.is_struct(module, structs) do - for field <- Struct.get_fields(module, structs), - field not in fields_so_far, - String.starts_with?("#{field}", hint) do - %{ - type: :field, - subtype: :struct_field, - name: Atom.to_string(field), - origin: inspect(module), - call?: false - } - end + @spec find(String.t(), String.t(), State.Env.t(), Metadata.t()) :: [suggestion()] + def find(hint, text_before, env, buffer_metadata) do + acc = %{result: [], reducers: Keyword.keys(@reducers), context: %{}} + + %{result: result} = + Enum.reduce_while(@reducers, acc, fn {key, fun}, acc -> + if key in acc.reducers do + fun.(hint, text_before, env, buffer_metadata, acc) else - [] + {:cont, acc} end + end) - _otherwise -> - [] - end - end - - @spec find_hint_mods_funcs( - String.t(), - State.Env.t(), - State.mods_funs_to_positions_t(), - State.specs_t(), - State.structs_t(), - String.t() - ) :: - %{ - hint: hint, - suggestions: [mod | func | field | variable | attribute] - } - defp find_hint_mods_funcs( - hint, - %State.Env{ - imports: imports, - aliases: aliases, - module: module, - vars: vars, - attributes: attributes, - scope: scope - }, - mods_and_funs, - metadata_specs, - structs, - text_before - ) do - env = %Complete.Env{ - aliases: aliases, - vars: vars, - attributes: attributes, - scope_module: module, - imports: imports, - mods_and_funs: mods_and_funs, - specs: metadata_specs, - structs: structs, - scope: scope - } - - {hint, prefix} = - case Source.get_v12_module_prefix(text_before, module) do - nil -> - {hint, ""} - - module_string -> - # v1.2 alias syntax detected - # prepend module prefix before running completion - prefix = module_string <> "." - {prefix <> hint, prefix} - end - - {hint, module_special_form_replaced} = - if String.starts_with?(hint, "__MODULE__") do - {hint |> String.replace_leading("__MODULE__", inspect(module)), true} - else - {hint, false} - end - - {%{type: :hint, value: prefixed_value}, suggestions} = Complete.complete(hint, env) - - prefixed_value = - if module_special_form_replaced do - prefixed_value |> String.replace_leading(inspect(module), "__MODULE__") - else - prefixed_value - end - - # drop module prefix from hint if added - value = - if prefix != "" do - prefixed_value |> String.replace_leading(prefix, "") - else - prefixed_value - end - - %{hint: %{type: :hint, value: value}, suggestions: suggestions} - end - - @spec find_returns( - [module], - nil | State.protocol_t(), - State.specs_t(), - String.t(), - module | nil, - State.scope() - ) :: [return] - defp find_returns(behaviours, protocol, specs, "", current_module, {fun, arity}) do - spec_returns = - case specs[{current_module, fun, arity}] do - nil -> - [] - - %State.SpecInfo{specs: info_specs} -> - for spec <- info_specs, - {:ok, {:@, _, [{_, _, [quoted]}]}} = Code.string_to_quoted(spec), - return <- Introspection.get_returns_from_spec_ast(quoted) do - format_return(return) - end - end - - callbacks = - for mod <- behaviours, - protocol == nil or mod != elem(protocol, 0), - Introspection.define_callback?(mod, fun, arity), - return <- Introspection.get_returns_from_callback(mod, fun, arity) do - format_return(return) - end - - protocol_functions = - case protocol do - {proto, _implementations} -> - if Introspection.define_callback?(proto, fun, arity) do - for return <- Introspection.get_returns_from_callback(proto, fun, arity) do - format_return(return) - end - else - [] - end - - nil -> - [] - end - - callbacks ++ protocol_functions ++ spec_returns - end - - defp find_returns(_behaviours, _protocol, _specs, _hint, _current_module, _scope) do - [] - end - - defp format_return(return) do - %{ - type: :return, - description: return.description, - spec: return.spec, - snippet: return.snippet - } - end - - @spec find_callbacks([module], nil | State.protocol_t(), String.t()) :: [callback] - defp find_callbacks(behaviours, protocol, hint) do - behaviours - |> Enum.flat_map(fn - mod when is_atom(mod) and (protocol == nil or mod != elem(protocol, 0)) -> - mod_name = inspect(mod) - - for %{ - name: name, - arity: arity, - callback: spec, - signature: signature, - doc: doc, - metadata: metadata - } <- - Introspection.get_callbacks_with_docs(mod), - hint == "" or String.starts_with?("#{name}", hint) do - desc = Introspection.extract_summary_from_docs(doc) - [_, args_str] = Regex.run(Regex.recompile!(~r/.\((.*)\)/), signature) - args = args_str |> String.replace(Regex.recompile!(~r/\s/), "") - - %{ - type: :callback, - name: Atom.to_string(name), - arity: arity, - args: args, - origin: mod_name, - summary: desc, - spec: spec, - metadata: metadata - } - end - - _ -> - [] - end) - |> Enum.sort() - end - - @spec find_protocol_functions(nil | State.protocol_t(), String.t()) :: [protocol_function] - defp find_protocol_functions(nil, _hint), do: [] - - defp find_protocol_functions({protocol, _implementations}, hint) do - for {{name, arity}, {_type, args, docs, metadata, spec}} <- - Introspection.module_functions_info(protocol), - hint == "" or String.starts_with?("#{name}", hint) do - %{ - type: :protocol_function, - name: Atom.to_string(name), - arity: arity, - args: args, - origin: inspect(protocol), - summary: docs, - metadata: metadata, - spec: spec - } - end - |> Enum.sort() - end - - @spec find_param_options( - String.t(), - String.t(), - [module], - [{module, module}], - module, - State.mods_funs_to_positions_t(), - State.types_t() - ) :: - [ - param_option - ] - defp find_param_options(prefix, hint, imports, aliases, module, mods_funs, metadata_types) do - with %{ - candidate: {mod, fun}, - elixir_prefix: elixir_prefix, - npar: npar, - pipe_before: _pipe_before - } <- - Source.which_func(prefix, module), - {mod, fun, true} <- - Introspection.actual_mod_fun( - {mod, fun}, - imports, - if(elixir_prefix, do: [], else: aliases), - module, - mods_funs, - metadata_types - ) do - TypeInfo.extract_param_options(mod, fun, npar) - |> options_to_suggestions(mod) - |> Enum.filter(&String.starts_with?(&1.name, hint)) - else - _ -> - [] - end - end - - defp options_to_suggestions(options, original_module) do - Enum.map(options, fn - {mod, name, type} -> - TypeInfo.get_type_info(mod, type, original_module) - |> Map.merge(%{type: :param_option, name: name |> Atom.to_string()}) - - {mod, name} -> - %{ - doc: "", - expanded_spec: "", - name: name |> Atom.to_string(), - origin: inspect(mod), - type: :param_option, - type_spec: "" - } - end) - end - - @spec find_typespecs( - String.t(), - [{module, module}], - module, - State.scope(), - State.mods_funs_to_positions_t(), - State.types_t() - ) :: - [ - type_spec - ] - - # We don't list typespecs when inside a function - defp find_typespecs(_hint, _aliases, _module, {_m, _f}, _, _) do - [] - end - - # We don't list typespecs outside of a module - defp find_typespecs(_hint, _aliases, _module, scope, _, _) when scope in [Elixir, nil] do - [] - end - - # We don't list typespecs when the hint is most likely an attribute - defp find_typespecs("@" <> _, _aliases, _module, _scope, _, _) do - [] - end - - defp find_typespecs(hint, aliases, module, _scope, mods_and_funs, metadata_types) do - {mod, hint} = - hint - |> Source.split_module_and_hint(module, aliases) - - find_typespecs_for_mod_and_hint({mod, hint}, aliases, module, mods_and_funs, metadata_types) - |> Kernel.++(find_builtin_types({mod, hint})) - end - - defp find_typespecs_for_mod_and_hint( - {mod, hint}, - aliases, - module, - mods_and_funs, - metadata_types - ) do - case Introspection.actual_module(mod, aliases, module, mods_and_funs) do - {actual_mod, true} -> - find_module_types(actual_mod, {mod, hint}, metadata_types, module) - - {nil, false} -> - find_module_types(module, {mod, hint}, metadata_types, module) - - {_, false} -> - [] - end - end - - defp find_builtin_types({nil, hint}) do - TypeInfo.find_all_builtin(&String.starts_with?("#{&1.name}", hint)) - |> Enum.map(&type_info_to_suggestion(&1, nil)) - end - - defp find_builtin_types({_mod, _hint}), do: [] - - defp find_module_types(actual_mod, {mod, hint}, metadata_types, module) do - find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) - |> Kernel.++(TypeInfo.find_all(actual_mod, &String.starts_with?("#{&1.name}", hint))) - |> Enum.map(&type_info_to_suggestion(&1, actual_mod)) - end - - defp find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) do - include_private = mod == nil and actual_mod == module - - for {{mod, type, arity}, type_info} when is_integer(arity) <- metadata_types, - mod == actual_mod, - type |> Atom.to_string() |> String.starts_with?(hint), - include_private or type_info.kind != :typep, - do: type_info - end - - defp type_info_to_suggestion(type_info, module) do - origin = if module, do: inspect(module), else: "" - - case type_info do - %ElixirSense.Core.State.TypeInfo{args: [args]} -> - args_stringified = Enum.join(args, ", ") - - %{ - type: :type_spec, - name: type_info.name |> Atom.to_string(), - arity: length(args), - signature: "#{type_info.name}(#{args_stringified})", - origin: origin, - doc: "", - spec: "", - # TODO extract doc and meta - metadata: %{} - } - - _ -> - %{ - type: :type_spec, - name: type_info.name |> Atom.to_string(), - arity: type_info.arity, - signature: type_info.signature, - origin: origin, - doc: type_info.doc, - spec: type_info.spec, - metadata: type_info.metadata - } - end + result end end diff --git a/lib/elixir_sense/providers/suggestion/complete.ex b/lib/elixir_sense/providers/suggestion/complete.ex index b0e9d03f..a6a3dd55 100644 --- a/lib/elixir_sense/providers/suggestion/complete.ex +++ b/lib/elixir_sense/providers/suggestion/complete.ex @@ -77,13 +77,18 @@ defmodule ElixirSense.Providers.Suggestion.Complete do scope: Elixir end + @type hint :: %{ + type: :hint, + value: String.t() + } + @spec complete(String.t(), Env.t()) :: - {ElixirSense.Providers.Suggestion.hint(), + {hint(), [ - ElixirSense.Providers.Suggestion.func() - | ElixirSense.Providers.Suggestion.mod() - | ElixirSense.Providers.Suggestion.field() - | ElixirSense.Providers.Suggestion.variable() + ElixirSense.Providers.Suggestion.Reducers.Common.func() + | ElixirSense.Providers.Suggestion.Reducers.Common.mod() + | ElixirSense.Providers.Suggestion.Reducers.Common.variable() + | ElixirSense.Providers.Suggestion.Reducers.Struct.field() ]} def complete(hint, %Env{} = env) do {_result, completion_hint, completions} = @@ -730,11 +735,11 @@ defmodule ElixirSense.Providers.Suggestion.Complete do ## Ad-hoc conversions @spec to_entries(map) :: [ - ElixirSense.Providers.Suggestion.mod() - | ElixirSense.Providers.Suggestion.func() - | ElixirSense.Providers.Suggestion.variable() - | ElixirSense.Providers.Suggestion.field() - | ElixirSense.Providers.Suggestion.attribute() + ElixirSense.Providers.Suggestion.Reducers.Common.mod() + | ElixirSense.Providers.Suggestion.Reducers.Common.func() + | ElixirSense.Providers.Suggestion.Reducers.Common.variable() + | ElixirSense.Providers.Suggestion.Reducers.Struct.field() + | ElixirSense.Providers.Suggestion.Reducers.Common.attribute() ] defp to_entries(%{kind: :field, subtype: subtype, name: name, origin: origin}) do [%{type: :field, name: name, subtype: subtype, origin: origin, call?: true}] diff --git a/lib/elixir_sense/providers/suggestion/reducer.ex b/lib/elixir_sense/providers/suggestion/reducer.ex new file mode 100644 index 00000000..ac981677 --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducer.ex @@ -0,0 +1,14 @@ +defmodule ElixirSense.Providers.Suggestion.Reducer do + @moduledoc !""" + Provides common functions for reducers. + """ + + def put_context(acc, key, value) do + updated_context = Map.put(acc.context, key, value) + put_in(acc.context, updated_context) + end + + def get_context(acc, key) do + get_in(acc, [:context, key]) + end +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/callbacks.ex b/lib/elixir_sense/providers/suggestion/reducers/callbacks.ex new file mode 100644 index 00000000..ea472f20 --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/callbacks.ex @@ -0,0 +1,80 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.Callbacks do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.State + + @type callback :: %{ + type: :callback, + name: String.t(), + arity: non_neg_integer, + args: String.t(), + origin: String.t(), + summary: String.t(), + spec: String.t(), + metadata: map + } + + @doc """ + A reducer that adds suggestions of callbacks. + """ + def add_callbacks(hint, text_before, env, _buffer_metadata, acc) do + %State.Env{protocol: protocol, behaviours: behaviours, scope: scope} = env + + list = + Enum.flat_map(behaviours, fn + mod when is_atom(mod) and (protocol == nil or mod != elem(protocol, 0)) -> + mod_name = inspect(mod) + + for %{ + name: name, + arity: arity, + callback: spec, + signature: signature, + doc: doc, + metadata: metadata + } <- + Introspection.get_callbacks_with_docs(mod), + def_prefix?(hint, spec) or String.starts_with?("#{name}", hint) do + desc = Introspection.extract_summary_from_docs(doc) + [_, args_str] = Regex.run(Regex.recompile!(~r/.\((.*)\)/), signature) + args = args_str |> String.replace(Regex.recompile!(~r/\s/), "") + + %{ + type: :callback, + name: Atom.to_string(name), + arity: arity, + args: args, + origin: mod_name, + summary: desc, + spec: spec, + metadata: metadata + } + end + + _ -> + [] + end) + + list = Enum.sort(list) + + cond do + Regex.match?(~r/\s(def|defmacro)\s+[a-z|_]*$/, text_before) -> + {:halt, %{acc | result: list}} + + match?({_f, _a}, scope) -> + {:cont, acc} + + true -> + {:cont, %{acc | result: acc.result ++ list}} + end + end + + defp def_prefix?(hint, spec) do + if String.starts_with?(spec, "@macrocallback") do + String.starts_with?("defmacro", hint) + else + String.starts_with?("def", hint) + end + end +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/common.ex b/lib/elixir_sense/providers/suggestion/reducers/common.ex new file mode 100644 index 00000000..3090abfc --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/common.ex @@ -0,0 +1,204 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.Common do + @moduledoc false + + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Providers.Suggestion.Complete + alias ElixirSense.Providers.Suggestion.Reducer + + @type attribute :: %{ + type: :attribute, + name: String.t() + } + + @type variable :: %{ + type: :variable, + name: String.t() + } + + @type func :: %{ + type: :function | :macro, + name: String.t(), + arity: non_neg_integer, + args: String.t(), + origin: String.t(), + summary: String.t(), + spec: String.t(), + metadata: map + } + + @type mod :: %{ + type: :module, + name: String.t(), + subtype: String.t(), + summary: String.t(), + metadata: map + } + + @doc """ + A reducer that populates the context with the suggestions provided by + the `ElixirSense.Providers.Suggestion.Complete` module. + + The suggestions are grouped by type and saved in the context under the + `:common_suggestions_by_type` key and can be accessed by any reducer + that runs after. + + Available suggestions: + + * Modules + * Functions + * Macros + * Variables + * Module attributes + * Variable fields + + """ + def populate(hint, text_before, env, buffer_metadata, acc) do + %Metadata{ + structs: structs, + mods_funs_to_positions: mods_and_funs, + specs: metadata_specs + } = buffer_metadata + + suggestions = + find_hint_mods_funcs( + hint, + env, + mods_and_funs, + metadata_specs, + structs, + text_before + ).suggestions + + suggestions_by_type = Enum.group_by(suggestions, & &1.type) + + {:cont, Reducer.put_context(acc, :common_suggestions_by_type, suggestions_by_type)} + end + + @doc """ + A reducer that adds suggestions of existing modules. + + Note: requires populate/5. + """ + def add_modules(_hint, _text_before, _env, _file_metadata, acc) do + add_suggestions(:module, acc) + end + + @doc """ + A reducer that adds suggestions of existing functions. + + Note: requires populate/5. + """ + def add_functions(_hint, _text_before, _env, _file_metadata, acc) do + add_suggestions(:function, acc) + end + + @doc """ + A reducer that adds suggestions of existing macros. + + Note: requires populate/5. + """ + def add_macros(_hint, _text_before, _env, _file_metadata, acc) do + add_suggestions(:macro, acc) + end + + @doc """ + A reducer that adds suggestions of variable fields. + + Note: requires populate/5. + """ + def add_fields(_hint, _text_before, _env, _file_metadata, acc) do + add_suggestions(:field, acc) + end + + @doc """ + A reducer that adds suggestions of existing module attributes. + + Note: requires populate/5. + """ + def add_attributes(_hint, _text_before, _env, _file_metadata, acc) do + add_suggestions(:attribute, acc) + end + + @doc """ + A reducer that adds suggestions of existing variables. + + Note: requires populate/5. + """ + def add_variables(_hint, _text_before, _env, _file_metadata, acc) do + add_suggestions(:variable, acc) + end + + defp add_suggestions(type, acc) do + suggestions_by_type = Reducer.get_context(acc, :common_suggestions_by_type) + list = Map.get(suggestions_by_type, type, []) + {:cont, %{acc | result: acc.result ++ list}} + end + + defp find_hint_mods_funcs( + hint, + %State.Env{ + imports: imports, + aliases: aliases, + module: module, + vars: vars, + attributes: attributes, + scope: scope + }, + mods_and_funs, + metadata_specs, + structs, + text_before + ) do + env = %Complete.Env{ + aliases: aliases, + vars: vars, + attributes: attributes, + scope_module: module, + imports: imports, + mods_and_funs: mods_and_funs, + specs: metadata_specs, + structs: structs, + scope: scope + } + + {hint, prefix} = + case Source.get_v12_module_prefix(text_before, module) do + nil -> + {hint, ""} + + module_string -> + # v1.2 alias syntax detected + # prepend module prefix before running completion + prefix = module_string <> "." + {prefix <> hint, prefix} + end + + {hint, module_special_form_replaced} = + if String.starts_with?(hint, "__MODULE__") do + {hint |> String.replace_leading("__MODULE__", inspect(module)), true} + else + {hint, false} + end + + {%{type: :hint, value: prefixed_value}, suggestions} = Complete.complete(hint, env) + + prefixed_value = + if module_special_form_replaced do + prefixed_value |> String.replace_leading(inspect(module), "__MODULE__") + else + prefixed_value + end + + # drop module prefix from hint if added + value = + if prefix != "" do + prefixed_value |> String.replace_leading(prefix, "") + else + prefixed_value + end + + %{hint: %{type: :hint, value: value}, suggestions: suggestions} + end +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/params.ex b/lib/elixir_sense/providers/suggestion/reducers/params.ex new file mode 100644 index 00000000..794f3ffd --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/params.ex @@ -0,0 +1,72 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.Params do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInfo + + @type param_option :: %{ + type: :param_option, + name: String.t(), + origin: String.t(), + type_spec: String.t(), + doc: String.t(), + expanded_spec: String.t() + } + + @doc """ + A reducer that adds suggestions of keyword list options. + """ + def add_options(hint, prefix, env, buffer_metadata, acc) do + %State.Env{imports: imports, aliases: aliases, module: module} = env + %Metadata{mods_funs_to_positions: mods_funs, types: metadata_types} = buffer_metadata + + with %{ + candidate: {mod, fun}, + elixir_prefix: elixir_prefix, + npar: npar, + pipe_before: _pipe_before + } <- + Source.which_func(prefix, module), + {mod, fun, true} <- + Introspection.actual_mod_fun( + {mod, fun}, + imports, + if(elixir_prefix, do: [], else: aliases), + module, + mods_funs, + metadata_types + ) do + list = + mod + |> TypeInfo.extract_param_options(fun, npar) + |> options_to_suggestions(mod) + |> Enum.filter(&String.starts_with?(&1.name, hint)) + + {:cont, %{acc | result: acc.result ++ list}} + else + _ -> + {:cont, acc} + end + end + + defp options_to_suggestions(options, original_module) do + Enum.map(options, fn + {mod, name, type} -> + TypeInfo.get_type_info(mod, type, original_module) + |> Map.merge(%{type: :param_option, name: name |> Atom.to_string()}) + + {mod, name} -> + %{ + doc: "", + expanded_spec: "", + name: name |> Atom.to_string(), + origin: inspect(mod), + type: :param_option, + type_spec: "" + } + end) + end +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/protocol.ex b/lib/elixir_sense/providers/suggestion/reducers/protocol.ex new file mode 100644 index 00000000..953dea6b --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/protocol.ex @@ -0,0 +1,48 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.Protocol do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.State + + @type protocol_function :: %{ + type: :protocol_function, + name: String.t(), + arity: non_neg_integer, + args: String.t(), + origin: String.t(), + summary: String.t(), + spec: String.t(), + metadata: map + } + + @doc """ + A reducer that adds suggestions of protocol functions. + """ + def add_functions(_hint, _text_before, %State.Env{scope: {_f, _a}}, _buffer_metadata, acc), + do: {:cont, acc} + + def add_functions(_hint, _text_before, %State.Env{protocol: nil}, _buffer_metadata, acc), + do: {:cont, acc} + + def add_functions(hint, _text_before, env, _buffer_metadata, acc) do + %State.Env{protocol: {protocol, _implementations}} = env + + list = + for {{name, arity}, {_type, args, docs, metadata, spec}} <- + Introspection.module_functions_info(protocol), + hint == "" or String.starts_with?("#{name}", hint) do + %{ + type: :protocol_function, + name: Atom.to_string(name), + arity: arity, + args: args, + origin: inspect(protocol), + summary: docs, + metadata: metadata, + spec: spec + } + end + + {:cont, %{acc | result: acc.result ++ Enum.sort(list)}} + end +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/returns.ex b/lib/elixir_sense/providers/suggestion/reducers/returns.ex new file mode 100644 index 00000000..3554ff8b --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/returns.ex @@ -0,0 +1,80 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.Returns do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State + + @type return :: %{ + type: :return, + description: String.t(), + spec: String.t(), + snippet: String.t() + } + + @doc """ + A reducer that adds suggestions of possible return values. + """ + def add_returns( + "" = _hint, + _text_before, + %State.Env{scope: {fun, arity}} = env, + buffer_metadata, + acc + ) do + %State.Env{module: current_module, behaviours: behaviours, protocol: protocol} = env + %Metadata{specs: specs} = buffer_metadata + + spec_returns = + case specs[{current_module, fun, arity}] do + nil -> + [] + + %State.SpecInfo{specs: info_specs} -> + for spec <- info_specs, + {:ok, {:@, _, [{_, _, [quoted]}]}} = Code.string_to_quoted(spec), + return <- Introspection.get_returns_from_spec_ast(quoted) do + format_return(return) + end + end + + callbacks = + for mod <- behaviours, + protocol == nil or mod != elem(protocol, 0), + Introspection.define_callback?(mod, fun, arity), + return <- Introspection.get_returns_from_callback(mod, fun, arity) do + format_return(return) + end + + protocol_functions = + case protocol do + {proto, _implementations} -> + if Introspection.define_callback?(proto, fun, arity) do + for return <- Introspection.get_returns_from_callback(proto, fun, arity) do + format_return(return) + end + else + [] + end + + nil -> + [] + end + + list = callbacks ++ protocol_functions ++ spec_returns + {:cont, %{acc | result: acc.result ++ list}} + end + + def add_returns(_hint, _text_before, _env, _buffer_metadata, acc) do + {:cont, acc} + end + + defp format_return(return) do + %{ + type: :return, + description: return.description, + spec: return.spec, + snippet: return.snippet + } + end +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/struct.ex b/lib/elixir_sense/providers/suggestion/reducers/struct.ex new file mode 100644 index 00000000..5e072d98 --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/struct.ex @@ -0,0 +1,216 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.Struct do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.Struct + + @type field :: %{ + type: :field, + subtype: :struct_field | :map_key, + name: String.t(), + origin: String.t() | nil, + call?: boolean + } + + @doc """ + A reducer that adds suggestions of struct fields. + """ + def add_fields(hint, text_before, env, buffer_metadata, acc) do + case find_struct_fields(hint, text_before, env, buffer_metadata) do + {[], _} -> + {:cont, acc} + + {fields, nil} -> + {:halt, %{acc | result: fields}} + + {fields, :maybe_struct_update} -> + reducers = [:populate_common, :modules, :functions, :macros, :variables, :attributes] + {:cont, %{acc | result: fields, reducers: reducers}} + end + end + + defp find_struct_fields(hint, text_before, env, buffer_metadata) do + %State.Env{module: module, vars: vars, attributes: attributes} = env + + %Metadata{ + structs: structs, + mods_funs_to_positions: mods_funs, + types: metadata_types + } = buffer_metadata + + case Source.which_struct(text_before, module) do + {{:attribute, attr}, fields_so_far, _elixir_prefix, _var} = struct -> + mod = + case Enum.find(attributes, fn %_{name: name} -> name == attr end) do + %_{type: {:atom, mod}} -> mod + _ -> nil + end + + result = + get_fields( + env, + mods_funs, + metadata_types, + structs, + hint, + struct |> put_elem(0, mod) + ) + + {result, if(fields_so_far == [], do: :maybe_struct_update)} + + {mod, fields_so_far, _elixir_prefix, _var} = struct when is_atom(mod) and mod != :_ -> + result = + get_fields( + env, + mods_funs, + metadata_types, + structs, + hint, + struct + ) + + {result, if(fields_so_far == [], do: :maybe_struct_update)} + + {:_, fields_so_far, false, _var} when is_list(fields_so_far) -> + result = + [:__struct__] + |> Kernel.--(fields_so_far) + |> Enum.filter(fn field -> String.starts_with?("#{field}", hint) end) + |> Enum.map(fn field -> + %{ + type: :field, + subtype: :struct_field, + name: Atom.to_string(field), + origin: nil, + call?: false + } + end) + + {result, if(fields_so_far == [], do: :maybe_struct_update)} + + {:map, fields_so_far, var_or_attr} when not is_nil(var_or_attr) -> + result = + case var_or_attr do + {:variable, var} -> + get_fields_from_var_or_attr(vars, structs, var, fields_so_far, hint) + + {:attribute, attr} -> + get_fields_from_var_or_attr(attributes, structs, attr, fields_so_far, hint) + end + + {result, if(fields_so_far == [], do: :maybe_struct_update)} + + _ -> + {[], nil} + end + end + + defp get_fields( + %State.Env{ + imports: imports, + aliases: aliases, + module: module, + vars: vars, + attributes: attributes + }, + mods_funs, + metadata_types, + structs, + hint, + {mod, fields_so_far, elixir_prefix, var_or_attr} + ) do + with {actual_mod, _, true} <- + Introspection.actual_mod_fun( + {expand_current_module(mod, module), nil}, + imports, + if(elixir_prefix, do: [], else: aliases), + module, + mods_funs, + metadata_types + ), + true <- Struct.is_struct(actual_mod, structs) do + fields = Struct.get_fields(actual_mod, structs) + + fields + |> Kernel.--(fields_so_far) + |> Enum.filter(fn field -> String.starts_with?("#{field}", hint) end) + |> Enum.map(fn field -> + %{ + type: :field, + subtype: :struct_field, + name: Atom.to_string(field), + call?: false, + origin: inspect(actual_mod) + } + end) + else + _ -> + case var_or_attr do + nil -> + [] + + {:variable, var} -> + get_fields_from_var_or_attr(vars, structs, var, fields_so_far, hint) + + {:attribute, attr} -> + get_fields_from_var_or_attr(attributes, structs, attr, fields_so_far, hint) + end + end + end + + defp get_fields_from_var_or_attr(vars, structs, var, fields_so_far, hint) do + case Enum.find(vars, fn %_{name: name} -> name == var end) do + %_{type: {:map, fields}} -> + for {field, _} <- fields, + field not in fields_so_far, + String.starts_with?("#{field}", hint) do + %{ + type: :field, + subtype: :map_key, + name: Atom.to_string(field), + origin: nil, + call?: false + } + end + + %_{type: {:struct, fields, nil}} -> + for {field, _} <- fields |> Keyword.put_new(:__struct__, nil), + field not in fields_so_far, + String.starts_with?("#{field}", hint) do + %{ + type: :field, + subtype: :struct_field, + name: Atom.to_string(field), + origin: nil, + call?: false + } + end + + %_{type: {:struct, _fields, module}} -> + if Struct.is_struct(module, structs) do + for field <- Struct.get_fields(module, structs), + field not in fields_so_far, + String.starts_with?("#{field}", hint) do + %{ + type: :field, + subtype: :struct_field, + name: Atom.to_string(field), + origin: inspect(module), + call?: false + } + end + else + [] + end + + _otherwise -> + [] + end + end + + defp expand_current_module(:__MODULE__, current_module), do: current_module + defp expand_current_module(module, _current_module), do: module +end diff --git a/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex b/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex new file mode 100644 index 00000000..ca962065 --- /dev/null +++ b/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex @@ -0,0 +1,129 @@ +defmodule ElixirSense.Providers.Suggestion.Reducers.TypeSpecs do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInfo + + @type type_spec :: %{ + type: :type_spec, + name: String.t(), + arity: non_neg_integer, + origin: String.t(), + spec: String.t(), + doc: String.t(), + signature: String.t(), + metadata: map + } + + @doc """ + A reducer that adds suggestions of types. + """ + # We don't list typespecs when inside a function + def add_types(_hint, _text_before, %State.Env{scope: {_m, _f}}, _buffer_metadata, acc) do + {:cont, acc} + end + + # We don't list typespecs outside of a module + def add_types(_hint, _text_before, %State.Env{scope: scope}, _buffer_metadata, acc) + when scope in [Elixir, nil] do + {:cont, acc} + end + + # We don't list typespecs when the hint is most likely an attribute + def add_types("@" <> _, _text_before, _env, _buffer_metadata, acc) do + {:cont, acc} + end + + def add_types(hint, _text_before, env, file_metadata, acc) do + %State.Env{aliases: aliases, module: module} = env + %Metadata{mods_funs_to_positions: mods_and_funs, types: metadata_types} = file_metadata + + {mod, hint} = + hint + |> Source.split_module_and_hint(module, aliases) + + list = + find_typespecs_for_mod_and_hint({mod, hint}, aliases, module, mods_and_funs, metadata_types) + |> Kernel.++(find_builtin_types({mod, hint})) + + {:cont, %{acc | result: acc.result ++ list}} + end + + defp find_typespecs_for_mod_and_hint( + {mod, hint}, + aliases, + module, + mods_and_funs, + metadata_types + ) do + case Introspection.actual_module(mod, aliases, module, mods_and_funs) do + {actual_mod, true} -> + find_module_types(actual_mod, {mod, hint}, metadata_types, module) + + {nil, false} -> + find_module_types(module, {mod, hint}, metadata_types, module) + + {_, false} -> + [] + end + end + + defp find_builtin_types({nil, hint}) do + TypeInfo.find_all_builtin(&String.starts_with?("#{&1.name}", hint)) + |> Enum.map(&type_info_to_suggestion(&1, nil)) + end + + defp find_builtin_types({_mod, _hint}), do: [] + + defp find_module_types(actual_mod, {mod, hint}, metadata_types, module) do + find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) + |> Kernel.++(TypeInfo.find_all(actual_mod, &String.starts_with?("#{&1.name}", hint))) + |> Enum.map(&type_info_to_suggestion(&1, actual_mod)) + end + + defp find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) do + include_private = mod == nil and actual_mod == module + + for {{mod, type, arity}, type_info} when is_integer(arity) <- metadata_types, + mod == actual_mod, + type |> Atom.to_string() |> String.starts_with?(hint), + include_private or type_info.kind != :typep, + do: type_info + end + + defp type_info_to_suggestion(type_info, module) do + origin = if module, do: inspect(module), else: "" + + case type_info do + %ElixirSense.Core.State.TypeInfo{args: [args]} -> + args_stringified = Enum.join(args, ", ") + + %{ + type: :type_spec, + name: type_info.name |> Atom.to_string(), + arity: length(args), + signature: "#{type_info.name}(#{args_stringified})", + origin: origin, + doc: "", + spec: "", + # TODO extract doc and meta + metadata: %{} + } + + _ -> + %{ + type: :type_spec, + name: type_info.name |> Atom.to_string(), + arity: type_info.arity, + signature: type_info.signature, + origin: origin, + doc: type_info.doc, + spec: type_info.spec, + metadata: type_info.metadata + } + end + end +end diff --git a/test/elixir_sense/providers/suggestion_test.exs b/test/elixir_sense/providers/suggestion_test.exs index beb6f947..5e5dc751 100644 --- a/test/elixir_sense/providers/suggestion_test.exs +++ b/test/elixir_sense/providers/suggestion_test.exs @@ -2,6 +2,7 @@ defmodule ElixirSense.Providers.SuggestionTest do use ExUnit.Case, async: true alias ElixirSense.Providers.Suggestion alias ElixirSense.Core.State.StructInfo + alias ElixirSense.Core.Metadata doctest Suggestion @@ -20,20 +21,9 @@ defmodule ElixirSense.Providers.SuggestionTest do } test "find definition of built-in functions" do - result = - Suggestion.find( - "ElixirSenseExample.EmptyModule.", - @env, - %{}, - %{}, - %{}, - %{}, - "" - ) + result = Suggestion.find("ElixirSenseExample.EmptyModule.", "", @env, %Metadata{}) - assert result |> Enum.at(0) == %{type: :hint, value: "ElixirSenseExample.EmptyModule."} - - assert result |> Enum.at(1) == %{ + assert result |> Enum.at(0) == %{ args: "atom", arity: 1, name: "__info__", @@ -45,7 +35,7 @@ defmodule ElixirSense.Providers.SuggestionTest do metadata: %{builtin: true} } - assert result |> Enum.at(2) == %{ + assert result |> Enum.at(1) == %{ args: "", arity: 0, name: "module_info", @@ -57,7 +47,7 @@ defmodule ElixirSense.Providers.SuggestionTest do metadata: %{builtin: true} } - assert result |> Enum.at(3) == %{ + assert result |> Enum.at(2) == %{ args: "key", arity: 1, name: "module_info", @@ -71,17 +61,8 @@ defmodule ElixirSense.Providers.SuggestionTest do end test "return completion candidates for 'Str'" do - assert Suggestion.find( - "ElixirSenseExample.ModuleWithDo", - @env, - %{}, - %{}, - %{}, - %{}, - "" - ) == + assert Suggestion.find("ElixirSenseExample.ModuleWithDo", "", @env, %Metadata{}) == [ - %{type: :hint, value: "ElixirSenseExample.ModuleWithDoc"}, %{ name: "ModuleWithDocFalse", subtype: nil, @@ -101,7 +82,6 @@ defmodule ElixirSense.Providers.SuggestionTest do test "return completion candidates for 'List.del'" do assert [ - %{type: :hint, value: "List.delete"}, %{ args: "list," <> _, arity: 2, @@ -120,21 +100,11 @@ defmodule ElixirSense.Providers.SuggestionTest do summary: "Produces a new list by " <> _, type: :function } - ] = - Suggestion.find( - "List.del", - @env, - %{}, - %{}, - %{}, - %{}, - "" - ) + ] = Suggestion.find("List.del", "", @env, %Metadata{}) end test "return completion candidates for module with alias" do assert [ - %{type: :hint, value: "MyList.delete"}, %{ args: "list," <> _, arity: 2, @@ -153,53 +123,28 @@ defmodule ElixirSense.Providers.SuggestionTest do summary: "Produces a new list " <> _, type: :function } - ] = - Suggestion.find( - "MyList.del", - %{@env | aliases: [{MyList, List}]}, - %{}, - %{}, - %{}, - %{}, - "" - ) + ] = Suggestion.find("MyList.del", "", %{@env | aliases: [{MyList, List}]}, %Metadata{}) end test "return completion candidates for functions from import" do - assert Suggestion.find( - "say", - %{@env | imports: [MyModule]}, - %{}, - %{}, - %{}, - %{}, - "" - ) == [ - %{type: :hint, value: "say_hi"}, - %{ - args: "", - arity: 0, - name: "say_hi", - origin: "ElixirSense.Providers.SuggestionTest.MyModule", - spec: "", - summary: "", - type: :function, - metadata: %{} - } - ] + assert Suggestion.find("say", "", %{@env | imports: [MyModule]}, %Metadata{}) == + [ + %{ + args: "", + arity: 0, + name: "say_hi", + origin: "ElixirSense.Providers.SuggestionTest.MyModule", + spec: "", + summary: "", + type: :function, + metadata: %{} + } + ] end test "local calls should not return built-in functions" do list = - Suggestion.find( - "mo", - @env, - %{}, - %{}, - %{}, - %{}, - "" - ) + Suggestion.find("mo", "", @env, %Metadata{}) |> Enum.filter(fn item -> item.type in [:function] end) assert list == [] @@ -207,15 +152,7 @@ defmodule ElixirSense.Providers.SuggestionTest do test "empty hint should not return built-in functions" do suggestions_names = - Suggestion.find( - "", - @env, - %{}, - %{}, - %{}, - %{}, - "" - ) + Suggestion.find("", "", @env, %Metadata{}) |> Enum.filter(&Map.has_key?(&1, :name)) |> Enum.map(& &1.name) @@ -227,105 +164,90 @@ defmodule ElixirSense.Providers.SuggestionTest do end test "return completion candidates for struct starting with %" do - assert [%{type: :hint, value: "%ElixirSense.Providers.SuggestionTest.MyStruct"} | _] = + assert [%{type: :module, name: "MyStruct"} | _] = Suggestion.find( "%ElixirSense.Providers.SuggestionTest.MyStr", + "", @env_func, - %{}, - %{}, - %{}, - %{}, - "" + %Metadata{} ) end test "return completion candidates for &func" do - assert [%{type: :hint, value: "f = &Enum.all?"} | _] = + assert [%{type: :function, name: "all?", origin: "Enum"} | _] = Suggestion.find( "f = &Enum.al", + "", @env_func, - %{}, - %{}, - %{}, - %{}, - "" + %Metadata{} ) end test "do not return completion candidates for unknown erlang modules" do - assert [%{type: :hint, value: "Enum:"}] = + assert [] = Suggestion.find( "Enum:", + "", @env_func, - %{}, - %{}, - %{}, - %{}, - "" + %Metadata{} ) end test "do not return completion candidates for unknown modules" do - assert [%{type: :hint, value: "x.Foo.get_by"}] = + assert [] = Suggestion.find( "x.Foo.get_by", + "", @env_func, - %{}, - %{}, - %{}, - %{}, - "" + %Metadata{} ) end test "return completion candidates for metadata modules" do - assert [%{type: :hint, value: "my_func"} | _] = + assert [%{type: :function, name: "my_func"} | _] = Suggestion.find( "my_f", + "", @env_func, - %{}, - %{ - {SomeModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defmodule}, - {SomeModule, :my_func, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defp}, - {SomeModule, :my_func, 1} => %ElixirSense.Core.State.ModFunInfo{ - type: :defp, - params: [[[:a, [], nil]]] + %Metadata{ + mods_funs_to_positions: %{ + {SomeModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defmodule}, + {SomeModule, :my_func, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defp}, + {SomeModule, :my_func, 1} => %ElixirSense.Core.State.ModFunInfo{ + type: :defp, + params: [[[:a, [], nil]]] + } } - }, - %{}, - %{}, - "" + } ) - assert [%{type: :hint, value: "SomeModule"} | _] = + assert [%{type: :module, name: "SomeModule"} | _] = Suggestion.find( "So", + "", @env_func, - %{}, - %{ - {SomeModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defmodule} - }, - %{}, - %{}, - "" + %Metadata{ + mods_funs_to_positions: %{ + {SomeModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defmodule} + } + } ) end test "return completion candidates for metadata structs" do assert [ - %{type: :hint, value: "str_field"}, %{name: "str_field", origin: "SomeModule", type: :field} ] = Suggestion.find( "str_", + "%SomeModule{st", @env_func, - %{SomeModule => %StructInfo{type: :defstruct, fields: [str_field: 1]}}, - %{ - {SomeModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defmodule} - }, - %{}, - %{}, - "%SomeModule{st" + %Metadata{ + structs: %{SomeModule => %StructInfo{type: :defstruct, fields: [str_field: 1]}}, + mods_funs_to_positions: %{ + {SomeModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{type: :defmodule} + } + } ) end end diff --git a/test/elixir_sense/suggestions_test.exs b/test/elixir_sense/suggestions_test.exs index df06914a..97763142 100644 --- a/test/elixir_sense/suggestions_test.exs +++ b/test/elixir_sense/suggestions_test.exs @@ -56,7 +56,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 7) assert list == [ - %{type: :hint, value: "is_b"}, %{ args: "term", arity: 1, @@ -103,7 +102,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 3, 14) assert list == [ - %{type: :hint, value: "MyList.flatten"}, %{ args: "list", arity: 1, @@ -140,7 +138,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 3, 12) assert list == [ - %{type: :hint, value: "Macros.some"}, %{ args: "var", arity: 1, @@ -164,7 +161,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 34) assert list == [ - %{type: :hint, value: "ElixirSenseExample.ModuleWithDoc"}, %{ name: "ModuleWithDocFalse", subtype: nil, @@ -208,6 +204,79 @@ defmodule ElixirSense.SuggestionsTest do ] = list end + test "lists callbacks + def macros after de" do + buffer = """ + defmodule MyServer do + use GenServer + + de + # ^ + end + """ + + list = ElixirSense.suggestions(buffer, 4, 5) + assert Enum.any?(list, fn s -> s.type == :callback end) + assert Enum.any?(list, fn s -> s.type == :macro end) + assert Enum.all?(list, fn s -> s.type in [:callback, :macro] end) + end + + test "lists callbacks + def macros after def" do + buffer = """ + defmodule MyServer do + use GenServer + + def + # ^ + end + """ + + list = ElixirSense.suggestions(buffer, 4, 6) + assert Enum.any?(list, fn s -> s.type == :callback end) + assert Enum.any?(list, fn s -> s.type == :macro end) + assert Enum.all?(list, fn s -> s.type in [:callback, :macro] end) + end + + test "lists only callbacks after def + space" do + buffer = """ + defmodule MyServer do + use GenServer + + def t + # ^ + end + """ + + assert ElixirSense.suggestions(buffer, 4, 7) |> Enum.all?(fn s -> s.type == :callback end) + + buffer = """ + defmodule MyServer do + use GenServer + + def t + # ^ + end + """ + + assert [%{name: "terminate", type: :callback}] = ElixirSense.suggestions(buffer, 4, 8) + end + + test "do not list callbacks inside functions" do + buffer = """ + defmodule MyServer do + use GenServer + + def init(_) do + t + # ^ + end + end + """ + + list = ElixirSense.suggestions(buffer, 5, 6) + assert Enum.any?(list, fn s -> s.type == :function end) + refute Enum.any?(list, fn s -> s.type == :callback end) + end + test "lists macrocallbacks" do buffer = """ defmodule MyServer do @@ -244,6 +313,22 @@ defmodule ElixirSense.SuggestionsTest do ] == list end + test "lists macrocallbacks + def macros after defma" do + buffer = """ + defmodule MyServer do + @behaviour ElixirSenseExample.BehaviourWithMacrocallback + + defma + # ^ + end + """ + + list = ElixirSense.suggestions(buffer, 4, 8) + assert Enum.any?(list, fn s -> s.type == :callback end) + assert Enum.any?(list, fn s -> s.type == :macro end) + assert Enum.all?(list, fn s -> s.type in [:callback, :macro] end) + end + test "lists erlang callbacks" do buffer = """ defmodule MyServer do @@ -897,7 +982,7 @@ defmodule ElixirSense.SuggestionsTest do ElixirSense.suggestions(buffer, 2, 37) |> Enum.filter(fn s -> s.type in [:variable, :hint] end) - assert list == [%{type: :hint, value: "my_var"}, %{name: "my_var", type: :variable}] + assert list == [%{name: "my_var", type: :variable}] end test "variable shadowing function" do @@ -912,7 +997,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "my_fun"}, %{name: "my_fun", type: :variable}, %{name: "my_fun", type: :function} ] = ElixirSense.suggestions(buffer, 5, 9) @@ -993,7 +1077,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "test_fun_p"}, %{ arity: 0, name: "test_fun_priv", @@ -1009,7 +1092,6 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 5, 7) assert [ - %{type: :hint, value: "test_fun_priv"}, %{ arity: 0, name: "test_fun_priv", @@ -1019,7 +1101,6 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 6, 21) assert [ - %{type: :hint, value: "is_boo"}, %{ arity: 1, name: "is_boolean", @@ -1035,7 +1116,6 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 7, 10) assert [ - %{type: :hint, value: "delegate_"}, %{ arity: 0, name: "delegate_defined", @@ -1051,7 +1131,6 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 8, 8) assert [ - %{type: :hint, value: "my_guard_p"}, %{ args: "value", arity: 1, @@ -1088,7 +1167,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "ElixirSenseExample.ModuleO.test_fun_pub"}, %{ arity: 1, name: "test_fun_pub", @@ -1114,7 +1192,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "ModuleO.test_fun_pub"}, %{ arity: 1, name: "test_fun_pub", @@ -1142,7 +1219,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "test_fun_pub"}, %{ arity: 1, name: "test_fun_pub", @@ -1156,7 +1232,7 @@ defmodule ElixirSense.SuggestionsTest do ] == ElixirSense.suggestions(buffer, 10, 7) # builtin functions not called locally - assert [%{type: :hint, value: "__i"}] == ElixirSense.suggestions(buffer, 11, 8) + assert [] == ElixirSense.suggestions(buffer, 11, 8) end test "functions and module suggestions with __MODULE__" do @@ -1178,7 +1254,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "__MODULE__.SmodO"}, %{ name: "SmodO", type: :module @@ -1186,7 +1261,6 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 9, 18) assert [ - %{type: :hint, value: "__MODULE__.SmodO.test_fun_pub"}, %{ arity: 1, name: "test_fun_pub", @@ -1196,10 +1270,9 @@ defmodule ElixirSense.SuggestionsTest do ] = ElixirSense.suggestions(buffer, 10, 24) # no private on external call - assert [%{type: :hint, value: "__MODULE__.te"}] = ElixirSense.suggestions(buffer, 11, 18) + assert [] = ElixirSense.suggestions(buffer, 11, 18) assert [ - %{type: :hint, value: "__MODULE__.__info__"}, %{ arity: 1, name: "__info__", @@ -1218,9 +1291,7 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 5) - assert Enum.at(list, 0) == %{type: :hint, value: "Elixir"} - - assert Enum.at(list, 1) == %{ + assert Enum.at(list, 0) == %{ type: :module, name: "Elixir", subtype: nil, @@ -1238,7 +1309,7 @@ defmodule ElixirSense.SuggestionsTest do """ list = ElixirSense.suggestions(buffer, 3, 9) - assert Enum.at(list, 1).name == "is_odd" + assert Enum.at(list, 0).name == "is_odd" end test "suggestion for struct fields" do @@ -1251,10 +1322,9 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 14) - |> Enum.filter(&(&1.type in [:field, :hint])) + |> Enum.filter(&(&1.type in [:field])) assert list == [ - %{type: :hint, value: ""}, %{ name: "device", origin: "IO.Stream", @@ -1287,10 +1357,9 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 3, 18) - |> Enum.filter(&(&1.type in [:field, :hint])) + |> Enum.filter(&(&1.type in [:field])) assert list == [ - %{type: :hint, value: ""}, %{ name: "__exception__", origin: "ArgumentError", @@ -1328,7 +1397,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: ""}, %{ name: "device", origin: "IO.Stream", @@ -1373,7 +1441,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: ""}, %{ name: "__struct__", origin: nil, @@ -1388,7 +1455,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: ""}, %{ name: "__struct__", origin: nil, @@ -1412,7 +1478,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: ""}, %{ name: "device", origin: "IO.Stream", @@ -1464,7 +1529,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: ""}, %{ name: "field_1", origin: "MyServer", @@ -1491,7 +1555,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 9, 28) assert list == [ - %{type: :hint, value: ""}, %{ name: "field_1", origin: "MyServer", @@ -1529,7 +1592,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: ""}, %{ name: "field_1", origin: ":my_server", @@ -1556,7 +1618,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 9, 30) assert list == [ - %{type: :hint, value: ""}, %{ name: "field_1", origin: ":my_server", @@ -1594,7 +1655,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 10, 7) assert list == [ - %{type: :hint, value: ""}, %{ name: "field_1", origin: "MyServer", @@ -1629,7 +1689,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 8, 31) assert list == [ - %{type: :hint, value: ""}, %{ name: "field_1", origin: "MyServer", @@ -1667,7 +1726,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: "var_1.field_"}, %{ name: "field_1", origin: "MyServer", @@ -1700,7 +1758,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: "var_1.key_"}, %{name: "key_1", origin: nil, type: :field, call?: true, subtype: :map_key}, %{name: "key_2", origin: nil, type: :field, call?: true, subtype: :map_key} ] @@ -1721,7 +1778,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:field, :hint])) assert list == [ - %{type: :hint, value: "@var_1.key_"}, %{name: "key_1", origin: nil, type: :field, call?: true, subtype: :map_key}, %{name: "key_2", origin: nil, type: :field, call?: true, subtype: :map_key} ] @@ -1742,7 +1798,6 @@ defmodule ElixirSense.SuggestionsTest do |> Enum.filter(&(&1.type in [:function, :hint])) assert [ - %{type: :hint, value: "var_1.to_string"}, %{name: "to_string", origin: "Atom", type: :function} ] = list end @@ -1755,25 +1810,29 @@ defmodule ElixirSense.SuggestionsTest do some_field: "" ] + def some_func() do + false + end + def func(%MyServer{} = some_arg) do %MyServer{so end end """ - list = ElixirSense.suggestions(buffer, 8, 17) + list = ElixirSense.suggestions(buffer, 12, 17) - assert list == [ - %{type: :hint, value: "some_"}, - %{name: "some_arg", type: :variable}, + assert [ %{ origin: "MyServer", type: :field, name: "some_field", call?: false, subtype: :struct_field - } - ] + }, + %{name: "some_arg", type: :variable}, + %{name: "some_func", type: :function} + ] = list end test "suggestion for fields in struct update" do @@ -1793,7 +1852,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 8, 28) assert list == [ - %{type: :hint, value: "field_1"}, %{ call?: false, name: "field_1", @@ -1821,7 +1879,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 8, 20) assert list == [ - %{type: :hint, value: "field_1"}, %{ call?: false, name: "field_1", @@ -1849,7 +1906,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 9, 14) assert list == [ - %{type: :hint, value: "field_1"}, %{ call?: false, name: "field_1", @@ -1872,7 +1928,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 3, 20) assert list == [ - %{type: :hint, value: "field_1"}, %{call?: false, name: "field_1", origin: nil, subtype: :struct_field, type: :field} ] end @@ -1888,7 +1943,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 3, 9) assert list == [ - %{type: :hint, value: "hour"}, %{call?: false, name: "hour", origin: "Time", subtype: :struct_field, type: :field} ] end @@ -1905,7 +1959,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 3, 20) assert list == [ - %{type: :hint, value: "field_1"}, %{call?: false, name: "field_1", origin: nil, subtype: :map_key, type: :field} ] end @@ -1930,7 +1983,6 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 11, 18) assert list == [ - %{type: :hint, value: "other_"}, %{name: "other_arg", type: :variable}, %{ name: "other_func", @@ -1983,8 +2035,7 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 22) - assert [%{type: :hint, value: "__MODULE__.Reducers"}, %{name: "Reducers", type: :module} | _] = - list + assert [%{name: "Reducers", type: :module} | _] = list end test "suggest modules to alias v1.2 syntax" do @@ -1996,7 +2047,7 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 19) - assert [%{type: :hint, value: "Reducers"}, %{name: "Reducers", type: :module}] = list + assert [%{name: "Reducers", type: :module}] = list end test "suggest modules to alias v1.2 syntax with __MODULE__" do @@ -2008,7 +2059,7 @@ defmodule ElixirSense.SuggestionsTest do list = ElixirSense.suggestions(buffer, 2, 23) - assert [%{type: :hint, value: "Reducers"}, %{name: "Reducers", type: :module}] = list + assert [%{name: "Reducers", type: :module}] = list end describe "suggestion for param options" do @@ -2641,7 +2692,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "SameModule.test_fun"}, %{origin: "ElixirSenseExample.SameModule"} ] = ElixirSense.suggestions(buffer, 4, 17) @@ -2657,7 +2707,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "SameModule.test_fun"}, %{origin: "ElixirSenseExample.SameModule"} ] = ElixirSense.suggestions(buffer, 5, 17) @@ -2670,7 +2719,6 @@ defmodule ElixirSense.SuggestionsTest do """ assert [ - %{type: :hint, value: "SameModule.some_test_macro"}, %{origin: "ElixirSenseExample.SameModule"} ] = ElixirSense.suggestions(buffer, 4, 15) end diff --git a/test/server_test.exs b/test/server_test.exs index 895446e8..92affaf9 100644 --- a/test/server_test.exs +++ b/test/server_test.exs @@ -141,7 +141,7 @@ defmodule ElixirSense.ServerTest do } } - assert send_request(socket, request) |> Enum.at(0) == %{type: :hint, value: "List."} + assert %{type: :module, name: "Chars"} = send_request(socket, request) |> Enum.at(0) end test "all_modules request", %{socket: socket, auth_token: auth_token} do @@ -247,6 +247,6 @@ defmodule ElixirSense.ServerUnixSocketTest do } } - assert send_request(socket, request) |> Enum.at(0) == %{type: :hint, value: "List."} + assert %{type: :module, name: "Chars"} = send_request(socket, request) |> Enum.at(0) end end