diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a654725..5c10755b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.22.3](https://github.com/elixir-tools/next-ls/compare/v0.22.2...v0.22.3) (2024-05-15) + + +### Bug Fixes + +* ensure some elixir internals are ready ([#478](https://github.com/elixir-tools/next-ls/issues/478)) ([f4685d0](https://github.com/elixir-tools/next-ls/commit/f4685d01266b4afb7f557d9a361fc7770aa22ec6)), closes [#467](https://github.com/elixir-tools/next-ls/issues/467) + +## [0.22.2](https://github.com/elixir-tools/next-ls/compare/v0.22.1...v0.22.2) (2024-05-14) + + +### Bug Fixes + +* **completions:** work in guards ([#475](https://github.com/elixir-tools/next-ls/issues/475)) ([e0573ab](https://github.com/elixir-tools/next-ls/commit/e0573ab23c439313ed2546015f12a21dfe573d1d)) + ## [0.22.1](https://github.com/elixir-tools/next-ls/compare/v0.22.0...v0.22.1) (2024-05-13) diff --git a/flake.nix b/flake.nix index 10830b1d..a5ae57ab 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,7 @@ }: let inherit (nixpkgs) lib; - version = "0.22.1"; # x-release-please-version + version = "0.22.3"; # x-release-please-version # Helper to provide system-specific attributes forAllSystems = f: diff --git a/lib/next_ls/runtime.ex b/lib/next_ls/runtime.ex index 4ca68420..749122d6 100644 --- a/lib/next_ls/runtime.ex +++ b/lib/next_ls/runtime.ex @@ -231,22 +231,32 @@ defmodule NextLS.Runtime do true <- connect(node, port, 120) do NextLS.Logger.info(logger, "Connected to node #{node}") - :next_ls - |> :code.priv_dir() - |> Path.join("monkey/_next_ls_private_compiler.ex") - |> then(&:rpc.call(node, Code, :compile_file, [&1])) - |> tap(fn - {:badrpc, error} -> - NextLS.Logger.error(logger, "Bad RPC call to node #{node}: #{inspect(error)}") - send(me, {:cancel, error}) - - _ -> - :ok - end) - - {:ok, _} = :rpc.call(node, :_next_ls_private_compiler, :start, []) - - send(me, {:node, node}) + result = + :next_ls + |> :code.priv_dir() + |> Path.join("monkey/_next_ls_private_compiler.ex") + |> then(fn path -> + if await_config_table(node, 5) do + :rpc.call(node, Code, :compile_file, [path]) + else + {:badrpc, "internal ets table not found"} + end + end) + |> then(fn + {:badrpc, error} -> + NextLS.Logger.error(logger, "Bad RPC call to node #{node}: #{inspect(error)}") + send(me, {:cancel, error}) + :error + + _ -> + :ok + end) + + if result == :ok do + {:ok, _} = :rpc.call(node, :_next_ls_private_compiler, :start, []) + + send(me, {:node, node}) + end else error -> send(me, {:cancel, error}) @@ -275,6 +285,20 @@ defmodule NextLS.Runtime do end end + defp await_config_table(_node, 0) do + false + end + + defp await_config_table(node, attempts) do + # this is an Elixir implementation detail, handle with care + if :undefined == :rpc.call(node, :ets, :whereis, [:elixir_config]) do + Process.sleep(100) + await_config_table(node, attempts - 1) + else + true + end + end + @impl GenServer def handle_call(:ready?, _from, state) when is_ready(state) do {:reply, true, state} diff --git a/mix.exs b/mix.exs index 73f7a0ae..7dd22abb 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule NextLS.MixProject do use Mix.Project - @version "0.22.1" # x-release-please-version + @version "0.22.3" # x-release-please-version def project do [ diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 1ef1b1dc..d673b3ea 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -1032,14 +1032,13 @@ defmodule :_next_ls_private_compiler do @moduledoc false def start do + Code.put_compiler_option(:parser_options, columns: true, token_metadata: true) + children = [ :_next_ls_private_compiler_worker ] - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: :_next_ls_private_application_supervisor] - {:ok, pid} = Supervisor.start_link(children, opts) + {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one, name: :_next_ls_private_application_supervisor) Process.unlink(pid) {:ok, pid} end @@ -1049,7 +1048,6 @@ defmodule :_next_ls_private_compiler do 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]) @@ -1425,39 +1423,36 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end end - defp expand_macro(_meta, Kernel, type, [{name, _, params}, [{_, block}]], _callback, state, env) - when type in [:def, :defp] and is_tuple(block) and is_atom(name) and is_list(params) do - {_, state, penv} = - for p <- params, reduce: {nil, state, env} do - {_, state, penv} -> - expand_pattern(p, state, penv) - end - - {res, state, _env} = expand(block, state, penv) + defp expand_macro(_meta, Kernel, type, args, _callback, state, env) + when type in [:def, :defmacro, :defp, :defmacrop] do + # extract the name, params, guards, and blocks + {name, params, guards, blocks} = + case args do + [{:when, _, [{name, _, params} | guards]} | maybe_blocks] -> + {name, params, guards, maybe_blocks} - arity = length(List.wrap(params)) - functions = Map.update(state.functions, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity)) - {res, put_in(state.functions, functions), env} - end + [{name, _, params} | maybe_blocks] -> + {name, params, [], maybe_blocks} + end - defp expand_macro(_meta, Kernel, type, [{name, _, params}, block], _callback, state, env) - when type in [:defmacro, :defmacrop] do - {_res, state, penv} = expand(params, state, env) - {res, state, _env} = expand(block, state, penv) + blocks = List.first(blocks, []) - arity = length(List.wrap(params)) - macros = Map.update(state.macros, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity)) - {res, put_in(state.macros, macros), env} - end - - defp expand_macro(_meta, Kernel, type, [{name, _, params}, blocks], _callback, state, env) - when type in [:def, :defp] and is_atom(name) and is_list(params) and is_list(blocks) do + # collect the environment from the parameters + # parameters are always patterns {_, state, penv} = for p <- params, reduce: {nil, state, env} do {_, state, penv} -> expand_pattern(p, state, penv) end + # expand guards, which includes the env from params + {_, state, _} = + for guard <- guards, reduce: {nil, state, penv} do + {_, state, env} -> + expand(guard, state, env) + end + + # expand the blocks, there could be `:do`, `:after`, `:catch`, etc {blocks, state} = for {type, block} <- blocks, reduce: {[], state} do {acc, state} -> @@ -1467,26 +1462,21 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do arity = length(List.wrap(params)) - functions = Map.update(state.functions, env.module, [{name, arity}], &Keyword.put_new(&1, name, arity)) - {Enum.reverse(blocks), put_in(state.functions, functions), env} - end - - defp expand_macro(_meta, Kernel, type, [{_name, _, params}, blocks], _callback, state, env) - when type in [:def, :defp] and is_list(params) and is_list(blocks) do - {_, state, penv} = - for p <- params, reduce: {nil, state, env} do - {_, state, penv} -> - expand_pattern(p, state, penv) + # determine which key to save this function in state + state_key = + case type do + type when type in [:def, :defp] -> :functions + type when type in [:defmacro, :defmacrop] -> :macros end - {blocks, state} = - for {type, block} <- blocks, reduce: {[], state} do - {acc, state} -> - {res, state, _env} = expand(block, state, penv) - {[{type, res} | acc], state} + funcs = + if is_atom(name) do + Map.update(state[state_key], env.module, [{name, arity}], &Keyword.put_new(&1, name, arity)) + else + state[state_key] end - {Enum.reverse(blocks), state, env} + {Enum.reverse(blocks), put_in(state[state_key], funcs), env} end defp expand_macro(meta, Kernel, :@, [{name, _, p}] = args, callback, state, env) when is_list(p) do diff --git a/test/next_ls/completions_test.exs b/test/next_ls/completions_test.exs index e5422cc8..543fa377 100644 --- a/test/next_ls/completions_test.exs +++ b/test/next_ls/completions_test.exs @@ -809,4 +809,33 @@ defmodule NextLS.CompletionsTest do assert_match %{"kind" => 6, "label" => "items"} in results assert_match %{"kind" => 6, "label" => "item"} not in results end + + test "parameters are available inside guards", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + def run(items) when is_list(i + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 1, + character: 31 + } + } + } + + assert_result 2, results + + assert_match %{"kind" => 6, "label" => "items"} in results + end end