Packages
kino
0.15.3
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/smart_cell/server.ex
defmodule Kino.SmartCell.Server do
@moduledoc false
@behaviour GenServer
require Logger
import Kino.Utils, only: [has_function?: 3]
@chunk_joiner "\n\n"
@chunk_joiner_size byte_size(@chunk_joiner)
def start_link(module, ref, attrs, target_pid) do
case :proc_lib.start_link(__MODULE__, :init, [{module, ref, attrs, target_pid}]) do
{:error, error} ->
{:error, error}
{:ok, pid, source, chunks, init_opts} ->
editor =
if editor_opts = init_opts[:editor] do
# TODO: remove on v1.0
legacy_source = attrs[editor_opts[:attribute]] || editor_opts[:default_source]
%{
source: editor_opts[:source] || legacy_source,
language: editor_opts[:language],
placement: editor_opts[:placement],
intellisense_node: editor_opts[:intellisense_node],
visible: editor_opts[:visible]
}
end
{:ok, pid,
%{
source: source,
chunks: chunks,
js_view: %{
ref: ref,
pid: pid,
assets: module.__assets_info__()
},
editor: editor,
scan_binding: if(has_function?(module, :scan_binding, 3), do: &module.scan_binding/3),
scan_eval_result:
if(has_function?(module, :scan_eval_result, 2), do: &module.scan_eval_result/2)
}}
end
end
def reconfigure_smart_cell(ctx, opts) do
unless ctx.__private__.smart_cell do
raise ArgumentError,
"configure_smart_cell/2 can only be called in smart cell handlers"
end
opts = Keyword.validate!(opts, [:editor])
if editor_opts = opts[:editor] do
unless ctx.__private__.smart_cell.editor? do
raise ArgumentError,
"configure_smart_cell/2 called with :editor, but the editor is not enabled." <>
" Make sure to enable smart cell editor during init"
end
Keyword.validate!(editor_opts, [:source, :intellisense_node, :visible])
end
put_in(ctx.__private__.smart_cell[:reconfigure_options], opts)
end
@impl true
def init({module, ref, initial_attrs, target_pid}) do
{:ok, ctx, init_opts} = Kino.JS.Live.Server.call_init(module, initial_attrs, ref)
init_opts = validate_init_opts!(init_opts, module)
editor? = init_opts[:editor] != nil
reevaluate_on_change = Keyword.get(init_opts, :reevaluate_on_change, false)
# TODO: remove on v1.0
editor_source_attr = get_in(init_opts, [:editor, :attribute])
if editor_source_attr == nil and editor? and
not has_function?(module, :handle_editor_change, 2) do
raise ArgumentError,
"#{inspect(module)} must define handle_editor_change/2 when the smart cell editor is enabled"
end
attrs = module.to_attrs(ctx)
# TODO: remove on v1.0
attrs =
if editor_source_attr do
source = initial_attrs[editor_source_attr] || init_opts[:editor][:default_source]
Map.put(attrs, editor_source_attr, source)
else
attrs
end
{source, chunks} = to_source(module, attrs)
ctx = put_in(ctx.__private__[:smart_cell], %{editor?: editor?})
:proc_lib.init_ack({:ok, self(), source, chunks, init_opts})
state = %{
module: module,
ctx: ctx,
target_pid: target_pid,
attrs: attrs,
editor_source_attr: editor_source_attr,
reevaluate_on_change: reevaluate_on_change
}
:gen_server.enter_loop(__MODULE__, [], state)
end
defp validate_init_opts!(opts, module) do
opts
|> Keyword.validate!([:editor, :reevaluate_on_change])
|> Keyword.update(:editor, nil, fn editor_opts ->
# TODO: remove :attribute and :default_source on v1.0
if Keyword.has_key?(editor_opts, :attribute) do
require Logger
Logger.warning(
"[#{inspect(module)}] the editor option :attribute is deprecated, please refer" <>
" to the documentation to learn about the new API"
)
else
unless Keyword.has_key?(editor_opts, :source) do
raise ArgumentError, "missing required editor option :source"
end
end
editor_opts =
Keyword.validate!(editor_opts, [
:source,
:attribute,
language: nil,
intellisense_node: nil,
placement: :bottom,
default_source: "",
visible: true
])
unless editor_opts[:placement] in [:top, :bottom] do
raise ArgumentError,
"editor :placement must be either :top or :bottom, got #{inspect(editor_opts[:placement])}"
end
editor_opts
end)
end
defp handle_reconfigure(state) do
case pop_in(state.ctx.__private__.smart_cell[:reconfigure_options]) do
{nil, state} ->
state
{options, state} ->
if editor_options = options[:editor] do
options = Map.new(editor_options)
send(
state.target_pid,
{:runtime_smart_cell_editor_update, state.ctx.__private__.ref, options}
)
end
state
end
end
@impl true
def handle_info({:editor_source, source}, state) do
if state.editor_source_attr do
# TODO: remove this branch on v1.0
attrs = Map.put(state.attrs, state.editor_source_attr, source)
{:noreply, set_attrs(state, attrs)}
else
{:ok, ctx} = state.module.handle_editor_change(source, state.ctx)
{:noreply, put_context(state, ctx)}
end
end
def handle_info(msg, state) do
case Kino.JS.Live.Server.call_handle_info(msg, state.module, state.ctx) do
{:ok, ctx} -> {:noreply, put_context(state, ctx)}
:error -> {:noreply, state}
end
end
@impl true
def terminate(reason, state) do
Kino.JS.Live.Server.call_terminate(reason, state.module, state.ctx)
end
defp put_context(state, ctx) do
%{state | ctx: ctx}
|> handle_reconfigure()
|> recompute_attrs()
end
defp recompute_attrs(state) do
attrs = state.module.to_attrs(state.ctx)
# TODO: remove on v1.0
attrs =
if state.editor_source_attr do
Map.put(attrs, state.editor_source_attr, state.attrs[state.editor_source_attr])
else
attrs
end
set_attrs(state, attrs)
end
defp set_attrs(%{attrs: attrs} = state, attrs), do: state
defp set_attrs(state, attrs) do
{source, chunks} = to_source(state.module, attrs)
send(
state.target_pid,
{:runtime_smart_cell_update, state.ctx.__private__.ref, attrs, source,
%{chunks: chunks, reevaluate: state.reevaluate_on_change}}
)
%{state | attrs: attrs}
end
defp to_source(module, attrs) do
case module.to_source(attrs) do
sources when is_list(sources) ->
{chunks, _} =
Enum.map_reduce(sources, 0, fn source, offset ->
size = byte_size(source)
{{offset, size}, offset + size + @chunk_joiner_size}
end)
source = Enum.join(sources, @chunk_joiner)
{source, chunks}
source when is_binary(source) ->
{source, nil}
end
end
end