Skip to content
Closed
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
11 changes: 8 additions & 3 deletions lib/live_debugger/app/debugger/node_state/web/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Components do

use LiveDebugger.App.Web, :component

alias LiveDebugger.App.Debugger.Web.Components.ElixirDisplay
alias LiveDebugger.App.Debugger.Web.LiveComponents.OptimizedElixirDisplay
alias LiveDebugger.App.Utils.TermParser

def loading(assigns) do
Expand Down Expand Up @@ -41,12 +41,17 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Components do
</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)} />
<.live_component
module={OptimizedElixirDisplay}
id="assigns-display"
node={TermParser.term_to_display_tree(@assigns)}
/>
</div>
</.section>
<.fullscreen id={@fullscreen_id} title="Assigns">
<div class="p-4">
<ElixirDisplay.term
<.live_component
module={OptimizedElixirDisplay}
id="assigns-display-fullscreen-term"
node={TermParser.term_to_display_tree(@assigns)}
/>
Expand Down
22 changes: 8 additions & 14 deletions lib/live_debugger/app/debugger/web/components/elixir_display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do

use LiveDebugger.App.Web, :component

alias LiveDebugger.App.Utils.TermParser.DisplayElement
alias LiveDebugger.App.Utils.TermParser.TermNode
alias LiveDebugger.App.Utils.TermParser.DisplayElement

@max_auto_expand_size 6

Expand All @@ -22,8 +22,8 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do
def term(assigns) do
assigns =
assigns
|> assign(:expanded?, auto_expand?(assigns.node, assigns.level))
|> assign(:has_children?, has_children?(assigns.node))
|> assign(:auto_open?, auto_open?(assigns.node, assigns.level))
|> assign(:has_children?, TermNode.has_children?(assigns.node))

~H"""
<div class="font-code">
Expand All @@ -33,7 +33,7 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do
<.collapsible
:if={@has_children?}
id={@id <> "collapsible"}
open={@expanded?}
open={@auto_open?}
icon="icon-chevron-right"
label_class="max-w-max"
chevron_class="text-code-2 m-auto w-[2ch] h-[2ch]"
Expand Down Expand Up @@ -66,7 +66,7 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do

attr(:items, :list, required: true)

defp text_items(assigns) do
def text_items(assigns) do
~H"""
<div class="flex">
<%= for item <- @items do %>
Expand All @@ -82,15 +82,9 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do
if color, do: "#{color}", else: ""
end

defp auto_expand?(%TermNode{}, 1), do: true
defp auto_open?(%TermNode{}, 1), do: true

defp auto_expand?(%TermNode{} = node, _level) do
node.kind == "tuple" and children_number(node) <= @max_auto_expand_size
defp auto_open?(%TermNode{} = node, _level) do
node.kind == "tuple" and TermNode.children_number(node) <= @max_auto_expand_size
end

defp has_children?(%TermNode{children: []}), do: false
defp has_children?(%TermNode{}), do: true

defp children_number(%TermNode{children: nil}), do: 0
defp children_number(%TermNode{children: children}), do: length(children)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
defmodule LiveDebugger.App.Debugger.Web.LiveComponents.OptimizedElixirDisplay do
@moduledoc """
Optimized ElixirDisplay LiveComponent that can be used to display a tree of terms.
It removes children of collapsed nodes from HTML, and adds them when the node is opened.
"""

use LiveDebugger.App.Web, :live_component

alias LiveDebugger.App.Debugger.Web.Components.ElixirDisplay
alias LiveDebugger.App.Utils.TermParser.TermNode

@impl true
def update(assigns, socket) do
socket
|> assign(:id, assigns.id)
|> assign(:node, assigns.node)
|> ok()
end

attr(:id, :string, required: true)
attr(:node, TermNode, required: true)

@impl true
def render(assigns) do
~H"""
<div id={@id}>
<.term id={@id} node={@node} myself={@myself} />
</div>
"""
end

@impl true
def handle_event("toggle_node", %{"id" => id}, socket) do
node = update_node_children(socket.assigns.node, id)

socket
|> assign(:node, node)
|> noreply()
end

attr(:id, :string, required: true)
attr(:node, TermNode, required: true)
attr(:myself, :any, required: true)

defp term(assigns) do
assigns =
assigns
|> assign(:has_children?, TermNode.has_children?(assigns.node))

