Skip to content

Commit 7a9bbc3

Browse files
authored
Add support for function breakpoints in debugger (#656)
* remove not supported SetExceptionBreakpoints request handler Clients should only call this request if the capability ‘exceptionBreakpointFilters’ returns one or more filters. No way to implement it via :int module * implement function breakpoints * test erlang breakpoints * fix small memory leak when unsetting last breakpoint in file * add more breakpoint tests * make tests more synchronous * add function breakpoints tests * interpret modules * extract and test mfa parsing * run formatter * Update readme * cleanup
1 parent c8ff98b commit 7a9bbc3

File tree

7 files changed

+501
-45
lines changed

7 files changed

+501
-45
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Installing Elixir and Erlang from [ASDF](https://github.com/asdf-vm/asdf) is gen
9797

9898
## Debugger support
9999

100-
ElixirLS includes debugger support adhering to the [VS Code debugger protocol](https://code.visualstudio.com/docs/extensionAPI/api-debugging) which is closely related to the Language Server Protocol. At the moment, only line breakpoints are supported.
100+
ElixirLS includes debugger support adhering to the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) which is closely related to the Language Server Protocol. At the moment, line breakpoints and function breakpoints are supported.
101101

102102
When debugging in Elixir or Erlang, only modules that have been "interpreted" (using `:int.ni/1` or `:int.i/1`) will accept breakpoints or show up in stack traces. The debugger in ElixirLS automatically interprets all modules in the Mix project and dependencies prior to launching the Mix task, so you can set breakpoints anywhere in your project or dependency modules.
103103

@@ -177,6 +177,8 @@ Please note that due to `:int` limitation NIF modules cannot be interpreted and
177177
}
178178
```
179179

180+
Function breakpoints will break on the first line of every clause of the specified function. The function needs to be specified as MFA (module, function, arity) in the standard elixir format, e.g. `:some_module.function/1` or `Some.Module.some_function/2`.
181+
180182
## Automatic builds and error reporting
181183

182184
Builds are performed automatically when files are saved. If you want this to happen automatically when you type, you can turn on "autosave" in your IDE.

apps/elixir_ls_debugger/lib/debugger/protocol.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ defmodule ElixirLS.Debugger.Protocol do
3535
end
3636
end
3737

38-
defmacro set_exception_breakpoints_req(seq) do
38+
defmacro set_function_breakpoints_req(seq, breakpoints) do
3939
quote do
40-
request(unquote(seq), "setExceptionBreakpoints")
40+
request(unquote(seq), "setFunctionBreakpoints", %{
41+
"breakpoints" => unquote(breakpoints)
42+
})
4143
end
4244
end
4345

apps/elixir_ls_debugger/lib/debugger/server.ex

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule ElixirLS.Debugger.Server do
1414
defexception [:message, :format, :variables]
1515
end
1616

17-
alias ElixirLS.Debugger.{Output, Stacktrace, Protocol, Variables}
17+
alias ElixirLS.Debugger.{Output, Stacktrace, Protocol, Variables, Utils}
1818
alias ElixirLS.Debugger.Stacktrace.Frame
1919
use GenServer
2020
use Protocol
@@ -29,7 +29,8 @@ defmodule ElixirLS.Debugger.Server do
2929
paused_processes: %{},
3030
next_id: 1,
3131
output: Output,
32-
breakpoints: %{}
32+
breakpoints: %{},
33+
function_breakpoints: []
3334

3435
defmodule PausedProcess do
3536
defstruct stack: nil,
@@ -112,6 +113,8 @@ defmodule ElixirLS.Debugger.Server do
112113
paused_process = %PausedProcess{stack: Stacktrace.get(pid), ref: ref}
113114
state = put_in(state.paused_processes[pid], paused_process)
114115

116+
# Debugger Adapter Protocol requires us to return 'function breakpoint' reason
117+
# but we can't tell what kind of a breakpoint was hit
115118
body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false}
116119
Output.send_event("stopped", body)
117120
state
@@ -238,7 +241,13 @@ defmodule ElixirLS.Debugger.Server do
238241

239242
result = set_breakpoints(path, new_lines)
240243
new_bps = for {:ok, module, line} <- result, do: {module, line}
241-
state = put_in(state.breakpoints[path], new_bps)
244+
245+
state =
246+
if new_bps == [] do
247+
%{state | breakpoints: state.breakpoints |> Map.delete(path)}
248+
else
249+
put_in(state.breakpoints[path], new_bps)
250+
end
242251

243252
breakpoints_json =
244253
Enum.map(result, fn
@@ -249,8 +258,70 @@ defmodule ElixirLS.Debugger.Server do
249258
{%{"breakpoints" => breakpoints_json}, state}
250259
end
251260

252-
defp handle_request(set_exception_breakpoints_req(_), state = %__MODULE__{}) do
253-
{%{}, state}
261+
defp handle_request(
262+
set_function_breakpoints_req(_, breakpoints),
263+
state = %__MODULE__{}
264+
) do
265+
# condition and hitCondition not supported
266+
mfas =
267+
for %{"name" => name} <- breakpoints do
268+
Utils.parse_mfa(name)
269+
end
270+
271+
parsed_mfas = for {:ok, mfa} <- mfas, do: mfa
272+
273+
removed_breakpoints = state.function_breakpoints -- parsed_mfas
274+
new_breakpoints = parsed_mfas -- state.function_breakpoints
275+
276+
for {m, f, a} <- removed_breakpoints do
277+
case :int.del_break_in(m, f, a) do
278+
:ok ->
279+
:ok
280+
281+
{:error, :function_not_found} ->
282+
IO.warn("Unable to delete function breakpoint on #{inspect({m, f, a})}")
283+
end
284+
end
285+
286+
results =
287+
for {m, f, a} <- new_breakpoints,
288+
into: %{},
289+
do:
290+
(
291+
result =
292+
case :int.ni(m) do
293+
{:module, _} ->
294+
:int.break_in(m, f, a)
295+
296+
_ ->
297+
{:error, "Cannot interpret module #{inspect(m)}"}
298+
end
299+
300+
{{m, f, a}, result}
301+
)
302+
303+
successful = for {mfa, :ok} <- results, do: mfa
304+
305+
state = %{
306+
state
307+
| function_breakpoints: (state.function_breakpoints -- removed_breakpoints) ++ successful
308+
}
309+
310+
breakpoints_json =
311+
Enum.map(mfas, fn
312+
{:ok, mfa} ->
313+
if mfa in state.function_breakpoints do
314+
%{"verified" => true}
315+
else
316+
{:error, error} = results[mfa]
317+
%{"verified" => false, "message" => inspect(error)}
318+
end
319+
320+
{:error, error} ->
321+
%{"verified" => false, "message" => error}
322+
end)
323+
324+
{%{"breakpoints" => breakpoints_json}, state}
254325
end
255326

256327
defp handle_request(configuration_done_req(_), state = %__MODULE__{}) do
@@ -751,7 +822,7 @@ defmodule ElixirLS.Debugger.Server do
751822
defp capabilities do
752823
%{
753824
"supportsConfigurationDoneRequest" => true,
754-
"supportsFunctionBreakpoints" => false,
825+
"supportsFunctionBreakpoints" => true,
755826
"supportsConditionalBreakpoints" => false,
756827
"supportsHitConditionalBreakpoints" => false,
757828
"supportsEvaluateForHovers" => false,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule ElixirLS.Debugger.Utils do
2+
def parse_mfa(mfa_str) do
3+
case Code.string_to_quoted(mfa_str) do
4+
{:ok, {:/, _, [{{:., _, [mod, fun]}, _, []}, arity]}}
5+
when is_atom(fun) and is_integer(arity) ->
6+
case mod do
7+
atom when is_atom(atom) ->
8+
{:ok, {atom, fun, arity}}
9+
10+
{:__aliases__, _, list} when is_list(list) ->
11+
{:ok, {list |> Module.concat(), fun, arity}}
12+
13+
_ ->
14+
{:error, "cannot parse MFA"}
15+
end
16+
17+
_ ->
18+
{:error, "cannot parse MFA"}
19+
end
20+
end
21+
end

0 commit comments

Comments
 (0)