Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Phoenix integration #281

Merged
merged 14 commits into from
Nov 27, 2023
60 changes: 45 additions & 15 deletions lib/elixir_sense/core/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -396,25 +396,26 @@ defmodule ElixirSense.Core.Source do

# TODO refactor to use Macro.path on elixir 1.14
with {:ok, ast} <- NormalizedCode.Fragment.container_cursor_to_quoted(prefix, columns: true),
{_, {:ok, call, npar, meta, options, cursor_at_option, option}} <-
Macro.prewalk(ast, nil, &find_call_pre/2),
{{m, elixir_prefix}, f} when f not in @excluded_funs <- get_mod_fun(call, binding_env) do
{_, {:ok, call_info}} <- Macro.prewalk(ast, nil, &find_call_pre/2),
{{m, elixir_prefix}, f} when f not in @excluded_funs <-
get_mod_fun(call_info.call, binding_env) do
%{
candidate: {m, f},
elixir_prefix: elixir_prefix,
npar: npar,
pos: {{meta[:line], meta[:column]}, {meta[:line], nil}},
cursor_at_option: cursor_at_option,
options_so_far: options,
option: option
params: call_info.params,
npar: call_info.npar,
pos: {{call_info.meta[:line], call_info.meta[:column]}, {call_info.meta[:line], nil}},
cursor_at_option: call_info.cursor_at_option,
options_so_far: call_info.options,
option: call_info.option
}
else
_ -> nil
end
end

def find_call_pre(ast, {:ok, call, npar, meta, options, cursor_at_option, option}),
do: {ast, {:ok, call, npar, meta, options, cursor_at_option, option}}
def find_call_pre(ast, {:ok, call_info}),
do: {ast, {:ok, call_info}}

# transform `a |> b(c)` calls into `b(a, c)`
def find_call_pre({:|>, _, [params_1, {call, meta, params_rest}]}, state) do
Expand All @@ -436,20 +437,45 @@ defmodule ElixirSense.Core.Source do
defp find_cursor_in_params(params, call, meta) do
case Enum.reverse(params) do
[{:__cursor__, _, []} | rest] ->
{:ok, call, length(rest), meta, [], :maybe, nil}
{:ok,
%{
call: call,
params: Enum.reverse(rest),
npar: length(rest),
meta: meta,
options: [],
cursor_at_option: :maybe,
option: nil
}}

[keyword_list | rest] when is_list(keyword_list) ->
case Enum.reverse(keyword_list) do
[{:__cursor__, _, []} | kl_rest] ->
if Keyword.keyword?(kl_rest) do
{:ok, call, length(rest), meta, Enum.reverse(kl_rest) |> Enum.map(&elem(&1, 0)),
true, nil}
{:ok,
%{
call: call,
params: Enum.reverse(rest),
npar: length(rest),
meta: meta,
options: Enum.reverse(kl_rest) |> Enum.map(&elem(&1, 0)),
cursor_at_option: true,
option: nil
}}
end

[{atom, {:__cursor__, _, []}} | kl_rest] when is_atom(atom) ->
if Keyword.keyword?(kl_rest) do
{:ok, call, length(rest), meta, Enum.reverse(kl_rest) |> Enum.map(&elem(&1, 0)),
false, atom}
{:ok,
%{
call: call,
params: Enum.reverse(rest),
npar: length(rest),
meta: meta,
options: Enum.reverse(kl_rest) |> Enum.map(&elem(&1, 0)),
cursor_at_option: false,
option: atom
}}
end

_ ->
Expand Down Expand Up @@ -500,6 +526,10 @@ defmodule ElixirSense.Core.Source do
def get_mod_fun([atom, fun], _binding_env) when is_atom(atom), do: {{atom, false}, fun}
def get_mod_fun(_, _binding_env), do: nil

def get_mod([{:__aliases__, _, list} | _rest], binding_env) do
get_mod(list, binding_env)
end

def get_mod([{:__MODULE__, _, nil} | rest], binding_env) do
if binding_env.current_module not in [nil, Elixir] do
mod =
Expand Down
105 changes: 105 additions & 0 deletions lib/elixir_sense/plugins/phoenix.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule ElixirSense.Plugins.Phoenix do
@moduledoc false

