Skip to content

Commit 27ec700

Browse files
authored
Feature: Open callback implementation in editor (#959)
* add suggested function matcher * change function matcher approach * add open editor button in traces * fix tooltip scroll * add test * add moduledoc * fix button in fullscreen * CR suggestions * fix tooltip message
1 parent 869ade9 commit 27ec700

15 files changed

Lines changed: 496 additions & 61 deletions

File tree

assets/app/hooks/tooltip.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,20 @@ const Tooltip = {
8282
tooltipEl.style.display = 'none';
8383
};
8484

85+
this.handleScroll = () => {
86+
tooltipEl.style.display = 'none';
87+
};
88+
8589
this.el.addEventListener('mouseenter', this.handleMouseEnter);
8690
this.el.addEventListener('mouseleave', this.handleMouseLeave);
91+
window.addEventListener('scroll', this.handleScroll, true);
8792
},
8893
destroyed() {
8994
clearTimeout(this._hoverTimeout);
9095
document.querySelector('#tooltip').style.display = 'none';
9196
this.el.removeEventListener('mouseenter', this.handleMouseEnter);
9297
this.el.removeEventListener('mouseleave', this.handleMouseLeave);
98+
window.removeEventListener('scroll', this.handleScroll, true);
9399
},
94100
};
95101

dev/live_views/main.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ defmodule LiveDebuggerDev.LiveViews.Main do
123123
{:noreply, assign(socket, :message, %{name: "message name", text: "some text"})}
124124
end
125125

126+
def handle_event("increment", _, socket) when is_binary(socket) do
127+
{:noreply, update(socket, :counter, &(&1 + 1))}
128+
end
129+
126130
def handle_event("increment", _, socket) do
127131
{:noreply, update(socket, :counter, &(&1 + 1))}
128132
end

e2e/tests/async-jobs.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ test('user can see and track async jobs in LiveView and LiveComponent', async ({
6262
'No active async jobs found'
6363
);
6464

65+
await devApp.locator('#component-long-load-toggle').click();
6566
await devApp.locator('#component-start-cancelable-async-button').click();
6667
await expect(
6768
asyncJobName(dbgApp, ':component_cancelable_fetch')

e2e/tests/elements-inspection.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
expect,
33
findNodeModuleInfo,
4+
findSidebarBasicInfo,
45
prepareDevDebuggerPairTest,
56
getDevPid,
67
Page,
@@ -42,7 +43,7 @@ const selectLiveViewByPid = async (dbgApp: Page, pid: string) => {
4243
);
4344
await btn.hover();
4445
await btn.click();
45-
await expect(findNodeModuleInfo(dbgApp)).toBeVisible();
46+
await expect(findSidebarBasicInfo(dbgApp)).toBeVisible();
4647
};
4748

4849
const openDbgForLiveView = async (
@@ -67,7 +68,7 @@ const openMobileDbgForLiveView = async (
6768
);
6869
await btn.hover();
6970
await btn.click();
70-
await expect(findNodeModuleInfo(dbgApp)).toBeVisible();
71+
await expect(findSidebarBasicInfo(dbgApp)).toBeVisible();
7172
return dbgApp;
7273
};
7374

lib/live_debugger/app/debugger/callback_tracing/structs/trace_display.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Structs.TraceDisplay do
2525
:body,
2626
:side_section_left,
2727
:side_section_right,
28-
:error
28+
:error,
29+
:source
2930
]
3031

3132
@type type() :: :normal | :diff | :error
@@ -44,7 +45,8 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Structs.TraceDisplay do
4445
body: list({String.t(), term()}),
4546
side_section_left: side_section_left(),
4647
side_section_right: side_section_right(),
47-
error: ErrorTrace.t() | nil
48+
error: ErrorTrace.t() | nil,
49+
source: FunctionTrace.SourceLocation.t() | nil
4850
}
4951

5052
@spec from_trace(Trace.t(), boolean()) :: t()
@@ -60,7 +62,8 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Structs.TraceDisplay do
6062
body: get_body(trace),
6163
side_section_left: get_side_section_left(trace),
6264
side_section_right: get_side_section_right(trace),
63-
error: get_error(trace)
65+
error: get_error(trace),
66+
source: get_source(trace)
6467
}
6568
end
6669

