Skip to content

Commit 627bb94

Browse files
authored
feat(definition,references): go to definition when aliasing modules (#152)
1 parent b14a09d commit 627bb94

File tree

8 files changed

+357
-91
lines changed

8 files changed

+357
-91
lines changed

lib/next_ls/ast_helpers.ex

+131-55
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,151 @@
11
defmodule NextLS.ASTHelpers do
22
@moduledoc false
33

4-
@spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil
5-
def get_attribute_reference_name(file, line, column) do
6-
ast = ast_from_file(file)
7-
8-
{_ast, name} =
9-
Macro.prewalk(ast, nil, fn
10-
{:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"}
11-
other, acc -> {other, acc}
12-
end)
4+
defmodule Attributes do
5+
@moduledoc false
6+
@spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil
7+
def get_attribute_reference_name(file, line, column) do
8+
ast = ast_from_file(file)
9+
10+
{_ast, name} =
11+
Macro.prewalk(ast, nil, fn
12+
{:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"}
13+
other, acc -> {other, acc}
14+
end)
15+
16+
name
17+
end
1318

14-
name
15-
end
19+
@spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]
20+
def get_module_attributes(file, module) do
21+
reserved_attributes = Module.reserved_attributes()
1622

17-
@spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]
18-
def get_module_attributes(file, module) do
19-
reserved_attributes = Module.reserved_attributes()
23+
symbols = parse_symbols(file, module)
2024

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

23-
Enum.filter(symbols, fn
24-
{:attribute, "@" <> name, _, _} ->
25-
not Map.has_key?(reserved_attributes, String.to_atom(name))
29+
_other ->
30+
false
31+
end)
32+
end
2633

27-
_other ->
28-
false
29-
end)
30-
end
34+
defp parse_symbols(file, module) do
35+
ast = ast_from_file(file)
3136

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

35-
{_ast, %{symbols: symbols}} =
36-
Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module))
40+
symbols
41+
end
3742

38-
symbols
39-
end
43+
# add module name to modules stack on enter
44+
defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do
45+
modules = [module_name_atoms | acc.modules]
46+
{ast, %{acc | modules: modules}}
47+
end
4048

41-
# add module name to modules stack on enter
42-
defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do
43-
modules = [module_name_atoms | acc.modules]
44-
{ast, %{acc | modules: modules}}
45-
end
49+
defp prewalk(ast, acc), do: {ast, acc}
50+
51+
defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do
52+
ast_module =
53+
acc.modules
54+
|> Enum.reverse()
55+
|> List.flatten()
56+
|> Module.concat()
57+
58+
if module == ast_module do
59+
symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols]
60+
{ast, %{acc | symbols: symbols}}
61+
else
62+
{ast, acc}
63+
end
64+
end
4665

47-
defp prewalk(ast, acc), do: {ast, acc}
66+
# remove module name from modules stack on exit
67+
defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do
68+
[_exit_mudule | modules] = acc.modules
69+
{ast, %{acc | modules: modules}}
70+
end
4871

49-
defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do
50-
ast_module =
51-
acc.modules
52-
|> Enum.reverse()
53-
|> List.flatten()
54-
|> Module.concat()
72+
defp postwalk(ast, acc, _module), do: {ast, acc}
5573

56-
if module == ast_module do
57-
symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols]
58-
{ast, %{acc | symbols: symbols}}
59-
else
60-
{ast, acc}
74+
defp ast_from_file(file) do
75+
file |> File.read!() |> Code.string_to_quoted!(columns: true)
6176
end
6277
end
6378