@behaviour ElixirSense.Plugin

use ElixirSense.Providers.Suggestion.GenericReducer

alias ElixirSense.Core.Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Source

Check warning on line 8 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Source
alias ElixirSense.Core.Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Binding

Check warning on line 9 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Binding
alias ElixirSense.Core.Introspection
alias ElixirSense.Core.ModuleStore
alias ElixirSense.Plugins.Phoenix.Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Scope

Check warning on line 12 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Scope
alias ElixirSense.Plugins.Util
alias ElixirSense.Providers.Suggestion.Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

unused alias Matcher

Check warning on line 14 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

unused alias Matcher

@phoenix_route_funcs ~w(

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

module attribute @phoenix_route_funcs was set but never used

Check warning on line 16 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

module attribute @phoenix_route_funcs was set but never used
get put patch trace
delete head options
forward connect post
)a

@impl true
def setup(context) do
ModuleStore.ensure_compiled(context, Phoenix.Router)
end

if Version.match?(System.version(), ">= 1.14.0") do
@impl true
def suggestions(hint, {Phoenix.Router, func, 1, _info}, _list, opts)
when func in @phoenix_route_funcs do
binding = Binding.from_env(opts.env, opts.buffer_metadata)
{_, scope_alias} = Scope.within_scope(opts.cursor_context.text_before, binding)

case find_controllers(opts.module_store, opts.env, hint, scope_alias) do
[] -> :ignore
controllers -> {:override, controllers}
end
end

def suggestions(
hint,
{Phoenix.Router, func, 2, %{params: [_path, module]}},
_list,
opts
)
when func in @phoenix_route_funcs do
binding_env = Binding.from_env(opts.env, opts.buffer_metadata)
{_, scope_alias} = Scope.within_scope(opts.cursor_context.text_before)
{module, _} = Source.get_mod([module], binding_env)

module = Module.safe_concat(scope_alias, module)

suggestions =
for {export, {2, :function}} when export not in ~w(action call)a <-
Introspection.get_exports(module),
name = inspect(export),
Matcher.match?(name, hint) do
%{
type: :generic,
kind: :function,
label: name,
insert_text: Util.trim_leading_for_insertion(hint, name),
detail: "Phoenix action"
}
end

{:override, suggestions}
end
end

@impl true
def suggestions(_hint, _func_call, _list, _opts) do
:ignore
end

defp find_controllers(module_store, env, hint, scope_alias) do

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

function find_controllers/4 is unused

Check warning on line 76 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

function find_controllers/4 is unused
[prefix | _] =
env.module
|> inspect()
|> String.split(".")

for module <- module_store.list,
mod_str = inspect(module),
Util.match_module?(mod_str, prefix),
mod_str =~ "Controller",
Util.match_module?(mod_str, hint) do
{doc, _} = Introspection.get_module_docs_summary(module)

%{
type: :generic,
kind: :class,
label: mod_str,
insert_text: skip_scope_alias(scope_alias, mod_str),
detail: "Phoenix controller",
documentation: doc
}
end
|> Enum.sort_by(& &1.label)
end

defp skip_scope_alias(nil, insert_text), do: insert_text

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

function skip_scope_alias/2 is unused

Check warning on line 101 in lib/elixir_sense/plugins/phoenix.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

function skip_scope_alias/2 is unused

defp skip_scope_alias(scope_alias, insert_text),
do: String.replace_prefix(insert_text, "#{inspect(scope_alias)}.", "")
end
110 changes: 110 additions & 0 deletions lib/elixir_sense/plugins/phoenix/scope.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule ElixirSense.Plugins.Phoenix.Scope do
@moduledoc false

alias ElixirSense.Core.Source
alias ElixirSense.Core.Binding

import Module, only: [safe_concat: 2, safe_concat: 1]

def within_scope(buffer, binding_env \\ %Binding{}) do
{:ok, ast} = Code.Fragment.container_cursor_to_quoted(buffer)

with {true, scopes_ast} <- get_scopes(ast),
scopes_ast = Enum.reverse(scopes_ast),
scope_alias <- get_scope_alias(scopes_ast, binding_env) do
{true, scope_alias}
end
end

