Skip to content

Commit

Permalink
feat(definition): go to definition when aliasing modules
Browse files Browse the repository at this point in the history
  • Loading branch information
mhanberg committed Sep 16, 2023
1 parent b14a09d commit f799963
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 91 deletions.
186 changes: 131 additions & 55 deletions lib/next_ls/ast_helpers.ex
Original file line number Diff line number Diff line change
@@ -1,75 +1,151 @@
defmodule NextLS.ASTHelpers do
@moduledoc false

@spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil
def get_attribute_reference_name(file, line, column) do
ast = ast_from_file(file)

{_ast, name} =
Macro.prewalk(ast, nil, fn
{:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"}
other, acc -> {other, acc}
end)
defmodule Attributes do
@moduledoc false
@spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil
def get_attribute_reference_name(file, line, column) do
ast = ast_from_file(file)

{_ast, name} =
Macro.prewalk(ast, nil, fn
{:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"}
other, acc -> {other, acc}
end)

name
end

name
end
@spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]
def get_module_attributes(file, module) do
reserved_attributes = Module.reserved_attributes()

@spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]
def get_module_attributes(file, module) do
reserved_attributes = Module.reserved_attributes()
symbols = parse_symbols(file, module)

symbols = parse_symbols(file, module)
Enum.filter(symbols, fn
{:attribute, "@" <> name, _, _} ->
not Map.has_key?(reserved_attributes, String.to_atom(name))

Enum.filter(symbols, fn
{:attribute, "@" <> name, _, _} ->
not Map.has_key?(reserved_attributes, String.to_atom(name))
_other ->
false
end)
end

_other ->
false
end)
end
defp parse_symbols(file, module) do
ast = ast_from_file(file)

defp parse_symbols(file, module) do
ast = ast_from_file(file)
{_ast, %{symbols: symbols}} =
Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module))

{_ast, %{symbols: symbols}} =
Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module))
symbols
end

symbols
end
# add module name to modules stack on enter
defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do
modules = [module_name_atoms | acc.modules]
{ast, %{acc | modules: modules}}
end

# add module name to modules stack on enter
defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do
modules = [module_name_atoms | acc.modules]
{ast, %{acc | modules: modules}}
end
defp prewalk(ast, acc), do: {ast, acc}

defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do
ast_module =
acc.modules
|> Enum.reverse()
|> List.flatten()
|> Module.concat()

if module == ast_module do
symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols]
{ast, %{acc | symbols: symbols}}
else
{ast, acc}
end
end

defp prewalk(ast, acc), do: {ast, acc}
# remove module name from modules stack on exit
defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do
[_exit_mudule | modules] = acc.modules
{ast, %{acc | modules: modules}}
end

defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do
ast_module =
acc.modules
|> Enum.reverse()
|> List.flatten()
|> Module.concat()
defp postwalk(ast, acc, _module), do: {ast, acc}

if module == ast_module do
symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols]
{ast, %{acc | symbols: symbols}}
else
{ast, acc}
defp ast_from_file(file) do
file |> File.read!() |> Code.string_to_quoted!(columns: true)
end
end

# remove module name from modules stack on exit
defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do
[_exit_mudule | modules] = acc.modules
{ast, %{acc | modules: modules}}
end

defp postwalk(ast, acc, _module), do: {ast, acc}

defp ast_from_file(file) do
file |> File.read!() |> Code.string_to_quoted!(columns: true)
defmodule Aliases do
@moduledoc """
Responsible for extracting the relevant portion from a single or multi alias.
## Example
```elixir
alias Foo.Bar.Baz
# ^^^^^^^^^^^
alias Foo.Bar.{Baz, Bing}
# ^^^ ^^^^
alias Foo.Bar.{
Baz,
# ^^^
Bing
# ^^^^
}
```
"""

def extract_alias_range(code, {start, stop}, ale) do
lines =
code
|> String.split("\n")
|> Enum.map(&String.split(&1, ""))
|> Enum.slice((start.line - 1)..(stop.line - 1))

code =
if start.line == stop.line do
[line] = lines

line
|> Enum.slice(start.col..stop.col)
|> Enum.join()
else
[first | rest] = lines
first = Enum.drop(first, start.col)

[last | rest] = Enum.reverse(rest)

length = Enum.count(last)
last = Enum.drop(last, -(length - stop.col - 1))

Enum.map_join([first | Enum.reverse([last | rest])], "\n", &Enum.join(&1, ""))
end

{_, range} =
code
|> Code.string_to_quoted!(columns: true, column: start.col, token_metadata: true)
|> Macro.prewalk(nil, fn ast, range ->
range =
case ast do
{:__aliases__, meta, aliases} ->
if ale == List.last(aliases) do
{{meta[:line] + start.line - 1, meta[:column]},
{meta[:last][:line] + start.line - 1, meta[:last][:column] + String.length(to_string(ale)) - 1}}
else
range
end

_ ->
range
end

{ast, range}
end)

range
end
end
end
7 changes: 5 additions & 2 deletions lib/next_ls/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ defmodule NextLS.DB do
line = meta[:line] || 1
col = meta[:column] || 0

