Add support for updating smart cell editor source and intellisense node (#2465)

This commit is contained in:
Jonatan Kłosko 2024-01-31 12:44:20 +01:00 committed by GitHub
parent 6837a2f449
commit e98cf466fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 123 additions and 36 deletions

View file

@ -364,12 +364,6 @@ const JSView = {
preselect_name: message.preselectName,
options: message.options,
});
} else if (message.type === "setSmartCellEditorIntellisenseNode") {
this.pushEvent("set_smart_cell_editor_intellisense_node", {
js_view_ref: this.props.ref,
node: message.node,
cookie: message.cookie,
});
}
}
},

View file

@ -18,8 +18,7 @@ defmodule Livebook.Notebook.Cell.Smart do
:kind,
:attrs,
:js_view,
:editor,
:editor_intellisense_node
:editor
]
alias Livebook.Utils
@ -33,8 +32,7 @@ defmodule Livebook.Notebook.Cell.Smart do
kind: String.t() | nil,
attrs: attrs() | :__pruned__,
js_view: Livebook.Runtime.js_view() | nil,
editor: Livebook.Runtime.editor() | nil,
editor_intellisense_node: {String.t(), String.t()} | nil
editor: Livebook.Runtime.editor() | nil
}
@type attrs :: map()
@ -52,8 +50,7 @@ defmodule Livebook.Notebook.Cell.Smart do
kind: nil,
attrs: %{},
js_view: nil,
editor: nil,
editor_intellisense_node: nil
editor: nil
}
end
end

View file

@ -704,7 +704,12 @@ defprotocol Livebook.Runtime do
@typedoc """
Smart cell editor configuration.
"""
@type editor :: %{language: String.t() | nil, placement: :bottom | :top, source: String.t()}
@type editor :: %{
language: String.t() | nil,
placement: :bottom | :top,
source: String.t(),
intellisense_node: {atom(), atom()} | nil
}
@typedoc """
An opaque file reference.
@ -876,7 +881,7 @@ defprotocol Livebook.Runtime do
pid(),
intellisense_request(),
parent_locators(),
{String.t(), String.t()} | nil
{atom(), atom()} | nil
) :: reference()
def handle_intellisense(runtime, send_to, request, parent_locators, node)
@ -945,6 +950,17 @@ defprotocol Livebook.Runtime do
The attrs are persisted and may be used to restore the smart cell
state later. Note that for persistence they get serialized and
deserialized as JSON.
When the smart cell editor is enabled, the runtime owner sends the
new editor source whenever it changes as:
* `{:editor_source, source :: String.t()}`
The cell can also update some of the editor configuration or source
by sending:
* `{:runtime_smart_cell_editor_update, ref, %{optional(:source) => String.t(), optional(:intellisense_node) => {atom(), atom()} | nil}}`
"""
@spec start_smart_cell(
t(),

View file

@ -130,7 +130,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
pid(),
Runtime.intellisense_request(),
Runtime.Runtime.parent_locators(),
{String.t(), String.t()} | nil
{atom(), atom()} | nil
) :: reference()
def handle_intellisense(pid, send_to, request, parent_locators, node) do
ref = make_ref()
@ -921,7 +921,6 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
end
defp intellisense_node({node, cookie}) do
{node, cookie} = {String.to_atom(node), String.to_atom(cookie)}
Node.set_cookie(node, cookie)
if Node.connect(node), do: node, else: node()
end

View file