~H"""
<div class="font-code" phx-click="toggle_node" phx-value-id={@node.id} phx-target={@myself}>
<%= if @has_children? do %>
<.static_collapsible
open={@node.open?}
label_class="max-w-max"
chevron_class="text-code-2 m-auto w-[2ch] h-[2ch]"
phx-click="toggle_node"
phx-value-id={@node.id}
phx-target={@myself}
>
<:label :let={open}>
<%= if open do %>
<ElixirDisplay.text_items items={@node.expanded_before} />
<% else %>
<ElixirDisplay.text_items items={@node.content} />
<% end %>
</:label>
<ol class="m-0 ml-[2ch] block list-none p-0">
<%= for {child, index} <- Enum.with_index(@node.children) do %>
<li class="flex flex-col">
<.term id={@id <> "-#{index}"} node={child} myself={@myself} />
</li>
<% end %>
</ol>
<div class="ml-[2ch]">
<ElixirDisplay.text_items items={@node.expanded_after} />
</div>
</.static_collapsible>
<% else %>
<div class="ml-[2ch]">
<ElixirDisplay.text_items items={@node.content} />
</div>
<% end %>
</div>
"""
end

defp update_node_children(node, "root"), do: %TermNode{node | open?: !node.open?}

defp update_node_children(node, id) do
["root" | id_path] = id |> String.split(".")

id_path = Enum.map(id_path, &String.to_integer(&1))

recursively_update_node_children(node, id_path)
end

defp recursively_update_node_children(node, []) when is_struct(node, TermNode) do
%TermNode{node | open?: !node.open?}
end

defp recursively_update_node_children(node, [id | rest]) when is_struct(node, TermNode) do
child_node =
node.children
|> Enum.at(id)
|> recursively_update_node_children(rest)

updated_children = List.replace_at(node.children, id, child_node)

%TermNode{node | children: updated_children}
end
end
113 changes: 113 additions & 0 deletions lib/live_debugger/app/utils/term_differ.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
defmodule LiveDebugger.App.Utils.TermDiffer do
@moduledoc """
Module for getting diffs between two terms.
"""

defmodule Diff do

Check warning on line 6 in lib/live_debugger/app/utils/term_differ.ex

View workflow job for this annotation

GitHub Actions / Build and test

Modules should have a @moduledoc tag.
defstruct [:type, ins: [], del: [], diff: nil]

@type type() :: :map | :list | :tuple | :struct | :primitive
@type t() ::
%__MODULE__{
type: type(),
ins: [atom() | non_neg_integer()],
del: [atom() | non_neg_integer()],
diff: %{atom() => t()} | nil
}
end

@spec diff(term(), term()) :: Diff.t() | nil
def diff(term1, term2) when term1 === term2, do: nil

def diff(list1, list2) when is_list(list1) and is_list(list2) do
{ins, del} = do_list_index_diff(list1, list2)

%Diff{type: :list, ins: ins, del: del}
end

def diff(%struct{} = struct1, %struct{} = struct2) do
map1 = Map.from_struct(struct1)
map2 = Map.from_struct(struct2)
{_, _, diff} = do_map_index_diff(map1, map2)

%Diff{type: :struct, diff: diff}
end

def diff(struct1, struct2) when is_struct(struct1) and is_struct(struct2) do
%Diff{type: :primitive}
end

def diff(map1, map2) when is_map(map1) and is_map(map2) do
{ins, del, diff} = do_map_index_diff(map1, map2)

%Diff{type: :map, ins: ins, del: del, diff: diff}
end

def diff(tuple1, tuple2) when is_tuple(tuple1) and is_tuple(tuple2) do
list1 = Tuple.to_list(tuple1)
list2 = Tuple.to_list(tuple2)

{ins, del} = do_list_index_diff(list1, list2)

%Diff{type: :tuple, ins: ins, del: del}
end

def diff(_, _), do: %Diff{type: :primitive}

defp do_list_index_diff(list1, list2) do
diffs =
list1
|> List.myers_difference(list2)
|> Enum.group_by(fn {key, _} -> key end, fn {_, values} -> values end)

ins_values = Map.get(diffs, :ins, []) |> List.flatten()
del_values = Map.get(diffs, :del, []) |> List.flatten()

ins_indexes = indexes_from_values(ins_values, list2)
del_indexes = indexes_from_values(del_values, list1)

{ins_indexes, del_indexes}
end

defp indexes_from_values(values, list) do
{indexes, _} =
Enum.reduce(values, {[], list}, fn value, {indexes, list} ->
index = Enum.find_index(list, &(&1 === value))

values = List.replace_at(list, index, :live_debugger_value_used)

{[index | indexes], values}
end)

indexes
end

defp do_map_index_diff(map1, map2) do
map1_keys = Map.keys(map1)
map2_keys = Map.keys(map2)

key_del = map1_keys -- map2_keys
key_ins = map2_keys -- map1_keys

key_diff =
MapSet.intersection(MapSet.new(map1_keys), MapSet.new(map2_keys))

key_diff_map =
key_diff
|> Enum.map(fn key ->
value1 = Map.fetch!(map1, key)
value2 = Map.fetch!(map2, key)

diff = diff(value1, value2)

{key, diff}
end)
|> Enum.filter(fn {_, diff} -> diff !== nil end)
|> case do
[] -> nil
diffs -> Enum.into(diffs, %{})
end

{key_ins, key_del, key_diff_map}
end
end
Loading
Loading