Packages
kino
0.14.1
0.19.0
0.18.0
0.17.0
0.16.1
0.16.0
0.15.3
0.15.2
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.2
0.6.1
0.6.0
retired
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Interactive widgets for Livebook
Current section
Files
Jump to
Current section
Files
lib/kino/debug/pipeline.ex
defmodule Kino.Debug.Pipeline do
@moduledoc false
use Kino.JS, assets_path: "lib/assets/debug_pipeline/build"
use Kino.JS.Live
def new(sources, results, wrapped_funs, dbg_id, dbg_same_file, dbg_line) do
result_frame = Kino.Frame.new()
ui =
Kino.JS.Live.new(
__MODULE__,
{sources, results, wrapped_funs, dbg_id, dbg_same_file, dbg_line, result_frame}
)
Kino.Layout.grid([ui, result_frame], boxed: true, gap: 8)
end
@impl true
def init({sources, results, wrapped_funs, dbg_id, dbg_same_file, dbg_line, result_frame}, ctx) do
Kino.Debug.register_dbg_handler!(dbg_id)
funs = [nil | wrapped_funs.()]
items =
[sources, results, funs]
|> Enum.zip()
|> Enum.with_index(fn {source, result, fun}, idx ->
%{id: idx, source: source, result: result, fun: fun, enabled: true}
end)
%{id: last_id} = List.last(items)
send(self(), :update_result_frame)
{:ok,
ctx
|> assign(
dbg_same_file: dbg_same_file,
dbg_line: dbg_line,
items: items,
funs: funs,
result_frame: result_frame,
selected_id: last_id,
errored_id: nil,
error: nil,
call_count: 1,
changed?: false
)}
end
@impl true
def handle_connect(ctx) do
items = Enum.map(ctx.assigns.items, &Map.take(&1, [:id, :source, :enabled]))
{:ok,
%{
dbg_same_file: ctx.assigns.dbg_same_file,
dbg_line: ctx.assigns.dbg_line,
call_count: ctx.assigns.call_count,
items: items,
selected_id: ctx.assigns.selected_id,
errored_id: ctx.assigns.errored_id,
error: ctx.assigns.error,
changed: ctx.assigns.changed?
}, ctx}
end
@impl true
def handle_event("select_item", %{"id" => id}, ctx) do
item = Enum.find(ctx.assigns.items, &(&1.id == id))
ctx =
if item.enabled do
broadcast_event(ctx, "item_selected", %{"id" => id})
ctx |> assign(selected_id: id) |> update_result_frame()
else
ctx
end
{:noreply, ctx}
end
def handle_event("update_enabled", %{"id" => id, "enabled" => enabled}, ctx) do
index = Enum.find_index(ctx.assigns.items, &(&1.id == id))
items = put_in(ctx.assigns.items, [Access.at(index), :enabled], enabled)
selected_id = last_selectable_id(items, ctx.assigns.errored_id)
changed? = changed?(items)
ctx = assign(ctx, changed?: changed?, selected_id: selected_id)
broadcast_event(ctx, "enabled_updated", %{
"id" => id,
"enabled" => enabled,
"selected_id" => selected_id,
"changed" => changed?
})
ctx = handle_items_change(ctx, items)
{:noreply, update_result_frame(ctx)}
end
def handle_event("move_item", %{"id" => id, "index" => index}, ctx) do
current_index = Enum.find_index(ctx.assigns.items, &(&1.id == id))
{item, items} = List.pop_at(ctx.assigns.items, current_index)
items = List.insert_at(items, index, item)
changed? = changed?(items)
ctx = assign(ctx, changed?: changed?)
broadcast_event(ctx, "item_moved", %{"id" => id, "index" => index, "changed" => changed?})
ctx =
if item.enabled do
handle_items_change(ctx, items)
else
assign(ctx, items: items)
end
{:noreply, update_result_frame(ctx)}
end
@impl true
def handle_info(:dbg_call, ctx) do
ctx = update(ctx, :call_count, &(&1 + 1))
broadcast_event(ctx, "call_count_updated", %{"call_count" => ctx.assigns.call_count})
{:noreply, ctx}
end
def handle_info(:update_result_frame, ctx) do
{:noreply, update_result_frame(ctx)}
end
defp handle_items_change(ctx, items) do
id =
ctx.assigns.items
|> Enum.zip(items)
|> Enum.find_value(nil, fn
{item, item} -> nil
{_old, new} -> new.id
end)
if below_errored_item?(items, ctx.assigns.errored_id, id) do
assign(ctx, items: items)
else
{items, errored_id, error} =
case reevaluate_changed(items, id) do
{:ok, items} -> {items, nil, nil}
{:error, items, errored_id, error} -> {items, errored_id, error}
end
selected_id = last_selectable_id(items, errored_id)
ctx = assign(ctx, items: items, error: error)
if errored_id != ctx.assigns.errored_id or selected_id != ctx.assigns.selected_id do
ctx = assign(ctx, errored_id: errored_id, selected_id: selected_id)
broadcast_event(ctx, "set_errored", %{
"id" => errored_id,
"error" => error,
"selected_id" => selected_id
})
ctx
else
ctx
end
end
end
defp last_selectable_id(items, errored_id) do
items =
if errored_id do
Enum.take_while(items, &(&1.id != errored_id))
else
items
end
items
|> Enum.reverse()
|> Enum.find_value(fn item -> item.enabled && item.id end)
end
defp below_errored_item?(_items, nil, _id), do: false
defp below_errored_item?(items, errored_id, id) do
ids_with_idx =
items
|> Enum.with_index()
|> Map.new(fn {item, idx} -> {item.id, idx} end)
ids_with_idx[errored_id] < ids_with_idx[id]
end
defp reevaluate_changed([head_item | items], changed_id) do
{prev_items, items} = Enum.split_while(items, &(&1.id != changed_id))
{unchanged_items, result} =
Enum.reduce_while(prev_items, {[head_item], head_item.result}, fn
%{id: ^changed_id}, acc ->
{:halt, acc}
item, {unchanged_items, result} ->
result = if item.enabled, do: item.result, else: result
{:cont, {[item | unchanged_items], result}}
end)
reevaluate(items, {unchanged_items, result})
end
defp reevaluate([], {new_items, _result}), do: {:ok, Enum.reverse(new_items)}
defp reevaluate([%{enabled: false} = item | items], {new_items, result}) do
reevaluate(items, {[item | new_items], result})
end
defp reevaluate([item | items], {new_items, result}) do
try do
result = item.fun.(result)
item = %{item | result: result}
reevaluate(items, {[item | new_items], result})
catch
kind, error ->
formatted_error = format_error(kind, error, __STACKTRACE__)
{:error, Enum.reverse([item | new_items], items), item.id, formatted_error}
end
end
defp format_error(kind, error, stacktrace) do
stacktrace = prune_stacktrace(stacktrace)
formatted = Exception.format(kind, error, stacktrace)
String.replace_trailing(formatted, "\n", "")
end
defp prune_stacktrace(stacktrace) do
stacktrace
|> Enum.reverse()
|> Enum.drop_while(&(elem(&1, 0) != __MODULE__))
|> Enum.drop_while(&(elem(&1, 0) == __MODULE__))
# The anonymous function call and definition
|> Enum.drop(2)
|> Enum.reverse()
end
defp update_result_frame(ctx) do
item = Enum.find(ctx.assigns.items, &(&1.id == ctx.assigns.selected_id))
Kino.Frame.render(ctx.assigns.result_frame, item.result)
ctx
end
defp changed?(items) do
items
|> Enum.reduce_while(0, fn
%{id: id, enabled: true}, id -> {:cont, id + 1}
_item, _acc -> {:halt, nil}
end)
|> is_nil()
end
end