From 5e02409f2350d5a4255628e3da0a228046e93da6 Mon Sep 17 00:00:00 2001 From: Ajay Vignesh K Date: Mon, 9 May 2022 08:57:40 +0530 Subject: [PATCH 1/4] find elixir modules that require adding an alias --- lib/elixir_sense.ex | 4 +- lib/elixir_sense/providers/suggestion.ex | 21 ++- .../providers/suggestion/complete.ex | 132 ++++++++++++++---- .../providers/suggestion/reducers/common.ex | 10 +- .../providers/suggestion/complete_test.exs | 27 +++- 5 files changed, 155 insertions(+), 39 deletions(-) diff --git a/lib/elixir_sense.ex b/lib/elixir_sense.ex index 0de90504..dcc7abee 100644 --- a/lib/elixir_sense.ex +++ b/lib/elixir_sense.ex @@ -203,7 +203,7 @@ defmodule ElixirSense do 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()] - def suggestions(buffer, line, column) do + def suggestions(buffer, line, column, opts \\ []) do hint = Source.prefix(buffer, line, column) buffer_file_metadata = Parser.parse_string(buffer, true, true, line) {text_before, text_after} = Source.split_at(buffer, line, column) @@ -220,7 +220,7 @@ defmodule ElixirSense do at_module_body?: Metadata.at_module_body?(buffer_file_metadata, env) } - Suggestion.find(hint, env, buffer_file_metadata, cursor_context, module_store) + Suggestion.find(hint, env, buffer_file_metadata, cursor_context, module_store, opts) end @doc """ diff --git a/lib/elixir_sense/providers/suggestion.ex b/lib/elixir_sense/providers/suggestion.ex index 1cfdca81..1e7d3225 100644 --- a/lib/elixir_sense/providers/suggestion.ex +++ b/lib/elixir_sense/providers/suggestion.ex @@ -88,7 +88,7 @@ defmodule ElixirSense.Providers.Suggestion do overridable: &Reducers.Overridable.add_overridable/5, param_options: &Reducers.Params.add_options/5, typespecs: &Reducers.TypeSpecs.add_types/5, - populate_common: &Reducers.Common.populate/5, + populate_common: &Reducers.Common.populate/6, variables: &Reducers.Common.add_variables/5, modules: &Reducers.Common.add_modules/5, functions: &Reducers.Common.add_functions/5, @@ -99,12 +99,14 @@ defmodule ElixirSense.Providers.Suggestion do bitstring_options: &Reducers.Bitstring.add_bitstring_options/5 ] + @add_opts_for [:populate_common] + @doc """ Finds all suggestions for a hint based on context information. """ @spec find(String.t(), State.Env.t(), Metadata.t(), cursor_context, ModuleStore.t()) :: [suggestion()] - def find(hint, env, buffer_metadata, cursor_context, module_store) do + def find(hint, env, buffer_metadata, cursor_context, module_store, opts \\ []) do plugins = module_store.by_behaviour[ElixirSense.Plugin] || [] reducers = @@ -114,6 +116,7 @@ defmodule ElixirSense.Providers.Suggestion do {module, &module.reduce/5} end) |> Enum.concat(@reducers) + |> maybe_add_opts(opts) context = plugins @@ -139,4 +142,18 @@ defmodule ElixirSense.Providers.Suggestion do |> Enum.reduce(item, fn module, item -> module.decorate(item) end) end end + + defp maybe_add_opts(reducers, opts) do + Enum.map(reducers, fn {name, reducer} -> + if name in @add_opts_for do + {name, reducer_with_opts(reducer, opts)} + else + {name, reducer} + end + end) + end + + defp reducer_with_opts(fun, opts) do + fn a, b, c, d, e -> fun.(a, b, c, d, e, opts) end + end end diff --git a/lib/elixir_sense/providers/suggestion/complete.ex b/lib/elixir_sense/providers/suggestion/complete.ex index 0b4b069d..118aaad8 100644 --- a/lib/elixir_sense/providers/suggestion/complete.ex +++ b/lib/elixir_sense/providers/suggestion/complete.ex @@ -88,7 +88,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do behaviours: [] end - @spec complete(String.t(), Env.t()) :: + @spec complete(String.t(), Env.t(), list(keyword())) :: [ ElixirSense.Providers.Suggestion.Reducers.Common.func() | ElixirSense.Providers.Suggestion.Reducers.Common.mod() @@ -96,11 +96,11 @@ defmodule ElixirSense.Providers.Suggestion.Complete do | ElixirSense.Providers.Suggestion.Reducers.Common.attribute() | ElixirSense.Providers.Suggestion.Reducers.Struct.field() ] - def complete(hint, %Env{} = env) do - do_expand(hint |> String.to_charlist(), env) + def complete(hint, %Env{} = env, opts \\ []) do + do_expand(hint |> String.to_charlist(), env, opts) end - def do_expand(code, env) do + def do_expand(code, env, opts \\ []) do # TODO remove when we require elixir 1.13 only_structs = case code do @@ -110,7 +110,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do case NormalizedCode.CursorContext.cursor_context(code) do {:alias, hint} when is_list(hint) -> - expand_aliases(List.to_string(hint), env, false) + expand_aliases(List.to_string(hint), env, false, opts) {:alias, prefix, hint} -> expand_prefixed_aliases(prefix, hint, env, false) @@ -119,17 +119,17 @@ defmodule ElixirSense.Providers.Suggestion.Complete do expand_erlang_modules(List.to_string(unquoted_atom), env) {:dot, path, hint} -> - expand_dot(path, List.to_string(hint), false, env, only_structs) + expand_dot(path, List.to_string(hint), false, env, only_structs, opts) {:dot_arity, path, hint} -> - expand_dot(path, List.to_string(hint), true, env, only_structs) + expand_dot(path, List.to_string(hint), true, env, only_structs, opts) {:dot_call, _path, _hint} -> # no need to expand signatures here, we have signatures provider # IEx calls # expand_dot_call(path, List.to_atom(hint), env) # to provide signatures and falls back to expand_local_or_var - expand_expr(env) + expand_expr(env, opts) :expr -> # IEx calls expand_local_or_var("", env) @@ -137,8 +137,8 @@ defmodule ElixirSense.Providers.Suggestion.Complete do # TODO expand_expr(env) after we require elixir 1.13 case code do [?^] -> expand_var("", env) - [?%] -> expand_aliases("", env, true) - _ -> expand_expr(env) + [?%] -> expand_aliases("", env, true, opts) + _ -> expand_expr(env, opts) end {:local_or_var, local_or_var} -> @@ -155,13 +155,13 @@ defmodule ElixirSense.Providers.Suggestion.Complete do # IEx calls # expand_dot_call(path, List.to_atom(hint), env) # to provide signatures and falls back to expand_local_or_var - expand_expr(env) + expand_expr(env, opts) # elixir >= 1.13 {:operator, operator} -> case operator do [?^] -> expand_var("", env) - [?&] -> expand_expr(env) + [?&] -> expand_expr(env, opts) _ -> expand_local(List.to_string(operator), false, env) end @@ -185,7 +185,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do # elixir >= 1.13 {:struct, struct} when is_list(struct) -> - expand_aliases(List.to_string(struct), env, true) + expand_aliases(List.to_string(struct), env, true, opts) # elixir >= 1.14 {:struct, {:alias, prefix, hint}} -> @@ -193,7 +193,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do # elixir >= 1.14 {:struct, {:dot, path, hint}} -> - expand_dot(path, List.to_string(hint), false, env, true) + expand_dot(path, List.to_string(hint), false, env, true, opts) # elixir >= 1.14 {:struct, {:module_attribute, attribute}} -> @@ -213,12 +213,12 @@ defmodule ElixirSense.Providers.Suggestion.Complete do end end - defp expand_dot(path, hint, exact?, env, only_structs) do + defp expand_dot(path, hint, exact?, env, only_structs, opts) do filter = struct_module_filter(only_structs, env) case expand_dot_path(path, env) do {:ok, {:atom, mod}} when hint == "" -> - expand_aliases(mod, "", [], not only_structs, env, filter) + expand_aliases(mod, "", [], not only_structs, env, filter, opts) {:ok, {:atom, mod}} -> expand_require(mod, hint, exact?, env) @@ -315,10 +315,10 @@ defmodule ElixirSense.Providers.Suggestion.Complete do end end - defp expand_expr(env) do + defp expand_expr(env, opts) do local_or_var = expand_local_or_var("", env) erlang_modules = expand_erlang_modules("", env) - elixir_modules = expand_aliases("", env, false) + elixir_modules = expand_aliases("", env, false, opts) attributes = expand_attribute("", env) local_or_var ++ erlang_modules ++ elixir_modules ++ attributes @@ -461,35 +461,35 @@ defmodule ElixirSense.Providers.Suggestion.Complete do ## Elixir modules - defp expand_aliases(all, env, only_structs) do + defp expand_aliases(all, env, only_structs, opts) do filter = struct_module_filter(only_structs, env) case String.split(all, ".") do [hint] -> aliases = match_aliases(hint, env) - expand_aliases(Elixir, hint, aliases, false, env, filter) + expand_aliases(Elixir, hint, aliases, false, env, filter, opts) parts -> hint = List.last(parts) list = Enum.take(parts, length(parts) - 1) case value_from_alias(list, env) do - {:ok, alias} -> expand_aliases(alias, hint, [], false, env, filter) + {:ok, alias} -> expand_aliases(alias, hint, [], false, env, filter, opts) :error -> no() end end end - defp expand_aliases(mod, hint, aliases, include_funs, env, filter) do + defp expand_aliases(mod, hint, aliases, include_funs, env, filter, opts) do aliases - |> Kernel.++(match_elixir_modules(mod, hint, env, filter)) + |> Kernel.++(match_elixir_modules(mod, hint, env, filter, opts)) |> Kernel.++(if include_funs, do: match_module_funs(mod, hint, false, true, env), else: []) |> format_expansion() end defp expand_prefixed_aliases({:local_or_var, '__MODULE__'}, hint, env, only_structs) do if env.scope_module != nil and Introspection.elixir_module?(env.scope_module) do - expand_aliases("#{env.scope_module}.#{hint}", env, only_structs) + expand_aliases("#{env.scope_module}.#{hint}", env, only_structs, []) else no() end @@ -499,7 +499,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do case value_from_binding({:attribute, List.to_atom(attribute)}, env) do {:ok, {:atom, atom}} -> if Introspection.elixir_module?(atom) do - expand_aliases("#{atom}.#{hint}", env, only_structs) + expand_aliases("#{atom}.#{hint}", env, only_structs, []) else no() end @@ -528,7 +528,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do end end - defp match_elixir_modules(module, hint, env, filter) do + defp match_elixir_modules(module, hint, env, filter, opts) do name = Atom.to_string(module) depth = length(String.split(name, ".")) + 1 base = name <> "." <> hint @@ -542,13 +542,28 @@ defmodule ElixirSense.Providers.Suggestion.Complete do valid_alias_piece?("." <> name), concatted = parts |> Enum.take(depth) |> Module.concat(), filter.(concatted) do - {name, concatted} + {name, concatted, false} end + |> Kernel.++(match_elixir_modules_that_require_alias(module, hint, env, filter, opts)) + |> Enum.reject(fn + {_, concatted, true} -> + Enum.find(env.aliases, fn {_as, module} -> + concatted == module + end) + + _rest -> + false + end) |> Enum.uniq_by(&elem(&1, 1)) - |> Enum.map(fn {name, module} -> + |> Enum.map(fn {name, module, required_alias?} -> {desc, meta} = Introspection.get_module_docs_summary(module) subtype = Introspection.get_module_subtype(module) - %{kind: :module, type: :elixir, name: name, desc: {desc, meta}, subtype: subtype} + result = %{kind: :module, type: :elixir, name: name, desc: {desc, meta}, subtype: subtype} + + cond do + required_alias? -> Map.put(result, :required_alias, module) + true -> result + end end) end @@ -586,6 +601,46 @@ defmodule ElixirSense.Providers.Suggestion.Complete do end end + defp match_elixir_modules_that_require_alias(Elixir, hint, env, filter, opts) do + if Keyword.get(opts, :required_alias) do + for {suggestion, required_alias} <- + find_elixir_modules_that_require_alias(Elixir, hint, env), + mod_as_atom = required_alias |> String.to_atom(), + filter.(mod_as_atom), + required_alias_mod = required_alias |> String.split(".") |> Module.concat() do + {suggestion, required_alias_mod, true} + end + else + [] + end + end + + defp match_elixir_modules_that_require_alias(_module, _hint, _env, _filter, _opts), do: [] + + def find_elixir_modules_that_require_alias(Elixir, hint, env) do + get_modules(true, env) + |> Enum.sort() + |> Enum.dedup() + |> Enum.reduce([], fn module, acc -> + module_parts = module |> String.split(".") + + maybe_index = + Enum.find_index(module_parts, fn module_part -> Matcher.match?(module_part, hint) end) + + case maybe_index do + nil -> + acc + + index -> + required_alias = Enum.slice(module_parts, 0..index) + [suggestion | _] = Enum.reverse(required_alias) + required_alias = required_alias |> Module.concat() |> Atom.to_string() + [{suggestion, required_alias} | acc] + end + end) + |> Enum.filter(fn {suggestion, _required_alias} -> valid_alias_piece?("." <> suggestion) end) + end + defp match_modules(hint, root, env) do hint_parts = hint |> String.split(".") hint_parts_length = length(hint_parts) @@ -894,6 +949,25 @@ defmodule ElixirSense.Providers.Suggestion.Complete do [%{type: :field, name: name, subtype: subtype, origin: origin, call?: true}] end + defp to_entries(%{ + kind: :module, + name: name, + required_alias: module, + desc: {desc, metadata}, + subtype: subtype + }) do + [ + %{ + type: :module, + name: name, + required_alias: module, + subtype: subtype, + summary: desc, + metadata: metadata + } + ] + end + defp to_entries(%{kind: :module, name: name, desc: {desc, metadata}, subtype: subtype}) do [%{type: :module, name: name, subtype: subtype, summary: desc, metadata: metadata}] end diff --git a/lib/elixir_sense/providers/suggestion/reducers/common.ex b/lib/elixir_sense/providers/suggestion/reducers/common.ex index b73d5493..cae949b8 100644 --- a/lib/elixir_sense/providers/suggestion/reducers/common.ex +++ b/lib/elixir_sense/providers/suggestion/reducers/common.ex @@ -58,7 +58,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do * Variable fields """ - def populate(hint, env, buffer_metadata, context, acc) do + def populate(hint, env, buffer_metadata, context, acc, opts \\ []) do text_before = context.text_before %Metadata{ @@ -76,7 +76,8 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do metadata_specs, metadata_types, structs, - text_before + text_before, + opts ) suggestions_by_type = Enum.group_by(suggestions, & &1.type) @@ -159,7 +160,8 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do metadata_specs, metadata_types, structs, - text_before + text_before, + opts ) do env = %Complete.Env{ aliases: aliases, @@ -194,6 +196,6 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do hint end - Complete.complete(hint, env) + Complete.complete(hint, env, opts) end end diff --git a/test/elixir_sense/providers/suggestion/complete_test.exs b/test/elixir_sense/providers/suggestion/complete_test.exs index 337b1c2b..ec8fd444 100644 --- a/test/elixir_sense/providers/suggestion/complete_test.exs +++ b/test/elixir_sense/providers/suggestion/complete_test.exs @@ -24,8 +24,8 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do alias ElixirSense.Providers.Suggestion.Complete.Env alias ElixirSense.Core.State.{ModFunInfo, SpecInfo, VarInfo, AttributeInfo} - def expand(expr, env \\ %Env{}) do - ElixirSense.Providers.Suggestion.Complete.do_expand(expr, env) + def expand(expr, env \\ %Env{}, opts \\ []) do + ElixirSense.Providers.Suggestion.Complete.do_expand(expr, env, opts) end test "erlang module completion" do @@ -284,6 +284,29 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do }) end + test "find elixir modules that require alias" do + assert [ + %{metadata: %{}, name: "Chars", required_alias: String.Chars}, + %{metadata: %{}, name: "Chars", required_alias: List.Chars}, + %{ + metadata: %{}, + name: "CallerWithAliasesAndImports", + required_alias: + ElixirSense.Providers.ReferencesTest.Modules.CallerWithAliasesAndImports + } + ] = expand('Char', %Env{}, required_alias: true) + end + + test "does not suggest required_alias when alias already exists" do + env = %Env{ + aliases: [{MyChars, String.Chars}] + } + + results = expand('Char', env, required_alias: true) + + refute Enum.find(results, fn expansion -> expansion[:required_alias] == String.Chars end) + end + test "elixir submodule no completion" do assert expand('IEx.Xyz') == [] end From a8763899e8f23fb7b959c0c44ce8e7c0c3b5f572 Mon Sep 17 00:00:00 2001 From: Ajay Vignesh K Date: Sun, 17 Jul 2022 08:21:35 +0530 Subject: [PATCH 2/4] Add methods to get positions of first alias and moduledoc to be useful for inserting alias --- lib/elixir_sense/core/metadata.ex | 29 ++++++- lib/elixir_sense/core/metadata_builder.ex | 18 +++-- lib/elixir_sense/core/parser.ex | 4 +- lib/elixir_sense/core/state.ex | 75 ++++++++++++++++++- .../core/metadata_builder_test.exs | 47 ++++++++++++ test/elixir_sense/core/metadata_test.exs | 67 +++++++++++++++++ 6 files changed, 232 insertions(+), 8 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 3222635b..effb3f69 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -28,7 +28,9 @@ defmodule ElixirSense.Core.Metadata do types: %{}, specs: %{}, structs: %{}, - error: nil + error: nil, + first_alias_positions: nil, + moduledoc_positions: nil @type signature_t :: %{ name: String.t(), @@ -58,6 +60,31 @@ defmodule ElixirSense.Core.Metadata do end end + def get_position_to_insert_alias(%__MODULE__{} = metadata, line) do + env = get_env(metadata, line) + module = env.module + + cond do + Map.has_key?(metadata.first_alias_positions, module) -> + Map.get(metadata.first_alias_positions, module) + + Map.has_key?(metadata.moduledoc_positions, module) -> + Map.get(metadata.moduledoc_positions, module) + + true -> + mod_info = Map.get(metadata.mods_funs_to_positions, {env.module, nil, nil}) + + case mod_info do + %State.ModFunInfo{positions: [{line, column}]} -> + # Hacky :shrug + {line + 1, column - 10 + 2} + + _ -> + nil + end + end + end + def get_calls(%__MODULE__{} = metadata, line) do case Map.get(metadata.calls, line) do nil -> [] diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 95dd3409..cf1b6ece 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -619,6 +619,9 @@ defmodule ElixirSense.Core.MetadataBuilder do {get_binding_type(state, param), true} end + state = + add_moduledoc_positions(state, [line: line, column: column], [{name, meta, params}], line) + new_ast = {:@, meta_attr, [{name, add_no_call(meta), params}]} pre_module_attribute(new_ast, state, {line, column}, name, type, is_definition) end @@ -704,46 +707,50 @@ defmodule ElixirSense.Core.MetadataBuilder do # alias with `as` option defp pre( - {:alias, [line: line, column: _column], + {:alias, [line: line, column: column], [{_, _, module_expression = [_ | _]}, [as: alias_expression]]} = ast, state ) do module = concat_module_expression(state, module_expression) alias_tuple = alias_tuple(module, alias_expression) + state = add_first_alias_positions(state, line, column) pre_alias(ast, state, line, alias_tuple) end # alias for submodule of __MODULE__ with `as` option defp pre( - {:alias, [line: line, column: _column], [{:__MODULE__, _, nil}, [as: alias_expression]]} = + {:alias, [line: line, column: column], [{:__MODULE__, _, nil}, [as: alias_expression]]} = ast, state ) do module = get_current_module(state) alias_tuple = alias_tuple(module, alias_expression) + state = add_first_alias_positions(state, line, column) pre_alias(ast, state, line, alias_tuple) end # alias atom module with `as` option - defp pre({:alias, [line: line, column: _column], [mod, [as: alias_expression]]} = ast, state) + defp pre({:alias, [line: line, column: column], [mod, [as: alias_expression]]} = ast, state) when is_atom(mod) do alias_tuple = alias_tuple(mod, alias_expression) + state = add_first_alias_positions(state, line, column) pre_alias(ast, state, line, alias_tuple) end # alias defp pre( - {:alias, [line: line, column: _column], + {:alias, [line: line, column: column], [{:__aliases__, _, module_expression = [_ | _]}, _opts]} = ast, state ) do module = concat_module_expression(state, module_expression) alias_tuple = {Module.concat([List.last(module_expression)]), module} + state = add_first_alias_positions(state, line, column) pre_alias(ast, state, line, alias_tuple) end # alias atom module - defp pre({:alias, [line: line, column: _column], [mod, _opts]} = ast, state) + defp pre({:alias, [line: line, column: column], [mod, _opts]} = ast, state) when is_atom(mod) do alias_tuple = if Introspection.elixir_module?(mod) do @@ -752,6 +759,7 @@ defmodule ElixirSense.Core.MetadataBuilder do {mod, mod} end + state = add_first_alias_positions(state, line, column) pre_alias(ast, state, line, alias_tuple) end diff --git a/lib/elixir_sense/core/parser.ex b/lib/elixir_sense/core/parser.ex index b9f6d318..b483aff1 100644 --- a/lib/elixir_sense/core/parser.ex +++ b/lib/elixir_sense/core/parser.ex @@ -94,7 +94,9 @@ defmodule ElixirSense.Core.Parser do mods_funs_to_positions: acc.mods_funs_to_positions, lines_to_env: acc.lines_to_env, vars_info_per_scope_id: acc.vars_info_per_scope_id, - calls: acc.calls + calls: acc.calls, + first_alias_positions: acc.first_alias_positions, + moduledoc_positions: acc.moduledoc_positions } end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 80b2f077..248bb2ee 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -52,6 +52,8 @@ defmodule ElixirSense.Core.State do calls: calls_t, structs: structs_t, types: types_t, + first_alias_positions: map(), + moduledoc_positions: map(), # TODO binding_context: list } @@ -76,7 +78,9 @@ defmodule ElixirSense.Core.State do calls: %{}, structs: %{}, types: %{}, - binding_context: [] + binding_context: [], + first_alias_positions: %{}, + moduledoc_positions: %{} defmodule Env do @moduledoc """ @@ -303,6 +307,75 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} end + def add_moduledoc_positions( + %__MODULE__{} = state, + [line: line, column: column], + [{:moduledoc, _meta, [here_doc]}], + line + ) + when is_integer(line) and is_binary(here_doc) do + module_name = module_name(state) + + line_to_insert_alias = + here_doc |> String.split("\n") |> Enum.count() |> then(fn count -> line + count + 1 end) + + %__MODULE__{ + state + | moduledoc_positions: + Map.put(state.moduledoc_positions, module_name, {line_to_insert_alias, column}) + } + end + + def add_moduledoc_positions( + %__MODULE__{} = state, + [line: line, column: column], + [{:moduledoc, _meta, [params]}], + line + ) + when is_integer(line) and is_boolean(params) do + module_name = module_name(state) + + line_to_insert_alias = line + 1 + + %__MODULE__{ + state + | moduledoc_positions: + Map.put(state.moduledoc_positions, module_name, {line_to_insert_alias, column}) + } + end + + def add_moduledoc_positions(state, _, _, _), do: state + + def add_first_alias_positions(%__MODULE__{} = state, line, column) + when is_integer(line) and is_integer(column) do + current_scope = hd(hd(state.scopes)) + + is_module? = is_atom(current_scope) + + if is_module? do + module_name = module_name(state) + + %__MODULE__{ + state + | first_alias_positions: + Map.put_new(state.first_alias_positions, module_name, {line, column}) + } + else + state + end + end + + defp module_name(state) do + hd(state.scopes) + |> Enum.reverse() + |> then(fn + [Elixir | rest] -> rest + rest -> rest + end) + |> Enum.filter(&is_atom/1) + |> Module.concat() + end + def add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do call = %CallInfo{mod: mod, func: func, arity: arity, position: position} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index c83763a3..d2ff00ed 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -26,6 +26,37 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ) =~ "def function_arity_zero" end + test "moduledoc heredoc version" do + state = + """ + defmodule Outer do + @moduledoc \"\"\" + This is the here doc version + \"\"\" + defmodule Inner do + @moduledoc \"\"\" + This is the Inner modules moduledoc + \"\"\" + end + end + """ + |> string_to_state + + assert %{Outer => {5, 3}, Outer.Inner => {9, 5}} = state.moduledoc_positions + end + + test "moduledoc boolean version" do + state = + """ + defmodule Outer do + @moduledoc false + end + """ + |> string_to_state + + assert %{Outer => {3, 3}} = state.moduledoc_positions + end + test "module attributes" do state = """ @@ -3382,6 +3413,22 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end + test "first_alias_positions" do + state = + """ + defmodule OuterMod do + alias Foo.Bar + alias Foo1.Bar1 + defmodule InnerMod do + alias Baz.Quz + end + end + """ + |> string_to_state + + assert %{OuterMod => {2, 3}, OuterMod.InnerMod => {5, 5}} = state.first_alias_positions + end + test "use" do state = """ diff --git a/test/elixir_sense/core/metadata_test.exs b/test/elixir_sense/core/metadata_test.exs index 2c3b4477..5d03b17c 100644 --- a/test/elixir_sense/core/metadata_test.exs +++ b/test/elixir_sense/core/metadata_test.exs @@ -138,4 +138,71 @@ defmodule ElixirSense.Core.MetadataTest do env = Metadata.get_env(metadata, 17) refute Metadata.at_module_body?(metadata, env) end + + test "get_position_to_insert_alias when aliases exist" do + code = """ + defmodule MyModule do + alias Foo.Bar #2 + + def foo do + IO.puts() #5 + end + + defmodule Inner do + alias Foo.Bar #9 + def bar do + IO.puts() #11 + end + end + end + """ + + line_number = 5 + metadata = Parser.parse_string(code, true, true, line_number) + position = Metadata.get_position_to_insert_alias(metadata, line_number) + + assert {2, 3} == position + + line_number = 11 + metadata = Parser.parse_string(code, true, true, line_number) + position = Metadata.get_position_to_insert_alias(metadata, line_number) + + assert {9, 5} == position + end + + test "get_position_to_insert_alias when moduledoc exists" do + code = """ + defmodule MyModule do + @moduledoc \"\"\" + New module without any aliases + \"\"\" + + def foo do + #7 + end + end + """ + + line_number = 7 + metadata = Parser.parse_string(code, true, true, line_number) + position = Metadata.get_position_to_insert_alias(metadata, line_number) + + assert {5, 3} == position + end + + test "get_position_to_insert_alias when neither alias nor moduledoc exists" do + code = """ + defmodule MyModule do + def foo do + #3 + end + end + """ + + line_number = 3 + metadata = Parser.parse_string(code, true, true, line_number) + position = Metadata.get_position_to_insert_alias(metadata, line_number) + + assert {2, 3} == position + end end From 8771e49f1d71e4c1d8d723941a5ae1376ba17322 Mon Sep 17 00:00:00 2001 From: Ajay Vignesh K Date: Wed, 5 Oct 2022 14:29:24 +0530 Subject: [PATCH 3/4] Apply review suggestions --- lib/elixir_sense.ex | 2 +- lib/elixir_sense/core/metadata.ex | 12 ++++++++---- lib/elixir_sense/core/state.ex | 12 ++++++------ lib/elixir_sense/providers/suggestion.ex | 2 +- lib/elixir_sense/providers/suggestion/complete.ex | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/elixir_sense.ex b/lib/elixir_sense.ex index dcc7abee..2525af66 100644 --- a/lib/elixir_sense.ex +++ b/lib/elixir_sense.ex @@ -202,7 +202,7 @@ defmodule ElixirSense do name: "insert_at", metadata: %{}, snippet: nil, visibility: :public, 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()] + @spec suggestions(String.t(), pos_integer, pos_integer, keyword()) :: [Suggestion.suggestion()] def suggestions(buffer, line, column, opts \\ []) do hint = Source.prefix(buffer, line, column) buffer_file_metadata = Parser.parse_string(buffer, true, true, line) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index effb3f69..600ca9ea 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -17,7 +17,9 @@ defmodule ElixirSense.Core.Metadata do types: State.types_t(), specs: State.specs_t(), structs: State.structs_t(), - error: nil | term + error: nil | term, + first_alias_positions: map(), + moduledoc_positions: map() } defstruct source: "", @@ -29,8 +31,8 @@ defmodule ElixirSense.Core.Metadata do specs: %{}, structs: %{}, error: nil, - first_alias_positions: nil, - moduledoc_positions: nil + first_alias_positions: %{}, + moduledoc_positions: %{} @type signature_t :: %{ name: String.t(), @@ -77,7 +79,9 @@ defmodule ElixirSense.Core.Metadata do case mod_info do %State.ModFunInfo{positions: [{line, column}]} -> # Hacky :shrug - {line + 1, column - 10 + 2} + line_offset = 1 + column_offset = -8 + {line + line_offset, column + column_offset} _ -> nil diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 248bb2ee..83f514b8 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -316,8 +316,8 @@ defmodule ElixirSense.Core.State do when is_integer(line) and is_binary(here_doc) do module_name = module_name(state) - line_to_insert_alias = - here_doc |> String.split("\n") |> Enum.count() |> then(fn count -> line + count + 1 end) + new_line_count = here_doc |> String.split("\n") |> Enum.count() + line_to_insert_alias = new_line_count + line + 1 %__MODULE__{ state @@ -368,14 +368,14 @@ defmodule ElixirSense.Core.State do defp module_name(state) do hd(state.scopes) |> Enum.reverse() - |> then(fn - [Elixir | rest] -> rest - rest -> rest - end) + |> after_elixir_prefix() |> Enum.filter(&is_atom/1) |> Module.concat() end + defp after_elixir_prefix([Elixir | rest]), do: rest + defp after_elixir_prefix(rest), do: rest + def add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do call = %CallInfo{mod: mod, func: func, arity: arity, position: position} diff --git a/lib/elixir_sense/providers/suggestion.ex b/lib/elixir_sense/providers/suggestion.ex index 1e7d3225..071c2c6c 100644 --- a/lib/elixir_sense/providers/suggestion.ex +++ b/lib/elixir_sense/providers/suggestion.ex @@ -104,7 +104,7 @@ defmodule ElixirSense.Providers.Suggestion do @doc """ Finds all suggestions for a hint based on context information. """ - @spec find(String.t(), State.Env.t(), Metadata.t(), cursor_context, ModuleStore.t()) :: + @spec find(String.t(), State.Env.t(), Metadata.t(), cursor_context, ModuleStore.t(), keyword()) :: [suggestion()] def find(hint, env, buffer_metadata, cursor_context, module_store, opts \\ []) do plugins = module_store.by_behaviour[ElixirSense.Plugin] || [] diff --git a/lib/elixir_sense/providers/suggestion/complete.ex b/lib/elixir_sense/providers/suggestion/complete.ex index 118aaad8..bf98409b 100644 --- a/lib/elixir_sense/providers/suggestion/complete.ex +++ b/lib/elixir_sense/providers/suggestion/complete.ex @@ -88,7 +88,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do behaviours: [] end - @spec complete(String.t(), Env.t(), list(keyword())) :: + @spec complete(String.t(), Env.t(), keyword()) :: [ ElixirSense.Providers.Suggestion.Reducers.Common.func() | ElixirSense.Providers.Suggestion.Reducers.Common.mod() From d8769252eca80462fbf5812d3260a82dd839835b Mon Sep 17 00:00:00 2001 From: Ajay Vignesh K Date: Wed, 5 Oct 2022 17:59:18 +0530 Subject: [PATCH 4/4] Fix credo errors --- .../providers/suggestion/complete.ex | 7 +++-- .../providers/suggestion/reducers/common.ex | 29 +++++-------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/lib/elixir_sense/providers/suggestion/complete.ex b/lib/elixir_sense/providers/suggestion/complete.ex index bf98409b..16803230 100644 --- a/lib/elixir_sense/providers/suggestion/complete.ex +++ b/lib/elixir_sense/providers/suggestion/complete.ex @@ -560,9 +560,10 @@ defmodule ElixirSense.Providers.Suggestion.Complete do subtype = Introspection.get_module_subtype(module) result = %{kind: :module, type: :elixir, name: name, desc: {desc, meta}, subtype: subtype} - cond do - required_alias? -> Map.put(result, :required_alias, module) - true -> result + if required_alias? do + Map.put(result, :required_alias, module) + else + result end end) end diff --git a/lib/elixir_sense/providers/suggestion/reducers/common.ex b/lib/elixir_sense/providers/suggestion/reducers/common.ex index cae949b8..b609f28a 100644 --- a/lib/elixir_sense/providers/suggestion/reducers/common.ex +++ b/lib/elixir_sense/providers/suggestion/reducers/common.ex @@ -61,24 +61,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do def populate(hint, env, buffer_metadata, context, acc, opts \\ []) do text_before = context.text_before - %Metadata{ - structs: structs, - mods_funs_to_positions: mods_and_funs, - specs: metadata_specs, - types: metadata_types - } = buffer_metadata - - suggestions = - find_mods_funcs( - hint, - env, - mods_and_funs, - metadata_specs, - metadata_types, - structs, - text_before, - opts - ) + suggestions = find_mods_funcs(hint, env, buffer_metadata, text_before, opts) suggestions_by_type = Enum.group_by(suggestions, & &1.type) @@ -156,10 +139,12 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do scope: scope, behaviours: behaviours }, - mods_and_funs, - metadata_specs, - metadata_types, - structs, + %Metadata{ + structs: structs, + mods_funs_to_positions: mods_and_funs, + specs: metadata_specs, + types: metadata_types + }, text_before, opts ) do