Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elixir 1.16 support #1032

Merged
merged 20 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.17.10
0.18.0
2 changes: 1 addition & 1 deletion apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ defmodule ElixirLS.Debugger.Server do
_ -> -1
end

stack_frames = Enum.slice(paused_process.stack, start_frame..end_frame)
stack_frames = Enum.slice(paused_process.stack, start_frame..end_frame//1)
{state, frame_ids} = ensure_frame_ids(state, pid, stack_frames)

stack_frames_json =
Expand Down
25 changes: 9 additions & 16 deletions apps/elixir_ls_debugger/lib/debugger/variables.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,11 @@ defmodule ElixirLS.Debugger.Variables do
if Keyword.keyword?(var) do
:named
else
:indexed

try do
# this call will raise ArgumentError for improper list, no better way to check it
_ = length(var)
if List.improper?(var) do
# improper list has head and tail
:named
else
:indexed
rescue
ArgumentError ->
# improper list has head and tail
:named
end
end
end
Expand All @@ -48,7 +43,7 @@ defmodule ElixirLS.Debugger.Variables do
start = start || 0

try do
# this call will raise ArgumentError for improper list, no better way to check it
# this call will raise ArgumentError for improper list
max_count = length(var)
count = count || max_count

Expand Down Expand Up @@ -137,6 +132,7 @@ defmodule ElixirLS.Debugger.Variables do

def num_children(var) when is_list(var) do
try do
# this call will raise ArgumentError for improper list
length(var)
rescue
ArgumentError ->
Expand Down Expand Up @@ -202,13 +198,10 @@ defmodule ElixirLS.Debugger.Variables do
if Keyword.keyword?(var) and var != [] do
"keyword"
else
try do
# this call will raise ArgumentError for improper list, no better way to check it
_ = length(var)
if List.improper?(var) do
"improper list"
else
"list"
rescue
ArgumentError ->
"improper list"
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions apps/elixir_ls_debugger/test/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ defmodule ElixirLS.Debugger.ServerTest do

setup do
{:ok, packet_capture} = ElixirLS.Utils.PacketCapture.start_link(self())
default_group_leader = Process.info(Process.whereis(ElixirLS.Debugger.Output))[:group_leader]
Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), packet_capture)

{:ok, _} = start_supervised(BreakpointCondition)
{:ok, _} = start_supervised({ModuleInfoCache, %{}})
{:ok, server} = Server.start_link(name: Server)

on_exit(fn ->
Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), default_group_leader)
for mod <- :int.interpreted(), do: :int.nn(mod)
:int.auto_attach(false)
:int.no_break()
Expand Down
262 changes: 150 additions & 112 deletions apps/language_server/lib/language_server/build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,140 +4,171 @@ defmodule ElixirLS.LanguageServer.Build do
require Logger

