mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Push cells source to the client on initial render (#875)
This commit is contained in:
parent
f36eae436a
commit
6d1d4de767
|
@ -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) => {
|
||||
const { source, revision, evaluation_digest } = 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",
|
||||
|
|
|
@ -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}))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
@ -234,7 +257,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
defp content_placeholder(assigns) do
|
||||
~H"""
|
||||
<%= if @empty do %>
|
||||
<%= if @empty do %>
|
||||
<div class="h-4"></div>
|
||||
<% else %>
|
||||
<div class="max-w-2xl w-full animate-pulse">
|
||||
|
@ -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}>
|
||||
|
|
Loading…
Reference in a new issue