diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cbf6525d..d11526db 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,7 @@ jobs: run: rm -rf tmp - name: Run Tests - run: elixir --erl '-kernel prevent_overlapping_partitions false' -S mix test + run: elixir --erl '-kernel prevent_overlapping_partitions false' -S mix test --max-cases 2 formatter: runs-on: ubuntu-latest diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 65a51cf9..13448355 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -194,7 +194,7 @@ defmodule NextLS do WHERE refs.identifier = ? AND refs.type = ? AND refs.module = ? - AND NOT like('/home/runner/work/elixir/%', refs.file) + AND refs.source = 'user' """, [function, "function", module] ) @@ -207,7 +207,7 @@ defmodule NextLS do FROM "references" as refs WHERE refs.module = ? AND refs.type = ? - AND NOT like('/home/runner/work/elixir/%', refs.file) + AND refs.source = 'user' """, [module, "alias"] ) @@ -216,12 +216,12 @@ defmodule NextLS do [] end - for [file, start_line, end_line, start_column, end_column] <- references do + for [file, startl, endl, startc, endc] <- references, match?({:ok, _}, File.stat(file)) do %Location{ uri: "file://#{file}", range: %Range{ - start: %Position{line: clamp(start_line - 1), character: clamp(start_column - 1)}, - end: %Position{line: clamp(end_line - 1), character: clamp(end_column - 1)} + start: %Position{line: clamp(startl - 1), character: clamp(startc - 1)}, + end: %Position{line: clamp(endl - 1), character: clamp(endc - 1)} } } end @@ -241,9 +241,33 @@ defmodule NextLS do end end + symbols = fn pid -> + rows = + DB.query( + pid, + ~Q""" + SELECT * + FROM symbols + WHERE source = 'user'; + """, + [] + ) + + for [_pk, module, file, type, name, line, column | _] <- rows do + %{ + module: module, + file: file, + type: type, + name: name, + line: line, + column: column + } + end + end + symbols = dispatch(lsp.assigns.registry, :databases, fn entries -> - for {pid, _} <- entries, symbol <- DB.symbols(pid), filter.(symbol.name) do + for {pid, _} <- entries, symbol <- symbols.(pid), filter.(symbol.name) do name = if symbol.type != "defstruct" do "#{symbol.type} #{symbol.name}" diff --git a/lib/next_ls/db.ex b/lib/next_ls/db.ex index 672b6e8b..6d4664c1 100644 --- a/lib/next_ls/db.ex +++ b/lib/next_ls/db.ex @@ -13,9 +13,6 @@ defmodule NextLS.DB do @spec query(pid(), query(), list()) :: list() def query(server, query, args \\ []), do: GenServer.call(server, {:query, query, args}, :infinity) - @spec symbols(pid()) :: list(map()) - def symbols(server), do: GenServer.call(server, :symbols, :infinity) - @spec insert_symbol(pid(), map()) :: :ok def insert_symbol(server, payload), do: GenServer.cast(server, {:insert_symbol, payload}) @@ -51,34 +48,6 @@ defmodule NextLS.DB do {:reply, rows, s} end - def handle_call(:symbols, _from, %{conn: conn} = s) do - rows = - __query__( - {conn, s.logger}, - ~Q""" - SELECT - * - FROM - symbols; - """, - [] - ) - - symbols = - for [_pk, module, file, type, name, line, column | _] <- rows do - %{ - module: module, - file: file, - type: type, - name: name, - line: line, - column: column - } - end - - {:reply, symbols, s} - end - def handle_cast({:insert_symbol, symbol}, %{conn: conn} = s) do {:message_queue_len, count} = Process.info(self(), :message_queue_len) NextLS.DB.Activity.update(s.activity, count) @@ -88,7 +57,8 @@ defmodule NextLS.DB do module_line: module_line, struct: struct, file: file, - defs: defs + defs: defs, + source: source } = symbol __query__( @@ -103,10 +73,10 @@ defmodule NextLS.DB do __query__( {conn, s.logger}, ~Q""" - INSERT INTO symbols (module, file, type, name, line, 'column') - VALUES (?, ?, ?, ?, ?, ?); + INSERT INTO symbols (module, file, type, name, line, 'column', source) + VALUES (?, ?, ?, ?, ?, ?, ?); """, - [mod, file, "defmodule", mod, module_line, 1] + [mod, file, "defmodule", mod, module_line, 1, source] ) if struct do @@ -115,10 +85,10 @@ defmodule NextLS.DB do __query__( {conn, s.logger}, ~Q""" - INSERT INTO symbols (module, file, type, name, line, 'column') - VALUES (?, ?, ?, ?, ?, ?); + INSERT INTO symbols (module, file, type, name, line, 'column', source) + VALUES (?, ?, ?, ?, ?, ?, ?); """, - [mod, file, "defstruct", "%#{Macro.to_string(mod)}{}", meta[:line], 1] + [mod, file, "defstruct", "%#{Macro.to_string(mod)}{}", meta[:line], 1, source] ) end @@ -126,10 +96,10 @@ defmodule NextLS.DB do __query__( {conn, s.logger}, ~Q""" - INSERT INTO symbols (module, file, type, name, line, 'column') - VALUES (?, ?, ?, ?, ?, ?); + INSERT INTO symbols (module, file, type, name, line, 'column', source) + VALUES (?, ?, ?, ?, ?, ?, ?); """, - [mod, file, type, name, meta[:line], meta[:column] || 1] + [mod, file, type, name, meta[:line], meta[:column] || 1, source] ) end @@ -145,7 +115,8 @@ defmodule NextLS.DB do identifier: identifier, file: file, type: type, - module: module + module: module, + source: source } = reference line = meta[:line] || 1 @@ -157,10 +128,10 @@ defmodule NextLS.DB do __query__( {conn, s.logger}, ~Q""" - INSERT INTO 'references' (identifier, arity, file, type, module, start_line, start_column, end_line, end_column) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + INSERT INTO 'references' (identifier, arity, file, type, module, start_line, start_column, end_line, end_column, source) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, - [identifier, reference[:arity], file, type, module, start_line, start_column, end_line, end_column] + [identifier, reference[:arity], file, type, module, start_line, start_column, end_line, end_column, source] ) {:noreply, s} diff --git a/lib/next_ls/db/schema.ex b/lib/next_ls/db/schema.ex index 718da6a2..e86ab228 100644 --- a/lib/next_ls/db/schema.ex +++ b/lib/next_ls/db/schema.ex @@ -23,7 +23,7 @@ defmodule NextLS.DB.Schema do alias NextLS.DB - @version 3 + @version 4 def init(conn) do # FIXME: this is odd tech debt. not a big deal but is confusing @@ -95,6 +95,7 @@ defmodule NextLS.DB.Schema do start_column integer NOT NULL, end_line integer NOT NULL, end_column integer NOT NULL, + source text NOT NULL DEFAULT 'user', inserted_at text NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 306dea3a..63e59164 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -1,5 +1,57 @@ +defmodule NextLSPrivate.DepTracer do + @moduledoc false + + @source "dep" + + def trace(:start, _env) do + :ok + end + + def trace({:on_module, bytecode, _}, env) do + parent = parent_pid() + + defs = Module.definitions_in(env.module) + + defs = + for {name, arity} = _def <- defs do + {name, Module.get_definition(env.module, {name, arity})} + end + + {:ok, {_, [{~c"Dbgi", bin}]}} = :beam_lib.chunks(bytecode, [~c"Dbgi"]) + + {:debug_info_v1, _, {_, %{line: line, struct: struct}, _}} = :erlang.binary_to_term(bin) + + Process.send( + parent, + {:tracer, + %{ + file: env.file, + module: env.module, + module_line: line, + struct: struct, + defs: defs, + source: @source + }}, + [] + ) + + :ok + end + + def trace(_event, _env) do + :ok + end + + defp parent_pid do + "NEXTLS_PARENT_PID" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term() + end +end + defmodule NextLSPrivate.Tracer do @moduledoc false + + @source "user" + def trace(:start, _env) do :ok end @@ -17,7 +69,8 @@ defmodule NextLSPrivate.Tracer do identifier: Map.get(alias_map, module, module), file: env.file, type: :alias, - module: module + module: module, + source: @source }}, [] ) @@ -42,7 +95,8 @@ defmodule NextLSPrivate.Tracer do arity: arity, file: env.file, type: :function, - module: module + module: module, + source: @source }}, [] ) @@ -63,7 +117,8 @@ defmodule NextLSPrivate.Tracer do arity: arity, file: env.file, type: :function, - module: env.module + module: env.module, + source: @source }}, [] ) @@ -93,7 +148,8 @@ defmodule NextLSPrivate.Tracer do module: env.module, module_line: line, struct: struct, - defs: defs + defs: defs, + source: @source }}, [] ) @@ -113,11 +169,15 @@ end defmodule :_next_ls_private_compiler do @moduledoc false + @tracers Code.get_compiler_option(:tracers) + def compile do # keep stdout on this node Process.group_leader(self(), Process.whereis(:user)) Code.put_compiler_option(:parser_options, columns: true, token_metadata: true) + Code.put_compiler_option(:tracers, [NextLSPrivate.DepTracer | @tracers]) + Mix.Task.clear() # load the paths for deps and compile them @@ -129,12 +189,12 @@ defmodule :_next_ls_private_compiler do # task was not re-enabled it seems Mix.Task.rerun("deps.loadpaths") + Code.put_compiler_option(:tracers, [NextLSPrivate.Tracer | @tracers]) + Mix.Task.rerun("compile", [ "--ignore-module-conflict", "--no-protocol-consolidation", - "--return-errors", - "--tracer", - "NextLSPrivate.Tracer" + "--return-errors" ]) rescue e -> {:error, e} diff --git a/test/next_ls/dependency_test.exs b/test/next_ls/dependency_test.exs new file mode 100644 index 00000000..74ac1204 --- /dev/null +++ b/test/next_ls/dependency_test.exs @@ -0,0 +1,310 @@ +defmodule NextLS.DependencyTest do + use ExUnit.Case, async: true + + import GenLSP.Test + import NextLS.Support.Utils + + @moduletag :tmp_dir + @moduletag 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"), proj_mix_exs()) + + File.mkdir_p!(Path.join(tmp_dir, "bar/lib")) + File.write!(Path.join(tmp_dir, "bar/mix.exs"), bar_mix_exs()) + + File.mkdir_p!(Path.join(tmp_dir, "baz/lib")) + File.write!(Path.join(tmp_dir, "baz/mix.exs"), baz_mix_exs()) + + [cwd: tmp_dir] + end + + setup %{cwd: cwd} do + foo = Path.join(cwd, "my_proj/lib/foo.ex") + + File.write!(foo, """ + defmodule Foo do + def foo() do + Bar.bar() + Baz + end + + def call_baz() do + Baz.baz() + end + end + """) + + cache = Path.join(cwd, "my_proj/lib/cache.ex") + + File.write!(cache, """ + defmodule Cache do + use GenServer + + def init(_) do + {:ok, nil} + end + + def get() do + GenServer.call(__MODULE__, :get) + end + end + """) + + bar = Path.join(cwd, "bar/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + def bar() do + 42 + end + + def call_baz() do + Baz.baz() + end + end + """) + + baz = Path.join(cwd, "baz/lib/baz.ex") + + File.write!(baz, """ + defmodule Baz do + def baz() do + 42 + end + end + """) + + [foo: foo, bar: bar, baz: baz, cache: cache] + end + + setup :with_lsp + + test "go to dependency function definition", context do + %{client: client, foo: foo, bar: bar} = context + + 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(foo) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 9}, + textDocument: %{uri: uri} + } + }) + + uri = uri(bar) + + assert_result 4, %{ + "range" => %{ + "start" => %{ + "line" => 1, + "character" => 0 + }, + "end" => %{ + "line" => 1, + "character" => 0 + } + }, + "uri" => ^uri + } + end + + test "does not show in workspace symbols", context do + %{client: client, foo: foo, bar: bar} = context + 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!"}} + + request client, %{ + method: "workspace/symbol", + id: 2, + jsonrpc: "2.0", + params: %{ + query: "" + } + } + + assert_result 2, symbols + + uris = Enum.map(symbols, fn result -> result["location"]["uri"] end) + assert uri(foo) in uris + refute uri(bar) in uris + end + + test "does not show up in function references", %{client: client, foo: foo} = 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(foo) + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 7, character: 8}, + textDocument: %{uri: uri}, + context: %{includeDeclaration: true} + } + }) + + assert_result2( + 4, + [ + %{ + "range" => %{"start" => %{"character" => 8, "line" => 7}, "end" => %{"character" => 11, "line" => 7}}, + "uri" => uri + } + ] + ) + end + + test "does not show up in module references", %{client: client, foo: foo} = 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(foo) + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 3, character: 4}, + textDocument: %{uri: uri}, + context: %{includeDeclaration: true} + } + }) + + assert_result2( + 4, + [ + %{ + "range" => %{"start" => %{"character" => 4, "line" => 3}, "end" => %{"character" => 7, "line" => 3}}, + "uri" => uri + }, + %{ + "range" => %{"start" => %{"character" => 4, "line" => 7}, "end" => %{"character" => 7, "line" => 7}}, + "uri" => uri + } + ] + ) + end + + test "elixir source files do not show up in references", %{client: client, cache: cache} = 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" => "Compiled Elixir.NextLS.DependencyTest-my_proj!"} + } + + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(cache) + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 8, character: 6}, + textDocument: %{uri: uri}, + context: %{includeDeclaration: true} + } + }) + + assert_result2( + 4, + [ + %{ + "range" => %{"end" => %{"character" => 15, "line" => 1}, "start" => %{"character" => 6, "line" => 1}}, + "uri" => uri + }, + %{ + "range" => %{"end" => %{"character" => 8, "line" => 1}, "start" => %{"character" => 0, "line" => 1}}, + "uri" => uri + }, + %{ + "range" => %{"end" => %{"character" => 8, "line" => 1}, "start" => %{"character" => 0, "line" => 1}}, + "uri" => uri + }, + %{ + "range" => %{"end" => %{"character" => 13, "line" => 8}, "start" => %{"character" => 4, "line" => 8}}, + "uri" => uri + } + ] + ) + end + + defp proj_mix_exs do + """ + defmodule MyProj.MixProject do + use Mix.Project + + def project do + [ + app: :my_proj, + version: "0.1.0", + elixir: "~> 1.10", + deps: [ + {:bar, path: "../bar"}, + {:baz, path: "../baz"} + ] + ] + end + end + """ + end + + defp bar_mix_exs do + """ + defmodule Bar.MixProject do + use Mix.Project + + def project do + [ + app: :bar, + version: "0.1.0", + elixir: "~> 1.10", + deps: [ + {:baz, path: "../baz"} + ] + ] + end + end + """ + end + + defp baz_mix_exs do + """ + defmodule Baz.MixProject do + use Mix.Project + + def project do + [ + app: :baz, + version: "0.1.0", + elixir: "~> 1.10", + deps: [] + ] + end + end + """ + end +end