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 { md5Base64, smoothlyScrollToElement } from "../lib/utils";
import scrollIntoView from "scroll-into-view-if-needed";
import { loadLocalSettings } from "../lib/settings";
/**
* A hook managing a single cell.
@ -14,7 +13,6 @@ import { loadLocalSettings } from "../lib/settings";
*
* Configuration:
*
* * `data-focusable-id` - an identifier for the focus/insert navigation
* * `data-cell-id` - id of the cell being edited
* * `data-type` - type of the cell
* * `data-session-path` - root path to the current session
@ -27,13 +25,38 @@ const Cell = {
insertMode: false,
// For text cells (markdown or elixir)
liveEditor: null,
markdown: null,
evaluationDigest: null,
};
if (["markdown", "elixir"].includes(this.props.type)) {
this.pushEvent("cell_init", { cell_id: this.props.cellId }, (payload) => {
this.handleEvent(`cell_init:${this.props.cellId}`, (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(
`[data-element="editor-container"]`
);
@ -52,16 +75,6 @@ const Cell = {
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
if (this.props.type === "elixir") {
this.state.evaluationDigest = evaluation_digest;
@ -97,19 +110,10 @@ const Cell = {
});
}
// Setup markdown rendering
// Setup markdown updates
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) => {
markdown.setContent(newSource);
this.state.markdown.setContent(newSource);
});
}
@ -135,8 +139,8 @@ const Cell = {
this.state.liveEditor.onCursorSelectionChange((selection) => {
broadcastSelection(this, selection);
});
}, 0);
});
}
this._unsubscribeFromNavigationEvents = globalPubSub.subscribe(
"navigation",

View file

@ -1165,10 +1165,12 @@ defmodule Livebook.Session.Data do
Delta.transform(delta_ahead, transformed_new_delta, :left)
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 =
Map.get(cell, :source) &&
JSInterop.apply_delta_to_string(transformed_new_delta, cell.source)
case cell.source do
:__pruned__ -> :__pruned__
source -> JSInterop.apply_delta_to_string(transformed_new_delta, source)
end
data_actions
|> 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)
|> prune_outputs()
|> prune_cell_sources()
|> allow_upload(:cell_image,
accept: ~w(.jpg .jpeg .png .gif),
max_entries: 1,
@ -534,30 +535,6 @@ defmodule LivebookWeb.SessionLive do
{:reply, payload, socket}
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
idx = length(socket.private.data.notebook.sections)
Session.insert_section(socket.assigns.session.pid, idx)
@ -1119,6 +1096,8 @@ defmodule LivebookWeb.SessionLive do
end
defp after_operation(socket, _prev_socket, {:insert_cell, client_pid, _, _, _, cell_id}) do
socket = prune_cell_sources(socket)
if client_pid == self() do
push_event(socket, "cell_inserted", %{cell_id: cell_id})
else
@ -1144,6 +1123,8 @@ defmodule LivebookWeb.SessionLive do
end
defp after_operation(socket, _prev_socket, {:restore_cell, client_pid, cell_id}) do
socket = prune_cell_sources(socket)
if client_pid == self() do
push_event(socket, "cell_restored", %{cell_id: cell_id})
else
@ -1249,12 +1230,6 @@ defmodule LivebookWeb.SessionLive do
defp encode_digest(nil), do: nil
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(
%{range: %{from: from, to: to}} = response,
{:details, line, _column}
@ -1380,9 +1355,7 @@ defmodule LivebookWeb.SessionLive do
%{
id: cell.id,
type: :elixir,
# Note: we need this during initial loading,
# at which point we still have the source
empty?: cell.source == "",
source_info: cell_source_info(cell, info),
outputs: cell.outputs,
validity_status: info.validity_status,
evaluation_status: info.evaluation_status,
@ -1395,13 +1368,25 @@ defmodule LivebookWeb.SessionLive do
}
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,
type: :markdown,
# Note: we need this during initial loading,
# at which point we still have the source
empty?: cell.source == ""
source_info: cell_source_info(cell, info)
}
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
@ -1477,6 +1462,20 @@ defmodule LivebookWeb.SessionLive do
)
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
# have impact on dirtiness, so we need to always mirror it
defp update_dirty_status(data_view, data) do

View file

@ -1,6 +1,29 @@
defmodule LivebookWeb.SessionLive.CellComponent do
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
def render(assigns) do
~H"""
@ -52,7 +75,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
data-element="markdown-container"
id={"markdown-container-#{@cell_view.id}"}
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>
</.cell_body>
"""
@ -214,7 +237,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<div id={"editor-#{@cell_view.id}"} phx-update="ignore">
<div class="py-3 rounded-lg bg-editor" data-element="editor-container">
<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>
@ -248,6 +271,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
"""
end
defp empty?(%{source: ""} = _source_info), do: true
defp empty?(_source_info), do: false
defp cell_status(%{cell_view: %{evaluation_status: :evaluating}} = assigns) do
~H"""
<.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}>