Skip to content

Commit

Permalink
[dap] implement evaluate and stacktrace callbacks for dap
Browse files Browse the repository at this point in the history
  • Loading branch information
TheGeorge committed Feb 2, 2021
1 parent 72034ca commit b73ef60
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 35 deletions.
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
, {plt_apps, all_deps}
%% Depending on the OTP version, erl_types (used by
%% els_typer), is either part of hipe or dialyzer.
, {plt_extra_apps, [dialyzer, hipe, mnesia, common_test]}
, {plt_extra_apps, [dialyzer, hipe, mnesia, common_test, debugger]}
]}.

{xref_checks, [ undefined_function_calls
Expand Down
10 changes: 9 additions & 1 deletion src/els_dap_agent.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
%%=============================================================================
-module(els_dap_agent).

-export([ int_cb/2 ]).
-export([ int_cb/2, meta_eval/2 ]).

-spec int_cb(pid(), pid()) -> ok.
int_cb(Thread, ProviderPid) ->
ProviderPid ! {int_cb, Thread},
ok.

-spec meta_eval(pid(), string()) -> any().
meta_eval(Meta, Command) ->
_ = int:meta(Meta, eval, {ignored_module, Command}),
receive
{Meta, {eval_rsp, Return}} ->
Return
end.
168 changes: 136 additions & 32 deletions src/els_dap_general_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ is_enabled() -> true.
-spec init() -> state().
init() ->
#{ threads => #{}
, launch_params => #{} }.
, launch_params => #{}
, scope_bindings => #{}}.

-spec handle_request(request(), state()) -> {result(), state()}.
handle_request({<<"initialize">>, _Params}, State) ->
Expand Down Expand Up @@ -185,7 +186,7 @@ handle_request({<<"setFunctionBreakpoints">>, Params}
handle_request({<<"threads">>, _Params}, #{threads := Threads0} = State) ->
Threads =
[ #{ <<"id">> => Id
, <<"name">> => els_utils:to_binary(io_lib:format("~p", [Pid]))
, <<"name">> => format_term(Pid)
} || {Id, #{pid := Pid} = _Thread} <- maps:to_list(Threads0)
],
{#{<<"threads">> => Threads}, State};
Expand All @@ -212,17 +213,20 @@ handle_request({<<"stackTrace">>, Params}, #{threads := Threads} = State) ->
{#{<<"stackFrames">> => StackFrames}, State};
handle_request({<<"scopes">>, #{<<"frameId">> := FrameId} }
, #{ threads := Threads
} = State) ->
Frame = frame_by_id(FrameId, maps:values(Threads)),
Bindings = maps:get(bindings, Frame),
Ref = erlang:unique_integer([positive]),
{#{<<"scopes">> => [
#{
<<"name">> => <<"Locals">>,
<<"presentationHint">> => <<"locals">>,
<<"variablesReference">> => Ref
}
]}, State#{scope_bindings => #{Ref => {generic, Bindings}}}};
, scope_bindings := ExistingScopes} = State) ->
case frame_by_id(FrameId, maps:values(Threads)) of
undefined -> {#{<<"scopes">> => []}, State};
Frame ->
Bindings = maps:get(bindings, Frame),
Ref = erlang:unique_integer([positive]),
{#{<<"scopes">> => [
#{
<<"name">> => <<"Locals">>,
<<"presentationHint">> => <<"locals">>,
<<"variablesReference">> => Ref
}
]}, State#{scope_bindings => ExistingScopes#{Ref => {generic, Bindings}}}}
end;
handle_request( {<<"next">>, Params}
, #{ threads := Threads
, project_node := ProjectNode
Expand Down Expand Up @@ -263,15 +267,41 @@ handle_request({<<"evaluate">>, #{ <<"context">> := <<"hover">>
, <<"frameId">> := FrameId
, <<"expression">> := Input
} = _Params}
, #{ threads := Threads } = State
) ->
%% hover makes only sense for variables
%% use the expression as fallback
case frame_by_id(FrameId, maps:values(Threads)) of
undefined -> {#{<<"result">> => <<"not available">>}, State};
Frame ->
Bindings = maps:get(bindings, Frame),
VarName = erlang:list_to_atom(els_utils:to_list(Input)),
case proplists:lookup(VarName, Bindings) of
{VarName, VarValue} ->
build_evaluate_response(VarValue, State);
none ->
{#{<<"result">> => <<"not available">>}, State}
end
end;
handle_request({<<"evaluate">>, #{ <<"context">> := Context
, <<"frameId">> := FrameId
, <<"expression">> := Input
} = _Params}
, #{ threads := Threads
, project_node := ProjectNode
} = State
) ->
Frame = frame_by_id(FrameId, maps:values(Threads)),
Bindings = maps:get(bindings, Frame),
Return = els_dap_rpc:eval(ProjectNode, Input, Bindings),
Result = els_utils:to_binary(io_lib:format("~p", [Return])),
{#{<<"result">> => Result}, State};
) when Context =:= <<"watch">> orelse Context =:= <<"repl">> ->
%% repl and watch can use whole expressions,
%% but we still want structured variable scopes
case pid_by_frame_id(FrameId, maps:values(Threads)) of
undefined ->
{#{<<"result">> => <<"not available">>}, State};
Pid ->
{ok, Meta} = els_dap_rpc:get_meta(ProjectNode, Pid),
Command = els_utils:to_list(Input),
Return = els_dap_rpc:meta_eval(ProjectNode, Meta, Command),
build_evaluate_response(Return, State)
end;
handle_request({<<"variables">>, #{<<"variablesReference">> := Ref
} = _Params}
, #{ scope_bindings := AllBindings
Expand Down Expand Up @@ -328,19 +358,17 @@ id(Pid) ->
-spec stack_frames(pid(), atom()) -> #{frame_id() => frame()}.
stack_frames(Pid, Node) ->
{ok, Meta} = els_dap_rpc:get_meta(Node, Pid),
%% TODO: Also examine rest of list
[{_Level, {M, F, A}} | _] =
[{Level, {M, F, A}} | Rest] =
els_dap_rpc:meta(Node, Meta, backtrace, all),
Bindings = els_dap_rpc:meta(Node, Meta, bindings, nostack),
Bindings = els_dap_rpc:meta(Node, Meta, bindings, Level),
StackFrameId = erlang:unique_integer([positive]),
StackFrame = #{ module => M
, function => F
, arguments => A
, source => source(M, Node)
, line => break_line(Pid, Node)
, bindings => Bindings
},
#{StackFrameId => StackFrame}.
, bindings => Bindings},
collect_frames(Node, Meta, Level, Rest, #{StackFrameId => StackFrame}).

-spec break_line(pid(), atom()) -> integer().
break_line(Pid, Node) ->
Expand All @@ -351,6 +379,7 @@ break_line(Pid, Node) ->
-spec source(atom(), atom()) -> binary().
source(Module, Node) ->
CompileOpts = els_dap_rpc:module_info(Node, Module, compile),
els_dap_rpc:clear(Node),
Source = proplists:get_value(source, CompileOpts),
unicode:characters_to_binary(Source).

Expand All @@ -359,12 +388,25 @@ to_pid(ThreadId, Threads) ->
Thread = maps:get(ThreadId, Threads),
maps:get(pid, Thread).

-spec frame_by_id(frame_id(), [thread()]) -> frame().
-spec frame_by_id(frame_id(), [thread()]) -> frame() | undefined.
frame_by_id(FrameId, Threads) ->
[Frame] = [ maps:get(FrameId, Frames)
|| #{frames := Frames} <- Threads, maps:is_key(FrameId, Frames)
],
Frame.
case [ maps:get(FrameId, Frames)
|| #{frames := Frames} <- Threads, maps:is_key(FrameId, Frames)
] of
[Frame] -> Frame;
_ -> undefined
end.

-spec pid_by_frame_id(frame_id(), [thread()]) -> pid() | undefined.
pid_by_frame_id(FrameId, Threads) ->
case [ Pid
|| #{frames := Frames, pid := Pid} <- Threads
, maps:is_key(FrameId, Frames)
] of
[Proc] -> Proc;
_ ->
undefined
end.

-spec format_mfa(module(), atom(), integer()) -> binary().
format_mfa(M, F, A) ->
Expand Down Expand Up @@ -452,7 +494,7 @@ add_var_to_acc(Name, Value, Bindings, {VarAcc, BindAcc}) ->
build_variable(Name, Value, Ref) ->
%% print whole term to enable copying if the value
#{ <<"name">> => unicode:characters_to_binary(io_lib:format("~s", [Name]))
, <<"value">> => unicode:characters_to_binary(io_lib:format("~p", [Value]))
, <<"value">> => format_term(Value)
, <<"variablesReference">> => Ref }.

-spec build_list_bindings(
Expand All @@ -472,7 +514,7 @@ build_map_bindings(Map) ->
fun ({Key, Value}, {Cnt, Acc}) ->
Name =
unicode:characters_to_binary(
io_lib:format("~p => ~p", [Key, Value])),
io_lib:format("~s => ~s", [format_term(Key), format_term(Value)])),
{ Cnt + 1
, [{ Name
, {generic, [{'Value', Value}, {'Key', Key}]}
Expand All @@ -495,3 +537,65 @@ build_maybe_improper_list_bindings([E | Tail], Cnt, Acc) ->
build_maybe_improper_list_bindings(ImproperTail, _Cnt, Acc) ->
Binding = {"improper tail", ImproperTail},
build_maybe_improper_list_bindings([], 0, [Binding | Acc]).

-spec is_structured(term()) -> boolean().
is_structured(Term) when
is_list(Term) orelse
is_map(Term) orelse
is_tuple(Term) -> true;
is_structured(_) -> false.

-spec build_evaluate_response(term(), state()) -> {any(), state()}.
build_evaluate_response(
ResultValue,
State = #{scope_bindings := ExistingScopes}
) ->
ResultBinary = format_term(ResultValue),
case is_structured(ResultValue) of
true ->
{_, SubScope} = build_variables(generic, [{undefined, ResultValue}]),
%% there is onlye one sub-scope returned
[Ref] = maps:keys(SubScope),
NewScopes = maps:merge(ExistingScopes, SubScope),
{ #{<<"result">> => ResultBinary, <<"variablesReference">> => Ref}
, State#{scope_bindings => NewScopes}
};
false ->
{ #{<<"result">> => ResultBinary}
, State
}
end.

-spec format_term(term()) -> binary().
format_term(T) ->
%% print on one line and print strings
%% as printable characters (if possible)
els_utils:to_binary(
[ string:trim(Line)
|| Line <- string:split(io_lib:format("~tp", [T]), "\n", all)]).

-spec collect_frames(node(), pid(), pos_integer(), Backtrace, Acc) -> Acc
when Acc :: #{frame_id() => frame()},
Backtrace :: [{pos_integer(), {module(), atom(), non_neg_integer()}}].
collect_frames(_, _, _, [], Acc) -> Acc;
collect_frames(Node, Meta, Level, [{NextLevel, {M, F, A}} | Rest], Acc) ->
case els_dap_rpc:meta(Node, Meta, stack_frame, {up, Level}) of
{NextLevel, {_, Line}, Bindings} ->
StackFrameId = erlang:unique_integer([positive]),
StackFrame = #{ module => M
, function => F
, arguments => A
, source => source(M, Node)
, line => Line
, bindings => Bindings},
collect_frames( Node
, Meta
, NextLevel
, Rest
, Acc#{StackFrameId => StackFrame}
);
BadFrame ->
?LOG_ERROR( "Received a bad frame: ~p expected level ~p and module ~p"
, [BadFrame, NextLevel, M]),
Acc
end.
12 changes: 11 additions & 1 deletion src/els_dap_rpc.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
, auto_attach/3
, break/3
, break_in/4
, clear/1
, continue/2
, eval/3
, get_meta/2
, i/2
, load_binary/4
, meta/4
, meta_eval/3
, module_info/3
, next/2
, no_break/1
Expand All @@ -35,6 +37,10 @@ break(Node, Module, Line) ->
break_in(Node, Module, Func, Arity) ->
rpc:call(Node, int, break_in, [ Module, Func, Arity]).

-spec clear(node()) -> ok.
clear(Node) ->
rpc:call(Node, int, clear, []).

-spec continue(node(), pid()) -> any().
continue(Node, Pid) ->
rpc:call(Node, int, continue, [Pid]).
Expand All @@ -61,10 +67,14 @@ i(Node, Module) ->
load_binary(Node, Module, File, Bin) ->
rpc:call(Node, code, load_binary, [Module, File, Bin]).

-spec meta(node(), pid(), atom(), atom()) -> any().
-spec meta(node(), pid(), atom(), any()) -> any().
meta(Node, Meta, Flag, Opt) ->
rpc:call(Node, int, meta, [Meta, Flag, Opt]).

-spec meta_eval(node(), pid(), string()) -> any().
meta_eval(Node, Meta, Command) ->
rpc:call(Node, els_dap_agent, meta_eval, [Meta, Command]).

-spec next(node(), pid()) -> any().
next(Node, Pid) ->
rpc:call(Node, int, next, [Pid]).
Expand Down

0 comments on commit b73ef60

Please sign in to comment.