@ -1661,6 +1661,8 @@ defmodule Livebook.Session do
end
def handle_info({:runtime_smart_cell_started, id, info}, state) do
info = normalize_smart_cell_started_info(info)
info =
if info.editor do
normalize_newlines = &String.replace(&1, "\r\n", "\n")
@ -1672,11 +1674,10 @@ defmodule Livebook.Session do
case Notebook.fetch_cell_and_section(state.data.notebook, id) do
{:ok, cell, _section} ->
chunks = info[:chunks]
delta = Livebook.Text.Delta.diff(cell.source, info.source)
operation =
{:smart_cell_started, @client_id, id, delta, chunks, info.js_view, info.editor}
{:smart_cell_started, @client_id, id, delta, info.chunks, info.js_view, info.editor}
{:noreply, handle_operation(state, operation)}
@ -1714,6 +1715,42 @@ defmodule Livebook.Session do
end
end
def handle_info({:runtime_smart_cell_editor_update, id, options}, state) do
case Notebook.fetch_cell_and_section(state.data.notebook, id) do
{:ok, cell, _section} when cell.editor != nil ->
state =
case options do
%{source: source} ->
delta = Livebook.Text.Delta.diff(cell.editor.source, source)
revision = state.data.cell_infos[cell.id].sources.secondary.revision
operation =
{:apply_cell_delta, @client_id, cell.id, :secondary, delta, nil, revision}
handle_operation(state, operation)
%{} ->
state
end
state =
case options do
%{intellisense_node: intellisense_node} ->
editor = %{cell.editor | intellisense_node: intellisense_node}
operation = {:set_cell_attributes, @client_id, cell.id, %{editor: editor}}
handle_operation(state, operation)
%{} ->
state
end
{:noreply, state}
_ ->
{:noreply, state}
end
end
def handle_info({:pong, {:smart_cell_evaluation, cell_id}, _info}, state) do
state =
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(state.data.notebook, cell_id),
@ -3070,4 +3107,17 @@ defmodule Livebook.Session do
%{type: :unknown, output: other}
|> normalize_runtime_output()
end
# Normalizes :runtime_smart_cell_started info to match the latest
# specification.
defp normalize_smart_cell_started_info(info) when not is_map_key(info, :chunks) do
normalize_smart_cell_started_info(put_in(info[:chunks], nil))
end
defp normalize_smart_cell_started_info(info)
when info.editor != nil and not is_map_key(info.editor, :intellisense_node) do
normalize_smart_cell_started_info(put_in(info.editor[:intellisense_node], nil))
end
defp normalize_smart_cell_started_info(info), do: info
end

View file

@ -858,23 +858,6 @@ defmodule LivebookWeb.SessionLive do
push_patch(socket, to: ~p"/sessions/#{socket.assigns.session.id}/settings/custom-view")}
end
def handle_event(
"set_smart_cell_editor_intellisense_node",
%{"js_view_ref" => cell_id, "node" => node, "cookie" => cookie},
socket
) do
node =
if is_binary(node) and node =~ "@" and is_binary(cookie) and cookie != "" do
{node, cookie}
end
Session.set_cell_attributes(socket.assigns.session.pid, cell_id, %{
editor_intellisense_node: node
})
{:noreply, socket}
end
@impl true
def handle_call({:get_input_value, input_id}, _from, socket) do
reply =
@ -2105,7 +2088,7 @@ defmodule LivebookWeb.SessionLive do
"#{notebook_name} - Livebook"
end
defp intellisense_node(%Cell.Smart{editor_intellisense_node: node_cookie}), do: node_cookie
defp intellisense_node(%Cell.Smart{editor: %{intellisense_node: node_cookie}}), do: node_cookie
defp intellisense_node(_), do: nil
defp any_stale_cell?(data) do

View file

@ -1033,6 +1033,54 @@ defmodule Livebook.SessionTest do
} = Session.get_data(session.pid)
end
test "handles smart cell editor updates" do
smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: ""}
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
session = start_session(notebook: notebook)
runtime = connected_noop_runtime()
Session.set_runtime(session.pid, runtime)
send(
session.pid,
{:runtime_smart_cell_definitions,
[%{kind: "text", name: "Text", requirement_presets: []}]}
)
Session.subscribe(session.id)
editor = %{language: nil, placement: :bottom, source: "", intellisense_node: nil}
send(
session.pid,
{:runtime_smart_cell_started, smart_cell.id,
%{source: "1", js_view: %{pid: self(), ref: "ref"}, editor: editor}}
)
# Update editor source
send(
session.pid,
{:runtime_smart_cell_editor_update, smart_cell.id, %{source: "new source"}}
)
assert %{
notebook: %{sections: [%{cells: [%{editor: %{source: "new source"}}]}]}
} = Session.get_data(session.pid)
# Update intellisense node
send(
session.pid,
{:runtime_smart_cell_editor_update, smart_cell.id,
%{intellisense_node: {:test@test, :test}}}
)
assert %{
notebook: %{
sections: [%{cells: [%{editor: %{intellisense_node: {:test@test, :test}}}]}]
}
} = Session.get_data(session.pid)
end
test "pings the smart cell before evaluation to await all incoming messages" do
smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: ""}
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}