Skip to content

Commit

Permalink
[dap] add logpoints and fix watchlist
Browse files Browse the repository at this point in the history
  • Loading branch information
TheGeorge committed Feb 24, 2021
1 parent 97e5708 commit b4a8438
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 66 deletions.
98 changes: 98 additions & 0 deletions apps/els_dap/src/els_dap_breakpoints.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
-module(els_dap_breakpoints).
-export([
build_source_breakpoints/1,
get_function_breaks/2,
get_line_breaks/2,
do_line_breakpoints/4,
do_function_breaks/4,
type/3
]).

%%==============================================================================
%% Includes
%%==============================================================================
-include_lib("kernel/include/logger.hrl").

%%==============================================================================
%% Types
%%==============================================================================

-type breakpoints() :: #{
module() => #{
line => #{
line() => line_breaks()
},
function => [function_break()]
}
}.
-type line() :: non_neg_integer().
-type line_breaks() ::
regular
| {log, expression()}.
-type expression() :: string().
-type function_break() :: {atom(), non_neg_integer()}.

-export_type([breakpoints/0]).

-spec type(breakpoints(), module(), line()) -> line_breaks().
type(Breakpoints, Module, Line) ->
?LOG_DEBUG("checking breakpoint type for ~s:~b", [Module, Line]),
case Breakpoints of
#{Module := #{line := #{Line := Break}}} ->
Break;
_ ->
%% function breaks get handled like regular ones
regular
end.

