Skip to content

Commit

Permalink
use env from metadata builder for eval and completions
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsamson committed Sep 5, 2024
1 parent 79bc137 commit f0c4a81
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 48 deletions.
137 changes: 137 additions & 0 deletions apps/debug_adapter/lib/debug_adapter/code.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule ElixirLS.DebugAdapter.Code do
if Version.match?(System.version(), ">= 1.14.0-dev") do
defdelegate env_for_eval(env), to: Code
else
def env_for_eval(%{lexical_tracker: pid} = env) do
new_env = %{
env
| context: nil,
context_modules: [],
macro_aliases: [],
versioned_vars: %{}
}

if is_pid(pid) do
if Process.alive?(pid) do
new_env
else
IO.warn("""
an __ENV__ with outdated compilation information was given to eval, \
call Macro.Env.prune_compile_info/1 to prune it
""")

%{new_env | lexical_tracker: nil, tracers: []}
end
else
%{new_env | tracers: []}
end
end

def env_for_eval(opts) when is_list(opts) do
env = elixir_env.new()

line =
case Keyword.get(opts, :line) do
line_opt when is_integer(line_opt) -> line_opt
nil -> Map.get(env, :line)
end

file =
case Keyword.get(opts, :file) do
file_opt when is_binary(file_opt) -> file_opt
nil -> Map.get(env, :file)
end

module =
case Keyword.get(opts, :module) do
module_opt when is_atom(module_opt) -> module_opt
nil -> nil
end

fa =
case Keyword.get(opts, :function) do
{function, arity} when is_atom(function) and is_integer(arity) -> {function, arity}
nil -> nil
end

temp_tracers =
case Keyword.get(opts, :tracers) do
tracers_opt when is_list(tracers_opt) -> tracers_opt
nil -> []
end

aliases =
case Keyword.get(opts, :aliases) do
aliases_opt when is_list(aliases_opt) ->
IO.warn(":aliases option in eval is deprecated")
aliases_opt

nil ->
Map.get(env, :aliases)
end

requires =
case Keyword.get(opts, :requires) do
requires_opt when is_list(requires_opt) ->
IO.warn(":requires option in eval is deprecated")
MapSet.new(requires_opt)

nil ->
Map.get(env, :requires)
end

functions =
case Keyword.get(opts, :functions) do
functions_opt when is_list(functions_opt) ->
IO.warn(":functions option in eval is deprecated")
functions_opt

nil ->
Map.get(env, :functions)
end

macros =
case Keyword.get(opts, :macros) do
macros_opt when is_list(macros_opt) ->
IO.warn(":macros option in eval is deprecated")
macros_opt

nil ->
Map.get(env, :macros)
end

{lexical_tracker, tracers} =
case Keyword.get(opts, :lexical_tracker) do
pid when is_pid(pid) ->
IO.warn(":lexical_tracker option in eval is deprecated")

if Process.alive?(pid) do
{pid, temp_tracers}
else
{nil, []}
end

nil ->
IO.warn(":lexical_tracker option in eval is deprecated")
{nil, []}

_ ->
{nil, temp_tracers}
end

%{
env
| file: file,
module: module,
function: fa,
tracers: tracers,
macros: macros,
functions: functions,
lexical_tracker: lexical_tracker,
requires: requires,
aliases: aliases,
line: line
}
end
end
end
176 changes: 128 additions & 48 deletions apps/debug_adapter/lib/debug_adapter/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ defmodule ElixirLS.DebugAdapter.Server do
first_frame
| bindings: Map.new(binding),
dbg_frame?: true,
dbg_env: Code.env_for_eval(env),
dbg_env: env,
module: env.module,
function: env.function,
file: env.file,
Expand All @@ -276,7 +276,7 @@ defmodule ElixirLS.DebugAdapter.Server do
messages: [],
bindings: Map.new(binding),
dbg_frame?: true,
dbg_env: Code.env_for_eval(env),
dbg_env: env,
line: env.line || 1
}

Expand All @@ -294,11 +294,7 @@ defmodule ElixirLS.DebugAdapter.Server do
messages: [],
bindings: %{},
dbg_frame?: true,
dbg_env:
Code.env_for_eval(
file: file,
line: line
),
dbg_env: nil,
line: line
}
end
Expand Down Expand Up @@ -1335,7 +1331,8 @@ defmodule ElixirLS.DebugAdapter.Server do
end

