Skip to content

Commit

Permalink
Use token_metadata to get accurate metadata env (#234)
Browse files Browse the repository at this point in the history
* add failing test

* avert crash on invalid attribute

* include token_metadata

* exclude more from calls

* use token_metadata: true in parser

* register end positions for modules and functions

* wip

* make test pass

* skip generated code when finding scopes

* better handling for types

* fix edge case

* fix another edge case

* format

* add tests

fix more edge cases

* small improvement

* fix credo issue
  • Loading branch information
lukaszsamson committed Jul 6, 2023
1 parent daf98d3 commit e5c53b3
Show file tree
Hide file tree
Showing 15 changed files with 852 additions and 802 deletions.
22 changes: 11 additions & 11 deletions lib/elixir_sense.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ defmodule ElixirSense do
%{begin: begin_pos, end: end_pos, context: context} ->
metadata = Parser.parse_string(code, true, true, line)

env = Metadata.get_env(metadata, line)
env = Metadata.get_env(metadata, {line, column})

case Docs.all(context, env, metadata.mods_funs_to_positions, metadata.types) do
{actual_subject, docs} ->
Expand Down Expand Up @@ -101,7 +101,7 @@ defmodule ElixirSense do
...> '''
iex> %{file: path, line: line, column: column} = ElixirSense.definition(code, 3, 11)
iex> "#{Path.basename(path)}:#{to_string(line)}:#{to_string(column)}"
"module_with_functions.ex:6:7"
"module_with_functions.ex:6:3"
"""
@spec definition(String.t(), pos_integer, pos_integer) :: Location.t() | nil
def definition(code, line, column) do
Expand All @@ -112,7 +112,7 @@ defmodule ElixirSense do
%{context: context, begin: {line, col}} ->
buffer_file_metadata = Parser.parse_string(code, true, true, line)

env = Metadata.get_env(buffer_file_metadata, line)
env = Metadata.get_env(buffer_file_metadata, {line, column})

calls =
buffer_file_metadata.calls[line]
Expand Down Expand Up @@ -143,7 +143,7 @@ defmodule ElixirSense do
...> '''
iex> [%{file: path, line: line, column: column}, _] = ElixirSense.implementations(code, 1, 37) |> Enum.sort
iex> "#{Path.basename(path)}:#{to_string(line)}:#{to_string(column)}"
"example_protocol.ex:7:7"
"example_protocol.ex:7:3"
"""
@spec implementations(String.t(), pos_integer, pos_integer) :: [Location.t()]
def implementations(code, line, column) do
Expand All @@ -154,7 +154,7 @@ defmodule ElixirSense do
%{context: context} ->
buffer_file_metadata = Parser.parse_string(code, true, true, line)

env = Metadata.get_env(buffer_file_metadata, line)
env = Metadata.get_env(buffer_file_metadata, {line, column})

Implementation.find(
context,
Expand Down Expand Up @@ -225,13 +225,13 @@ defmodule ElixirSense do
buffer_file_metadata =
maybe_fix_autocomple_on_cursor(buffer_file_metadata, text_before, text_after, line)

env = Metadata.get_env(buffer_file_metadata, line)
env = Metadata.get_env(buffer_file_metadata, {line, column})
module_store = ModuleStore.build()

cursor_context = %{
text_before: text_before,
text_after: text_after,
at_module_body?: Metadata.at_module_body?(buffer_file_metadata, env)
at_module_body?: Metadata.at_module_body?(env)
}

Suggestion.find(hint, env, buffer_file_metadata, cursor_context, module_store, opts)
Expand Down Expand Up @@ -267,7 +267,7 @@ defmodule ElixirSense do
prefix = Source.text_before(code, line, column)
buffer_file_metadata = Parser.parse_string(code, true, true, line)

env = Metadata.get_env(buffer_file_metadata, line)
env = Metadata.get_env(buffer_file_metadata, {line, column})

Signature.find(prefix, env, buffer_file_metadata)
end
Expand Down Expand Up @@ -350,7 +350,7 @@ defmodule ElixirSense do
def expand_full(buffer, code, line) do
buffer_file_metadata = Parser.parse_string(buffer, true, true, line)

env = Metadata.get_env(buffer_file_metadata, line)
env = Metadata.get_env(buffer_file_metadata, {line, 1})

Expand.expand_full(code, env)
end
Expand Down Expand Up @@ -432,7 +432,7 @@ defmodule ElixirSense do
%State.Env{
module: module,
vars: vars
} = Metadata.get_env(buffer_file_metadata, line)
} = Metadata.get_env(buffer_file_metadata, {line, column})

# find last env of current module
attributes = get_attributes(buffer_file_metadata.lines_to_env, module)
Expand Down Expand Up @@ -492,7 +492,7 @@ defmodule ElixirSense do
...> end
...> '''
iex> ElixirSense.string_to_quoted(code, 1)
{:ok, {:defmodule, [line: 1, column: 1], [[do: {:__block__, [], []}]]}}
{:ok, {:defmodule, [do: [line: 1, column: 11], end: [line: 2, column: 1], line: 1, column: 1], [[do: {:__block__, [], []}]]}}
"""
@spec string_to_quoted(String.t(), pos_integer | nil, non_neg_integer, keyword) ::
{:ok, Macro.t()} | {:error, {line :: pos_integer(), term(), term()}}
Expand Down
146 changes: 128 additions & 18 deletions lib/elixir_sense/core/metadata.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,139 @@ defmodule ElixirSense.Core.Metadata do
documentation: String.t()
}