%% @doc build regular and log breakpoints from setBreakpoint request
-spec build_source_breakpoints(Params :: map()) -> {module(), #{line() => line_breaks()}}.
build_source_breakpoints(Params) ->
#{<<"source">> := #{<<"path">> := Path}} = Params,
Module = els_uri:module(els_uri:uri(Path)),
SourceBreakpoints = maps:get(<<"breakpoints">>, Params, []),
_SourceModified = maps:get(<<"sourceModified">>, Params, false),
{Module, maps:from_list(lists:map(fun build_source_breakpoint/1, SourceBreakpoints))}.

-spec build_source_breakpoint(map()) -> {line(), 'regular' | {'log', expression()}}.
build_source_breakpoint(#{<<"line">> := Line, <<"logMessage">> := LogExpr}) ->
{Line, {log, LogExpr}};
build_source_breakpoint(#{<<"line">> := Line}) ->
{Line, regular}.

-spec get_function_breaks(module(), breakpoints()) -> [function_break()].
get_function_breaks(Module, Breaks) ->
case Breaks of
#{Module := #{function := Functions}} -> Functions;
_ -> []
end.

-spec get_line_breaks(module(), breakpoints()) -> #{line() => line_breaks()}.
get_line_breaks(Module, Breaks) ->
case Breaks of
#{Module := #{line := Lines}} -> Lines;
_ -> []
end.

-spec do_line_breakpoints(node(), module(), #{line() => line_breaks()}, breakpoints()) ->
breakpoints().
do_line_breakpoints(Node, Module, LineBreakPoints, Breaks) ->
maps:map(
fun
(Line, regular) -> els_dap_rpc:break(Node, Module, Line);
(Line, {log, _}) -> els_dap_rpc:break(Node, Module, Line)
end,
LineBreakPoints
),
case Breaks of
#{Module := ModBreaks} -> Breaks#{Module => ModBreaks#{line => LineBreakPoints}};
_ -> Breaks#{Module => #{line => LineBreakPoints, function => []}}
end.

-spec do_function_breaks(node(), module(), [function_break()], breakpoints()) -> breakpoints().
do_function_breaks(Node, Module, FBreaks, Breaks) ->
[els_dap_rpc:break_in(Node, Module, Func, Arity) || {Func, Arity} <- FBreaks],
case Breaks of
#{Module := ModBreaks} -> Breaks#{Module => ModBreaks#{function => FBreaks}};
_ -> Breaks#{Module => #{line => #{}, function => FBreaks}}
end.
122 changes: 63 additions & 59 deletions apps/els_dap/src/els_dap_general_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,15 @@
, launch_params => #{}
, scope_bindings =>
#{pos_integer() => {binding_type(), bindings()}}
, breakpoints := breakpoints()
, breakpoints := els_dap_breakpoints:breakpoints()
, timeout := timeout()
}.
-type bindings() :: [{varname(), term()}].
-type varname() :: atom() | string().
%% extendable bindings type for customized pretty printing
-type binding_type() :: generic | map_assoc.
-type breakpoints() :: #{module() => #{lines => [line()], function => [function_break()]}}.
-type line() :: non_neg_integer().
-type function_break() :: {atom(), non_neg_integer()}.
-type line() :: non_neg_integer().

%%==============================================================================
%% els_provider functions
%%==============================================================================
Expand Down Expand Up @@ -174,24 +173,21 @@ handle_request( {<<"setBreakpoints">>, Params}
, timeout := Timeout} = State
) ->
ensure_connected(ProjectNode, Timeout),
#{<<"source">> := #{<<"path">> := Path}} = Params,
SourceBreakpoints = maps:get(<<"breakpoints">>, Params, []),
_SourceModified = maps:get(<<"sourceModified">>, Params, false),
Module = els_uri:module(els_uri:uri(Path)),
{Module, LineBreaks} = els_dap_breakpoints:build_source_breakpoints(Params),


{module, Module} = els_dap_rpc:i(ProjectNode, Module),
Lines = [Line || #{<<"line">> := Line} <- SourceBreakpoints],

%% purge all breakpoints from the module
els_dap_rpc:no_break(ProjectNode, Module),
Breakpoints1 = do_line_breakpoints(ProjectNode, Module, Lines, Breakpoints0),
Breakpoints1 = els_dap_breakpoints:do_line_breakpoints(ProjectNode, Module, LineBreaks, Breakpoints0),
BreakpointsRsps = [
#{<<"verified">> => true, <<"line">> => Line}
|| {{_, Line}, _} <- els_dap_rpc:all_breaks(ProjectNode, Module)
],

FunctionBreaks = get_function_breaks(Module, Breakpoints1),
Breakpoints2 = do_function_breaks(ProjectNode, Module, FunctionBreaks, Breakpoints1),
FunctionBreaks = els_dap_breakpoints:get_function_breaks(Module, Breakpoints1),
Breakpoints2 = els_dap_breakpoints:do_function_breaks(ProjectNode, Module, FunctionBreaks, Breakpoints1),

{#{<<"breakpoints">> => BreakpointsRsps}, State#{ breakpoints => Breakpoints2}};
handle_request({<<"setExceptionBreakpoints">>, _Params}, State) ->
Expand Down Expand Up @@ -235,7 +231,7 @@ handle_request({<<"setFunctionBreakpoints">>, Params}

Breakpoints2 = maps:fold(
fun(Module, FunctionBreaks, Acc) ->
do_function_breaks(ProjectNode, Module, FunctionBreaks, Acc)
els_dap_breakpoints:do_function_breaks(ProjectNode, Module, FunctionBreaks, Acc)
end,
Breakpoints1,
ModFuncBreaks
Expand All @@ -254,8 +250,8 @@ handle_request({<<"setFunctionBreakpoints">>, Params}
%% replay line breaks
Breakpoints3 = maps:fold(
fun(Module, _, Acc) ->
Lines = get_line_breaks(Module, Acc),
do_line_breakpoints(ProjectNode, Module, Lines, Acc)
Lines = els_dap_breakpoints:get_line_breaks(Module, Acc),
els_dap_breakpoints:do_line_breakpoints(ProjectNode, Module, Lines, Acc)
end,
Breakpoints2,
Breakpoints2
Expand Down Expand Up @@ -342,12 +338,12 @@ handle_request( {<<"stepOut">>, Params}
Pid = to_pid(ThreadId, Threads),
ok = els_dap_rpc:next(ProjectNode, Pid),
{#{}, State};
handle_request({<<"evaluate">>, #{ <<"context">> := Context
handle_request({<<"evaluate">>, #{ <<"context">> := <<"hover">>
, <<"frameId">> := FrameId
, <<"expression">> := Input
} = _Params}
, #{ threads := Threads } = State
) when Context =:= <<"watch">> orelse Context =:= <<"hover">> ->
) ->
%% hover makes only sense for variables
%% use the expression as fallback
case frame_by_id(FrameId, maps:values(Threads)) of
Expand All @@ -362,23 +358,26 @@ handle_request({<<"evaluate">>, #{ <<"context">> := Context
{#{<<"result">> => <<"not available">>}, State}
end
end;
handle_request({<<"evaluate">>, #{ <<"context">> := <<"repl">>
handle_request({<<"evaluate">>, #{ <<"context">> := Context
, <<"frameId">> := FrameId
, <<"expression">> := Input
} = _Params}
, #{ threads := Threads
, project_node := ProjectNode
} = 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),
Update =
case Context of
<<"watch">> -> no_update;
<<"repl">> -> update
end,
Return = safe_eval(ProjectNode, Pid, Input, Update),
build_evaluate_response(Return, State)
end;
handle_request({<<"variables">>, #{<<"variablesReference">> := Ref
Expand All @@ -399,16 +398,32 @@ handle_request({<<"disconnect">>, _Params}, State = #{project_node := ProjectNod
handle_info( {int_cb, ThreadPid}
, #{ threads := Threads
, project_node := ProjectNode
, breakpoints := Breakpoints
} = State
) ->
?LOG_DEBUG("Int CB called. thread=~p", [ThreadPid]),
ThreadId = id(ThreadPid),
Thread = #{ pid => ThreadPid
, frames => stack_frames(ThreadPid, ProjectNode)
},
els_dap_server:send_event(<<"stopped">>, #{ <<"reason">> => <<"breakpoint">>
, <<"threadId">> => ThreadId
}),
{Module, Line} = break_module_line(ThreadPid, ProjectNode),

%% handle breakpoints
case els_dap_breakpoints:type(Breakpoints, Module, Line) of
regular ->
els_dap_server:send_event(<<"stopped">>, #{ <<"reason">> => <<"breakpoint">>
, <<"threadId">> => ThreadId
});
{log, Expression} ->
Return = safe_eval(ProjectNode, ThreadPid, Expression, no_update),
LogMessage = unicode:characters_to_binary(
io_lib:format("@~s:~b -> ~w~n", [Module, Line, Return])
),
els_dap_server:send_event(<<"output">>, #{ <<"output">> => LogMessage }),
els_dap_rpc:continue(ProjectNode, ThreadPid)
end,


State#{threads => maps:put(ThreadId, Thread, Threads)};
handle_info({nodedown, Node}, State) ->
%% the project node is down, there is nothing left to do then to exit
Expand All @@ -424,7 +439,8 @@ handle_info({nodedown, Node}, State) ->
capabilities() ->
#{ <<"supportsConfigurationDoneRequest">> => true
, <<"supportsEvaluateForHovers">> => true
, <<"supportsFunctionBreakpoints">> => true}.
, <<"supportsFunctionBreakpoints">> => true
, <<"supportsLogPoints">> => true}.

%%==============================================================================
%% Internal Functions
Expand Down Expand Up @@ -455,10 +471,15 @@ stack_frames(Pid, Node) ->
, bindings => Bindings},
collect_frames(Node, Meta, Level, Rest, #{StackFrameId => StackFrame}).

-spec break_module_line(pid(), atom()) -> {module(), integer()}.
break_module_line(Pid, Node) ->
Snapshots = els_dap_rpc:snapshot(Node),
{Pid, _Function, break, Location} = lists:keyfind(Pid, 1, Snapshots),
Location.

-spec break_line(pid(), atom()) -> integer().
break_line(Pid, Node) ->
Snapshots = els_dap_rpc:snapshot(Node),
{Pid, _Function, break, {_Module, Line}} = lists:keyfind(Pid, 1, Snapshots),
{_, Line} = break_module_line(Pid, Node),
Line.

-spec source(atom(), atom()) -> binary().
Expand Down Expand Up @@ -685,37 +706,6 @@ collect_frames(Node, Meta, Level, [{NextLevel, {M, F, A}} | Rest], Acc) ->
Acc
end.

%% breakpoint management
-spec get_function_breaks(module(), breakpoints()) -> [function_break()].
get_function_breaks(Module, Breaks) ->
case Breaks of
#{Module := #{function := Functions}} -> Functions;
_ -> []
end.

-spec get_line_breaks(module(), breakpoints()) -> [line()].
get_line_breaks(Module, Breaks) ->
case Breaks of
#{Module := #{line := Lines}} -> Lines;
_ -> []
end.

-spec do_line_breakpoints(node(), module(), [line()], breakpoints()) -> breakpoints().
do_line_breakpoints(Node, Module, Lines, Breaks) ->
[els_dap_rpc:break(Node, Module, Line) || Line <- Lines],
case Breaks of
#{Module := ModBreaks} -> Breaks#{ Module => ModBreaks#{line => Lines}};
_ -> Breaks#{ Module => #{line => Lines, function => []}}
end.

-spec do_function_breaks(node(), module(), [function_break()], breakpoints()) -> breakpoints().
do_function_breaks(Node, Module, FBreaks, Breaks) ->
[els_dap_rpc:break_in(Node, Module, Func, Arity) || {Func, Arity} <- FBreaks],
case Breaks of
#{Module := ModBreaks} -> Breaks#{ Module => ModBreaks#{function => FBreaks}};
_ -> Breaks#{ Module => #{line => [], function => FBreaks}}
end.

-spec ensure_connected(node(), timeout()) -> ok.
ensure_connected(Node, Timeout) ->
case is_node_connected(Node) of
Expand All @@ -739,3 +729,17 @@ stop_debugger() ->
-spec is_node_connected(node()) -> boolean().
is_node_connected(Node) ->
lists:member(Node, erlang:nodes(connected)).

-spec safe_eval(node(), pid(), string(), update | no_update) -> term().
safe_eval(ProjectNode, Debugged, Expression, Update) ->
{ok, Meta} = els_dap_rpc:get_meta(ProjectNode, Debugged),
Command = els_utils:to_list(Expression),
Return = els_dap_rpc:meta_eval(ProjectNode, Meta, Command),
case Update of
update -> ok;
no_update ->
receive
{int_cb, Debugged} -> ok
end
end,
Return.
Loading

0 comments on commit b4a8438

Please sign in to comment.