Skip to content

Commit

Permalink
Add support for updating smart cell editor source and intellisense no…
Browse files Browse the repository at this point in the history
…de (#2465)
  • Loading branch information
jonatanklosko authored Jan 31, 2024
1 parent 6837a2f commit e98cf46
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 36 deletions.
6 changes: 0 additions & 6 deletions assets/js/hooks/js_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}
},
Expand Down
9 changes: 3 additions & 6 deletions lib/livebook/notebook/cell/smart.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ defmodule Livebook.Notebook.Cell.Smart do
:kind,
:attrs,
:js_view,
:editor,
:editor_intellisense_node
:editor
]

alias Livebook.Utils
Expand All @@ -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()
Expand All @@ -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
20 changes: 18 additions & 2 deletions lib/livebook/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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(),
Expand Down
3 changes: 1 addition & 2 deletions lib/livebook/runtime/erl_dist/runtime_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
54 changes: 52 additions & 2 deletions lib/livebook/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
19 changes: 1 addition & 18 deletions lib/livebook_web/live/session_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions test/livebook/session_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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]}]}
Expand Down

0 comments on commit e98cf46

Please sign in to comment.