diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 499a41bd..5df083b7 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -215,6 +215,20 @@ defmodule NextLS do [module, "alias"] ) + {:attribute, module, attribute} -> + DB.query( + database, + ~Q""" + SELECT file, start_line, end_line, start_column, end_column + FROM "references" as refs + WHERE refs.identifier = ? + AND refs.type = ? + AND refs.module = ? + AND refs.source = 'user' + """, + [attribute, "attribute", module] + ) + :unknown -> [] end @@ -266,7 +280,7 @@ defmodule NextLS do filtered_symbols = for {pid, _} <- entries, symbol <- symbols.(pid), score = fuzzy_match(symbol.name, query, case_sensitive?) do name = - if symbol.type != "defstruct" do + if symbol.type not in ["defstruct", "attribute"] do "#{symbol.type} #{symbol.name}" else "#{symbol.name}" @@ -679,6 +693,7 @@ defmodule NextLS do defp elixir_kind_to_lsp_kind("defmodule"), do: GenLSP.Enumerations.SymbolKind.module() defp elixir_kind_to_lsp_kind("defstruct"), do: GenLSP.Enumerations.SymbolKind.struct() + defp elixir_kind_to_lsp_kind("attribute"), do: GenLSP.Enumerations.SymbolKind.property() defp elixir_kind_to_lsp_kind(kind) when kind in ["def", "defp", "defmacro", "defmacrop"], do: GenLSP.Enumerations.SymbolKind.function() @@ -737,11 +752,17 @@ defmodule NextLS do [[module, "defmacro", function]] -> {:function, module, function} + [[module, "attribute", attribute]] -> + {:attribute, module, attribute} + _unknown_definition -> case DB.query(database, reference_query, [file, line, col]) do [[function, "function", module]] -> {:function, module, function} + [[attribute, "attribute", module]] -> + {:attribute, module, attribute} + [[_alias, "alias", module]] -> {:module, module} diff --git a/lib/next_ls/ast_helpers.ex b/lib/next_ls/ast_helpers.ex new file mode 100644 index 00000000..7c0d1079 --- /dev/null +++ b/lib/next_ls/ast_helpers.ex @@ -0,0 +1,75 @@ +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) + + 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() + + symbols = parse_symbols(file, module) + + Enum.filter(symbols, fn + {:attribute, "@" <> name, _, _} -> + not Map.has_key?(reserved_attributes, String.to_atom(name)) + + _other -> + false + end) + end + + 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)) + + 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 + + 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 + + # 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) + end +end diff --git a/lib/next_ls/db.ex b/lib/next_ls/db.ex index 99724167..a416a7d1 100644 --- a/lib/next_ls/db.ex +++ b/lib/next_ls/db.ex @@ -61,6 +61,7 @@ defmodule NextLS.DB do struct: struct, file: file, defs: defs, + symbols: symbols, source: source } = symbol @@ -106,6 +107,17 @@ defmodule NextLS.DB do ) end + for {type, name, line, column} <- symbols do + __query__( + {conn, s.logger}, + ~Q""" + INSERT INTO symbols (module, file, type, name, line, 'column', source) + VALUES (?, ?, ?, ?, ?, ?, ?); + """, + [mod, file, type, name, line, column, source] + ) + end + {:noreply, s} end diff --git a/lib/next_ls/definition.ex b/lib/next_ls/definition.ex index 4dc304f0..d8db4dab 100644 --- a/lib/next_ls/definition.ex +++ b/lib/next_ls/definition.ex @@ -41,6 +41,9 @@ defmodule NextLS.Definition do "function" -> [module, identifier] + "attribute" -> + [module, identifier] + _ -> nil end diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index 64ec9626..00606305 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -2,6 +2,7 @@ defmodule NextLS.Runtime.Sidecar do @moduledoc false use GenServer + alias NextLS.ASTHelpers alias NextLS.DB def start_link(args) do @@ -15,11 +16,20 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({:tracer, payload}, state) do + attributes = ASTHelpers.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, :attribute}, payload}, state) do + name = ASTHelpers.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} + end + def handle_info({{:tracer, :reference}, payload}, state) do DB.insert_reference(state.db, payload) diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index e382a96e..755a1371 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -84,6 +84,27 @@ defmodule NextLSPrivate.Tracer do :ok end + def trace({:imported_macro, meta, _module, :@, arity}, env) do + parent = parent_pid() + + Process.send( + parent, + {{:tracer, :reference, :attribute}, + %{ + meta: meta, + identifier: :@, + arity: arity, + file: env.file, + type: :attribute, + module: env.module, + source: @source + }}, + [] + ) + + :ok + end + def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] do parent = parent_pid() diff --git a/test/next_ls/definition_test.exs b/test/next_ls/definition_test.exs index 53d860e7..faba3942 100644 --- a/test/next_ls/definition_test.exs +++ b/test/next_ls/definition_test.exs @@ -403,4 +403,159 @@ defmodule NextLS.DefinitionTest do 500 end end + + describe "attribute" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + + setup %{cwd: cwd} do + bar = Path.join(cwd, "my_proj/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + @my_attr 1 + @second_attr 2 + + @spec run() :: :ok | :error + def run() do + if @my_attr == 1 do + :ok + else + {:error, @second_attr} + end + end + + defmodule Inner do + @inner_attr 123 + + def foo(a) do + if a, do: @inner_attr + end + end + + def foo() do + :nothing + end + end + + defmodule TopSecond.Some.Long.Name do + @top_second_attr "something" + + def run_second do + {:error, @top_second_attr} + end + end + """) + + [bar: bar] + end + + setup :with_lsp + + test "go to attribute definition", %{client: client, bar: bar} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(bar) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 6, character: 9}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, + %{ + "range" => %{ + "start" => %{ + "line" => 1, + "character" => 2 + }, + "end" => %{ + "line" => 1, + "character" => 2 + } + }, + "uri" => ^uri + }, + 500 + end + + test "go to attribute definition in second module", %{client: client, bar: bar} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(bar) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 30, character: 17}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, + %{ + "range" => %{ + "start" => %{ + "line" => 27, + "character" => 2 + }, + "end" => %{ + "line" => 27, + "character" => 2 + } + }, + "uri" => ^uri + }, + 500 + end + + test "go to attribute definition in inner module", %{client: client, bar: bar} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(bar) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 17, character: 20}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, + %{ + "range" => %{ + "start" => %{ + "line" => 14, + "character" => 4 + }, + "end" => %{ + "line" => 14, + "character" => 4 + } + }, + "uri" => ^uri + }, + 500 + end + end end diff --git a/test/next_ls/references_test.exs b/test/next_ls/references_test.exs index be8ef570..e840cb02 100644 --- a/test/next_ls/references_test.exs +++ b/test/next_ls/references_test.exs @@ -33,6 +33,18 @@ defmodule NextLS.ReferencesTest do Peace.and_love() end end + + defmodule Foo do + @foo_attr 123 + + def foo_foo(a) do + {:ok, a + @foo_attr} + end + + def foo2 do + {:error, @foo_attr} + end + end """) [bar: bar, peace: peace] @@ -105,4 +117,45 @@ defmodule NextLS.ReferencesTest do } ] end + + test "list attribute references", %{client: client, bar: bar} = 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_compiled(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 8, character: 4}, + textDocument: %{uri: uri(bar)}, + context: %{includeDeclaration: true} + } + }) + + uri = uri(bar) + + assert_result2( + 4, + [ + %{ + "uri" => uri, + "range" => %{ + "start" => %{"line" => 11, "character" => 14}, + "end" => %{"line" => 11, "character" => 23} + } + }, + %{ + "uri" => uri, + "range" => %{ + "start" => %{"line" => 15, "character" => 13}, + "end" => %{"line" => 15, "character" => 22} + } + } + ] + ) + end end