Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TraceExecutionTime from './hooks/trace_execution_time';
import CopyButton from './hooks/copy_button';
import TraceBodySearchHighlight from './hooks/trace_body_search_highlight';
import TraceLabelSearchHighlight from './hooks/trace_label_search_highlight';
import AssignsBodySearchHighlight from './hooks/assigns_body_search_highlight';

import topbar from './vendor/topbar';

Expand All @@ -36,6 +37,7 @@ function createHooks() {
CopyButton,
TraceBodySearchHighlight,
TraceLabelSearchHighlight,
AssignsBodySearchHighlight,
};
}

Expand Down
43 changes: 43 additions & 0 deletions assets/app/hooks/assigns_body_search_highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { findRanges } from '../utils/dom';

function highlightSearchRanges(allRanges) {
if (allRanges.length === 0) {
CSS.highlights.clear();
return;
}

const highlight = new Highlight(...allRanges);
CSS.highlights.set('search-highlight', highlight);
}

function handleHighlight(phrase) {
if (phrase === undefined || phrase === '') {
CSS.highlights.clear();
return;
}

const allRanges = [];
const assignsFullscreenContainer = document.getElementById(
'assigns-display-fullscreen-container'
);
const assignsContainer = document.getElementById('assigns-display-container');

[assignsContainer, assignsFullscreenContainer].forEach((el) => {
if (el && el.dataset.search_phrase === phrase) {
const ranges = findRanges(el, phrase);
allRanges.push(...ranges);
}
});

highlightSearchRanges(allRanges);
}

const AssignsBodySearchHighlight = {
mounted() {
this.handleEvent('search_in_assigns', ({ search_phrase }) => {
handleHighlight(search_phrase);
});
},
};

export default AssignsBodySearchHighlight;
45 changes: 1 addition & 44 deletions assets/app/hooks/trace_body_search_highlight.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,6 @@
import { highlightSearchRanges } from '../utils/dom';

function findRanges(root, search) {
let text = '';
const parts = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);

while (walker.nextNode()) {
const node = walker.currentNode;

if (node.parentElement.dataset.text_item === 'true') {
const start = text.length;
text += node.nodeValue;
const end = text.length;
parts.push({ node, start, end });
}
}

const searchRegexp = new RegExp(RegExp.escape(search), 'gi');
const ranges = [];

for (const match of text.matchAll(searchRegexp)) {
const matchStart = match.index;
const matchEnd = matchStart + search.length;

for (const { node, start, end } of parts) {
if (end <= matchStart) continue;
if (start >= matchEnd) break;

let collapsibleElement = node.parentElement.closest('details');
while (collapsibleElement) {
collapsibleElement.dataset.open = true;
collapsibleElement.open = true;
collapsibleElement =
collapsibleElement.parentElement.closest('details');
}

const range = document.createRange();
range.setStart(node, Math.max(0, matchStart - start));
range.setEnd(node, Math.min(node.nodeValue.length, matchEnd - start));
ranges.push(range);
}
}

return ranges;
}
import { findRanges } from '../utils/dom';