async_fn = fn ->
{binding, env_for_eval} = binding_and_env(state.paused_processes, args["frameId"])
frame = frame_for_eval(state.paused_processes, args["frameId"])
{_metadata, _env, binding, env_for_eval} = binding_and_env(state.paused_processes, frame)
value = evaluate_code_expression(expr, binding, env_for_eval)

child_type = Variables.child_type(value)
Expand Down Expand Up @@ -1449,23 +1446,18 @@ defmodule ElixirLS.DebugAdapter.Server do
column = Utils.dap_character_to_elixir(line, column)
prefix = String.slice(line, 0, column)

{binding, _env_for_eval} =
binding_and_env(state.paused_processes, args["arguments"]["frameId"])
frame = frame_for_eval(state.paused_processes, args["arguments"]["frameId"])

vars =
binding
|> Enum.map(fn {name, value} ->
%ElixirSense.Core.State.VarInfo{
name: name,
type: ElixirSense.Core.Binding.from_var(value)
}
end)
{metadata, env, _binding, _env_for_eval} = binding_and_env(state.paused_processes, frame)

env = %ElixirSense.Core.State.Env{vars: vars}
metadata = %ElixirSense.Core.Metadata{}
cursor_position =
case frame do
nil -> {1, 1}
frame -> {frame.line, 1}
end

results =
ElixirLS.Utils.CompletionEngine.complete(prefix, env, metadata, {1, 1})
ElixirLS.Utils.CompletionEngine.complete(prefix, env, metadata, cursor_position)
|> Enum.map(&ElixirLS.DebugAdapter.Completions.map/1)

%{"targets" => results}
Expand Down Expand Up @@ -1609,7 +1601,9 @@ defmodule ElixirLS.DebugAdapter.Server do

defp evaluate_code_expression(expr, binding, env_or_opts) do
try do
{term, _bindings} = Code.eval_string(expr, binding, env_or_opts)
# TODO use Code.env_for_eval when we require elixir 1.14
env = ElixirLS.DebugAdapter.Code.env_for_eval(env_or_opts)
{term, _bindings} = Code.eval_string(expr, binding, env)
term
catch
kind, error ->
Expand All @@ -1628,6 +1622,22 @@ defmodule ElixirLS.DebugAdapter.Server do
end
end

defp frame_for_eval(_paused_processes, nil), do: nil

defp frame_for_eval(paused_processes, frame_id) do
case find_frame(paused_processes, frame_id) do
{_pid, %Frame{} = frame} ->
frame

_ ->
raise ServerError,
message: "argumentError",
format: "Unable to find frame {frameId}",
variables: %{"frameId" => frame_id},
send_telemetry: false
end
end

# for null frameId DAP spec suggest to return variables in the global scope
# there is no global scope in erlang/elixir so instead we flat map all variables
# from all paused processes and evaluator
Expand All @@ -1647,35 +1657,38 @@ defmodule ElixirLS.DebugAdapter.Server do
Binding.to_elixir_variable_names(bindings)
end)

{binding, []}
{%ElixirSense.Core.Metadata{},
update_env_vars_from_binding(%ElixirSense.Core.State.Env{}, binding), binding, []}
end

defp binding_and_env(paused_processes, frame_id) do
case find_frame(paused_processes, frame_id) do
{_pid, %Frame{bindings: bindings, dbg_frame?: dbg_frame?} = frame} when is_map(bindings) ->
if dbg_frame? do
{bindings |> Enum.to_list(), frame.dbg_env}
else
{Binding.to_elixir_variable_names(bindings),
[
file: frame.file,
line: frame.line
]}
end
defp binding_and_env(
_paused_processes,
%Frame{bindings: bindings, dbg_frame?: dbg_frame?} = frame
)
when is_map(bindings) do
{metadata, env, macro_env} = parse_file(frame.file, frame.line)

{_pid, %Frame{} = frame} ->
{[],
[
file: frame.file,
line: frame.line
]}