64-
# remove module name from modules stack on exit
65-
defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do
66-
[_exit_mudule | modules] = acc.modules
67-
{ast, %{acc | modules: modules}}
68-
end
69-
70-
defp postwalk(ast, acc, _module), do: {ast, acc}
71-
72-
defp ast_from_file(file) do
73-
file |> File.read!() |> Code.string_to_quoted!(columns: true)
79+
defmodule Aliases do
80+
@moduledoc """
81+
Responsible for extracting the relevant portion from a single or multi alias.
82+
83+
## Example
84+
85+
```elixir
86+
alias Foo.Bar.Baz
87+
# ^^^^^^^^^^^
88+
89+
alias Foo.Bar.{Baz, Bing}
90+
# ^^^ ^^^^
91+
92+
alias Foo.Bar.{
93+
Baz,
94+
# ^^^
95+
Bing
96+
# ^^^^
97+
}
98+
```
99+
"""
100+
101+
def extract_alias_range(code, {start, stop}, ale) do
102+
lines =
103+
code
104+
|> String.split("\n")
105+
|> Enum.map(&String.split(&1, ""))
106+
|> Enum.slice((start.line - 1)..(stop.line - 1))
107+
108+
code =
109+
if start.line == stop.line do
110+
[line] = lines
111+
112+
line
113+
|> Enum.slice(start.col..stop.col)
114+
|> Enum.join()
115+
else
116+
[first | rest] = lines
117+
first = Enum.drop(first, start.col)
118+
119+
[last | rest] = Enum.reverse(rest)
120+
121+
length = Enum.count(last)
122+
last = Enum.drop(last, -(length - stop.col - 1))
123+
124+
Enum.map_join([first | Enum.reverse([last | rest])], "\n", &Enum.join(&1, ""))
125+
end
126+
127+
{_, range} =
128+
code
129+
|> Code.string_to_quoted!(columns: true, column: start.col, token_metadata: true)
130+
|> Macro.prewalk(nil, fn ast, range ->
131+
range =
132+
case ast do
133+
{:__aliases__, meta, aliases} ->
134+
if ale == List.last(aliases) do
135+
{{meta[:line] + start.line - 1, meta[:column]},
136+
{meta[:last][:line] + start.line - 1, meta[:last][:column] + String.length(to_string(ale)) - 1}}
137+
else
138+
range
139+
end
140+
141+
_ ->
142+
range
143+
end
144+
145+
{ast, range}
146+
end)
147+
148+
range
149+
end
74150
end
75151
end

lib/next_ls/db.ex

+5-2
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,11 @@ defmodule NextLS.DB do
137137
line = meta[:line] || 1
138138
col = meta[:column] || 0
139139

140-
{start_line, start_column} = {line, col}
141-
{end_line, end_column} = {line, col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))}
140+
{start_line, start_column} = reference[:range][:start] || {line, col}
141+
142+
{end_line, end_column} =
143+
reference[:range][:stop] ||
144+
{line, col + String.length(identifier |> to_string() |> String.replace("Elixir.", ""))}
142145