@@ -121,4 +124,8 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Structs.TraceDisplay do
121124
defp get_error(%FunctionTrace{error: error}), do: error
122125

123126
defp get_error(_), do: nil
127+
128+
defp get_source(%FunctionTrace{source: source}), do: source
129+
130+
defp get_source(_), do: nil
124131
end

lib/live_debugger/app/debugger/callback_tracing/web/components/trace.ex

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,57 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace do
1414

1515
alias LiveDebugger.Structs.Trace.ErrorTrace
1616

17+
alias LiveDebugger.App.Debugger.Utils.Editor
1718
alias Phoenix.LiveView.JS
1819

20+
@doc """
21+
Button to open the trace source in an external editor.
22+
"""
23+
attr(:id, :string, required: true)
24+
attr(:elixir_editor, :string, default: nil)
25+
attr(:source, SourceLocation, default: nil)
26+
attr(:fullscreen?, :boolean, default: false)
27+
28+
def open_in_editor_button(assigns) do
29+
assigns = assign(assigns, :editor_docs_url, Editor.editor_docs_url())
30+
31+
~H"""
32+
<.tooltip
33+
:if={@source}
34+
id={@id <> "-open-in-editor-tooltip"}
35+
class="my-2"
36+
content={
37+
if(@elixir_editor,
38+
do: "Open in editor",
39+
else: "Editor not configured. Click for setup instructions."
40+
)
41+
}
42+
position="top-center"
43+
fullscreen?={@fullscreen?}
44+
>
45+
<.icon_button
46+
:if={@elixir_editor}
47+
id={"#{@id}-open-in-editor-button"}
48+
icon="icon-external-link"
49+
phx-click="open-in-editor"
50+
phx-value-file={@source.source_file}
51+
phx-value-line={@source.line}
52+
variant="secondary"
53+
/>
54+
<.button_link
55+
:if={@elixir_editor == nil}
56+
href={@editor_docs_url}
57+
id={"#{@id}-open-in-editor"}
58+
variant="secondary"
59+
size="sm"
60+
class="opacity-50 cursor-pointer"
61+
>
62+
<.icon name="icon-external-link" class="w-4 h-4" />
63+
</.button_link>
64+
</.tooltip>
65+
"""
66+
end
67+
1968
@doc """
2069
Displays the label of the trace with a polymorphic composition.
2170
"""
@@ -106,6 +155,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace do
106155
attr(:trace_display, TraceDisplay, required: true)
107156
attr(:search_phrase, :string, required: true)
108157
attr(:fullscreen?, :boolean, default: false)
158+
attr(:elixir_editor, :string, default: nil)
109159

110160
def trace_body_navbar_wrapper(assigns) do
111161
assigns =
@@ -133,6 +183,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace do
133183
"peer-checked/content:[&_.tab-content]:text-navbar-selected-bg peer-checked/content:[&_.tab-content]:border-navbar-selected-bg",
134184
"peer-checked/stack:[&_.tab-stack]:text-navbar-selected-bg peer-checked/stack:[&_.tab-stack]:border-navbar-selected-bg",
135185
"peer-checked/raw:[&_.tab-raw]:text-navbar-selected-bg peer-checked/raw:[&_.tab-raw]:border-navbar-selected-bg",
186+
"peer-checked/content:[&_.editor-btn-content]:block",
136187
"peer-checked/stack:[&_.copy-btn-stack]:block",
137188
"peer-checked/raw:[&_.copy-btn-raw]:block"
138189
]}>
@@ -183,6 +234,15 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace do
183234
/>
184235
</div>
185236
237+
<div class="editor-btn-content hidden">
238+
<.open_in_editor_button
239+
id={@id}
240+
elixir_editor={@elixir_editor}
241+
source={@trace_display.source}
242+
fullscreen?={@fullscreen?}
243+
/>
244+
</div>
245+
186246
<.fullscreen_button
187247
id={"trace-fullscreen-#{@id}"}
188248
class="m-2"
@@ -237,6 +297,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace do
237297
attr(:displayed_trace, TraceDisplay, required: true)
238298
attr(:search_phrase, :string, required: true)
239299
attr(:page, :atom, required: true, values: [:node_inspector, :global_callbacks])
300+
attr(:elixir_editor, :string, default: nil)
240301

