From ef2b57670252f71ac3e074e0ce52df91352d8bcc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 6 Feb 2022 12:22:15 +0100 Subject: [PATCH 1/2] Add support for variable expansion of evaluate results compatibility improvement: only return variable type and pagination properties when supported by client run evaluate in a stacktrace frame if specified --- .../elixir_ls_debugger/lib/debugger/server.ex | 116 ++++++++++++------ .../elixir_ls_debugger/test/debugger_test.exs | 9 +- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 0f300b7ed..e6cb87976 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -35,7 +35,12 @@ defmodule ElixirLS.Debugger.Server do task_ref: nil, threads: %{}, threads_inverse: %{}, - paused_processes: %{}, + paused_processes: %{ + evaluator: %{ + vars: %{}, + vars_inverse: %{} + } + }, next_id: 1, output: Output, breakpoints: %{}, @@ -494,15 +499,26 @@ defmodule ElixirLS.Debugger.Server do end defp handle_request( - request(_cmd, "evaluate", %{"expression" => expr} = _args), + request(_cmd, "evaluate", %{"expression" => expr} = args), state = %__MODULE__{} ) do timeout = Map.get(state.config, "debugExpressionTimeoutMs", 10_000) - bindings = all_variables(state.paused_processes) + bindings = all_variables(state.paused_processes, args["frameId"]) + + value = evaluate_code_expression(expr, bindings, timeout) - result = evaluate_code_expression(expr, bindings, timeout) + child_type = Variables.child_type(value) + {state, var_id} = get_variable_reference(child_type, state, :evaluator, value) + + json = + %{ + "result" => inspect(value), + "variablesReference" => var_id + } + |> maybe_append_children_number(state.client_info, child_type, value) + |> maybe_append_variable_type(state.client_info, value) - {%{"result" => inspect(result), "variablesReference" => 0}, state} + {json, state} end defp handle_request(continue_req(_, thread_id), state = %__MODULE__{}) do @@ -618,34 +634,39 @@ defmodule ElixirLS.Debugger.Server do end Enum.reduce(children, {state, []}, fn {name, value}, {state = %__MODULE__{}, result} -> - num_children = Variables.num_children(value) child_type = Variables.child_type(value) - - {state, var_id} = - if child_type do - ensure_var_id(state, pid, value) - else - {state, 0} - end - - json = %{ - "name" => to_string(name), - "value" => inspect(value), - "variablesReference" => var_id, - "type" => Variables.type(value) - } + {state, var_id} = get_variable_reference(child_type, state, pid, value) json = - case child_type do - :indexed -> Map.put(json, "indexedVariables", num_children) - :named -> Map.put(json, "namedVariables", num_children) - nil -> json - end + %{ + "name" => to_string(name), + "value" => inspect(value), + "variablesReference" => var_id + } + |> maybe_append_children_number(state.client_info, child_type, value) + |> maybe_append_variable_type(state.client_info, value) {state, result ++ [json]} end) end + defp get_variable_reference(nil, state, _pid, _value), do: {state, 0} + + defp get_variable_reference(_child_type, state, pid, value), + do: ensure_var_id(state, pid, value) + + defp maybe_append_children_number(map, %{"supportsVariablePaging" => true}, atom, value) + when atom in [:indexed, :named], + do: Map.put(map, Atom.to_string(atom) <> "Variables", Variables.num_children(value)) + + defp maybe_append_children_number(map, _, _, _value), do: map + + defp maybe_append_variable_type(map, %{"supportsVariableType" => true}, value) do + Map.put(map, "type", Variables.type(value)) + end + + defp maybe_append_variable_type(map, _, _value), do: map + defp evaluate_code_expression(expr, bindings, timeout) do task = Task.async(fn -> @@ -673,10 +694,15 @@ defmodule ElixirLS.Debugger.Server do end end - defp all_variables(paused_processes) do + defp all_variables(paused_processes, nil) do paused_processes - |> Enum.flat_map(fn {_pid, %PausedProcess{} = paused_process} -> - paused_process.frames |> Map.values() + |> Enum.flat_map(fn + {:evaluator, _} -> + # TODO setVariable? + [] + + {_pid, %PausedProcess{} = paused_process} -> + paused_process.frames |> Map.values() end) |> Enum.filter(&match?(%Frame{bindings: bindings} when is_map(bindings), &1)) |> Enum.flat_map(fn %Frame{bindings: bindings} -> @@ -684,23 +710,37 @@ defmodule ElixirLS.Debugger.Server do end) end + defp all_variables(paused_processes, frame_id) do + case find_frame(paused_processes, frame_id) do + {_pid, %Frame{bindings: bindings}} when is_map(bindings) -> + Binding.to_elixir_variable_names(bindings) + + _ -> + [] + end + end + defp find_var(paused_processes, var_id) do - Enum.find_value(paused_processes, fn {pid, %PausedProcess{} = paused_process} -> - if Map.has_key?(paused_process.vars, var_id) do - {pid, paused_process.vars[var_id]} + Enum.find_value(paused_processes, fn {pid, %{vars: vars}} -> + if Map.has_key?(vars, var_id) do + {pid, vars[var_id]} end end) end defp find_frame(paused_processes, frame_id) do - Enum.find_value(paused_processes, fn {pid, %PausedProcess{} = paused_process} -> - if Map.has_key?(paused_process.frames, frame_id) do - {pid, paused_process.frames[frame_id]} - end + Enum.find_value(paused_processes, fn + {pid, %{frames: frames}} -> + if Map.has_key?(frames, frame_id) do + {pid, frames[frame_id]} + end + + {:evaluator, _} -> + nil end) end - defp ensure_thread_id(state = %__MODULE__{}, pid) do + defp ensure_thread_id(state = %__MODULE__{}, pid) when is_pid(pid) do if Map.has_key?(state.threads_inverse, pid) do {state, state.threads_inverse[pid]} else @@ -719,7 +759,7 @@ defmodule ElixirLS.Debugger.Server do end) end - defp ensure_var_id(state = %__MODULE__{}, pid, var) do + defp ensure_var_id(state = %__MODULE__{}, pid, var) when is_pid(pid) or pid == :evaluator do unless Map.has_key?(state.paused_processes, pid) do raise ArgumentError, message: "paused process #{inspect(pid)} not found" end @@ -742,7 +782,7 @@ defmodule ElixirLS.Debugger.Server do end) end - defp ensure_frame_id(state = %__MODULE__{}, pid, %Frame{} = frame) do + defp ensure_frame_id(state = %__MODULE__{}, pid, %Frame{} = frame) when is_pid(pid) do unless Map.has_key?(state.paused_processes, pid) do raise ArgumentError, message: "paused process #{inspect(pid)} not found" end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index f68479561..a31d933aa 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -92,7 +92,14 @@ defmodule ElixirLS.Debugger.ServerTest do @tag :fixture test "basic debugging", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> - Server.receive_packet(server, initialize_req(1, %{})) + Server.receive_packet( + server, + initialize_req(1, %{ + "supportsVariablePaging" => true, + "supportsVariableType" => true + }) + ) + assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})) Server.receive_packet( From 0242a22ad86c26f6bc9112830704593ecfeab03b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 6 Feb 2022 13:42:18 +0100 Subject: [PATCH 2/2] add support for hex formatting --- apps/elixir_ls_debugger/lib/debugger/server.ex | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index e6cb87976..409baa2f3 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -484,7 +484,7 @@ defmodule ElixirLS.Debugger.Server do {state, vars_json} = case find_var(state.paused_processes, var_id) do {pid, var} -> - variables(state, pid, var, args["start"], args["count"], args["filter"]) + variables(state, pid, var, args["start"], args["count"], args["filter"], args["format"]) nil -> raise ServerError, @@ -512,7 +512,7 @@ defmodule ElixirLS.Debugger.Server do json = %{ - "result" => inspect(value), + "result" => format_variable(value, args["format"]), "variablesReference" => var_id } |> maybe_append_children_number(state.client_info, child_type, value) @@ -623,7 +623,7 @@ defmodule ElixirLS.Debugger.Server do %__MODULE__{state | paused_processes: paused_processes} end - defp variables(state = %__MODULE__{}, pid, var, start, count, filter) do + defp variables(state = %__MODULE__{}, pid, var, start, count, filter, format) do var_child_type = Variables.child_type(var) children = @@ -640,7 +640,7 @@ defmodule ElixirLS.Debugger.Server do json = %{ "name" => to_string(name), - "value" => inspect(value), + "value" => format_variable(value, format), "variablesReference" => var_id } |> maybe_append_children_number(state.client_info, child_type, value) @@ -667,6 +667,11 @@ defmodule ElixirLS.Debugger.Server do defp maybe_append_variable_type(map, _, _value), do: map + defp format_variable(value, %{"hex" => true}) when is_binary(value) do + Base.encode16(value) + end + defp format_variable(value, _), do: inspect(value) + defp evaluate_code_expression(expr, bindings, timeout) do task = Task.async(fn -> @@ -896,7 +901,7 @@ defmodule ElixirLS.Debugger.Server do "supportedChecksumAlgorithms" => [], "supportsRestartRequest" => false, "supportsExceptionOptions" => false, - "supportsValueFormattingOptions" => false, + "supportsValueFormattingOptions" => true, "supportsExceptionInfoRequest" => false, "supportTerminateDebuggee" => false }