defp get_scopes(ast) do
path = Macro.path(ast, &match?({:__cursor__, _, _}, &1))

scopes =
path
|> Enum.filter(&match?({:scope, _, _}, &1))
|> Enum.map(fn {:scope, meta, params} ->
params = Enum.reject(params, &match?([{:do, _} | _], &1))
{:scope, meta, params}
end)

case scopes do
[] -> {false, nil}
scopes -> {true, scopes}
end
end

defp get_scope_alias(scopes_ast, binding_env, module \\ nil)

# is this possible? scope do ... end
defp get_scope_alias([{:scope, _, []}], _binding_env, module), do: module

# scope "/" do ... end
defp get_scope_alias([{:scope, _, [scope_params]}], _binding_env, module)
when not is_list(scope_params),
do: module

# scope path: "/", alias: ExampleWeb do ... end
defp get_scope_alias([{:scope, _, [scope_params]}], binding_env, module) do
scope_alias = Keyword.get(scope_params, :alias)
scope_alias = get_mod(scope_alias, binding_env)
safe_concat(module, scope_alias)
end

# scope "/", alias: ExampleWeb do ... end
defp get_scope_alias(
[{:scope, _, [_scope_path, scope_params]}],
binding_env,
module
)
when is_list(scope_params) do
scope_alias = Keyword.get(scope_params, :alias)
scope_alias = get_mod(scope_alias, binding_env)
safe_concat(module, scope_alias)
end

# scope "/", ExampleWeb do ... end
defp get_scope_alias(
[{:scope, _, [_scope_path, scope_alias]}],
binding_env,
module
) do
scope_alias = get_mod(scope_alias, binding_env)
safe_concat(module, scope_alias)
end

# scope "/", ExampleWeb, host: "api." do ... end
defp get_scope_alias(
[{:scope, _, [_scope_path, scope_alias, _scope_params]}],
binding_env,
module
) do
scope_alias = get_mod(scope_alias, binding_env)
safe_concat(module, scope_alias)
end

# recurse
defp get_scope_alias([head | tail], binding_env, module) do
scope_alias = get_scope_alias([head], binding_env, module)
safe_concat([module, scope_alias, get_scope_alias(tail, binding_env)])
end

defp get_mod({:__aliases__, _, [scope_alias]}, binding_env) do
get_mod(scope_alias, binding_env)
end

defp get_mod({name, _, nil}, binding_env) when is_atom(name) do
case Binding.expand(binding_env, {:variable, name}) do
{:atom, atom} ->
atom

_ ->
nil
end
end

defp get_mod(scope_alias, binding_env) do
with {mod, _} <- Source.get_mod([scope_alias], binding_env) do
mod
end
end
end
30 changes: 22 additions & 8 deletions lib/elixir_sense/providers/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ defmodule ElixirSense.Providers.Definition do
alias ElixirSense.Core.State.ModFunInfo
alias ElixirSense.Core.State.TypeInfo
alias ElixirSense.Core.State.VarInfo
alias ElixirSense.Core.Source
alias ElixirSense.Core.SurroundContext
alias ElixirSense.Location
alias ElixirSense.Plugins.Phoenix.Scope

@doc """
Finds out where a module, function, macro or variable was defined.
Expand Down Expand Up @@ -174,14 +176,7 @@ defmodule ElixirSense.Providers.Definition do
scope: scope
} = env

m =
case module do
{:atom, a} ->
a

_ ->
nil
end
m = get_module(module, context, env, metadata)

case {m, function}
|> Introspection.actual_mod_fun(
Expand Down Expand Up @@ -249,4 +244,23 @@ defmodule ElixirSense.Providers.Definition do
end
end
end

defp get_module(module, %{end: {line, col}}, env, metadata) do
with {true, module} <- get_phoenix_module(module, env) do
text_before = Source.text_before(metadata.source, line, col)

case Scope.within_scope(text_before) do
{false, _} -> module
{true, scope_alias} -> Module.safe_concat(scope_alias, module)
end
end
end

defp get_phoenix_module(module, env) do
case {Phoenix.Router in env.requires, module} do
{true, {:atom, module}} -> {true, module}
{false, {:atom, module}} -> module
_ -> nil
end
end
end
Loading
Loading