_ ->
raise ServerError,
message: "argumentError",
format: "Unable to find frame {frameId}",
variables: %{"frameId" => frame_id},
send_telemetry: false
if dbg_frame? do
if frame.dbg_env do
# we are evaluating an expression in dbg breakpoint - take dbg macro env as env for eval
# we have exact elixir bindings here
env = ElixirSense.Core.State.Env.update_from_macro_env(env, frame.dbg_env)
binding = bindings |> Enum.to_list()
{metadata, update_env_vars_from_binding(env, binding), binding, frame.dbg_env}
else
# we are evaluating an expression in dbg breakpoint but frame is upper in the stacktrace
# we don't have any bindings here
# take env from metadata builder as env for eval
{metadata, env, [], macro_env}
end
else
# we are evaluating an expression in OTP debugger breakpoint
# take env from metadata builder as env for eval
# bindings come from OTP debugger
# unfortunately there is no way to select right versions of variables in binding
# translation in :elixir_erl_var.translate is one way
binding = Binding.to_elixir_variable_names(bindings)
{metadata, update_env_vars_from_binding(env, binding), binding, macro_env}
end
end

Expand Down Expand Up @@ -2597,6 +2610,16 @@ defmodule ElixirLS.DebugAdapter.Server do
status not in [:exit, :no_conn],
into: %{},
do: {pid, {function, status, info}}
rescue
e in MatchError ->
if Exception.message(e) =~ ":already_started" do
# workaround for a crash in tests (probably caused by race conditions)
# ** (MatchError) no match of right hand side value: {:error, {:already_started, #PID<0.385.0>}}
# (debugger 5.3.4) dbg_iserver.erl:75: :dbg_iserver.safe_call/1
%{}
else
reraise(e, __STACKTRACE__)
end
end

defp update_threads(state = %__MODULE__{}, snapshot_by_pid \\ get_snapshot_by_pid()) do
Expand Down Expand Up @@ -2889,4 +2912,61 @@ defmodule ElixirLS.DebugAdapter.Server do
GenServer.call(parent, {:request_finished, packet, start_time, result}, :infinity)
end)
end

defp parse_file(file, line) do
try do
if String.ends_with?(file, ".ex") or String.ends_with?(file, ".exs") do
code = File.read!(file)
buffer_file_metadata = ElixirSense.Core.Parser.parse_string(code, false, true, {line, 1})

env = ElixirSense.Core.Metadata.get_env(buffer_file_metadata, {line, 1})
# TODO env_for_eval?
# should we clear versioned_vars?
{buffer_file_metadata, env, ElixirSense.Core.State.Env.to_macro_env(env, file, line)}
else
# do not try to parse non elixir files
{%ElixirSense.Core.Metadata{}, %ElixirSense.Core.State.Env{}, [file: file, line: line]}
end
rescue
error ->
{payload, stacktrace} = Exception.blame(:error, error, __STACKTRACE__)
message = Exception.format(:error, payload, stacktrace)

Output.debugger_console(
"Unable to parse file #{file}: #{message}; Using stub evaluator environment."
)

{%ElixirSense.Core.Metadata{}, %ElixirSense.Core.State.Env{}, [file: file, line: line]}
end
end

defp update_env_vars_from_binding(env, binding) do
env_vars = env.vars |> Map.new(&{&1.name, &1})
env_var_names = env_vars |> Map.keys()
binding_var_names = binding |> Keyword.keys()

vars =
for var_name <- Enum.uniq(env_var_names ++ binding_var_names) do
case {Keyword.fetch(binding, var_name), Map.fetch(env_vars, var_name)} do
{{:ok, binding_value}, {:ok, env_var}} ->
# var both in env and in binding - prefer type from binding
type = ElixirSense.Core.Binding.from_var(binding_value)
%ElixirSense.Core.State.VarInfo{env_var | type: type}

{_, {:ok, env_var}} ->
# var only in env - keep it, binding may not have everything
env_var

{{:ok, binding_value}, _} ->
# var only in binding - add var to env
%ElixirSense.Core.State.VarInfo{
name: var_name,
type: ElixirSense.Core.Binding.from_var(binding_value)
}
end
|> IO.inspect()
end

%{env | vars: vars}
end
end

0 comments on commit f0c4a81

Please sign in to comment.