diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index 8a0b4b301..f619a10df 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -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, - }); } } }, diff --git a/lib/livebook/notebook/cell/smart.ex b/lib/livebook/notebook/cell/smart.ex index 74f34b1fa..ea44f05a4 100644 --- a/lib/livebook/notebook/cell/smart.ex +++ b/lib/livebook/notebook/cell/smart.ex @@ -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 diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index c8b31c964..eb5e162a7 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -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(), diff --git a/lib/livebook/runtime/erl_dist/runtime_server.ex b/lib/livebook/runtime/erl_dist/runtime_server.ex index acdd0f07b..b0a624235 100644 --- a/lib/livebook/runtime/erl_dist/runtime_server.ex +++ b/lib/livebook/runtime/erl_dist/runtime_server.ex @@ -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 diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index 794715795..fffc2daba 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -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 diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 89603037d..270226a01 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -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 diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index dafdd9adc..17d8a8b8b 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -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]}]}