const TraceBodySearchHighlight = {
mounted() {
Expand Down
45 changes: 45 additions & 0 deletions assets/app/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,48 @@ export function highlightSearchRanges(ranges) {
highlight = new Highlight(...ranges);
CSS.highlights.set('search-highlight', highlight);
}

export function findRanges(root, search) {
let text = '';
const parts = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);

while (walker.nextNode()) {
const node = walker.currentNode;

if (node.parentElement.dataset.text_item === 'true') {
const start = text.length;
text += node.nodeValue;
const end = text.length;
parts.push({ node, start, end });
}
}

const searchRegexp = new RegExp(RegExp.escape(search), 'gi');
const ranges = [];

for (const match of text.matchAll(searchRegexp)) {
const matchStart = match.index;
const matchEnd = matchStart + search.length;

for (const { node, start, end } of parts) {
if (end <= matchStart) continue;
if (start >= matchEnd) break;

let collapsibleElement = node.parentElement.closest('details');
while (collapsibleElement) {
collapsibleElement.dataset.open = true;
collapsibleElement.open = true;
collapsibleElement =
collapsibleElement.parentElement.closest('details');
}

const range = document.createRange();
range.setStart(node, Math.max(0, matchStart - start));
range.setEnd(node, Math.min(node.nodeValue.length, matchEnd - start));
ranges.push(range);
}
}

return ranges;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.SearchInp

alias LiveDebugger.App.Debugger.CallbackTracing.Web.Hooks

alias LiveDebugger.App.Debugger.Components

@required_assigns [:trace_search_phrase]

@impl true
Expand All @@ -26,31 +28,12 @@ defmodule LiveDebugger.App.Debugger.CallbackTracing.Web.HookComponents.SearchInp
@impl true
def render(assigns) do
~H"""
<div class={[
"flex shrink items-center rounded-[7px] outline outline-1 -outline-offset-1",
"has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2",
"outline-default-border has-[input:focus-within]:outline-ui-accent"
]}>
<form phx-change="search" phx-submit="search-submit" class="flex items-center w-full h-full">
<.icon
name="icon-search"
class={[
"h-4 w-4 ml-3",
(@disabled? && "text-gray-400") || "text-primary-icon"
]}
/>
<input
disabled={@disabled?}
id="trace-search-input"
placeholder={@placeholder}
value={@trace_search_phrase}
type="text"
name="search_phrase"
class="block remove-arrow w-16 sm:w-64 min-w-32 bg-surface-0-bg border-none py-2.5 pl-2 pr-3 text-xs text-primary-text placeholder:text-ui-muted focus:ring-0 disabled:!text-gray-500 disabled:placeholder-grey-300
"
/>
</form>
</div>
<Components.search_bar
disabled?={@disabled?}
search_phrase={@trace_search_phrase}
input_id="trace-search-input"
debounce={1}
/>
"""
end

Expand Down
44 changes: 44 additions & 0 deletions lib/live_debugger/app/debugger/components.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule LiveDebugger.App.Debugger.Components do
@moduledoc """
UI components used in the Debugger
"""
use LiveDebugger.App.Web, :component

attr(:placeholder, :string, default: "Search...")
attr(:disabled?, :boolean, default: false)
attr(:search_phrase, :string, default: "", doc: "The current search query")
attr(:input_id, :string, default: "", doc: "The ID of the input element")
attr(:debounce, :integer, default: 250, doc: "The debounce time in milliseconds")
attr(:class, :string, default: "", doc: "Additional CSS classes for the input element")

def search_bar(assigns) do
~H"""
<div class={[
"flex shrink items-center rounded-[7px] outline outline-1 -outline-offset-1",
"has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2",
"outline-default-border has-[input:focus-within]:outline-ui-accent",
@class
]}>
<form phx-change="search" phx-submit="search-submit" class="flex items-center w-full h-full">
<.icon
name="icon-search"
class={[
"h-4 w-4 ml-3",
(@disabled? && "text-gray-400") || "text-primary-icon"
]}
/>
<input
id={@input_id}
disabled={@disabled?}
placeholder={@placeholder}
value={@search_phrase}
phx-debounce={@debounce}
type="text"
name="search_phrase"
class="block remove-arrow w-16 sm:w-64 min-w-32 bg-surface-0-bg border-none py-2.5 pl-2 pr-3 text-xs text-primary-text placeholder:text-ui-muted focus:ring-0 disabled:!text-gray-500 disabled:placeholder-gray-300"
/>
</form>
</div>
"""
end
end
65 changes: 44 additions & 21 deletions lib/live_debugger/app/debugger/node_state/web/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Components do

alias LiveDebugger.App.Debugger.Web.Components.ElixirDisplay
alias LiveDebugger.App.Utils.TermParser
alias LiveDebugger.App.Debugger.NodeState.Web.AssignsSearch

def loading(assigns) do
~H"""
Expand All @@ -26,32 +27,54 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Components do

attr(:assigns, :list, required: true)
attr(:fullscreen_id, :string, required: true)
attr(:assigns_search_phrase, :string, default: "")

def assigns_section(assigns) do
~H"""
<.section id="assigns" class="h-max overflow-y-hidden" title="Assigns">
<:right_panel>
<div class="flex gap-2">
<.copy_button
id="assigns-copy-button"
variant="icon-button"
value={TermParser.term_to_copy_string(@assigns)}
<div id="assigns-section-container" phx-hook="AssignsBodySearchHighlight">
<.section id="assigns" class="h-max overflow-y-hidden" title="Assigns">
<:right_panel>
<div class="flex gap-2">
<AssignsSearch.render
assigns_search_phrase={@assigns_search_phrase}
input_id="assigns-search-input"
/>
<.copy_button
id="assigns-copy-button"
variant="icon-button"
value={TermParser.term_to_copy_string(@assigns)}
/>
<.fullscreen_button id={@fullscreen_id} />
</div>
</:right_panel>
<div
id="assigns-display-container"
class="relative w-full h-max max-h-full p-4 overflow-y-auto"
data-search_phrase={@assigns_search_phrase}
>
<ElixirDisplay.term id="assigns-display" node={TermParser.term_to_display_tree(@assigns)} />
</div>
</.section>
<.fullscreen id={@fullscreen_id} title="Assigns">
<div class="flex justify-between p-2 border-b border-default-border">
<AssignsSearch.render
placeholder="Search assigns"
assigns_search_phrase={@assigns_search_phrase}
input_id="assigns-search-input-fullscreen"
/>
</div>
<div
id="assigns-display-fullscreen-container"
class="p-4"
data-search_phrase={@assigns_search_phrase}
>
<ElixirDisplay.term
id="assigns-display-fullscreen-term"
node={TermParser.term_to_display_tree(@assigns)}
/>
<.fullscreen_button id={@fullscreen_id} />
</div>
</:right_panel>
<div class="relative w-full h-max max-h-full p-4 overflow-y-auto">
<ElixirDisplay.term id="assigns-display" node={TermParser.term_to_display_tree(@assigns)} />
</div>
</.section>
<.fullscreen id={@fullscreen_id} title="Assigns">
<div class="p-4">
<ElixirDisplay.term
id="assigns-display-fullscreen-term"
node={TermParser.term_to_display_tree(@assigns)}
/>
</div>
</.fullscreen>
</.fullscreen>
</div>
"""
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule LiveDebugger.App.Debugger.NodeState.Web.AssignsSearch do
@moduledoc """
This component is used to add search functionality for assigns.
It produces `search` and `search-submit` events handled by hook added via `init/1`.
"""

use LiveDebugger.App.Web, :hook_component

alias LiveDebugger.App.Debugger.Components

@required_assigns [:assigns_search_phrase]

@impl true
def init(socket) do
socket
|> check_assigns!(@required_assigns)
|> attach_hook(:search_input, :handle_event, &handle_event/3)
|> register_hook(:search_input)
end

attr(:placeholder, :string, default: "Search...")
attr(:disabled?, :boolean, default: false)
attr(:assigns_search_phrase, :string, default: "", doc: "The current search query for assigns")
attr(:input_id, :string, default: "", doc: "The ID of the input element")

@impl true
def render(assigns) do
~H"""
<Components.search_bar
placeholder={@placeholder}
search_phrase={@assigns_search_phrase}
input_id={@input_id}
debounce={250}
class="h-7!"
/>
"""
end

defp handle_event("search", %{"search_phrase" => search_phrase}, socket) do
socket
|> assign(assigns_search_phrase: search_phrase)
|> push_event("search_in_assigns", %{search_phrase: search_phrase})
|> halt()
end

defp handle_event("search-submit", _params, socket), do: {:halt, socket}
defp handle_event(_, _, socket), do: {:cont, socket}
end
Loading