Skip to content

Commit

Permalink
Make the smart cell editor source explicitly managed
Browse files Browse the repository at this point in the history
  • Loading branch information
jonatanklosko committed Jan 31, 2024
1 parent 9f5d073 commit 1a26c8e
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 42 deletions.
14 changes: 10 additions & 4 deletions lib/kino/remote_execution_cell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ defmodule Kino.RemoteExecutionCell do

intellisense_node = intellisense_node(fields)

ctx = assign(ctx, fields: fields)
code = attrs["code"] || @default_code

ctx = assign(ctx, fields: fields, code: code)

{:ok, ctx,
editor: [
attribute: "code",
source: code,
language: "elixir",
default_source: @default_code,
intellisense_node: intellisense_node
]}
end
Expand Down Expand Up @@ -97,9 +98,14 @@ defmodule Kino.RemoteExecutionCell do
{:noreply, ctx}
end

@impl true
def handle_editor_change(source, ctx) do
{:ok, assign(ctx, code: source)}
end

@impl true
def to_attrs(ctx) do
ctx.assigns.fields
Map.put(ctx.assigns.fields, "code", ctx.assigns.code)
end

@impl true
Expand Down
75 changes: 69 additions & 6 deletions lib/kino/smart_cell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -153,27 +153,77 @@ defmodule Kino.SmartCell do
@impl true
def init(attrs, ctx) do
# ...
{:ok, ctx, editor: [attribute: "code", language: "elixir"]}
{:ok, ctx, editor: [source: "", language: "elixir"]}
end
You also need to define `c:handle_editor_change/2`, which usually
simply stores the new source in the context.
### Example
Here is a minimal example, similar to the one before, but using the
editor feature.
defmodule KinoDocs.SmartCell.Editor do
use Kino.JS
use Kino.JS.Live
use Kino.SmartCell, name: "Built-in code editor"
@impl true
def init(attrs, ctx) do
source = attrs["source"] || ""
{:ok, assign(ctx, source: source), editor: [source: source]}
end
@impl true
def handle_connect(ctx) do
{:ok, %{}, ctx}
end
@impl true
def handle_editor_change(source, ctx) do
{:ok, assign(ctx, source: source)}
end
@impl true
def to_attrs(ctx) do
%{"source" => ctx.assigns.source}
end
@impl true
def to_source(attrs) do
attrs["source"]
end
asset "main.js" do
"""
export function init(ctx, payload) {
ctx.importCSS("main.css");
ctx.root.innerHTML = `Editor:`;
}
"""
end
end
### Options
* `:attribute` - the key to put the source text under in `attrs`.
Required
* `:source` - the initial editor source. Required
* `:language` - the editor language, used for syntax highlighting.
Defaults to `nil`
* `:placement` - editor placement within the smart cell, either
`:top` or `:bottom`. Defaults to `:bottom`
* `:default_source` - the initial editor source. Defaults to `""`
* `:intellisense_node` - a `{node, cookie}` atom tuple specifying
a remote node that should be introspected for editor intellisense.
This is only applicable when `:language` is Elixir. Defaults to
`nil`
Note that you can programmatically reconfigure some of these options
later using `Kino.JS.Live.Context.reconfigure_smart_cell/2`.
## Other options
Other than the editor configuration, the following options are
Expand All @@ -183,6 +233,7 @@ defmodule Kino.SmartCell do
whenever the generated source code changes. This option may be
helpful in cases where the cell output is a crucial element of
the UI interactions. Defaults to `false`
'''

require Logger
Expand Down Expand Up @@ -255,7 +306,19 @@ defmodule Kino.SmartCell do
{:ok, result :: any()}
| {:error, Exception.kind(), error :: any(), Exception.stacktrace()}

@optional_callbacks scan_binding: 3, scan_eval_result: 2
@doc """
Invoked when the smart cell editor content changes.
Usually you just want to put the new source in the corresponding
assign.
This callback is required if the smart cell enables editor in the
`c:Kino.JS.Live.init/2` configuration.
"""
@callback handle_editor_change(source :: String.t(), ctx :: Context.t()) ::
{:ok, ctx :: Context.t()}

@optional_callbacks scan_binding: 3, scan_eval_result: 2, handle_editor_change: 2

defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
Expand Down
60 changes: 28 additions & 32 deletions lib/kino/smart_cell/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ defmodule Kino.SmartCell.Server do
{:ok, pid, source, chunks, init_opts} ->
editor =
if editor_opts = init_opts[:editor] do
source = attrs[editor_opts[:attribute]] || editor_opts[:default_source]

%{
source: editor_opts[:source],
language: editor_opts[:language],
placement: editor_opts[:placement],
source: source,
intellisense_node: editor_opts[:intellisense_node]
}
end
Expand Down Expand Up @@ -71,22 +69,18 @@ defmodule Kino.SmartCell.Server do
{:ok, ctx, init_opts} = Kino.JS.Live.Server.call_init(module, initial_attrs, ref)
init_opts = validate_init_opts!(init_opts)

editor_source_attr = get_in(init_opts, [:editor, :attribute])
editor? = init_opts[:editor] != nil
reevaluate_on_change = Keyword.get(init_opts, :reevaluate_on_change, false)

attrs = module.to_attrs(ctx)
if 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 =
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
attrs = module.to_attrs(ctx)

{source, chunks} = to_source(module, attrs)

editor? = editor_source_attr != nil
ctx = put_in(ctx.__private__[:smart_cell], %{editor?: editor?})

:proc_lib.init_ack({:ok, self(), source, chunks, init_opts})
Expand All @@ -96,7 +90,6 @@ defmodule Kino.SmartCell.Server do
ctx: ctx,
target_pid: target_pid,
attrs: attrs,
editor_source_attr: editor_source_attr,
reevaluate_on_change: reevaluate_on_change
}

Expand All @@ -107,17 +100,22 @@ defmodule Kino.SmartCell.Server do
opts
|> Keyword.validate!([:editor, :reevaluate_on_change])
|> Keyword.update(:editor, nil, fn editor_opts ->
if Keyword.has_key?(editor_opts, :attribute) do
raise ArgumentError,
"the editor option :attribute is no longer supported, please refer" <>
" to the documentation to learn about the new API"
end

editor_opts =
Keyword.validate!(editor_opts, [
:attribute,
:language,
:intellisense_node,
placement: :bottom,
default_source: ""
:source,
language: nil,
intellisense_node: nil,
placement: :bottom
])

unless Keyword.has_key?(editor_opts, :attribute) do
raise ArgumentError, "missing editor option :attribute"
unless Keyword.has_key?(editor_opts, :source) do
raise ArgumentError, "missing required editor option :source"
end

unless editor_opts[:placement] in [:top, :bottom] do
Expand Down Expand Up @@ -150,13 +148,13 @@ defmodule Kino.SmartCell.Server do

@impl true
def handle_info({:editor_source, source}, state) do
attrs = Map.put(state.attrs, state.editor_source_attr, source)
{:noreply, set_attrs(state, attrs)}
{:ok, ctx} = state.module.handle_editor_change(source, state.ctx)
{:noreply, put_context(state, ctx)}
end

def handle_info(msg, state) do
case Kino.JS.Live.Server.call_handle_info(msg, state.module, state.ctx) do
{:ok, ctx} -> {:noreply, %{state | ctx: ctx} |> handle_reconfigure() |> recompute_attrs()}
{:ok, ctx} -> {:noreply, put_context(state, ctx)}
:error -> {:noreply, state}
end
end
Expand All @@ -166,16 +164,14 @@ defmodule Kino.SmartCell.Server 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)

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

Expand Down

0 comments on commit 1a26c8e

Please sign in to comment.