Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for function breakpoints in debugger #656

Merged
merged 12 commits into from
Jan 23, 2022
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 @@ -156,6 +156,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