241302
def trace_fullscreen(assigns) do
242303
~H"""
@@ -261,16 +322,31 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace do
261322
/>
262323
</div>
263324
</:header>
264-
<div class={[
265-
"flex flex-col gap-4 items-start justify-center hover:[&>div>div>div>button]:hidden",
266-
if(is_nil(@displayed_trace.error), do: "p-4", else: "[&>div>div>div>div>button]:hidden")
267-
]}>
268-
<.trace_body_navbar_wrapper
269-
id={@id <> "-fullscreen"}
270-
trace_display={@displayed_trace}
271-
search_phrase={@search_phrase}
272-
fullscreen?={true}
273-
/>
325+
326+
<div class="relative">
327+
<div
328+
:if={is_nil(@displayed_trace.error)}
329+
class="absolute right-4 top-0 z-10"
330+
>
331+
<.open_in_editor_button
332+
id={@id <> "-fullscreen"}
333+
elixir_editor={@elixir_editor}
334+
source={@displayed_trace.source}
335+
fullscreen?={true}
336+
/>
337+
</div>
338+
<div class={[
339+
"max-h-[70vh] overflow-y-auto overflow-x-auto flex flex-col gap-4 items-start justify-center hover:[&>div>div>div>button]:hidden",
340+
if(is_nil(@displayed_trace.error), do: "p-4", else: "[&>div>div>div>div>button]:hidden")
341+
]}>
342+
<.trace_body_navbar_wrapper
343+
id={@id <> "-fullscreen"}
344+
trace_display={@displayed_trace}
345+
search_phrase={@search_phrase}
346+
fullscreen?={true}
347+
elixir_editor={@elixir_editor}
348+
/>
349+
</div>
274350
</div>
275351
</.fullscreen>
276352
"""

lib/live_debugger/app/debugger/callback_tracing/web/global_traces_live.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.GlobalTracesLive do
3131
alias LiveDebugger.Services.ProcessMonitor.Events.LiveComponentCreated
3232
alias LiveDebugger.Services.ProcessMonitor.Events.LiveComponentDeleted
3333
alias LiveDebugger.App.Debugger.CallbackTracing.Web.LiveComponents.FiltersForm
34+
alias LiveDebugger.App.Debugger.Utils.Editor
3435

3536
@live_stream_limit 128
3637
@page_size 25
@@ -97,7 +98,8 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.GlobalTracesLive do
9798
node_id: nil,
9899
url: url,
99100
inspect_mode?: inspect_mode?,
100-
return_link: return_link
101+
return_link: return_link,
102+
elixir_editor: Editor.detect_editor()
101103
)
102104
|> stream(:existing_traces, [], reset: true)
103105
|> put_private(:page_size, @page_size)
@@ -168,7 +170,11 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.GlobalTracesLive do
168170
existing_traces={@streams.existing_traces}
169171
>
170172
<:trace :let={{id, trace_display}}>
171-
<HookComponents.TraceWrapper.render id={id} trace_display={trace_display}>
173+
<HookComponents.TraceWrapper.render
174+
id={id}
175+
trace_display={trace_display}
176+
elixir_editor={@elixir_editor}
177+
>
172178
<:label>
173179
<.trace_label
174180
id={id <> "-label"}
@@ -184,6 +190,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.GlobalTracesLive do
184190
id={id <> "-body"}
185191
trace_display={trace_display}
186192
search_phrase={@trace_search_phrase}
193+
elixir_editor={@elixir_editor}
187194
/>
188195
</:body>
189196
</HookComponents.TraceWrapper.render>
@@ -199,6 +206,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.GlobalTracesLive do
199206
displayed_trace={@displayed_trace}
200207
search_phrase={@trace_search_phrase}
201208
page={:global_callbacks}
209+
elixir_editor={@elixir_editor}
202210
/>
203211
</div>
204212
</div>

lib/live_debugger/app/debugger/callback_tracing/web/hook_components/trace_wrapper.ex

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.TraceWrap
2121

