Push cells source to the client on initial render (#875)

This commit is contained in:
Jonatan Kłosko 2022-01-17 13:20:59 +01:00 committed by GitHub
parent f36eae436a
commit 6d1d4de767
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 71 deletions

View file

@ -4,7 +4,6 @@ import Markdown from "./markdown";
import { globalPubSub } from "../lib/pub_sub"; import { globalPubSub } from "../lib/pub_sub";
import { md5Base64, smoothlyScrollToElement } from "../lib/utils"; import { md5Base64, smoothlyScrollToElement } from "../lib/utils";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import { loadLocalSettings } from "../lib/settings";
/** /**
* A hook managing a single cell. * A hook managing a single cell.
@ -14,7 +13,6 @@ import { loadLocalSettings } from "../lib/settings";
* *
* Configuration: * Configuration:
* *
* * `data-focusable-id` - an identifier for the focus/insert navigation
* * `data-cell-id` - id of the cell being edited * * `data-cell-id` - id of the cell being edited
* * `data-type` - type of the cell * * `data-type` - type of the cell
* * `data-session-path` - root path to the current session * * `data-session-path` - root path to the current session
@ -27,13 +25,38 @@ const Cell = {
insertMode: false, insertMode: false,
// For text cells (markdown or elixir) // For text cells (markdown or elixir)
liveEditor: null, liveEditor: null,
markdown: null,
evaluationDigest: null, evaluationDigest: null,
}; };
if (["markdown", "elixir"].includes(this.props.type)) { this.handleEvent(`cell_init:${this.props.cellId}`, (payload) => {
this.pushEvent("cell_init", { cell_id: this.props.cellId }, (payload) => {
const { source, revision, evaluation_digest } = payload; const { source, revision, evaluation_digest } = payload;
// Setup markdown rendering
if (this.props.type === "markdown") {
const markdownContainer = this.el.querySelector(
`[data-element="markdown-container"]`
);
this.state.markdown = new Markdown(markdownContainer, source, {
baseUrl: this.props.sessionPath,
emptyText: "Empty markdown cell",
});
}
// Setup action handlers
if (this.props.type === "elixir") {
const amplifyButton = this.el.querySelector(
`[data-element="amplify-outputs-button"]`
);
amplifyButton.addEventListener("click", (event) => {
this.el.toggleAttribute("data-js-amplified");
});
}
// Setting up an editor takes relatively much synchronous time,
// so we postpone it, so that other immediate initializations
// are done first. This is relevant if there are a lot of cells
setTimeout(() => {
const editorContainer = this.el.querySelector( const editorContainer = this.el.querySelector(
`[data-element="editor-container"]` `[data-element="editor-container"]`
); );
@ -52,16 +75,6 @@ const Cell = {
revision revision
); );
// Setup action handlers
if (this.props.type === "elixir") {
const amplifyButton = this.el.querySelector(
`[data-element="amplify-outputs-button"]`
);
amplifyButton.addEventListener("click", (event) => {
this.el.toggleAttribute("data-js-amplified");
});
}
// Setup change indicator // Setup change indicator
if (this.props.type === "elixir") { if (this.props.type === "elixir") {
this.state.evaluationDigest = evaluation_digest; this.state.evaluationDigest = evaluation_digest;
@ -97,19 +110,10 @@ const Cell = {
}); });
} }
// Setup markdown rendering // Setup markdown updates
if (this.props.type === "markdown") { if (this.props.type === "markdown") {
const markdownContainer = this.el.querySelector(
`[data-element="markdown-container"]`
);
const baseUrl = this.props.sessionPath;
const markdown = new Markdown(markdownContainer, source, {
baseUrl,
emptyText: "Empty markdown cell",
});
this.state.liveEditor.onChange((newSource) => { this.state.liveEditor.onChange((newSource) => {
markdown.setContent(newSource); this.state.markdown.setContent(newSource);
}); });
} }
@ -135,8 +139,8 @@ const Cell = {
this.state.liveEditor.onCursorSelectionChange((selection) => { this.state.liveEditor.onCursorSelectionChange((selection) => {
broadcastSelection(this, selection); broadcastSelection(this, selection);
}); });
}, 0);
}); });
}
this._unsubscribeFromNavigationEvents = globalPubSub.subscribe( this._unsubscribeFromNavigationEvents = globalPubSub.subscribe(
"navigation", "navigation",

View file

@ -1165,10 +1165,12 @@ defmodule Livebook.Session.Data do
Delta.transform(delta_ahead, transformed_new_delta, :left) Delta.transform(delta_ahead, transformed_new_delta, :left)
end) end)
# Note: the session LV drops cell's source once it's no longer needed # Note: the clients drop cell's source once it's no longer needed
new_source = new_source =
Map.get(cell, :source) && case cell.source do
JSInterop.apply_delta_to_string(transformed_new_delta, cell.source) :__pruned__ -> :__pruned__
source -> JSInterop.apply_delta_to_string(transformed_new_delta, source)
end
data_actions data_actions
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &%{&1 | source: new_source})) |> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &%{&1 | source: new_source}))

View file

@ -43,6 +43,7 @@ defmodule LivebookWeb.SessionLive do
) )
|> assign_private(data: data) |> assign_private(data: data)
|> prune_outputs() |> prune_outputs()
|> prune_cell_sources()
|> allow_upload(:cell_image, |> allow_upload(:cell_image,
accept: ~w(.jpg .jpeg .png .gif), accept: ~w(.jpg .jpeg .png .gif),
max_entries: 1, max_entries: 1,
@ -534,30 +535,6 @@ defmodule LivebookWeb.SessionLive do
{:reply, payload, socket} {:reply, payload, socket}
end end
def handle_event("cell_init", %{"cell_id" => cell_id}, socket) do
data = socket.private.data
case Notebook.fetch_cell_and_section(data.notebook, cell_id) do
{:ok, cell, _section} ->
info = data.cell_infos[cell.id]
payload = %{
source: cell.source,
revision: info.revision,
evaluation_digest: encode_digest(info.evaluation_digest)
}
# From this point on we don't need cell source in the LV,
# so we are going to drop it altogether
socket = remove_cell_source(socket, cell_id)
{:reply, payload, socket}
:error ->
{:noreply, socket}
end
end
def handle_event("append_section", %{}, socket) do def handle_event("append_section", %{}, socket) do
idx = length(socket.private.data.notebook.sections) idx = length(socket.private.data.notebook.sections)
Session.insert_section(socket.assigns.session.pid, idx) Session.insert_section(socket.assigns.session.pid, idx)
@ -1119,6 +1096,8 @@ defmodule LivebookWeb.SessionLive do
end end
defp after_operation(socket, _prev_socket, {:insert_cell, client_pid, _, _, _, cell_id}) do defp after_operation(socket, _prev_socket, {:insert_cell, client_pid, _, _, _, cell_id}) do
socket = prune_cell_sources(socket)
if client_pid == self() do if client_pid == self() do
push_event(socket, "cell_inserted", %{cell_id: cell_id}) push_event(socket, "cell_inserted", %{cell_id: cell_id})
else else
@ -1144,6 +1123,8 @@ defmodule LivebookWeb.SessionLive do
end end
defp after_operation(socket, _prev_socket, {:restore_cell, client_pid, cell_id}) do defp after_operation(socket, _prev_socket, {:restore_cell, client_pid, cell_id}) do
socket = prune_cell_sources(socket)
if client_pid == self() do if client_pid == self() do
push_event(socket, "cell_restored", %{cell_id: cell_id}) push_event(socket, "cell_restored", %{cell_id: cell_id})
else else
@ -1249,12 +1230,6 @@ defmodule LivebookWeb.SessionLive do
defp encode_digest(nil), do: nil defp encode_digest(nil), do: nil
defp encode_digest(digest), do: Base.encode64(digest) defp encode_digest(digest), do: Base.encode64(digest)
defp remove_cell_source(socket, cell_id) do
update_in(socket.private.data.notebook, fn notebook ->
Notebook.update_cell(notebook, cell_id, &%{&1 | source: nil})
end)
end
defp process_intellisense_response( defp process_intellisense_response(
%{range: %{from: from, to: to}} = response, %{range: %{from: from, to: to}} = response,
{:details, line, _column} {:details, line, _column}
@ -1380,9 +1355,7 @@ defmodule LivebookWeb.SessionLive do
%{ %{
id: cell.id, id: cell.id,
type: :elixir, type: :elixir,
# Note: we need this during initial loading, source_info: cell_source_info(cell, info),
# at which point we still have the source
empty?: cell.source == "",
outputs: cell.outputs, outputs: cell.outputs,
validity_status: info.validity_status, validity_status: info.validity_status,
evaluation_status: info.evaluation_status, evaluation_status: info.evaluation_status,
@ -1395,13 +1368,25 @@ defmodule LivebookWeb.SessionLive do
} }
end end
defp cell_to_view(%Cell.Markdown{} = cell, _data) do defp cell_to_view(%Cell.Markdown{} = cell, data) do
info = data.cell_infos[cell.id]
%{ %{
id: cell.id, id: cell.id,
type: :markdown, type: :markdown,
# Note: we need this during initial loading, source_info: cell_source_info(cell, info)
# at which point we still have the source }
empty?: cell.source == "" end
defp cell_source_info(%{source: :__pruned__}, _info) do
:__pruned__
end
defp cell_source_info(cell, info) do
%{
source: cell.source,
revision: info.revision,
evaluation_digest: encode_digest(info.evaluation_digest)
} }
end end
@ -1477,6 +1462,20 @@ defmodule LivebookWeb.SessionLive do
) )
end end
defp prune_cell_sources(%{private: %{data: data}} = socket) do
assign_private(
socket,
data:
update_in(
data.notebook,
&Notebook.update_cells(&1, fn
%{source: _} = cell -> %{cell | source: :__pruned__}
cell -> cell
end)
)
)
end
# Changes that affect only a single cell are still likely to # Changes that affect only a single cell are still likely to
# have impact on dirtiness, so we need to always mirror it # have impact on dirtiness, so we need to always mirror it
defp update_dirty_status(data_view, data) do defp update_dirty_status(data_view, data) do

View file

@ -1,6 +1,29 @@
defmodule LivebookWeb.SessionLive.CellComponent do defmodule LivebookWeb.SessionLive.CellComponent do
use LivebookWeb, :live_component use LivebookWeb, :live_component
@impl true
def mount(socket) do
{:ok, assign(socket, initialized: false)}
end
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
socket =
if not connected?(socket) or socket.assigns.initialized do
socket
else
%{id: id, source_info: info} = socket.assigns.cell_view
socket
|> push_event("cell_init:#{id}", info)
|> assign(initialized: true)
end
{:ok, socket}
end
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@ -52,7 +75,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
data-element="markdown-container" data-element="markdown-container"
id={"markdown-container-#{@cell_view.id}"} id={"markdown-container-#{@cell_view.id}"}
phx-update="ignore"> phx-update="ignore">
<.content_placeholder bg_class="bg-gray-200" empty={@cell_view.empty?} /> <.content_placeholder bg_class="bg-gray-200" empty={empty?(@cell_view.source_info)} />
</div> </div>
</.cell_body> </.cell_body>
""" """
@ -214,7 +237,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<div id={"editor-#{@cell_view.id}"} phx-update="ignore"> <div id={"editor-#{@cell_view.id}"} phx-update="ignore">
<div class="py-3 rounded-lg bg-editor" data-element="editor-container"> <div class="py-3 rounded-lg bg-editor" data-element="editor-container">
<div class="px-8"> <div class="px-8">
<.content_placeholder bg_class="bg-gray-500" empty={@cell_view.empty?} /> <.content_placeholder bg_class="bg-gray-500" empty={empty?(@cell_view.source_info)} />
</div> </div>
</div> </div>
</div> </div>
@ -248,6 +271,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
""" """
end end
defp empty?(%{source: ""} = _source_info), do: true
defp empty?(_source_info), do: false
defp cell_status(%{cell_view: %{evaluation_status: :evaluating}} = assigns) do defp cell_status(%{cell_view: %{evaluation_status: :evaluating}} = assigns) do
~H""" ~H"""
<.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}> <.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}>