@spec get_env(__MODULE__.t(), pos_integer) :: State.Env.t()
def get_env(%__MODULE__{} = metadata, line) do
case Map.get(metadata.lines_to_env, line) do
nil -> State.default_env()
ctx -> ctx
@spec get_env(__MODULE__.t(), {pos_integer, pos_integer}) :: State.Env.t()
def get_env(%__MODULE__{} = metadata, {line, column}) do
all_scopes =
Enum.to_list(metadata.types) ++
Enum.to_list(metadata.specs) ++
Enum.to_list(metadata.mods_funs_to_positions)

closest_scopes =
all_scopes
|> Enum.map(fn
{{_, fun, nil}, _} when fun != nil ->
nil

{key, %type{positions: positions, end_positions: end_positions, generated: generated}} ->
closest_scope =
Enum.zip([positions, end_positions, generated])
|> Enum.map(fn
{_, _, true} ->
nil

{{begin_line, begin_column}, {end_line, end_column}, _}
when (line > begin_line or (line == begin_line and column >= begin_column)) and
(line < end_line or (line == end_line and column <= end_column)) ->
{{begin_line, begin_column}, {end_line, end_column}}

{{begin_line, begin_column}, nil, _}
when line > begin_line or (line == begin_line and column >= begin_column) ->
case find_closest_ending(all_scopes, {begin_line, begin_column}) do
nil ->
{{begin_line, begin_column}, nil}

{end_line, end_column} ->
if line < end_line or (line == end_line and column < end_column) do
{{begin_line, begin_column}, {end_line, end_column}}
end
end

_ ->
nil
end)
|> Enum.filter(&(&1 != nil))
|> Enum.max(fn -> nil end)

if closest_scope do
{key, type, closest_scope}
end
end)
|> Enum.filter(&(&1 != nil))
|> Enum.sort_by(
fn {_key, _type, {begin_position, _end_position}} ->
begin_position
end,
:desc
)

# |> dbg()

case closest_scopes do
[_ | _] = scopes ->
metadata.lines_to_env
|> Enum.filter(fn {metadata_line, env} ->
Enum.any?(scopes, fn {key, type, {{begin_line, _begin_column}, _}} ->
if metadata_line >= begin_line do
case {key, type} do
{{module, nil, nil}, _} ->
module in env.module_variants and is_atom(env.scope) and env.scope != Elixir

{{module, fun, arity}, State.ModFunInfo} ->
module in env.module_variants and env.scope == {fun, arity}

{{module, fun, arity}, type} when type in [State.TypeInfo, State.SpecInfo] ->
module in env.module_variants and env.scope == {:typespec, fun, arity}
end
end
end)
end)

# |> dbg()
[] ->
metadata.lines_to_env
end
|> Enum.max_by(
fn
{metadata_line, _env} when metadata_line <= line -> metadata_line
_ -> 0
end,
fn ->
{line, State.default_env()}
end
)
|> elem(1)
end

@spec at_module_body?(__MODULE__.t(), State.Env.t()) :: boolean()
def at_module_body?(%__MODULE__{} = metadata, env) do
mod_info = Map.get(metadata.mods_funs_to_positions, {env.module, nil, nil})
defp find_closest_ending(all_scopes, {line, column}) do
all_scopes
|> Enum.map(fn
{{_, fun, nil}, _} when fun != nil ->
nil

with %State.ModFunInfo{positions: [{line, _}]} <- mod_info,
%State.Env{scope_id: mod_scope_id} <- metadata.lines_to_env[line] do
env.scope_id in mod_scope_id..(mod_scope_id + 1) and not match?({_, _}, env.scope)
else
_ ->
false
end
{_key, %{positions: positions, end_positions: end_positions, generated: generated}} ->
Enum.zip([positions, end_positions, generated])
|> Enum.map(fn
{_, _, true} ->
nil

{{begin_line, begin_column}, {end_line, end_column}, _} ->
if {begin_line, begin_column} > {line, column} do
{begin_line, begin_column}
else
if {end_line, end_column} > {line, column} do
{end_line, end_column}
end
end

{{begin_line, begin_column}, nil, _} ->
if {begin_line, begin_column} > {line, column} do
{begin_line, begin_column}
end
end)
|> Enum.filter(&(&1 != nil))
|> Enum.min(fn -> nil end)
end)
|> Enum.filter(&(&1 != nil))
|> Enum.min(fn -> nil end)
end

@spec at_module_body?(State.Env.t()) :: boolean()
def at_module_body?(env) do
is_atom(env.scope) and env.scope != Elixir
end

def get_position_to_insert_alias(%__MODULE__{} = metadata, line) do
env = get_env(metadata, line)
def get_position_to_insert_alias(%__MODULE__{} = metadata, {line, column}) do
env = get_env(metadata, {line, column})
module = env.module

cond do
Expand All @@ -80,7 +190,7 @@ defmodule ElixirSense.Core.Metadata do
%State.ModFunInfo{positions: [{line, column}]} ->
# Hacky :shrug
line_offset = 1
column_offset = -8
column_offset = 2
{line + line_offset, column + column_offset}

_ ->
Expand Down
Loading

0 comments on commit e5c53b3

Please sign in to comment.