Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/elixir_sense/core/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,27 @@ defmodule ElixirSense.Core.Compiler do

# Macro handling

defp expand_macro(
meta,
Kernel,
:use,
[target_module | _opts] = args,
callback,
state,
env
) do
{expanded_module, state, env} = expand(target_module, state, env)

state =
if is_atom(expanded_module) do
State.add_use(expanded_module, state, env)
else
state
end

expand_macro_callback(meta, Kernel, :use, args, callback, state, env)
end

defp expand_macro(
meta,
Kernel,
Expand Down Expand Up @@ -2153,8 +2174,10 @@ defmodule ElixirSense.Core.Compiler do
# If expanding the macro fails, we just give up.
_kind, _payload ->
# Logger.warning(Exception.format(kind, payload, __STACKTRACE__))
uses = state.uses
# look for cursor in args
{_ast, state, _env} = expand(args, state, env)
state = %{state | uses: uses}

{{{:., meta, [module, fun]}, meta, args}, state, env}
else
Expand Down
8 changes: 8 additions & 0 deletions lib/elixir_sense/core/compiler/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ defmodule ElixirSense.Core.Compiler.State do
attributes: list(list(ElixirSense.Core.State.AttributeInfo.t())),
scope_attributes: list(list(atom)),
behaviours: %{optional(module) => [module]},
uses: %{optional(module) => [module]},
specs: specs_t,
types: types_t,
mods_funs_to_positions: mods_funs_to_positions_t,
Expand Down Expand Up @@ -88,6 +89,7 @@ defmodule ElixirSense.Core.Compiler.State do
defstruct attributes: [[]],
scope_attributes: [[]],
behaviours: %{},
uses: %{},
specs: %{},
types: %{},
mods_funs_to_positions: %{},
Expand Down Expand Up @@ -1050,6 +1052,12 @@ defmodule ElixirSense.Core.Compiler.State do

def add_behaviour(_module, %__MODULE__{} = state, env), do: {nil, state, env}

def add_use(module, %__MODULE__{} = state, env) when is_atom(module) and not is_nil(module) do
update_in(state.uses[env.module], &Enum.uniq([module | &1 || []]))
end

def add_use(_module, %__MODULE__{} = state, _env), do: state

def register_doc(%__MODULE__{} = state, env, :moduledoc, doc_arg) do
current_module = env.module
doc_arg_formatted = format_doc_arg(doc_arg)
Expand Down
3 changes: 3 additions & 0 deletions lib/elixir_sense/core/metadata.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ defmodule ElixirSense.Core.Metadata do
specs: ElixirSense.Core.Compiler.State.specs_t(),
structs: ElixirSense.Core.Compiler.State.structs_t(),
records: ElixirSense.Core.Compiler.State.records_t(),
uses: %{optional(module) => [module]},
error: nil | term,
first_alias_positions: map(),
moduledoc_positions: map()
Expand All @@ -41,6 +42,7 @@ defmodule ElixirSense.Core.Metadata do
specs: %{},
structs: %{},
records: %{},
uses: %{},
error: nil,
first_alias_positions: %{},
moduledoc_positions: %{}
Expand All @@ -62,6 +64,7 @@ defmodule ElixirSense.Core.Metadata do
specs: acc.specs,
structs: acc.structs,
records: acc.records,
uses: acc.uses,
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,
Expand Down
1 change: 1 addition & 0 deletions lib/elixir_sense/core/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ defmodule ElixirSense.Core.Parser do
specs: acc.specs,
structs: acc.structs,
records: acc.records,
uses: acc.uses,
mods_funs_to_positions: acc.mods_funs_to_positions,
cursor_env: acc.cursor_env,
closest_env: acc.closest_env,
Expand Down
171 changes: 162 additions & 9 deletions lib/elixir_sense/providers/definition/locator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -250,19 +250,47 @@ defmodule ElixirSense.Providers.Definition.Locator do

case fn_definition do
nil ->
Location.find_mod_fun_source(mod, fun, call_arity)
# Try to find function in __using__ macro before giving up and pointing to the macro use
case find_function_in_module_using_macro(mod, fun, metadata) do
%Location{file: file} = location when not is_nil(file) ->
location

_ ->
Location.find_mod_fun_source(mod, fun, call_arity)
end

%ModFunInfo{} = info ->
{{line, column}, {end_line, end_column}} = Location.info_to_range(info)

%Location{
file: nil,
type: ModFunInfo.get_category(info),
line: line,
column: column,
end_line: end_line,
end_column: end_column
}
if ModFunInfo.get_category(info) in [:function, :macro] do
# First try to use find_function_in_module_using_macro for external modules
# This is needed when the function is defined in a different file via __using__
case find_function_in_module_using_macro(mod, fun, metadata) do
%Location{file: file} = location when not is_nil(file) ->
location

_ ->
find_function_in_using_macro(
metadata,
env,
line,
column,
end_line,
end_column,
fun,
info
)
end
else
%Location{
file: nil,
type: ModFunInfo.get_category(info),
line: line,
column: column,
end_line: end_line,
end_column: end_column
}
end
end

{mod, fun, true, :type} ->
Expand Down Expand Up @@ -313,4 +341,129 @@ defmodule ElixirSense.Providers.Definition.Locator do
_ -> nil
end
end

defp resolve_use_module({:__aliases__, _, parts}, env) do
[head | tail] = parts

case Keyword.fetch(env.aliases, Module.concat(Elixir, head)) do
{:ok, aliased_mod} ->
Module.concat([aliased_mod | tail])

:error ->
Module.concat(parts)
end
end

defp resolve_use_module(atom, _env) when is_atom(atom), do: atom
defp resolve_use_module(_, _), do: nil

defp find_function_in_module_using_macro(mod, fun, metadata) do
if Map.has_key?(metadata.mods_funs_to_positions, {mod, nil, nil}) do
# Module is in the current source - use metadata.uses
used_modules = Map.get(metadata.uses, mod, [])

Enum.find_value(used_modules, fn used_module ->
search_in_using_macro(used_module, fun)
end)
else
# Module is external - read the file contents and parse it
with file when not is_nil(file) <- get_module_source_file(mod),
{:ok, content} <- File.read(file) do
external_metadata = Parser.parse_string(content, false, false, nil)
used_modules = Map.get(external_metadata.uses, mod, [])

Enum.find_value(used_modules, fn used_module ->
search_in_using_macro(used_module, fun)
end)
else
_ -> nil
end
end
end

defp get_module_source_file(mod) do
compile_info = mod.__info__(:compile)
source = Keyword.get(compile_info, :source)
if source, do: to_string(source)
catch
_, _ -> nil
end

defp search_in_using_macro(module, fun) do
case Location.find_mod_fun_source(module, :__using__, :any) do
%Location{file: file, line: using_line, column: using_col} when not is_nil(file) ->
find_function_def_in_using_macro_source(file, using_line, using_col, fun)

_ ->
nil
end
end

defp find_function_def_in_using_macro_source(file, using_line, using_col, fun) do
content = File.read!(file)
{_, suffix} = Source.split_at(content, using_line, using_col)
regex = ~r/def\s+(#{Regex.escape(Atom.to_string(fun))}\b)/

case Regex.run(regex, suffix, return: :index) do
[_entire_match, {fun_offset, _fun_len}] ->
{line_offset, col_offset} = calculate_offset(suffix, fun_offset)
target_line = using_line + line_offset
target_column = if line_offset == 0, do: using_col + col_offset, else: 1 + col_offset

%Location{
file: file,
type: :function,
line: target_line,
column: target_column,
end_line: target_line,
end_column: target_column
}

nil ->
nil
end
end

defp calculate_offset(suffix, fun_offset) do
suffix
|> String.slice(0, fun_offset)
|> Source.split_lines()
|> then(fn lines -> {length(lines) - 1, String.length(List.last(lines) || "")} end)
end

defp fallback_location(line, column, end_line, end_column, info) do
%Location{
file: nil,
type: if(info, do: ModFunInfo.get_category(info), else: :function),
line: line,
column: column,
end_line: end_line,
end_column: end_column
}
end

defp find_function_in_using_macro(metadata, env, line, column, end_line, end_column, fun, info) do
# This might be a function defined via `use`
# We need to find the `use` statement and the module being used
source_line = Source.split_lines(metadata.source) |> Enum.at(line - 1)

used_module =
case Code.string_to_quoted(source_line) do
{:ok, {:use, _, [module_ast | _]}} ->
resolve_use_module(module_ast, env)

_ ->
nil
end

case used_module && Location.find_mod_fun_source(used_module, :__using__, :any) do
%Location{file: file, line: using_line, column: using_col} = using_location
when not is_nil(file) ->
find_function_def_in_using_macro_source(file, using_line, using_col, fun) ||
using_location

_ ->
fallback_location(line, column, end_line, end_column, info)
end
end
end
Loading