diff --git a/apps/remote_control/lib/lexical/remote_control/code_intelligence/definition.ex b/apps/remote_control/lib/lexical/remote_control/code_intelligence/definition.ex index b854f00b8..1dcca5e31 100644 --- a/apps/remote_control/lib/lexical/remote_control/code_intelligence/definition.ex +++ b/apps/remote_control/lib/lexical/remote_control/code_intelligence/definition.ex @@ -7,6 +7,7 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do alias Lexical.Document.Location alias Lexical.Document.Position alias Lexical.Formats + alias Lexical.RemoteControl.Analyzer alias Lexical.RemoteControl.CodeIntelligence.Entity alias Lexical.RemoteControl.Search.Indexer.Entry alias Lexical.RemoteControl.Search.Store @@ -22,76 +23,127 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do end end - defp fetch_definition({type, entity} = resolved, %Analysis{} = analysis, %Position{} = position) + defp fetch_definition({type, _entity} = resolved, %Analysis{} = analysis, %Position{} = pos) when type in [:struct, :module] do + with {:ok, nil} <- exact_module_definition(resolved) do + elixir_sense_definition(analysis, pos) + end + end + + defp fetch_definition({:call, _m, _f, _a} = resolved, %Analysis{} = nlss, %Position{} = pos) do + with {:ok, nil} <- exact_call_definition(resolved, nlss, pos), + {:ok, nil} <- elixir_sense_definition(nlss, pos) do + nearest_arity_call_definition(resolved, nlss, pos) + end + end + + defp fetch_definition(_, %Analysis{} = analysis, %Position{} = position) do + elixir_sense_definition(analysis, position) + end + + defp exact_module_definition({type, entity} = resolved) do module = Formats.module(entity) locations = - case Store.exact(module, type: type, subtype: :definition) do - {:ok, entries} -> - for entry <- entries, - result = to_location(entry), - match?({:ok, _}, result) do - {:ok, location} = result - location - end - - _ -> - [] - end - - maybe_fallback_to_elixir_sense(resolved, locations, analysis, position) - end - - defp fetch_definition( - {:call, module, function, arity} = resolved, - %Analysis{} = analysis, - %Position{} = position - ) do - mfa = Formats.mfa(module, function, arity) + module + |> query_search_index_exact(type: type, subtype: :definition) + |> entries_to_locations() - definitions = - mfa - |> query_search_index(subtype: :definition) - |> Stream.flat_map(fn entry -> - case entry do - %Entry{type: {:function, :delegate}} -> - mfa = get_in(entry, [:metadata, :original_mfa]) - query_search_index(mfa, subtype: :definition) ++ [entry] - - _ -> - [entry] - end - end) - |> Stream.uniq_by(& &1.subject) + case locations do + [location] -> + {:ok, location} - locations = - for entry <- definitions, - result = to_location(entry), - match?({:ok, _}, result) do - {:ok, location} = result - location - end + [_ | _] -> + {:ok, locations} - maybe_fallback_to_elixir_sense(resolved, locations, analysis, position) + [] -> + Logger.info("No definition found for #{inspect(resolved)} with Indexer.") + {:ok, nil} + end end - defp fetch_definition(_, %Analysis{} = analysis, %Position{} = position) do - elixir_sense_definition(analysis, position) - end + defp exact_call_definition({:call, module, function, arity} = resolved, analysis, position) do + mfa = Formats.mfa(module, function, arity) + + locations = + mfa + |> query_search_index_exact(subtype: :definition) + |> Stream.flat_map(&resolve_defdelegate/1) + |> Stream.uniq_by(& &1.subject) + |> maybe_reject_private_defs(module, analysis, position) + |> entries_to_locations() - defp maybe_fallback_to_elixir_sense(resolved, locations, analysis, position) do case locations do + [location] -> + {:ok, location} + + [_ | _] -> + {:ok, locations} + [] -> Logger.info("No definition found for #{inspect(resolved)} with Indexer.") + {:ok, nil} + end + end - elixir_sense_definition(analysis, position) + defp nearest_arity_call_definition({:call, m, f, _a} = resolved, nlss, pos) do + mf_prefix = Formats.mf(m, f) + locations = + mf_prefix + |> query_search_index_prefix(subtype: :definition) + |> Stream.flat_map(&resolve_defdelegate/1) + |> Stream.uniq_by(& &1.subject) + |> maybe_reject_private_defs(m, nlss, pos) + # sort by arity and take the lowest. + |> Enum.sort(fn %Entry{} = a, %Entry{} = b -> + String.last(a.subject) < String.last(b.subject) + end) + |> Enum.take(1) + |> entries_to_locations() + + case locations do [location] -> {:ok, location} - _ -> + [_ | _] -> {:ok, locations} + + [] -> + Logger.info("No nearest-arity definition found for #{inspect(resolved)} with Indexer.") + {:ok, nil} + end + end + + def resolve_defdelegate(%Entry{type: {:function, :delegate}} = entry) do + mfa = get_in(entry, [:metadata, :original_mfa]) + query_search_index_exact(mfa, subtype: :definition) ++ [entry] + end + + def resolve_defdelegate(entry) do + [entry] + end + + defp maybe_reject_private_defs(entries, module, analysis, position) do + case Analyzer.current_module(analysis, position) do + {:ok, module_at_position} -> + if module != module_at_position do + Stream.reject(entries, &(&1.type == {:function, :private})) + else + entries + end + + :error -> + entries + end + end + + defp entries_to_locations(entries) do + for entry <- entries, + result = to_location(entry), + match?({:ok, _}, result) do + {:ok, location} = result + location end end @@ -171,8 +223,18 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do end end - defp query_search_index(subject, condition) do - case Store.exact(subject, condition) do + defp query_search_index_exact(subject, constraints) do + case Store.exact(subject, constraints) do + {:ok, entries} -> + entries + + _ -> + [] + end + end + + defp query_search_index_prefix(subject, constraints) do + case Store.prefix(subject, constraints) do {:ok, entries} -> entries diff --git a/apps/remote_control/test/fixtures/navigations/lib/my_definition.ex b/apps/remote_control/test/fixtures/navigations/lib/my_definition.ex index 28d9d4e54..f40056b68 100644 --- a/apps/remote_control/test/fixtures/navigations/lib/my_definition.ex +++ b/apps/remote_control/test/fixtures/navigations/lib/my_definition.ex @@ -18,6 +18,10 @@ defmodule MyDefinition do "Hello, #{name}!" end + def greet(name, name2) do + "Hello, #{name} and #{name2}!" + end + defmacro print_hello do quote do IO.puts("Hello, world!") diff --git a/apps/remote_control/test/lexical/remote_control/code_intelligence/definition_test.exs b/apps/remote_control/test/lexical/remote_control/code_intelligence/definition_test.exs index fc286bf75..75e6c1f44 100644 --- a/apps/remote_control/test/lexical/remote_control/code_intelligence/definition_test.exs +++ b/apps/remote_control/test/lexical/remote_control/code_intelligence/definition_test.exs @@ -433,6 +433,27 @@ defmodule Lexical.RemoteControl.CodeIntelligence.DefinitionTest do end end + describe "definition/2 when no exact is available" do + setup [:with_referenced_file] + + test "find the definition of a remote function call", %{project: project, uri: referenced_uri} do + subject_module = ~q[ + defmodule UsesRemoteFunction do + alias MyDefinition + + def uses_greet() do + MyDefinition.gree|t("World", "Bad", "Arity") + end + end + ] + + assert {:ok, ^referenced_uri, definition_line} = + definition(project, subject_module, referenced_uri) + + assert definition_line == ~S[ def «greet(name)» do] + end + end + describe "edge cases" do setup [:with_referenced_file] diff --git a/projects/lexical_shared/lib/lexical/formats.ex b/projects/lexical_shared/lib/lexical/formats.ex index f4fc6c9ba..d77613d16 100644 --- a/projects/lexical_shared/lib/lexical/formats.ex +++ b/projects/lexical_shared/lib/lexical/formats.ex @@ -90,6 +90,7 @@ defmodule Lexical.Formats do millis end + @spec plural(integer(), String.t(), String.t()) :: String.t() def plural(count, singular, plural) do case count do 0 -> templatize(count, plural) @@ -98,10 +99,38 @@ defmodule Lexical.Formats do end end + @doc """ + Formats a module, function, and arity into a string. + + ## Examples + + iex> alias LXical.Formats + LXical.Formats + iex> mfa(Formats, :mfa, 3) + "LXical.Formats.mfa/3" + iex> mfa("Formats", "mfa", 3) + "LXical.Formats.mfa/3" + + """ + @spec mfa(atom() | binary(), any(), any()) :: String.t() def mfa(module, function, arity) do "#{module(module)}.#{function}/#{arity}" end + @doc """ + Formats a module and function without arity. + + ## Examples + + iex> mf(LXical.Formats, mf) + "LXical.Formats.mf/" + + """ + @spec mf(atom() | binary(), any()) :: String.t() + def mf(module, function) do + "#{module(module)}.#{function}/" + end + defp templatize(count, template) do count_string = Integer.to_string(count) String.replace(template, "${count}", count_string)