Skip to content

Commit

Permalink
Add support for function breakpoints in debugger (#656)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lukaszsamson authored Jan 23, 2022
1 parent c8ff98b commit 7a9bbc3
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 45 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Installing Elixir and Erlang from [ASDF](https://github.com/asdf-vm/asdf) is gen

## Debugger support

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.
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.

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.

Expand Down Expand Up @@ -177,6 +177,8 @@ Please note that due to `:int` limitation NIF modules cannot be interpreted and
}
```

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`.

## Automatic builds and error reporting

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.
Expand Down
6 changes: 4 additions & 2 deletions apps/elixir_ls_debugger/lib/debugger/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ defmodule ElixirLS.Debugger.Protocol do
end
end

defmacro set_exception_breakpoints_req(seq) do
defmacro set_function_breakpoints_req(seq, breakpoints) do
quote do
request(unquote(seq), "setExceptionBreakpoints")
request(unquote(seq), "setFunctionBreakpoints", %{
"breakpoints" => unquote(breakpoints)
})
end
end

Expand Down
83 changes: 77 additions & 6 deletions apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule ElixirLS.Debugger.Server do
defexception [:message, :format, :variables]
end

alias ElixirLS.Debugger.{Output, Stacktrace, Protocol, Variables}
alias ElixirLS.Debugger.{Output, Stacktrace, Protocol, Variables, Utils}
alias ElixirLS.Debugger.Stacktrace.Frame
use GenServer
use Protocol
Expand All @@ -29,7 +29,8 @@ defmodule ElixirLS.Debugger.Server do
paused_processes: %{},
next_id: 1,
output: Output,
breakpoints: %{}
breakpoints: %{},
function_breakpoints: []

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

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

result = set_breakpoints(path, new_lines)
new_bps = for {:ok, module, line} <- result, do: {module, line}
state = put_in(state.breakpoints[path], new_bps)

state =
if new_bps == [] do
%{state | breakpoints: state.breakpoints |> Map.delete(path)}
else
put_in(state.breakpoints[path], new_bps)
end

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

defp handle_request(set_exception_breakpoints_req(_), state = %__MODULE__{}) do
{%{}, state}
defp handle_request(
set_function_breakpoints_req(_, breakpoints),
state = %__MODULE__{}
) do
# condition and hitCondition not supported
mfas =
for %{"name" => name} <- breakpoints do
Utils.parse_mfa(name)
end

parsed_mfas = for {:ok, mfa} <- mfas, do: mfa

removed_breakpoints = state.function_breakpoints -- parsed_mfas
new_breakpoints = parsed_mfas -- state.function_breakpoints

for {m, f, a} <- removed_breakpoints do
case :int.del_break_in(m, f, a) do
:ok ->
:ok

{:error, :function_not_found} ->
IO.warn("Unable to delete function breakpoint on #{inspect({m, f, a})}")
end
end

results =
for {m, f, a} <- new_breakpoints,
into: %{},
do:
(
result =
case :int.ni(m) do
{:module, _} ->
:int.break_in(m, f, a)

_ ->
{:error, "Cannot interpret module #{inspect(m)}"}
end

{{m, f, a}, result}
)

successful = for {mfa, :ok} <- results, do: mfa

state = %{
state
| function_breakpoints: (state.function_breakpoints -- removed_breakpoints) ++ successful
}

breakpoints_json =
Enum.map(mfas, fn
{:ok, mfa} ->
if mfa in state.function_breakpoints do
%{"verified" => true}
else
{:error, error} = results[mfa]
%{"verified" => false, "message" => inspect(error)}
end

{:error, error} ->
%{"verified" => false, "message" => error}
end)

{%{"breakpoints" => breakpoints_json}, state}
end

defp handle_request(configuration_done_req(_), state = %__MODULE__{}) do
Expand Down Expand Up @@ -751,7 +822,7 @@ defmodule ElixirLS.Debugger.Server do
defp capabilities do
%{
"supportsConfigurationDoneRequest" => true,
"supportsFunctionBreakpoints" => false,
"supportsFunctionBreakpoints" => true,
"supportsConditionalBreakpoints" => false,
"supportsHitConditionalBreakpoints" => false,
"supportsEvaluateForHovers" => false,
Expand Down
21 changes: 21 additions & 0 deletions apps/elixir_ls_debugger/lib/debugger/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule ElixirLS.Debugger.Utils do
def parse_mfa(mfa_str) do
case Code.string_to_quoted(mfa_str) do
{:ok, {:/, _, [{{:., _, [mod, fun]}, _, []}, arity]}}
when is_atom(fun) and is_integer(arity) ->
case mod do
atom when is_atom(atom) ->
{:ok, {atom, fun, arity}}

{:__aliases__, _, list} when is_list(list) ->
{:ok, {list |> Module.concat(), fun, arity}}

_ ->
{:error, "cannot parse MFA"}
end

_ ->
{:error, "cannot parse MFA"}
end
end
end
Loading

0 comments on commit 7a9bbc3

Please sign in to comment.