Introduce a setup cell (#1075)

* Introduce a setup cell

* Don't collapse setup cell when dirty

* Collapse fresh setup cell when empty

* Reword collapsed setup cell text
This commit is contained in:
Jonatan Kłosko 2022-03-28 21:36:57 +02:00 committed by GitHub
parent 6f6b7e3ee0
commit 5476fd001d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 659 additions and 392 deletions

View file

@ -34,7 +34,7 @@ solely client-side operations.
}
[data-element="session"][data-js-insert-mode]
[data-element="cell"][data-type="markdown"][data-js-focused]
[data-element="cell"][data-js-focused]
[data-element="enable-insert-mode-button"] {
@apply hidden;
}
@ -82,12 +82,12 @@ solely client-side operations.
@apply opacity-100 pointer-events-auto;
}
[data-element="cell"] [data-element="cell-status"][data-js-changed] {
[data-element="cell"][data-js-changed] [data-element="cell-status"] {
@apply italic;
}
[data-element="cell"]
[data-element="cell-status"]:not([data-js-changed])
[data-element="cell"]:not([data-js-changed])
[data-element="cell-status"]
[data-element="change-indicator"] {
@apply invisible;
}
@ -137,6 +137,39 @@ solely client-side operations.
@apply hidden;
}
[data-element="session"]:not([data-js-insert-mode])
[data-element="cell"][data-type="setup"]:not([data-eval-validity="fresh"]:not([data-js-empty])):not([data-js-changed])
[data-element="editor-box"],
[data-element="session"]
[data-element="cell"][data-type="setup"]:not([data-eval-validity="fresh"]:not([data-js-empty])):not([data-js-changed]):not([data-js-focused])
[data-element="editor-box"] {
@apply h-0 overflow-hidden;
}
[data-element="session"][data-js-insert-mode]
[data-element="cell"][data-type="setup"][data-js-focused]
[data-element="enable-insert-mode-button"],
[data-element="session"]
[data-element="cell"][data-type="setup"][data-eval-validity="fresh"]:not([data-js-empty])
[data-element="enable-insert-mode-button"],
[data-element="session"]
[data-element="cell"][data-type="setup"][data-js-changed]
[data-element="enable-insert-mode-button"] {
@apply hidden;
}
[data-element="session"][data-js-insert-mode]
[data-element="cell"][data-type="setup"][data-js-focused]
[data-element="info-box"],
[data-element="session"]
[data-element="cell"][data-type="setup"][data-eval-validity="fresh"]:not([data-js-empty])
[data-element="info-box"],
[data-element="session"]
[data-element="cell"][data-type="setup"][data-js-changed]
[data-element="info-box"] {
@apply hidden;
}
[data-element="cell"][data-type="smart"]:not([data-js-source-visible])
[data-element="show-ui-icon"] {
@apply hidden;
@ -147,17 +180,6 @@ solely client-side operations.
@apply hidden;
}
[data-element="cell"][data-type="smart"]:not([data-js-source-visible])
[data-element="cell-status-container"] {
@apply flex justify-end;
}
[data-element="cell"][data-type="smart"]:not([data-js-source-visible])
[data-element="cell-status-container"]
> *:first-child {
@apply mt-2;
}
[data-element="cell"][data-type="smart"][data-js-source-visible]
[data-element="cell-status-container"] {
@apply absolute bottom-2 right-2;

View file

@ -174,19 +174,23 @@ const Cell = {
});
if (tag === "primary") {
const source = liveEditor.getSource();
this.el.toggleAttribute("data-js-empty", source === "");
liveEditor.onChange((newSource) => {
this.el.toggleAttribute("data-js-empty", newSource === "");
});
// Setup markdown rendering
if (this.props.type === "markdown") {
const markdownContainer = this.el.querySelector(
`[data-element="markdown-container"]`
);
const markdown = new Markdown(
markdownContainer,
liveEditor.getSource(),
{
baseUrl: this.props.sessionPath,
emptyText: "Empty markdown cell",
}
);
const markdown = new Markdown(markdownContainer, source, {
baseUrl: this.props.sessionPath,
emptyText: "Empty markdown cell",
});
liveEditor.onChange((newSource) => {
markdown.setContent(newSource);
@ -259,7 +263,7 @@ const Cell = {
const source = this.liveEditors.primary.getSource();
const digest = md5Base64(source);
const changed = this.props.evaluationDigest !== digest;
cellStatus.toggleAttribute("data-js-changed", changed);
this.el.toggleAttribute("data-js-changed", changed);
}
},

View file

@ -28,9 +28,9 @@ class LiveEditor {
this.language = language;
this.intellisense = intellisense;
this.readOnly = readOnly;
this._onChange = null;
this._onBlur = null;
this._onCursorSelectionChange = null;
this._onChange = [];
this._onBlur = [];
this._onCursorSelectionChange = [];
this._remoteUserByClientPid = {};
this._mountEditor();
@ -49,7 +49,7 @@ class LiveEditor {
this.editorClient.onDelta((delta) => {
this.source = delta.applyToString(this.source);
this._onChange && this._onChange(this.source);
this._onChange.forEach((callback) => callback(this.source));
});
this.editor.onDidFocusEditorWidget(() => {
@ -58,12 +58,13 @@ class LiveEditor {
this.editor.onDidBlurEditorWidget(() => {
this.editor.updateOptions({ matchBrackets: "never" });
this._onBlur && this._onBlur();
this._onBlur.forEach((callback) => callback());
});
this.editor.onDidChangeCursorSelection((event) => {
this._onCursorSelectionChange &&
this._onCursorSelectionChange(event.selection);
this._onCursorSelectionChange.forEach((callback) =>
callback(event.selection)
);
});
}
@ -78,21 +79,21 @@ class LiveEditor {
* Registers a callback called with a new cell content whenever it changes.
*/
onChange(callback) {
this._onChange = callback;
this._onChange.push(callback);
}
/**
* Registers a callback called with a new cursor selection whenever it changes.
*/
onCursorSelectionChange(callback) {
this._onCursorSelectionChange = callback;
this._onCursorSelectionChange.push(callback);
}
/**
* Registers a callback called whenever the editor loses focus.
*/
onBlur(callback) {
this._onBlur = callback;
this._onBlur.push(callback);
}
focus() {

View file

@ -475,11 +475,15 @@ const Session = {
* Enters insert mode when a markdown cell is double-clicked.
*/
handleDocumentDoubleClick(event) {
const markdownCell = event.target.closest(
`[data-element="cell"][data-type="markdown"]`
);
const cell = event.target.closest(`[data-element="cell"]`);
const type = cell && cell.getAttribute("data-type");
if (markdownCell && this.focusedId && !this.insertMode) {
if (
type &&
["markdown", "setup"].includes(type) &&
this.focusedId &&
!this.insertMode
) {
this.setInsertMode(true);
}
},

View file

@ -2,12 +2,12 @@
* Checks if the given cell type is eligible for evaluation.
*/
export function isEvaluable(cellType) {
return ["code", "smart"].includes(cellType);
return ["code", "smart", "setup"].includes(cellType);
}
/**
* Checks if the given cell type has primary editable editor.
*/
export function isDirectlyEditable(cellType) {
return ["markdown", "code"].includes(cellType);
return ["markdown", "code", "setup"].includes(cellType);
}

View file

@ -44,6 +44,8 @@ defmodule Livebook.LiveMarkdown.Export do
end
defp render_notebook(notebook, ctx) do
%{setup_section: %{cells: [setup_cell]}} = notebook
comments =
Enum.map(notebook.leading_comments, fn
[line] -> ["<!-- ", line, " -->"]
@ -51,12 +53,14 @@ defmodule Livebook.LiveMarkdown.Export do
end)
name = ["# ", notebook.name]
setup_cell = render_setup_cell(setup_cell, ctx)
sections = Enum.map(notebook.sections, &render_section(&1, notebook, ctx))
metadata = notebook_metadata(notebook)
notebook_with_metadata =
[name | sections]
[name, setup_cell | sections]
|> Enum.reject(&is_nil/1)
|> Enum.intersperse("\n\n")
|> prepend_metadata(metadata)
@ -103,6 +107,9 @@ defmodule Livebook.LiveMarkdown.Export do
%{"branch_parent_index" => parent_idx}
end
defp render_setup_cell(%{source: ""}, _ctx), do: nil
defp render_setup_cell(cell, ctx), do: render_cell(cell, ctx)
defp render_cell(%Cell.Markdown{} = cell, _ctx) do
metadata = cell_metadata(cell)

View file

@ -209,10 +209,9 @@ defmodule Livebook.LiveMarkdown.Import do
defp take_outputs(ast, outputs), do: {outputs, ast}
# Builds a notebook from the list of elements obtained in the previous step.
# Note that the list of elements is reversed:
# first we group elements by traversing Earmark AST top-down
# and then aggregate elements into data strictures going bottom-up.
# Builds a notebook from the list of elements obtained in the
# previous step. The elements are in reversed order, because we
# want to aggregate them into data structures going bottom-up.
defp build_notebook(elems) do
build_notebook(elems, _cells = [], _sections = [], _messages = [], _output_counter = 0)
end
@ -281,39 +280,21 @@ defmodule Livebook.LiveMarkdown.Import do
build_notebook(elems, cells, sections, messages ++ [warning], output_counter)
end
defp build_notebook(
[{:section_name, name} | elems],
cells,
sections,
messages,
output_counter
) do
defp build_notebook([{:section_name, name} | elems], cells, sections, messages, output_counter) do
{metadata, elems} = grab_metadata(elems)
attrs = section_metadata_to_attrs(metadata)
section = %{Notebook.Section.new() | name: name, cells: cells} |> Map.merge(attrs)
build_notebook(elems, [], [section | sections], messages, output_counter)
end
# If there are section-less cells, put them in a default one.
defp build_notebook(
[{:notebook_name, _name} | _] = elems,
cells,
sections,
messages,
output_counter
)
when cells != [] do
section = %{Notebook.Section.new() | cells: cells}
build_notebook(elems, [], [section | sections], messages, output_counter)
end
defp build_notebook(elems, cells, sections, messages, output_counter) do
# At this point we expect the heading, otherwise we use the default
{name, elems} =
case elems do
[{:notebook_name, name} | elems] -> {name, elems}
[] -> {nil, []}
end
# If there are section-less cells, put them in a default one.
defp build_notebook([] = elems, cells, sections, messages, output_counter) when cells != [] do
section = %{Notebook.Section.new() | cells: cells}
build_notebook(elems, [], [section | sections], messages, output_counter)
end
defp build_notebook([{:notebook_name, name} | elems], [], sections, messages, output_counter) do
{metadata, elems} = grab_metadata(elems)
# If there are any non-metadata comments we keep them
{comments, elems} = grab_leading_comments(elems)
@ -330,24 +311,34 @@ defmodule Livebook.LiveMarkdown.Import do
attrs = notebook_metadata_to_attrs(metadata)
# We identify a single leading cell as the setup cell, in any
# other case all extra cells are put in a default section
{setup_cell, extra_sections} =
case cells do
[] -> {nil, []}
[%Notebook.Cell.Code{} = setup_cell] when name != nil -> {setup_cell, []}
extra_cells -> {nil, [%{Notebook.Section.new() | cells: extra_cells}]}
end
notebook =
%{
Notebook.new()
| name: name,
sections: sections,
| sections: extra_sections ++ sections,
leading_comments: comments,
output_counter: output_counter
}
|> maybe_put_name(name)
|> maybe_put_setup_cell(setup_cell)
|> Map.merge(attrs)
{notebook, messages}
end
# If there's no explicit notebook heading, use the defaults.
defp build_notebook([], [], sections, messages, output_counter) do
notebook = %{Notebook.new() | sections: sections, output_counter: output_counter}
{notebook, messages}
end
defp maybe_put_name(notebook, nil), do: notebook
defp maybe_put_name(notebook, name), do: %{notebook | name: name}
defp maybe_put_setup_cell(notebook, nil), do: notebook
defp maybe_put_setup_cell(notebook, cell), do: Notebook.put_setup_cell(notebook, cell)
# Takes optional leading metadata JSON object and returns {metadata, rest}.
defp grab_metadata([{:metadata, metadata} | elems]) do

View file

@ -16,6 +16,7 @@ defmodule Livebook.Notebook do
defstruct [
:name,
:version,
:setup_section,
:sections,
:leading_comments,
:persist_outputs,
@ -30,6 +31,7 @@ defmodule Livebook.Notebook do
@type t :: %__MODULE__{
name: String.t(),
version: String.t(),
setup_section: Section.t(),
sections: list(Section.t()),
leading_comments: list(list(line :: String.t())),
persist_outputs: boolean(),
@ -47,12 +49,22 @@ defmodule Livebook.Notebook do
%__MODULE__{
name: "Untitled notebook",
version: @version,
setup_section: %{Section.new() | id: "setup-section", name: "Setup", cells: []},
sections: [],
leading_comments: [],
persist_outputs: default_persist_outputs(),
autosave_interval_s: default_autosave_interval_s(),
output_counter: 0
}
|> put_setup_cell(Cell.new(:code))
end
@doc """
Sets the given cell as the setup cell.
"""
@spec put_setup_cell(t(), Cell.Code.t()) :: t()
def put_setup_cell(notebook, %Cell.Code{} = cell) do
put_in(notebook.setup_section.cells, [%{cell | id: "setup"}])
end
@doc """
@ -79,12 +91,22 @@ defmodule Livebook.Notebook do
}
end
@doc """
Returns all notebook sections, including the implicit ones.
"""
@spec all_sections(t()) :: list(Section.t())
def all_sections(notebook) do
get_in(notebook, [access_all_sections()])
end
@doc """
Finds notebook section by id.
"""
@spec fetch_section(t(), Section.id()) :: {:ok, Section.t()} | :error
def fetch_section(notebook, section_id) do
Enum.find_value(notebook.sections, :error, fn section ->
notebook
|> all_sections()
|> Enum.find_value(:error, fn section ->
section.id == section_id && {:ok, section}
end)
end
@ -95,7 +117,7 @@ defmodule Livebook.Notebook do
@spec fetch_cell_and_section(t(), Cell.id()) :: {:ok, Cell.t(), Section.t()} | :error
def fetch_cell_and_section(notebook, cell_id) do
for(
section <- notebook.sections,
section <- all_sections(notebook),
cell <- section.cells,
cell.id == cell_id,
do: {cell, section}
@ -206,7 +228,7 @@ defmodule Livebook.Notebook do
def update_cell(notebook, cell_id, fun) do
update_in(
notebook,
[Access.key(:sections), Access.all(), Access.key(:cells), access_by_id(cell_id)],
[access_all_sections(), Access.all(), Access.key(:cells), access_by_id(cell_id)],
fun
)
end
@ -218,7 +240,7 @@ defmodule Livebook.Notebook do
def update_cells(notebook, fun) do
update_in(
notebook,
[Access.key(:sections), Access.all(), Access.key(:cells), Access.all()],
[access_all_sections(), Access.all(), Access.key(:cells), Access.all()],
fun
)
end
@ -229,13 +251,13 @@ defmodule Livebook.Notebook do
@spec update_reduce_cells(t(), acc, (Cell.t(), acc -> {Cell.t(), acc})) :: {t(), acc}
when acc: term()
def update_reduce_cells(notebook, acc, fun) do
{sections, acc} =
Enum.map_reduce(notebook.sections, acc, fn section, acc ->
{[setup_section | sections], acc} =
Enum.map_reduce([notebook.setup_section | notebook.sections], acc, fn section, acc ->
{cells, acc} = Enum.map_reduce(section.cells, acc, fun)
{%{section | cells: cells}, acc}
end)
{%{notebook | sections: sections}, acc}
{%{notebook | setup_section: setup_section, sections: sections}, acc}
end
@doc """
@ -243,7 +265,21 @@ defmodule Livebook.Notebook do
"""
@spec update_section(t(), Section.id(), (Section.t() -> Section.t())) :: t()
def update_section(notebook, section_id, fun) do
update_in(notebook, [Access.key(:sections), access_by_id(section_id)], fun)
update_in(notebook, [access_all_sections(), access_by_id(section_id)], fun)
end
defp access_all_sections() do
fn
:get, %__MODULE__{} = notebook, next ->
next.([notebook.setup_section | notebook.sections])
:get_and_update, %__MODULE__{} = notebook, next ->
{gets, [setup_section | sections]} = next.([notebook.setup_section | notebook.sections])
{gets, %{notebook | setup_section: setup_section, sections: sections}}
_op, data, _next ->
raise "access_all_sections/0 expected %Livebook.Notebook{}, got: #{inspect(data)}"
end
end
@doc """
@ -375,7 +411,7 @@ defmodule Livebook.Notebook do
"""
@spec cells_with_section(t()) :: list({Cell.t(), Section.t()})
def cells_with_section(notebook) do
for section <- notebook.sections,
for section <- all_sections(notebook),
cell <- section.cells,
do: {cell, section}
end
@ -469,7 +505,8 @@ defmodule Livebook.Notebook do
"""
@spec cell_dependency_graph(t()) :: Graph.t(Cell.id())
def cell_dependency_graph(notebook, opts \\ []) do
notebook.sections
notebook
|> all_sections()
|> Enum.reduce(
{%{}, nil, %{}},
fn section, {graph, prev_regular_section, last_id_by_section} ->
@ -535,7 +572,9 @@ defmodule Livebook.Notebook do
"""
@spec find_asset_info(t(), String.t()) :: (asset_info :: map()) | nil
def find_asset_info(notebook, hash) do
Enum.find_value(notebook.sections, fn section ->
notebook
|> all_sections()
|> Enum.find_value(fn section ->
Enum.find_value(section.cells, fn
%Cell.Smart{js_view: %{assets: %{hash: ^hash} = assets_info}} ->
assets_info
@ -671,7 +710,7 @@ defmodule Livebook.Notebook do
"""
@spec find_frame_outputs(t(), String.t()) :: list(Cell.indexed_output())
def find_frame_outputs(notebook, frame_ref) do
for section <- notebook.sections,
for section <- all_sections(notebook),
%{outputs: outputs} <- section.cells,
output <- outputs,
frame_output <- do_find_frame_outputs(output, frame_ref),

View file

@ -67,4 +67,13 @@ defmodule Livebook.Notebook.Cell do
end
def find_inputs_in_output(_output), do: []
@doc """
Checks if the given cell is the setup code cell.
"""
@spec setup?(t()) :: boolean()
def setup?(cell)
def setup?(%Cell.Code{id: "setup"}), do: true
def setup?(_cell), do: false
end

View file

@ -13,10 +13,14 @@ defmodule Livebook.Notebook.Export.Elixir do
end
defp render_notebook(notebook) do
%{setup_section: %{cells: [setup_cell]} = setup_section} = notebook
name = ["# Title: ", notebook.name]
setup_cell = render_setup_cell(setup_cell, setup_section)
sections = Enum.map(notebook.sections, &render_section(&1, notebook))
[name | sections]
[name, setup_cell | sections]
|> Enum.reject(&is_nil/1)
|> Enum.intersperse("\n\n")
end
@ -40,6 +44,9 @@ defmodule Livebook.Notebook.Export.Elixir do
|> Enum.intersperse("\n\n")
end
defp render_setup_cell(%{source: ""}, _section), do: nil
defp render_setup_cell(cell, section), do: render_cell(cell, section)
defp render_cell(%Cell.Markdown{} = cell, _section) do
cell.source
|> Livebook.LiveMarkdown.MarkdownHelpers.reformat()

View file

@ -221,20 +221,20 @@ defmodule Livebook.Session.Data do
end
defp initial_section_infos(notebook) do
for section <- notebook.sections,
for section <- Notebook.all_sections(notebook),
into: %{},
do: {section.id, new_section_info()}
end
defp initial_cell_infos(notebook) do
for section <- notebook.sections,
for section <- Notebook.all_sections(notebook),
cell <- section.cells,
into: %{},
do: {cell.id, new_cell_info(cell, %{})}
end
defp initial_input_values(notebook) do
for section <- notebook.sections,
for section <- Notebook.all_sections(notebook),
cell <- section.cells,
Cell.evaluable?(cell),
output <- cell.outputs,
@ -372,7 +372,8 @@ defmodule Livebook.Session.Data do
end
def apply_operation(data, {:delete_cell, _client_pid, id}) do
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, id) do
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, id),
false <- Cell.setup?(cell) do
data
|> with_actions()
|> delete_cell(cell, section)
@ -380,6 +381,8 @@ defmodule Livebook.Session.Data do
|> update_smart_cell_bases(data)
|> set_dirty()
|> wrap_ok()
else
_ -> :error
end
end
@ -400,6 +403,7 @@ defmodule Livebook.Session.Data do
def apply_operation(data, {:move_cell, _client_pid, id, offset}) do
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, id),
false <- Cell.setup?(cell),
true <- offset != 0,
true <- can_move_cell_by?(data, cell, section, offset) do
data
@ -1014,7 +1018,8 @@ defmodule Livebook.Session.Data do
main_flow_evaluating? = main_flow_evaluating?(data)
{awaiting_branch_sections, awaiting_regular_sections} =
data.notebook.sections
data.notebook
|> Notebook.all_sections()
|> Enum.filter(&section_awaits_evaluation?(data, &1.id))
|> Enum.split_with(& &1.parent_id)
@ -1055,7 +1060,9 @@ defmodule Livebook.Session.Data do
end
defp main_flow_evaluating?(data) do
Enum.any?(data.notebook.sections, fn section ->
data.notebook
|> Notebook.all_sections()
|> Enum.any?(fn section ->
section.parent_id == nil and section_evaluating?(data, section.id)
end)
end
@ -1066,7 +1073,9 @@ defmodule Livebook.Session.Data do
end
defp any_section_evaluating?(data) do
Enum.any?(data.notebook.sections, fn section ->
data.notebook
|> Notebook.all_sections()
|> Enum.any?(fn section ->
section_evaluating?(data, section.id)
end)
end
@ -1124,11 +1133,14 @@ defmodule Livebook.Session.Data do
defp clear_all_evaluation({data, _} = data_actions) do
data_actions
|> reduce(data.notebook.sections, &clear_section_evaluation/2)
|> reduce(Notebook.all_sections(data.notebook), &clear_section_evaluation/2)
end
defp clear_main_evaluation({data, _} = data_actions) do
regular_sections = Enum.filter(data.notebook.sections, &(&1.parent_id == nil))
regular_sections =
data.notebook
|> Notebook.all_sections()
|> Enum.filter(&(&1.parent_id == nil))
data_actions
|> reduce(regular_sections, &clear_section_evaluation/2)
@ -1448,7 +1460,7 @@ defmodule Livebook.Session.Data do
end
defp dead_smart_cells_with_section(data) do
for section <- data.notebook.sections,
for section <- Notebook.all_sections(data.notebook),
%Cell.Smart{} = cell <- section.cells,
info = data.cell_infos[cell.id],
info.status == :dead,

View file

@ -137,7 +137,7 @@ defmodule LivebookWeb.SessionLive do
<div class="grow overflow-y-auto relative" data-element="notebook">
<div data-element="js-view-iframes" phx-update="ignore" id="js-view-iframes"></div>
<div class="w-full max-w-screen-lg px-16 mx-auto py-7" data-element="notebook-content">
<div class="flex items-center pb-4 mb-6 space-x-4 border-b border-gray-200"
<div class="flex items-center pb-4 mb-2 space-x-4 border-b border-gray-200"
data-element="notebook-headline"
data-focusable-id="notebook"
id="notebook"
@ -190,7 +190,14 @@ defmodule LivebookWeb.SessionLive do
</:content>
</.menu>
</div>
<div class="flex flex-col w-full space-y-16">
<div>
<.live_component module={LivebookWeb.SessionLive.CellComponent}
id={@data_view.setup_cell_view.id}
session_id={@session.id}
runtime={@data_view.runtime}
cell_view={@data_view.setup_cell_view} />
</div>
<div class="mt-8 flex flex-col w-full space-y-16">
<%= if @data_view.section_views == [] do %>
<div class="flex justify-center">
<button class="button-base button-small"
@ -715,6 +722,17 @@ defmodule LivebookWeb.SessionLive do
end
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
data = socket.private.data
socket =
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
true <- Cell.setup?(cell),
false <- data.cell_infos[cell.id].eval.validity == :fresh do
maybe_restart_runtime(socket)
else
_ -> socket
end
Session.queue_cell_evaluation(socket.assigns.session.pid, cell_id)
{:noreply, socket}
@ -761,21 +779,7 @@ defmodule LivebookWeb.SessionLive do
end
def handle_event("restart_runtime", %{}, socket) do
socket =
if runtime = socket.private.data.runtime do
case Runtime.duplicate(runtime) do
{:ok, new_runtime} ->
Session.connect_runtime(socket.assigns.session.pid, new_runtime)
clear_flash(socket, :error)
{:error, message} ->
put_flash(socket, :error, "Failed to setup runtime - #{message}")
end
else
socket
end
{:noreply, socket}
{:noreply, maybe_restart_runtime(socket)}
end
def handle_event("connect_default_runtime", %{}, socket) do
@ -1312,6 +1316,19 @@ defmodule LivebookWeb.SessionLive do
defp autofocus_cell_id(%Notebook{sections: [%{cells: [%{id: id, source: ""}]}]}), do: id
defp autofocus_cell_id(_notebook), do: nil
defp maybe_restart_runtime(%{private: %{data: %{runtime: nil}}} = socket), do: socket
defp maybe_restart_runtime(%{private: %{data: data}} = socket) do
case Runtime.duplicate(data.runtime) do
{:ok, new_runtime} ->
Session.connect_runtime(socket.assigns.session.pid, new_runtime)
clear_flash(socket, :error)
{:error, message} ->
put_flash(socket, :error, "Failed to setup runtime - #{message}")
end
end
# Builds view-specific structure of data by cherry-picking
# only the relevant attributes.
# We then use `@data_view` in the templates and consequently
@ -1340,6 +1357,7 @@ defmodule LivebookWeb.SessionLive do
data.clients_map
|> Enum.map(fn {client_pid, user_id} -> {client_pid, data.users_map[user_id]} end)
|> Enum.sort_by(fn {_client_pid, user} -> user.name end),
setup_cell_view: %{cell_to_view(hd(data.notebook.setup_section.cells), data) | type: :setup},
section_views: section_views(data.notebook.sections, data),
bin_entries: data.bin_entries
}
@ -1497,7 +1515,7 @@ defmodule LivebookWeb.SessionLive do
{:apply_cell_delta, _pid, _cell_id, _tag, _delta, _revision} ->
update_dirty_status(data_view, data)
{:update_smart_cell, _pid, _cell_id, _cell_state, _delta} ->
{:update_smart_cell, _pid, _cell_id, _cell_state, _delta, _reevaluate} ->
update_dirty_status(data_view, data)
# For outputs that update existing outputs we send the update directly

View file

@ -12,7 +12,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
data-focusable-id={@cell_view.id}
data-type={@cell_view.type}
data-session-path={Routes.session_path(@socket, :page, @session_id)}
data-evaluation-digest={get_in(@cell_view, [:eval, :evaluation_digest])}>
data-evaluation-digest={get_in(@cell_view, [:eval, :evaluation_digest])}
data-eval-validity={get_in(@cell_view, [:eval, :validity])}
data-js-empty={empty?(@cell_view.source_view)}>
<%= render_cell(assigns) %>
</div>
"""
@ -80,7 +82,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
language="elixir"
intellisense />
<div class="absolute bottom-2 right-2">
<.cell_status cell_view={@cell_view} />
<.cell_status id={@cell_view.id} cell_view={@cell_view} />
</div>
</div>
<.evaluation_outputs
@ -92,6 +94,51 @@ defmodule LivebookWeb.SessionLive.CellComponent do
"""
end
defp render_cell(%{cell_view: %{type: :setup}} = assigns) do
~H"""
<.cell_actions>
<:primary>
<.setup_cell_evaluation_button
cell_id={@cell_view.id}
validity={@cell_view.eval.validity}
status={@cell_view.eval.status} />
</:primary>
<:secondary>
<.enable_insert_mode_button />
<.cell_link_button cell_id={@cell_view.id} />
<.setup_cell_info />
</:secondary>
</.cell_actions>
<.cell_body>
<div data-element="info-box">
<div class="p-3 flex items-center justify-between border border-gray-200 text-sm text-gray-400 font-medium rounded-lg">
<span>Notebook dependencies and setup</span>
<.cell_status id={"#{@cell_view.id}-1"} cell_view={@cell_view} />
</div>
</div>
<div data-element="editor-box">
<div class="relative">
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
id={"#{@cell_view.id}-primary"}
cell_id={@cell_view.id}
tag="primary"
source_view={@cell_view.source_view}
language="elixir"
intellisense />
<div class="absolute bottom-2 right-2">
<.cell_status id={"#{@cell_view.id}-2"} cell_view={@cell_view} />
</div>
</div>
<.evaluation_outputs
cell_view={@cell_view}
socket={@socket}
session_id={@session_id}
runtime={@runtime} />
</div>
</.cell_body>
"""
end
defp render_cell(%{cell_view: %{type: :smart}} = assigns) do
~H"""
<.cell_actions>
@ -114,37 +161,41 @@ defmodule LivebookWeb.SessionLive.CellComponent do
</:secondary>
</.cell_actions>
<.cell_body>
<div class="relative">
<div data-element="ui-box">
<%= case @cell_view.status do %>
<% :started -> %>
<div class={"flex #{if(@cell_view.editor && @cell_view.editor.placement == :top, do: "flex-col-reverse", else: "flex-col")}"}>
<.live_component module={LivebookWeb.JSViewComponent}
id={@cell_view.id}
js_view={@cell_view.js_view}
session_id={@session_id} />
<%= if @cell_view.editor do %>
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
id={"#{@cell_view.id}-secondary"}
cell_id={@cell_view.id}
tag="secondary"
source_view={@cell_view.editor.source_view}
language={@cell_view.editor.language} />
<% end %>
</div>
<div data-element="ui-box">
<%= case @cell_view.status do %>
<% :started -> %>
<div class={"flex #{if(@cell_view.editor && @cell_view.editor.placement == :top, do: "flex-col-reverse", else: "flex-col")}"}>
<.live_component module={LivebookWeb.JSViewComponent}
id={@cell_view.id}
js_view={@cell_view.js_view}
session_id={@session_id} />
<%= if @cell_view.editor do %>
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
id={"#{@cell_view.id}-secondary"}
cell_id={@cell_view.id}
tag="secondary"
source_view={@cell_view.editor.source_view}
language={@cell_view.editor.language} />
<% end %>
</div>
<% :dead -> %>
<div class="info-box">
Evaluate and install dependencies to show the contents of this Smart cell.
</div>
<% :dead -> %>
<div class="info-box">
Evaluate and install dependencies to show the contents of this Smart cell.
</div>
<% :starting -> %>
<div class="delay-200">
<.content_skeleton empty={false} />
</div>
<% end %>
<% :starting -> %>
<div class="delay-200">
<.content_skeleton empty={false} />
</div>
<% end %>
<div class="flex flex-col items-end space-y-2">
<div></div>
<.cell_status id={"#{@cell_view.id}-1"} cell_view={@cell_view} />
</div>
<div data-element="editor-box">
</div>
<div data-element="editor-box">
<div class="relative">
<.live_component module={LivebookWeb.SessionLive.CellEditorComponent}
id={"#{@cell_view.id}-primary"}
cell_id={@cell_view.id}
@ -153,9 +204,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
language="elixir"
intellisense
read_only />
</div>
<div data-element="cell-status-container">
<.cell_status cell_view={@cell_view} />
<div class="absolute bottom-2 right-2">
<.cell_status id={"#{@cell_view.id}-2"} cell_view={@cell_view} />
</div>
</div>
</div>
<.evaluation_outputs
@ -168,7 +219,10 @@ defmodule LivebookWeb.SessionLive.CellComponent do
end
defp cell_actions(assigns) do
assigns = assign_new(assigns, :primary, fn -> [] end)
assigns =
assigns
|> assign_new(:primary, fn -> [] end)
|> assign_new(:secondary, fn -> [] end)
~H"""
<div class="mb-1 flex items-center justify-between">
@ -238,6 +292,35 @@ defmodule LivebookWeb.SessionLive.CellComponent do
"""
end
defp setup_cell_evaluation_button(%{status: :ready} = assigns) do
~H"""
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="queue_cell_evaluation"
phx-value-cell_id={@cell_id}>
<%= if @validity == :fresh do %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">Setup</span>
<% else %>
<.remix_icon icon="restart-fill" class="text-xl" />
<span class="text-sm font-medium">Restart and setup</span>
<% end %>
</button>
"""
end
defp setup_cell_evaluation_button(assigns) do
~H"""
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
phx-value-cell_id={@cell_id}>
<.remix_icon icon="stop-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
Stop
</span>
</button>
"""
end
defp enable_insert_mode_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Edit content" data-element="enable-insert-mode-button">
@ -379,6 +462,23 @@ defmodule LivebookWeb.SessionLive.CellComponent do
"""
end
defp setup_cell_info(assigns) do
~H"""
<span class="tooltip top"
data-tooltip={
~s'''
The setup cell includes code that initializes the notebook
and should run only once. This is the best place to install
dependencies and set global configuration.\
'''
}>
<span class="icon-button">
<.remix_icon icon="question-line" class="text-xl" />
</span>
</span>
"""
end
defp evaluation_outputs(assigns) do
~H"""
<div class="flex flex-col"
@ -404,7 +504,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
~H"""
<.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}>
<span class="font-mono"
id={"cell-timer-#{@cell_view.id}"}
id={"#{@id}-cell-timer"}
phx-hook="Timer"
phx-update="ignore"
data-start={DateTime.to_iso8601(@cell_view.eval.evaluation_start)}>

View file

@ -311,7 +311,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
assert expected_document == document
end
test "formats code in Code cells" do
test "formats code in code cells" do
notebook = %{
Notebook.new()
| name: "My Notebook",
@ -347,7 +347,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
assert expected_document == document
end
test "does not format code in Code cells which have formatting disabled" do
test "does not format code in code cells which have formatting disabled" do
notebook = %{
Notebook.new()
| name: "My Notebook",
@ -1000,6 +1000,32 @@ defmodule Livebook.LiveMarkdown.ExportTest do
assert expected_document == document
end
describe "setup cell" do
test "includes the leading setup cell when it has content" do
notebook =
%{
Notebook.new()
| name: "My Notebook",
sections: [%{Notebook.Section.new() | name: "Section 1"}]
}
|> Notebook.put_setup_cell(%{Notebook.Cell.new(:code) | source: "Mix.install([...])"})
expected_document = """
# My Notebook
```elixir
Mix.install([...])
```
## Section 1
"""
document = Export.notebook_to_livemd(notebook)
assert expected_document == document
end
end
defp spawn_widget_with_data(ref, data) do
spawn(fn ->
receive do

View file

@ -845,11 +845,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do
} = notebook
end
test "import notebook with parent section being a branching section itself produces a warning" do
test "importing notebook with parent section being a branching section itself produces a warning" do
markdown = """
# My Notebook
## Section 1
```elixir
@ -895,4 +894,57 @@ defmodule Livebook.LiveMarkdown.ImportTest do
]
} = notebook
end
describe "setup cell" do
test "imports a leading setup cell" do
markdown = """
# My Notebook
```elixir
Mix.install([...])
```
## Section 1
"""
{notebook, []} = Import.notebook_from_livemd(markdown)
assert %Notebook{
name: "My Notebook",
setup_section: %{
cells: [
%Cell.Code{id: "setup", source: "Mix.install([...])"}
]
},
sections: [
%Notebook.Section{
name: "Section 1",
cells: []
}
]
} = notebook
end
test "does not add an implicit section when there is just setup cell" do
markdown = """
# My Notebook
```elixir
Mix.install([...])
```
"""
{notebook, []} = Import.notebook_from_livemd(markdown)
assert %Notebook{
name: "My Notebook",
setup_section: %{
cells: [
%Cell.Code{id: "setup", source: "Mix.install([...])"}
]
},
sections: []
} = notebook
end
end
end

View file

@ -105,4 +105,28 @@ defmodule Livebook.Notebook.Export.ElixirTest do
assert expected_document == document
end
describe "setup cell" do
test "includes the leading setup cell when it has content" do
notebook =
%{
Notebook.new()
| name: "My Notebook",
sections: [%{Notebook.Section.new() | name: "Section 1"}]
}
|> Notebook.put_setup_cell(%{Notebook.Cell.new(:code) | source: "Mix.install([...])"})
expected_document = """
# Title: My Notebook
Mix.install([...])
# ── Section 1 ──
"""
document = Export.Elixir.notebook_to_elixir(notebook)
assert expected_document == document
end
end
end

View file

@ -92,7 +92,8 @@ defmodule Livebook.NotebookTest do
"c4" => "c3",
"c3" => "c2",
"c2" => "c1",
"c1" => nil
"c1" => "setup",
"setup" => nil
}
end
@ -108,7 +109,8 @@ defmodule Livebook.NotebookTest do
assert Notebook.cell_dependency_graph(notebook) == %{
"c2" => "c1",
"c1" => nil
"c1" => "setup",
"setup" => nil
}
end
@ -150,7 +152,8 @@ defmodule Livebook.NotebookTest do
"c4" => "c3",
"c3" => "c2",
"c2" => "c1",
"c1" => nil
"c1" => "setup",
"setup" => nil
}
end
@ -183,7 +186,8 @@ defmodule Livebook.NotebookTest do
assert Notebook.cell_dependency_graph(notebook) == %{
"c2" => "c1",
"c1" => nil
"c1" => "setup",
"setup" => nil
}
end
@ -227,7 +231,8 @@ defmodule Livebook.NotebookTest do
"c4" => "c2",
"c3" => "c1",
"c2" => "c1",
"c1" => nil
"c1" => "setup",
"setup" => nil
}
end
@ -250,7 +255,8 @@ defmodule Livebook.NotebookTest do
assert Notebook.cell_dependency_graph(notebook, cell_filter: &Cell.evaluable?/1) ==
%{
"c3" => "c1",
"c1" => nil
"c1" => "setup",
"setup" => nil
}
end
end

View file

@ -15,10 +15,11 @@ defmodule Livebook.Session.DataTest do
describe "new/1" do
test "called with no arguments defaults to a blank notebook" do
empty_map = %{}
assert %{notebook: %{sections: []}, cell_infos: ^empty_map, section_infos: ^empty_map} =
assert %{notebook: %{sections: []}, cell_infos: cell_infos, section_infos: section_infos} =
Data.new()
assert map_size(cell_infos) == 1
assert map_size(section_infos) == 1
end
test "called with a notebook, sets default cell and section infos" do
@ -221,10 +222,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c3", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
operation = {:set_section_parent, self(), "s2", "s1"}
@ -250,8 +248,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c3", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2", "c3"]}
])
operation = {:set_section_parent, self(), "s2", "s1"}
@ -335,10 +333,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s2", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
operation = {:unset_section_parent, self(), "s2"}
@ -365,8 +360,8 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s2", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2", "c3"]}
])
operation = {:unset_section_parent, self(), "s2"}
@ -486,15 +481,16 @@ defmodule Livebook.Session.DataTest do
])
operation = {:delete_section, self(), "s1", true}
empty_map = %{}
assert {:ok,
%{
notebook: %{
sections: []
},
section_infos: ^empty_map
section_infos: section_infos
}, []} = Data.apply_operation(data, operation)
refute Map.has_key?(section_infos, "s1")
end
test "returns error when cell deletion is disabled for the first cell" do
@ -536,9 +532,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"])
])
operation = {:delete_section, self(), "s2", true}
@ -571,9 +565,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c2", %{}},
# Evaluate both cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"])
])
operation = {:delete_section, self(), "s1", true}
@ -596,10 +588,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s2", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
operation = {:delete_section, self(), "s2", false}
@ -626,10 +615,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s2", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
operation = {:delete_section, self(), "s2", true}
@ -671,6 +657,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -691,14 +678,13 @@ defmodule Livebook.Session.DataTest do
])
operation = {:delete_cell, self(), "c1"}
empty_map = %{}
assert {:ok,
%{
notebook: %{
sections: [%{cells: []}]
},
cell_infos: ^empty_map,
cell_infos: cell_infos,
bin_entries: [
%{
cell: %{id: "c1"},
@ -709,6 +695,8 @@ defmodule Livebook.Session.DataTest do
}
]
}, _actions} = Data.apply_operation(data, operation)
refute Map.has_key?(cell_infos, "c1")
end
test "unqueues the cell if it's queued for evaluation" do
@ -718,6 +706,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -737,9 +726,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
# Evaluate both cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"])
])
operation = {:delete_cell, self(), "c1"}
@ -759,9 +746,7 @@ defmodule Livebook.Session.DataTest do
{:set_cell_attributes, self(), "c2", %{reevaluate_automatically: true}},
# Evaluate both cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"])
])
operation = {:delete_cell, self(), "c1"}
@ -780,8 +765,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
# Evaluate the Code cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c2"]},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c2"])
])
operation = {:delete_cell, self(), "c1"}
@ -798,8 +782,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"])
])
operation = {:delete_cell, self(), "c1"}
@ -831,6 +814,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil},
{:queue_cells_evaluation, self(), ["c1"]},
@ -842,7 +826,8 @@ defmodule Livebook.Session.DataTest do
assert {:ok, %{},
[
{:forget_evaluation, _, _},
{:set_smart_cell_base, %{id: "c2"}, %{id: "s1"}, nil}
{:set_smart_cell_base, %{id: "c2"}, %{id: "s1"},
{%{id: "setup"}, %{id: "setup-section"}}}
]} = Data.apply_operation(data, operation)
end
end
@ -962,6 +947,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -980,11 +966,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 3, :code, "c4", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"])
])
operation = {:move_cell, self(), "c3", -1}
@ -1018,11 +1000,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 3, :code, "c4", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"])
])
operation = {:move_cell, self(), "c2", 1}
@ -1080,10 +1058,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c3", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
operation = {:move_cell, self(), "c1", 1}
@ -1103,8 +1078,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :markdown, "c2", %{}},
# Evaluate the Code cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"])
])
operation = {:move_cell, self(), "c2", -1}
@ -1126,6 +1100,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
# Evaluate the Code cell
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -1149,9 +1124,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c3"])
])
operation = {:move_cell, self(), "c1", 1}
@ -1178,11 +1151,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s2", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"])
])
operation = {:move_cell, self(), "c2", 1}
@ -1214,12 +1183,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s4", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4", "c5"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c5", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4", "c5"])
])
operation = {:move_cell, self(), "c2", 1}
@ -1248,10 +1212,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
{:ok, data_moved, []} = Data.apply_operation(data, {:move_cell, self(), "c2", -1})
@ -1291,11 +1252,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 1, :code, "c4", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"])
])
operation = {:move_section, self(), "s2", -1}
@ -1333,11 +1290,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 1, :code, "c4", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"])
])
operation = {:move_section, self(), "s1", 1}
@ -1375,10 +1328,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c3", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
operation = {:move_section, self(), "s1", 1}
@ -1399,8 +1349,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :markdown, "c2", %{}},
# Evaluate the Code cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"])
])
operation = {:move_section, self(), "s2", -1}
@ -1423,6 +1372,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c2", %{}},
# Evaluate the Code cell
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -1450,9 +1400,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s4", 0, :markdown, "c4", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c3"])
])
operation = {:move_section, self(), "s4", -1}
@ -1480,11 +1428,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s2", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"])
])
operation = {:move_section, self(), "s2", 1}
@ -1561,6 +1505,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -1612,7 +1557,8 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()}
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"])
])
operation = {:queue_cells_evaluation, self(), ["c1"]}
@ -1629,7 +1575,8 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()}
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"])
])
operation = {:queue_cells_evaluation, self(), ["c1"]}
@ -1645,6 +1592,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -1665,6 +1613,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -1691,12 +1640,9 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 1, :code, "c4", %{}},
# Evaluate first 2 cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
evaluate_cells_operations(["setup", "c1", "c2"]),
# Evaluate the first cell, so the second becomes stale
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["c1"])
])
# The above leads to:
@ -1737,7 +1683,8 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 2, "s3"},
{:insert_cell, self(), "s3", 0, :code, "c3", %{}},
{:set_section_parent, self(), "s3", "s1"},
{:set_runtime, self(), NoopRuntime.new()}
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"])
])
operation = {:queue_cells_evaluation, self(), ["c3"]}
@ -1770,8 +1717,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c3", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c3"]}
])
operation = {:queue_cells_evaluation, self(), ["c2"]}
@ -1798,8 +1745,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c2", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"])
])
operation = {:queue_cells_evaluation, self(), ["c2"]}
@ -1825,9 +1771,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c4", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"]),
{:queue_cells_evaluation, self(), ["c4"]}
])
operation = {:queue_cells_evaluation, self(), ["c3"]}
@ -1856,8 +1801,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c3", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2"]}
])
operation = {:queue_cells_evaluation, self(), ["c3"]}
@ -1877,6 +1822,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -1900,7 +1846,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -1911,7 +1857,7 @@ defmodule Livebook.Session.DataTest do
notebook: %{
sections: [
%{
cells: [%{outputs: [{0, {:stdout, "Hello!"}}]}]
cells: [%{outputs: [{1, {:stdout, "Hello!"}}]}]
}
]
}
@ -1924,8 +1870,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"])
])
operation = {:add_cell_evaluation_output, self(), "c1", {:stdout, "Hello!"}}
@ -1935,7 +1880,7 @@ defmodule Livebook.Session.DataTest do
notebook: %{
sections: [
%{
cells: [%{outputs: [{1, {:stdout, "Hello!"}}, _result]}]
cells: [%{outputs: [{2, {:stdout, "Hello!"}}, _result]}]
}
]
}
@ -1948,6 +1893,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:set_notebook_attributes, self(), %{persist_outputs: true}},
{:mark_as_not_dirty, self()}
@ -1966,6 +1912,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -1977,7 +1924,7 @@ defmodule Livebook.Session.DataTest do
notebook: %{
sections: [
%{
cells: [%{outputs: [{0, {:ok, [1, 2, 3]}}]}]
cells: [%{outputs: [{1, {:ok, [1, 2, 3]}}]}]
}
]
}
@ -1990,6 +1937,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -2009,9 +1957,9 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
# Evaluate the first cell
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
evaluate_cells_operations(["c1"]),
# Start evaluating the second cell
{:queue_cells_evaluation, self(), ["c2"]},
# Remove the first cell, marking the second as stale
@ -2033,6 +1981,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -2055,6 +2004,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -2082,10 +2032,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c3", %{}},
# Evaluate all cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
evaluate_cells_operations(["setup", "c1", "c2", "c3"]),
# Queue the first cell again
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -2116,11 +2063,7 @@ defmodule Livebook.Session.DataTest do
{:set_section_parent, self(), "s4", "s1"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta},
evaluate_cells_operations(["setup", "c1", "c2", "c3", "c4"]),
# Queue the second cell again
{:queue_cells_evaluation, self(), ["c2"]}
])
@ -2150,10 +2093,7 @@ defmodule Livebook.Session.DataTest do
{:set_cell_attributes, self(), "c3", %{reevaluate_automatically: true}},
# Evaluate all cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
evaluate_cells_operations(["setup", "c1", "c2", "c3"]),
# Queue the first cell again
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -2178,6 +2118,7 @@ defmodule Livebook.Session.DataTest do
{:set_cell_attributes, self(), "c2", %{reevaluate_automatically: true}},
# Evaluate all cells
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -2201,6 +2142,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
@ -2228,6 +2170,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -2250,6 +2193,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:set_notebook_attributes, self(), %{persist_outputs: true}},
{:mark_as_not_dirty, self()}
@ -2269,6 +2213,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]}
])
@ -2285,6 +2230,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:set_input_value, self(), "i1", "value"},
@ -2305,6 +2251,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:set_input_value, self(), "i1", "value"},
@ -2328,6 +2275,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", {:input, input}, @eval_meta},
@ -2354,6 +2302,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c1", %{}},
{:insert_cell, self(), "s3", 0, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:set_input_value, self(), "i1", "value"},
@ -2374,6 +2323,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil},
{:queue_cells_evaluation, self(), ["c1"]}
@ -2420,6 +2370,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:queue_cells_evaluation, self(), ["c2"]}
@ -2447,8 +2398,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2", "c3"]}
])
operation = {:reflect_main_evaluation_failure, self()}
@ -2476,9 +2427,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 1, :code, "c3", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"]),
{:queue_cells_evaluation, self(), ["c3"]}
])
operation = {:reflect_main_evaluation_failure, self()}
@ -2511,9 +2461,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c4", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"]),
{:queue_cells_evaluation, self(), ["c3", "c4"]}
])
operation = {:reflect_evaluation_failure, self(), "s2"}
@ -2548,8 +2497,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"])
])
operation = {:cancel_cell_evaluation, self(), "c1"}
@ -2565,8 +2513,8 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2", "c3"]}
])
operation = {:cancel_cell_evaluation, self(), "c2"}
@ -2592,6 +2540,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -2613,8 +2562,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :code, "c4", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2", "c3", "c4"]}
])
operation = {:cancel_cell_evaluation, self(), "c2"}
@ -2642,6 +2591,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]}
])
@ -2664,6 +2614,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]}
])
@ -2767,6 +2718,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
{:smart_cell_started, self(), "c1", Delta.new(), %{}, nil},
@ -2794,8 +2746,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :code, "c3", %{}},
{:set_section_parent, self(), "s2", "s1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1"]),
{:queue_cells_evaluation, self(), ["c2", "c3"]}
])
operation = {:erase_outputs, self()}
@ -2822,9 +2774,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :markdown, "c2", %{}},
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c3"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c3"])
])
operation = {:erase_outputs, self()}
@ -3332,11 +3282,8 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"]),
evaluate_cells_operations(["c1"])
])
attrs = %{reevaluate_automatically: true}
@ -3368,6 +3315,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta}
])
@ -3389,11 +3337,10 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:insert_cell, self(), "s1", 3, :code, "c4", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3", "c4"]},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta},
evaluate_cells_operations(["c2", "c3", "c4"]),
{:bind_input, self(), "c3", "i1"}
])
@ -3428,6 +3375,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1", "c2"]},
# Second section with evaluating and queued cells
{:insert_section, self(), 1, "s2"},
@ -3459,7 +3407,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:queue_cells_evaluation, self(), ["c1"]}
{:queue_cells_evaluation, self(), ["setup"]}
])
runtime = NoopRuntime.new()
@ -3468,13 +3416,13 @@ defmodule Livebook.Session.DataTest do
assert {:ok,
%{
cell_infos: %{
"c1" => %{eval: %{status: :evaluating}}
"setup" => %{eval: %{status: :evaluating}}
},
section_infos: %{
"s1" => %{evaluating_cell_id: "c1", evaluation_queue: []}
"setup-section" => %{evaluating_cell_id: "setup", evaluation_queue: []}
}
},
[{:start_evaluation, %{id: "c1"}, %{id: "s1"}}]} =
[{:start_evaluation, %{id: "setup"}, %{id: "setup-section"}}]} =
Data.apply_operation(data, operation)
end
end
@ -3536,6 +3484,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:insert_cell, self(), "s1", 4, :code, "c4", %{}},
{:set_runtime, self(), NoopRuntime.new()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "c1", {:input, input}, @eval_meta},
{:bind_input, self(), "c2", "i1"},
@ -3557,13 +3506,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:evaluation_started, self(), "c1", @empty_digest},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:evaluation_started, self(), "c2", @empty_digest},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:evaluation_started, self(), "c3", @empty_digest},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
evaluate_cells_operations(["setup", "c1", "c2", "c3"]),
# Modify cell 2
{:client_join, self(), User.new()},
{:apply_cell_delta, self(), "c2", :primary, Delta.new() |> Delta.insert("cats"), 1}
@ -3579,11 +3522,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c3"]},
{:evaluation_started, self(), "c1", @empty_digest},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:evaluation_started, self(), "c3", @empty_digest},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
evaluate_cells_operations(["setup", "c1", "c3"]),
# Insert a fresh cell between cell 1 and cell 3
{:insert_cell, self(), "s1", 1, :code, "c2", %{}}
])
@ -3598,15 +3537,9 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2"]},
{:evaluation_started, self(), "c1", @empty_digest},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:evaluation_started, self(), "c2", @empty_digest},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
# Reevaluate cell 2
{:queue_cells_evaluation, self(), ["c1"]},
{:evaluation_started, self(), "c1", @empty_digest},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2"]),
# Reevaluate cell 1
evaluate_cells_operations(["c1"])
])
assert Data.cell_ids_for_full_evaluation(data, []) |> Enum.sort() == ["c2"]
@ -3620,16 +3553,23 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :code, "c2", %{}},
{:insert_cell, self(), "s1", 2, :code, "c3", %{}},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1", "c2", "c3"]},
{:evaluation_started, self(), "c1", @empty_digest},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:evaluation_started, self(), "c2", @empty_digest},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:evaluation_started, self(), "c3", @empty_digest},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta}
evaluate_cells_operations(["setup", "c1", "c2", "c3"])
])
assert Data.cell_ids_for_full_evaluation(data, ["c2"]) |> Enum.sort() == ["c2", "c3"]
end
end
defp evaluate_cells_operations(cell_ids) do
[
{:queue_cells_evaluation, self(), cell_ids},
for(
cell_id <- cell_ids,
do: [
{:evaluation_started, self(), cell_id, @empty_digest},
{:add_cell_evaluation_response, self(), cell_id, @eval_resp, @eval_meta}
]
)
]
end
end

View file

@ -660,35 +660,10 @@ defmodule Livebook.SessionTest do
end
test "given cell in main flow returns nil if there is no previous cell" do
cell1 = %{Cell.new(:markdown) | id: "c1"}
section1 = %{Section.new() | id: "s1", cells: [cell1]}
cell2 = %{Cell.new(:code) | id: "c2"}
section2 = %{Section.new() | id: "s2", cells: [cell2]}
notebook = %{Notebook.new() | sections: [section1, section2]}
%{setup_section: %{cells: [setup_cell]} = setup_section} = notebook = Notebook.new()
data = Data.new(notebook)
assert {:main_flow, nil} = Session.find_base_locator(data, cell2, section2)
end
test "given cell in branching section returns nil in that section if there is no previous cell" do
cell1 = %{Cell.new(:markdown) | id: "c1"}
section1 = %{Section.new() | id: "s1", cells: [cell1]}
cell2 = %{Cell.new(:code) | id: "c2"}
section2 = %{
Section.new()
| id: "s2",
parent_id: "s1",
cells: [cell2]
}
notebook = %{Notebook.new() | sections: [section1, section2]}
data = Data.new(notebook)
assert {"s2", nil} = Session.find_base_locator(data, cell2, section2)
assert {:main_flow, nil} = Session.find_base_locator(data, setup_cell, setup_section)
end
test "when :existing is set ignores fresh and aborted cells" do
@ -708,6 +683,7 @@ defmodule Livebook.SessionTest do
data_after_operations!(data, [
{:set_runtime, self(), Livebook.Runtime.NoopRuntime.new()},
{:queue_cells_evaluation, self(), ["c1"]},
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, %{evaluation_time_ms: 10}},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, %{evaluation_time_ms: 10}}
])

View file

@ -114,6 +114,9 @@ defmodule LivebookWeb.SessionLiveTest do
end
test "queueing cell evaluation", %{conn: conn, session: session} do
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
evaluate_setup(session.pid)
section_id = insert_section(session.pid)
cell_id = insert_text_cell(session.pid, section_id, :code, "Process.sleep(50)")
@ -127,6 +130,19 @@ defmodule LivebookWeb.SessionLiveTest do
Session.get_data(session.pid)
end
test "reevaluting the setup cell", %{conn: conn, session: session} do
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
evaluate_setup(session.pid)
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
view
|> element(~s{[data-element="session"]})
|> render_hook("queue_cell_evaluation", %{"cell_id" => "setup"})
assert_receive {:operation, {:set_runtime, _pid, %{} = _runtime}}
end
test "cancelling cell evaluation", %{conn: conn, session: session} do
section_id = insert_section(session.pid)
cell_id = insert_text_cell(session.pid, section_id, :code, "Process.sleep(2000)")
@ -314,6 +330,9 @@ defmodule LivebookWeb.SessionLiveTest do
describe "outputs" do
test "stdout output update", %{conn: conn, session: session} do
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
evaluate_setup(session.pid)
section_id = insert_section(session.pid)
cell_id = insert_text_cell(session.pid, section_id, :code)
@ -332,6 +351,9 @@ defmodule LivebookWeb.SessionLiveTest do
end
test "frame output update", %{conn: conn, session: session} do
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
evaluate_setup(session.pid)
section_id = insert_section(session.pid)
cell_id = insert_text_cell(session.pid, section_id, :code)
@ -897,6 +919,11 @@ defmodule LivebookWeb.SessionLiveTest do
cell.id
end
defp evaluate_setup(session_pid) do
Session.queue_cell_evaluation(session_pid, "setup")
assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}}
end
defp insert_cell_with_output(session_pid, section_id, output) do
code =
quote do

View file

@ -27,7 +27,9 @@ defmodule Livebook.TestHelpers do
Raises if any of the operations results in an error.
"""
def data_after_operations!(data \\ Data.new(), operations) do
Enum.reduce(operations, data, fn operation, data ->
operations
|> List.flatten()
|> Enum.reduce(data, fn operation, data ->
case Data.apply_operation(data, operation) do
{:ok, data, _action} ->
data