2222
import LiveDebugger.App.Web.Hooks.Flash, only: [push_flash: 4]
2323

24+
import LiveDebugger.App.Debugger.CallbackTracing.Web.Components.Trace,
25+
only: [open_in_editor_button: 1]
26+
2427
alias LiveDebugger.API.TracesStorage
2528
alias LiveDebugger.App.Debugger.CallbackTracing.Structs.TraceDisplay
29+
alias LiveDebugger.App.Debugger.Utils.Editor
30+
alias LiveDebugger.Utils.FunctionMatcher
31+
alias LiveDebugger.Services.CallbackTracer.Actions.FunctionTrace
32+
alias LiveDebugger.Structs.Trace.DiffTrace
2633

27-
@required_assigns [:lv_process, :displayed_trace, :parent_pid]
34+
@required_assigns [:lv_process, :displayed_trace, :parent_pid, :elixir_editor]
2835
@trace_not_found_close_delay_ms 200
2936

3037
@impl true
@@ -38,6 +45,7 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.TraceWrap
3845

3946
attr(:id, :string, required: true)
4047
attr(:trace_display, TraceDisplay, required: true)
48+
attr(:elixir_editor, :string, default: nil)
4149

4250
slot(:body, required: true)
4351
slot(:label, required: true)
@@ -66,12 +74,20 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.TraceWrap
6674
:if={@trace_display.render_body? && is_nil(@trace_display.error)}
6775
class="absolute right-0 top-0 z-10"
6876
>
69-
<.fullscreen_button
70-
id={"trace-fullscreen-#{@id}"}
71-
class="m-2"
72-
phx-click="open-trace"
73-
phx-value-trace-id={@trace_display.id}
74-
/>
77+
<div class="flex flex-row">
78+
<.open_in_editor_button
79+
id={@id}
80+
elixir_editor={@elixir_editor}
81+
source={@trace_display.source}
82+
/>
83+
84+
<.fullscreen_button
85+
id={"trace-fullscreen-#{@id}"}
86+
class="m-2"
87+
phx-click="open-trace"
88+
phx-value-trace-id={@trace_display.id}
89+
/>
90+
</div>
7591
</div>
7692
<div class={[
7793
"overflow-x-auto max-w-full max-h-[30vh] overflow-y-auto",
@@ -119,12 +135,28 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.TraceWrap
119135
handle_trace_not_found(string_trace_id)
120136
socket
121137

138+
diff_trace = %DiffTrace{} ->
139+
stream_insert_trace(socket, diff_trace, !render_body?)
140+
122141
trace ->
142+
trace = maybe_resolve_and_persist_source(trace)
123143
stream_insert_trace(socket, trace, !render_body?)
124144
end
125145
|> halt()
126146
end
127147

148+
defp handle_event("open-in-editor", %{"file" => file, "line" => line}, socket) do
149+
Editor.open_in_editor(
150+
socket.assigns.elixir_editor,
151+
file,
152+
String.to_integer(line),
153+
socket.assigns.parent_pid
154+
)
155+
156+
socket
157+
|> halt()
158+
end
159+
128160
defp handle_event(_, _, socket), do: {:cont, socket}
129161

130162
defp handle_info({:trace_wrapper_not_found, string_trace_id}, socket) do
@@ -136,6 +168,22 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.TraceWrap
136168

137169
defp handle_info(_, socket), do: {:cont, socket}
138170

171+
defp maybe_resolve_and_persist_source(
172+
%{source: nil, module: module, function: function, args: args} = trace
173+
) do
174+
case FunctionMatcher.find_matching_clause_line(module, function, args) do
175+
{:ok, source} ->
176+
new_trace = %{trace | source: source}
177+
FunctionTrace.persist_trace(new_trace)
178+
new_trace
179+
180+
_ ->
181+
trace
182+
end
183+
end
184+
185+
defp maybe_resolve_and_persist_source(trace), do: trace
186+
139187
defp get_trace(socket, string_trace_id) do
140188
TracesStorage.get_by_id!(socket.assigns.lv_process.pid, String.to_integer(string_trace_id))
141189
end

0 commit comments

Comments
 (0)