Skip to content

Commit 4c24f1f

Browse files
committed
dbg backport 3
1 parent 8c9fa65 commit 4c24f1f

File tree

3 files changed

+263
-0
lines changed

3 files changed

+263
-0
lines changed

apps/debug_adapter/lib/debug_adapter/server.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ defmodule ElixirLS.DebugAdapter.Server do
7777
GenServer.cast(server, {:paused, pid})
7878
end
7979

80+
# Kernel.dbg backend - adapted from IEx.Pry (lib/iex/lib/iex/pry.ex)
81+
# When upgrading Elixir, diff against upstream IEx.Pry.dbg/3 and sync changes.
82+
# DAP-specific: Uses GenServer.call to debug adapter instead of IEx.Broker.
8083
@spec dbg(Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t()
8184
def dbg({:|>, _meta, _args} = ast, options, %Macro.Env{} = env) when is_list(options) do
8285
[first_ast_chunk | asts_chunks] = ast |> Macro.unpipe() |> chunk_pipeline_asts_by_line(env)
@@ -133,6 +136,8 @@ defmodule ElixirLS.DebugAdapter.Server do
133136
end
134137
end
135138

139+
# Copied from IEx.Pry.annotate_quoted/3 - keep in sync with upstream.
140+
# DAP-specific: Calls __next__/3 which uses GenServer instead of IEx.Broker.
136141
@doc """
137142
Annotate a quoted expression with line-by-line debugging steps.
138143
"""
@@ -185,6 +190,9 @@ defmodule ElixirLS.DebugAdapter.Server do
185190
[]
186191
end
187192

193+
# DAP-specific: Unlike IEx.Pry.__next__/3 which calls IEx.Broker,
194+
# this uses GenServer.call to the debug adapter to pause execution.
195+
# Supports both list options (for IEx.Pry compatibility) and Macro.Env.
188196
def __next__(next?, binding, opts) when is_boolean(next?) and is_list(opts) do
189197
vars = for {key, _} when is_atom(key) <- binding, do: {key, nil}
190198

@@ -3206,6 +3214,7 @@ defmodule ElixirLS.DebugAdapter.Server do
32063214
end
32073215
end
32083216

3217+
# Copied verbatim from IEx.Pry - keep in sync with upstream during Elixir upgrades.
32093218
# Made public to be called from dbg/3 to reduce the amount of generated code.
32103219
@doc false
32113220
def __dbg_pipe_step__(value, string_asts, start_with_pipe?, options) do
@@ -3225,6 +3234,7 @@ defmodule ElixirLS.DebugAdapter.Server do
32253234
value
32263235
end
32273236

3237+
# Pipeline helpers copied verbatim from IEx.Pry - keep in sync with upstream.
32283238
defp chunk_pipeline_asts_by_line(asts, %Macro.Env{line: env_line}) do
32293239
Enum.chunk_by(asts, fn
32303240
{{_fun_or_var, meta, _args}, _pipe_index} -> meta[:line] || env_line
@@ -3240,6 +3250,8 @@ defmodule ElixirLS.DebugAdapter.Server do
32403250
Enum.map(asts, fn {ast, _pipe_index} -> Macro.to_string(ast) end)
32413251
end
32423252

3253+
# annotate_quoted helpers copied from IEx.Pry - keep in sync with upstream.
3254+
# Functions: line_range/2, next_binding/2, match_binding/2, next_var/1, unwrap_block/1,2
32433255
defp line_range(ast, line) do
32443256
{_, {min, max}} =
32453257
Macro.prewalk(ast, {:infinity, line}, fn

apps/debug_adapter/test/debugger_test.exs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3124,6 +3124,231 @@ defmodule ElixirLS.DebugAdapter.ServerTest do
31243124
end)
31253125
end
31263126

3127+
test "next steps through multi-stage dbg pipeline", %{server: server} do
3128+
in_fixture(__DIR__, "mix_project", fn ->
3129+
abs_path = Path.absname("lib/dbg.ex")
3130+
Server.receive_packet(server, initialize_req_(1))
3131+
assert_receive(response(_, 1, "initialize", _))
3132+
3133+
Server.receive_packet(
3134+
server,
3135+
launch_req(2, %{
3136+
"request" => "launch",
3137+
"type" => "mix_task",
3138+
"task" => "run",
3139+
"taskArgs" => ["-e", "MixProject.Dbg.pipe_multi_stage()"],
3140+
"projectDir" => File.cwd!()
3141+
})
3142+
)
3143+
3144+
assert_receive(response(_, 2, "launch"), 3000)
3145+
assert_receive(event(_, "initialized", _), 5000)
3146+
3147+
Process.sleep(100)
3148+
3149+
assert MixProject.Dbg in :int.interpreted()
3150+
3151+
Server.receive_packet(server, request(5, "configurationDone", %{}))
3152+
assert_receive(response(_, 5, "configurationDone"))
3153+
3154+
Server.receive_packet(server, request(6, "threads", %{}))
3155+
assert_receive(response(_, 6, "threads", %{"threads" => threads}))
3156+
assert Enum.count(Enum.uniq(Enum.map(threads, & &1["id"]))) == Enum.count(threads)
3157+
3158+
assert_receive event(_, "stopped", %{
3159+
"allThreadsStopped" => false,
3160+
"reason" => "breakpoint",
3161+
"threadId" => thread_id
3162+
}),
3163+
5_000
3164+
3165+
Server.receive_packet(server, stacktrace_req(7, thread_id))
3166+
3167+
assert_receive response(_, 7, "stackTrace", %{
3168+
"stackFrames" => [
3169+
%{
3170+
"name" => "MixProject.Dbg.pipe_multi_stage/0",
3171+
"source" => %{"path" => ^abs_path}
3172+
}
3173+
],
3174+
"totalFrames" => 1
3175+
})
3176+
3177+
Server.receive_packet(server, next_req(14, thread_id))
3178+
assert_receive response(_, 14, "next")
3179+
3180+
assert_receive event(_, "stopped", %{
3181+
"allThreadsStopped" => false,
3182+
"reason" => "breakpoint",
3183+
"threadId" => ^thread_id
3184+
}),
3185+
5_000
3186+
3187+
Server.receive_packet(server, stacktrace_req(141, thread_id))
3188+
3189+
assert_receive response(_, 141, "stackTrace", %{
3190+
"stackFrames" => [
3191+
%{
3192+
"line" => line_stage_1,
3193+
"name" => "MixProject.Dbg.pipe_multi_stage/0",
3194+
"source" => %{"path" => ^abs_path}
3195+
}
3196+
]
3197+
})
3198+
3199+
assert line_stage_1 in 22..27
3200+
3201+
Server.receive_packet(server, next_req(15, thread_id))
3202+
assert_receive response(_, 15, "next")
3203+
3204+
assert_receive event(_, "stopped", %{
3205+
"allThreadsStopped" => false,
3206+
"reason" => "breakpoint",
3207+
"threadId" => ^thread_id
3208+
}),
3209+
5_000
3210+
3211+
Server.receive_packet(server, stacktrace_req(151, thread_id))
3212+
3213+
assert_receive response(_, 151, "stackTrace", %{
3214+
"stackFrames" => [
3215+
%{
3216+
"line" => line_stage_2,
3217+
"name" => "MixProject.Dbg.pipe_multi_stage/0",
3218+
"source" => %{"path" => ^abs_path}
3219+
}
3220+
]
3221+
})
3222+
3223+
assert line_stage_2 in 22..27
3224+
assert line_stage_2 != line_stage_1
3225+
3226+
Server.receive_packet(server, continue_req(16, thread_id))
3227+
assert_receive response(_, 16, "continue", %{"allThreadsContinued" => true})
3228+
3229+
refute_receive event(_, "stopped", %{
3230+
"allThreadsStopped" => false,
3231+
"reason" => "breakpoint",
3232+
"threadId" => ^thread_id
3233+
}),
3234+
1_000
3235+
end)
3236+
end
3237+
3238+
test "dbg pipeline inside case exposes branch bindings", %{server: server} do
3239+
in_fixture(__DIR__, "mix_project", fn ->
3240+
abs_path = Path.absname("lib/dbg.ex")
3241+
Server.receive_packet(server, initialize_req_(1))
3242+
assert_receive(response(_, 1, "initialize", _))
3243+
3244+
Server.receive_packet(
3245+
server,
3246+
launch_req(2, %{
3247+
"request" => "launch",
3248+
"type" => "mix_task",
3249+
"task" => "run",
3250+
"taskArgs" => ["-e", "MixProject.Dbg.case_pipe_dbg()"],
3251+
"projectDir" => File.cwd!()
3252+
})
3253+
)
3254+
3255+
assert_receive(response(_, 2, "launch"), 3000)
3256+
assert_receive(event(_, "initialized", _), 5000)
3257+
3258+
Process.sleep(100)
3259+
3260+
assert MixProject.Dbg in :int.interpreted()
3261+
3262+
Server.receive_packet(server, request(5, "configurationDone", %{}))
3263+
assert_receive(response(_, 5, "configurationDone"))
3264+
3265+
Server.receive_packet(server, request(6, "threads", %{}))
3266+
assert_receive(response(_, 6, "threads", %{"threads" => threads}))
3267+
assert Enum.count(Enum.uniq(Enum.map(threads, & &1["id"]))) == Enum.count(threads)
3268+
3269+
assert_receive event(_, "stopped", %{
3270+
"allThreadsStopped" => false,
3271+
"reason" => "breakpoint",
3272+
"threadId" => thread_id
3273+
}),
3274+
5_000
3275+
3276+
Server.receive_packet(server, stacktrace_req(7, thread_id))
3277+
3278+
assert_receive response(_, 7, "stackTrace", %{
3279+
"stackFrames" => [
3280+
%{
3281+
"column" => 0,
3282+
"id" => frame_id,
3283+
"line" => initial_line,
3284+
"name" => "MixProject.Dbg.case_pipe_dbg/1",
3285+
"source" => %{"path" => ^abs_path}
3286+
}
3287+
],
3288+
"totalFrames" => 1
3289+
})
3290+
3291+
assert initial_line in 34..44
3292+
3293+
Server.receive_packet(server, scopes_req(8, frame_id))
3294+
3295+
assert_receive response(_, 8, "scopes", %{
3296+
"scopes" => [
3297+
%{
3298+
"name" => "variables",
3299+
"variablesReference" => vars_id
3300+
}
3301+
| _
3302+
]
3303+
})
3304+
3305+
Server.receive_packet(server, vars_req(9, vars_id))
3306+
3307+
assert_receive response(_, 9, "variables", %{
3308+
"variables" => vars
3309+
})
3310+
3311+
assert Enum.any?(vars, fn
3312+
%{"name" => "list", "value" => "[\"a\", \"b\"]"} -> true
3313+
_ -> false
3314+
end)
3315+
3316+
Server.receive_packet(server, next_req(14, thread_id))
3317+
assert_receive response(_, 14, "next")
3318+
3319+
assert_receive event(_, "stopped", %{
3320+
"allThreadsStopped" => false,
3321+
"reason" => "breakpoint",
3322+
"threadId" => ^thread_id
3323+
}),
3324+
5_000
3325+
3326+
Server.receive_packet(server, stacktrace_req(141, thread_id))
3327+
3328+
assert_receive response(_, 141, "stackTrace", %{
3329+
"stackFrames" => [
3330+
%{
3331+
"line" => line_after_step,
3332+
"name" => "MixProject.Dbg.case_pipe_dbg/1",
3333+
"source" => %{"path" => ^abs_path}
3334+
}
3335+
]
3336+
})
3337+
3338+
assert line_after_step in 34..37
3339+
3340+
Server.receive_packet(server, continue_req(15, thread_id))
3341+
assert_receive response(_, 15, "continue", %{"allThreadsContinued" => true})
3342+
3343+
refute_receive event(_, "stopped", %{
3344+
"allThreadsStopped" => false,
3345+
"reason" => "breakpoint",
3346+
"threadId" => ^thread_id
3347+
}),
3348+
1_000
3349+
end)
3350+
end
3351+
31273352
test "breaks on dbg when module is not interpreted", %{server: server} do
31283353
in_fixture(__DIR__, "mix_project", fn ->
31293354
abs_path = Path.absname("lib/dbg.ex")

apps/debug_adapter/test/fixtures/mix_project/lib/dbg.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,31 @@ if Version.match?(System.version(), ">= 1.14.0-dev") do
1717
|> File.exists?()
1818
|> dbg()
1919
end
20+
21+
def pipe_multi_stage() do
22+
__ENV__.file
23+
|> Path.dirname()
24+
|> Path.split()
25+
|> Enum.reject(&(&1 == ""))
26+
|> Enum.map(&String.upcase/1)
27+
|> Enum.join("/")
28+
|> dbg()
29+
end
30+
31+
def case_pipe_dbg(arg \\ {:ok, ["a", "b"]}) do
32+
case arg do
33+
{:ok, list} ->
34+
list
35+
|> Enum.reverse()
36+
|> Enum.map(&String.upcase/1)
37+
|> Enum.join("/")
38+
|> dbg()
39+
40+
{:error, reason} ->
41+
reason
42+
|> Atom.to_string()
43+
|> dbg()
44+
end
45+
end
2046
end
2147
end

0 commit comments

Comments
 (0)