diff --git a/lib/next_ls/ast_helpers.ex b/lib/next_ls/ast_helpers.ex index 7c0d1079..0f40759a 100644 --- a/lib/next_ls/ast_helpers.ex +++ b/lib/next_ls/ast_helpers.ex @@ -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 diff --git a/lib/next_ls/db.ex b/lib/next_ls/db.ex index a416a7d1..5f88def8 100644 --- a/lib/next_ls/db.ex +++ b/lib/next_ls/db.ex @@ -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}, diff --git a/lib/next_ls/definition.ex b/lib/next_ls/definition.ex index d8db4dab..0fe7cb46 100644 --- a/lib/next_ls/definition.ex +++ b/lib/next_ls/definition.ex @@ -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 @@ -53,9 +66,6 @@ defmodule NextLS.Definition do else nil end - else - _ -> - nil end end end diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index 00606305..b725c110 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -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 @@ -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} @@ -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 diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 755a1371..eb353091 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -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() @@ -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 @@ -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 diff --git a/test/next_ls/ast_helpers_test.exs b/test/next_ls/ast_helpers_test.exs new file mode 100644 index 00000000..9183ac79 --- /dev/null +++ b/test/next_ls/ast_helpers_test.exs @@ -0,0 +1,52 @@ +defmodule NextLS.ASTHelpersTest do + use ExUnit.Case, async: true + + alias NextLS.ASTHelpers.Aliases + + describe "extract_aliases" do + test "extracts a normal alias" do + code = """ + defmodule Foo do + alias One.Two.Three + end + """ + + start = %{line: 2, col: 3} + stop = %{line: 2, col: 21} + ale = :Three + + assert {{2, 9}, {2, 21}} == Aliases.extract_alias_range(code, {start, stop}, ale) + end + + test "extract an inline multi alias" do + code = """ + defmodule Foo do + alias One.Two.{Three, Four} + end + """ + + start = %{line: 2, col: 3} + stop = %{line: 2, col: 29} + + assert {{2, 18}, {2, 22}} == Aliases.extract_alias_range(code, {start, stop}, :Three) + assert {{2, 25}, {2, 28}} == Aliases.extract_alias_range(code, {start, stop}, :Four) + end + + test "extract a multi line, multi alias" do + code = """ + defmodule Foo do + alias One.Two.{ + Three, + Four + } + end + """ + + start = %{line: 2, col: 3} + stop = %{line: 5, col: 3} + + assert {{3, 5}, {3, 9}} == Aliases.extract_alias_range(code, {start, stop}, :Three) + assert {{4, 5}, {4, 8}} == Aliases.extract_alias_range(code, {start, stop}, :Four) + end + end +end diff --git a/test/next_ls/definition_test.exs b/test/next_ls/definition_test.exs index faba3942..d7fa3f81 100644 --- a/test/next_ls/definition_test.exs +++ b/test/next_ls/definition_test.exs @@ -353,7 +353,7 @@ defmodule NextLS.DefinitionTest do bar = Path.join(cwd, "my_proj/lib/bar.ex") File.write!(bar, """ - defmodule Bar do + defmodule Bar.Bell do alias MyApp.Peace def run() do Peace.and_love() @@ -361,7 +361,21 @@ defmodule NextLS.DefinitionTest do end """) - [bar: bar, peace: peace] + baz = Path.join(cwd, "my_proj/lib/baz.ex") + + File.write!(baz, """ + defmodule Baz do + alias Bar.Bell + alias MyApp.{ + Peace + } + def run() do + Peace.and_love() + end + end + """) + + [bar: bar, peace: peace, baz: baz] end setup :with_lsp @@ -389,14 +403,63 @@ defmodule NextLS.DefinitionTest do assert_result 4, %{ "range" => %{ - "start" => %{ - "line" => 0, - "character" => 0 - }, - "end" => %{ - "line" => 0, - "character" => 0 - } + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} + }, + "uri" => ^uri + }, + 500 + end + + test "go to module alias definition", %{client: client, peace: peace, bar: bar, baz: baz} = context do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_is_ready(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(baz) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 3, character: 5}, + textDocument: %{uri: uri} + } + }) + + uri = uri(peace) + + assert_result 4, + %{ + "range" => %{ + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} + }, + "uri" => ^uri + }, + 500 + + uri = uri(baz) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 1, character: 10}, + textDocument: %{uri: uri} + } + }) + + uri = uri(bar) + + assert_result 4, + %{ + "range" => %{ + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} }, "uri" => ^uri }, diff --git a/test/next_ls/references_test.exs b/test/next_ls/references_test.exs index e840cb02..9de7bef9 100644 --- a/test/next_ls/references_test.exs +++ b/test/next_ls/references_test.exs @@ -108,6 +108,13 @@ defmodule NextLS.ReferencesTest do assert_result 4, [ + %{ + "uri" => ^uri, + "range" => %{ + "start" => %{"line" => 1, "character" => 8}, + "end" => %{"line" => 1, "character" => 18} + } + }, %{ "uri" => ^uri, "range" => %{