143146
__query__(
144147
{conn, s.logger},

lib/next_ls/definition.ex

+30-20
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,36 @@ defmodule NextLS.Definition do
55
alias NextLS.DB
66

77
def fetch(file, {line, col}, db) do
8-
with [[_pk, identifier, _arity, _file, type, module, _start_l, _start_c, _end_l, _end_c | _]] <-
9-
DB.query(
10-
db,
11-
~Q"""
12-
SELECT
13-
*
14-
FROM
15-
'references' AS refs
16-
WHERE
17-
refs.file = ?
18-
AND ? BETWEEN refs.start_line AND refs.end_line
19-
AND ? BETWEEN refs.start_column AND refs.end_column
20-
ORDER BY refs.id asc
21-
LIMIT 1;
22-
""",
23-
[file, line, col]
24-
) do
8+
rows =
9+
DB.query(
10+
db,
11+
~Q"""
12+
SELECT
13+
*
14+
FROM
15+
'references' AS refs
16+
WHERE
17+
refs.file = ?
18+
AND refs.start_line <= ?
19+
AND ? <= refs.end_line
20+
AND refs.start_column <= ?
21+
AND ? <= refs.end_column
22+
ORDER BY refs.id asc
23+
LIMIT 1;
24+
""",
25+
[file, line, line, col, col]
26+
)
27+
28+
reference =
29+
case rows do
30+
[[_pk, identifier, _arity, _file, type, module, _start_l, _start_c, _end_l, _end_c | _]] ->
31+
%{identifier: identifier, type: type, module: module}
32+
33+
[] ->
34+
nil
35+
end
36+
37+
with %{identifier: identifier, type: type, module: module} <- reference do
2538
query =
2639
~Q"""
2740
SELECT
@@ -53,9 +66,6 @@ defmodule NextLS.Definition do
5366
else
5467
nil
5568
end
56-
else
57-
_ ->
58-
nil
5969
end
6070
end
6171
end

lib/next_ls/runtime/sidecar.ex

+31-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ defmodule NextLS.Runtime.Sidecar do
22
@moduledoc false
33
use GenServer
44

5-
alias NextLS.ASTHelpers
5+
alias NextLS.ASTHelpers.Aliases
6+
alias NextLS.ASTHelpers.Attributes
67
alias NextLS.DB
78

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

1819
def handle_info({:tracer, payload}, state) do
19-
attributes = ASTHelpers.get_module_attributes(payload.file, payload.module)
20+
attributes = Attributes.get_module_attributes(payload.file, payload.module)
2021
payload = Map.put_new(payload, :symbols, attributes)
2122
DB.insert_symbol(state.db, payload)
2223

2324
{:noreply, state}
2425
end
2526

27+
def handle_info({{:tracer, :reference, :alias}, payload}, state) do
28+
if payload.meta[:end_of_expression] do
29+
start = %{line: payload.meta[:line], col: payload.meta[:column]}
30+
stop = %{line: payload.meta[:end_of_expression][:line], col: payload.meta[:end_of_expression][:column]}
31+
32+
{start, stop} =
33+
Aliases.extract_alias_range(
34+
File.read!(payload.file),
35+
{start, stop},
36+
payload.identifier |> Macro.to_string() |> String.to_atom()
37+
)
38+
39+
payload =
40+
payload
41+
|> Map.put(:identifier, payload.module)
42+
|> Map.put(:range, %{start: start, stop: stop})
43+
44+
DB.insert_reference(state.db, payload)
45+
end
46+
47+
{:noreply, state}
48+
end
49+
2650
def handle_info({{:tracer, :reference, :attribute}, payload}, state) do
27-
name = ASTHelpers.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column])
51+
name = Attributes.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column])
2852
if name, do: DB.insert_reference(state.db, %{payload | identifier: name})
2953

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

3963
def handle_info({{:tracer, :start}, filename}, state) do
4064
DB.clean_references(state.db, filename)
65+
{:noreply, state}
66+
end
4167

68+
def handle_info({{:tracer, :dbg}, payload}, state) do
69+
dbg(payload)
4270
{:noreply, state}
4371
end
4472
end

priv/monkey/_next_ls_private_compiler.ex

+28-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ defmodule NextLSPrivate.Tracer do
6262
:ok
6363
end
6464

65+
def trace({:alias, meta, alias, as, _opts} = term, env) do
66+
parent = parent_pid()
67+
68+
Process.send(
69+
parent,
70+
{{:tracer, :reference, :alias},
71+
%{
72+
meta: meta,
73+
identifier: as,
74+
file: env.file,
75+
type: :alias,
76+
module: alias,
77+
source: @source
78+
}},
79+
[]
80+
)
81+
82+
:ok
83+
end
84+
6585
def trace({:alias_reference, meta, module}, env) do
6686
parent = parent_pid()
6787

@@ -105,7 +125,8 @@ defmodule NextLSPrivate.Tracer do
105125
:ok
106126
end
107127

108-
def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] do
128+
def trace({type, meta, module, func, arity} = it, env)
129+
when type in [:remote_function, :remote_macro, :imported_macro] do
109130
parent = parent_pid()
110131

111132
if type == :remote_macro && meta[:closing][:line] != meta[:line] do
@@ -184,6 +205,12 @@ defmodule NextLSPrivate.Tracer do
184205
:ok
185206
end
186207

208+
# def trace(it, env) do
209+
# parent = parent_pid()
210+
# Process.send(parent, {{:tracer, :dbg}, {it, env.aliases}}, [])
211+
# :ok
212+
# end
213+
187214
def trace(_event, _env) do
188215
:ok
189216
end

0 commit comments

Comments
 (0)