{start_line, start_column} = {line, col}
{end_line, end_column} = {line, col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))}
{start_line, start_column} = reference[:range][:start] || {line, col}

{end_line, end_column} =
reference[:range][:stop] ||
{line, col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))}

__query__(
{conn, s.logger},
Expand Down
50 changes: 30 additions & 20 deletions lib/next_ls/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,36 @@ defmodule NextLS.Definition do
alias NextLS.DB

def fetch(file, {line, col}, db) do
with [[_pk, identifier, _arity, _file, type, module, _start_l, _start_c, _end_l, _end_c | _]] <-
DB.query(
db,
~Q"""
SELECT
*
FROM
'references' AS refs
WHERE
refs.file = ?
AND ? BETWEEN refs.start_line AND refs.end_line
AND ? BETWEEN refs.start_column AND refs.end_column
ORDER BY refs.id asc
LIMIT 1;
""",
[file, line, col]
) do
rows =
DB.query(
db,
~Q"""
SELECT
*
FROM
'references' AS refs
WHERE
refs.file = ?
AND refs.start_line <= ?
AND ? <= refs.end_line
AND refs.start_column <= ?
AND ? <= refs.end_column
ORDER BY refs.id asc
LIMIT 1;
""",
[file, line, line, col, col]
)

reference =
case rows do
[[_pk, identifier, _arity, _file, type, module, _start_l, _start_c, _end_l, _end_c | _]] ->
%{identifier: identifier, type: type, module: module}

[] ->
nil
end

with %{identifier: identifier, type: type, module: module} <- reference do
query =
~Q"""
SELECT
Expand Down Expand Up @@ -53,9 +66,6 @@ defmodule NextLS.Definition do
else
nil
end
else
_ ->
nil
end
end
end
34 changes: 31 additions & 3 deletions lib/next_ls/runtime/sidecar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ defmodule NextLS.Runtime.Sidecar do
@moduledoc false
use GenServer

alias NextLS.ASTHelpers
alias NextLS.ASTHelpers.Aliases
alias NextLS.ASTHelpers.Attributes
alias NextLS.DB

def start_link(args) do
Expand All @@ -16,15 +17,38 @@ defmodule NextLS.Runtime.Sidecar do
end

def handle_info({:tracer, payload}, state) do
attributes = ASTHelpers.get_module_attributes(payload.file, payload.module)
attributes = Attributes.get_module_attributes(payload.file, payload.module)
payload = Map.put_new(payload, :symbols, attributes)
DB.insert_symbol(state.db, payload)

{:noreply, state}
end

def handle_info({{:tracer, :reference, :alias}, payload}, state) do
if payload.meta[:end_of_expression] do
start = %{line: payload.meta[:line], col: payload.meta[:column]}
stop = %{line: payload.meta[:end_of_expression][:line], col: payload.meta[:end_of_expression][:column]}

{start, stop} =
Aliases.extract_alias_range(
File.read!(payload.file),
{start, stop},
payload.identifier |> Macro.to_string() |> String.to_atom()
)

payload =
payload
|> Map.put(:identifier, payload.module)
|> Map.put(:range, %{start: start, stop: stop})

DB.insert_reference(state.db, payload)
end

{:noreply, state}
end

def handle_info({{:tracer, :reference, :attribute}, payload}, state) do
name = ASTHelpers.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column])
name = Attributes.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column])
if name, do: DB.insert_reference(state.db, %{payload | identifier: name})

{:noreply, state}
Expand All @@ -38,7 +62,11 @@ defmodule NextLS.Runtime.Sidecar do

def handle_info({{:tracer, :start}, filename}, state) do
DB.clean_references(state.db, filename)
{:noreply, state}
end

def handle_info({{:tracer, :dbg}, payload}, state) do
dbg(payload)
{:noreply, state}
end
end
29 changes: 28 additions & 1 deletion priv/monkey/_next_ls_private_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ defmodule NextLSPrivate.Tracer do
:ok
end

def trace({:alias, meta, alias, as, _opts} = term, env) do
parent = parent_pid()

Process.send(
parent,
{{:tracer, :reference, :alias},
%{
meta: meta,
identifier: as,
file: env.file,
type: :alias,
module: alias,
source: @source
}},
[]
)

:ok
end

def trace({:alias_reference, meta, module}, env) do
parent = parent_pid()

Expand Down Expand Up @@ -105,7 +125,8 @@ defmodule NextLSPrivate.Tracer do
:ok
end

def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] do
def trace({type, meta, module, func, arity} = it, env)
when type in [:remote_function, :remote_macro, :imported_macro] do
parent = parent_pid()

if type == :remote_macro && meta[:closing][:line] != meta[:line] do
Expand Down Expand Up @@ -184,6 +205,12 @@ defmodule NextLSPrivate.Tracer do
:ok
end

# def trace(it, env) do
# parent = parent_pid()
# Process.send(parent, {{:tracer, :dbg}, {it, env.aliases}}, [])
# :ok
# end

def trace(_event, _env) do
:ok
end
Expand Down
Loading

0 comments on commit f799963

Please sign in to comment.