def build(parent, root_path, opts) when is_binary(root_path) do
spawn_monitor(fn ->
with_build_lock(fn ->
{us, result} =
:timer.tc(fn ->
Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}")

# read cache before cleaning up mix state in reload_project
cached_deps = read_cached_deps()
mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path)

case reload_project(mixfile, root_path) do
{:ok, mixfile_diagnostics} ->
{deps_result, deps_raw_diagnostics} =
with_diagnostics([log: true], fn ->
try do
# this call can raise
current_deps = Mix.Dep.load_on_environment([])

purge_changed_deps(current_deps, cached_deps)

if Keyword.get(opts, :fetch_deps?) and current_deps != cached_deps do
fetch_deps(current_deps)
build_pid_reference =
spawn_monitor(fn ->
with_build_lock(fn ->
{us, result} =
:timer.tc(fn ->
Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}")

# read cache before cleaning up mix state in reload_project
cached_deps = read_cached_deps()
mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path)

case reload_project(mixfile, root_path) do
{:ok, mixfile_diagnostics} ->
{deps_result, deps_raw_diagnostics} =
with_diagnostics([log: true], fn ->
try do
# this call can raise
current_deps =
if Version.match?(System.version(), "< 1.16.0-dev") do
Mix.Dep.load_on_environment([])
else
Mix.Dep.Converger.converge([])
end

purge_changed_deps(current_deps, cached_deps)

if Keyword.get(opts, :fetch_deps?) and current_deps != cached_deps do
fetch_deps(current_deps)
end

state = %{
get: Mix.Project.get(),
# project_file: Mix.Project.project_file(),
config: Mix.Project.config(),
# config_files: Mix.Project.config_files(),
config_mtime: Mix.Project.config_mtime(),
umbrella?: Mix.Project.umbrella?(),
apps_paths: Mix.Project.apps_paths(),
# deps_path: Mix.Project.deps_path(),
# deps_apps: Mix.Project.deps_apps(),
# deps_scms: Mix.Project.deps_scms(),
deps_paths: Mix.Project.deps_paths(),
# build_path: Mix.Project.build_path(),
manifest_path: Mix.Project.manifest_path()
}

ElixirLS.LanguageServer.MixProject.store(state)

:ok
catch
kind, err ->
{payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__)
{:error, kind, payload, stacktrace}
end
end)

deps_diagnostics =
deps_raw_diagnostics
|> Enum.map(&Diagnostics.code_diagnostic/1)

case deps_result do
:ok ->
if Keyword.get(opts, :compile?) do
{status, compile_diagnostics} =
run_mix_compile(Keyword.get(opts, :force?, false))

compile_diagnostics =
Diagnostics.normalize(compile_diagnostics, root_path, mixfile)

Server.build_finished(
parent,
{status, mixfile_diagnostics ++ deps_diagnostics ++ compile_diagnostics}
)

:"mix_compile_#{status}"
else
Server.build_finished(
parent,
{:ok, mixfile_diagnostics ++ deps_diagnostics}
)

:mix_compile_disabled
end

state = %{
get: Mix.Project.get(),
# project_file: Mix.Project.project_file(),
config: Mix.Project.config(),
# config_files: Mix.Project.config_files(),
config_mtime: Mix.Project.config_mtime(),
umbrella?: Mix.Project.umbrella?(),
apps_paths: Mix.Project.apps_paths(),
# deps_path: Mix.Project.deps_path(),
# deps_apps: Mix.Project.deps_apps(),
# deps_scms: Mix.Project.deps_scms(),
deps_paths: Mix.Project.deps_paths(),
# build_path: Mix.Project.build_path(),
manifest_path: Mix.Project.manifest_path()
}

ElixirLS.LanguageServer.MixProject.store(state)

:ok
catch
kind, err ->
{payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__)
{:error, kind, payload, stacktrace}
end
end)

deps_diagnostics =
deps_raw_diagnostics
|> Enum.map(&Diagnostics.code_diagnostic/1)

case deps_result do
:ok ->
if Keyword.get(opts, :compile?) do
{status, compile_diagnostics} = run_mix_compile()

compile_diagnostics =
Diagnostics.normalize(compile_diagnostics, root_path, mixfile)

{:error, kind, err, stacktrace} ->
# TODO get path from exception message
Server.build_finished(
parent,
{status, mixfile_diagnostics ++ deps_diagnostics ++ compile_diagnostics}
{:error,
mixfile_diagnostics ++
deps_diagnostics ++
[
Diagnostics.error_to_diagnostic(
kind,
err,
stacktrace,
mixfile,
root_path
)
]}
)

:"mix_compile_#{status}"
else
Server.build_finished(
parent,
{:ok, mixfile_diagnostics ++ deps_diagnostics}
)
:deps_error
end

:mix_compile_disabled
end

{:error, kind, err, stacktrace} ->
# TODO get path from exception message
Server.build_finished(
parent,
{:error,
mixfile_diagnostics ++
deps_diagnostics ++
[
Diagnostics.error_to_diagnostic(
kind,
err,
stacktrace,
mixfile,
root_path
)
]}
)
{:error, mixfile_diagnostics} ->
Server.build_finished(parent, {:error, mixfile_diagnostics})
:mixfile_error

:deps_error
end
:no_mixfile ->
Server.build_finished(parent, {:no_mixfile, []})
:no_mixfile
end
end)

{:error, mixfile_diagnostics} ->
Server.build_finished(parent, {:error, mixfile_diagnostics})
:mixfile_error
if Keyword.get(opts, :compile?) do
Tracer.save()
Logger.info("Compile took #{div(us, 1000)} milliseconds")
else
Logger.info("Mix project load took #{div(us, 1000)} milliseconds")
end

:no_mixfile ->
Server.build_finished(parent, {:no_mixfile, []})
:no_mixfile
end
end)
JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{
"elixir_ls.build_time" => div(us, 1000)
})
end)
end)

if Keyword.get(opts, :compile?) do
Tracer.save()
Logger.info("Compile took #{div(us, 1000)} milliseconds")
else
Logger.info("Mix project load took #{div(us, 1000)} milliseconds")
end
spawn(fn ->
Process.monitor(parent)
{build_process, _ref} = build_pid_reference
Process.monitor(build_process)

JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{
"elixir_ls.build_time" => div(us, 1000)
})
end)
receive do
{:DOWN, _ref, _, ^build_process, _reason} ->
:ok

{:DOWN, _ref, _, ^parent, _reason} ->
Process.exit(build_process, :kill)
end
end)

build_pid_reference
end

def clean(clean_deps? \\ false) do
def clean(root_path, clean_deps? \\ false) when is_binary(root_path) do
with_build_lock(fn ->
Mix.Task.clear()
run_mix_clean(clean_deps?)
mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path)

case reload_project(mixfile, root_path) do
{:ok, _} ->
Mix.Task.clear()
run_mix_clean(clean_deps?)

other ->
other
end
end)
end

def with_build_lock(func) do
:global.trans({__MODULE__, self()}, func)
end

def reload_project(mixfile, root_path) do
defp reload_project(mixfile, root_path) do
if File.exists?(mixfile) do
if module = Mix.Project.get() do
build_path = Mix.Project.config()[:build_path]
Expand Down Expand Up @@ -321,7 +352,7 @@ defmodule ElixirLS.LanguageServer.Build do
end
end

defp run_mix_compile do
defp run_mix_compile(force?) do
opts = [
"--return-errors",
"--ignore-module-conflict",
Expand All @@ -335,6 +366,13 @@ defmodule ElixirLS.LanguageServer.Build do
opts ++ ["--all-warnings"]
end

opts =
if force? do
opts ++ ["--force"]
else
opts
end

case Mix.Task.run("compile", opts) do
{status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) ->
{status, diagnostics}
Expand Down
Loading
Loading