Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added automatic cell evaluation #637

Merged
merged 11 commits into from
Oct 26, 2021
4 changes: 2 additions & 2 deletions lib/livebook/live_markdown/export.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ defmodule Livebook.LiveMarkdown.Export do
defp cell_metadata(%Cell.Elixir{} = cell) do
put_unless_default(
%{},
Map.take(cell, [:disable_formatting]),
Map.take(Cell.Elixir.new(), [:disable_formatting])
Map.take(cell, [:disable_formatting, :reevaluate_automatically]),
Map.take(Cell.Elixir.new(), [:disable_formatting, :reevaluate_automatically])
)
end

Expand Down
3 changes: 3 additions & 0 deletions lib/livebook/live_markdown/import.ex
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ defmodule Livebook.LiveMarkdown.Import do
{"disable_formatting", disable_formatting}, attrs ->
Map.put(attrs, :disable_formatting, disable_formatting)

{"reevaluate_automatically", reevaluate_automatically}, attrs ->
Map.put(attrs, :reevaluate_automatically, reevaluate_automatically)

_entry, attrs ->
attrs
end)
Expand Down
8 changes: 5 additions & 3 deletions lib/livebook/notebook/cell/elixir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Livebook.Notebook.Cell.Elixir do
# It consists of text content that the user can edit
# and produces some output once evaluated.

defstruct [:id, :source, :outputs, :disable_formatting]
defstruct [:id, :source, :outputs, :disable_formatting, :reevaluate_automatically]

alias Livebook.Utils
alias Livebook.Notebook.Cell
Expand All @@ -15,7 +15,8 @@ defmodule Livebook.Notebook.Cell.Elixir do
id: Cell.id(),
source: String.t(),
outputs: list(output()),
disable_formatting: boolean()
disable_formatting: boolean(),
reevaluate_automatically: boolean()
}

@typedoc """
Expand Down Expand Up @@ -49,7 +50,8 @@ defmodule Livebook.Notebook.Cell.Elixir do
id: Utils.random_id(),
source: "",
outputs: [],
disable_formatting: false
disable_formatting: false,
reevaluate_automatically: false
}
end
end
21 changes: 21 additions & 0 deletions lib/livebook/session/data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,10 @@ defmodule Livebook.Session.Data do
data_actions
|> compute_snapshots()
|> update_validity()
# After updating validity there may be new stale cells, so we check
# if any of them is configured for automatic reevaluation
|> maybe_queue_reevaluating_cells()
|> maybe_evaluate_queued()
end

defp compute_snapshots({data, _} = data_actions) do
Expand Down Expand Up @@ -1419,4 +1423,21 @@ defmodule Livebook.Session.Data do
end)
end)
end

defp maybe_queue_reevaluating_cells({data, _} = data_actions) do
cells_to_reeavaluete =
data.notebook
|> Notebook.elixir_cells_with_section()
|> Enum.filter(fn {cell, _section} ->
info = data.cell_infos[cell.id]
info.validity_status == :stale and cell.reevaluate_automatically
end)

data_actions
|> reduce(cells_to_reeavaluete, fn data_actions, {cell, section} ->
data_actions
|> queue_prerequisite_cells_evaluation(cell)
|> queue_cell_evaluation(cell, section)
end)
end
end
3 changes: 2 additions & 1 deletion lib/livebook_web/live/session_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,8 @@ defmodule LivebookWeb.SessionLive do
validity_status: info.validity_status,
evaluation_status: info.evaluation_status,
evaluation_time_ms: info.evaluation_time_ms,
number_of_evaluations: info.number_of_evaluations
number_of_evaluations: info.number_of_evaluations,
reevaluate_automatically: cell.reevaluate_automatically
}
end

Expand Down
26 changes: 18 additions & 8 deletions lib/livebook_web/live/session_live/cell_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,24 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<div class="mb-1 flex items-center justify-between">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions" data-primary>
<%= if @cell_view.evaluation_status == :ready do %>
<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_view.id}>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
<%= if(@cell_view.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
</span>
</button>
<%= if @cell_view.validity_status != :fresh and @cell_view.reevaluate_automatically do %>
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id),
class: "text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center" do %>
<.remix_icon icon="check-line" class="text-xl" />
<span class="text-sm font-medium">
Reevaluates automatically
</span>
<% end %>
<% else %>
<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_view.id}>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
<%= if(@cell_view.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
</span>
</button>
<% end %>
<% else %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
socket
|> assign(assigns)
|> assign_new(:disable_formatting, fn -> cell.disable_formatting end)
|> assign_new(:reevaluate_automatically, fn -> cell.reevaluate_automatically end)

{:ok, socket}
end
Expand All @@ -29,6 +30,12 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
label="Disable code formatting (when saving to file)"
checked={@disable_formatting} />
</div>
<div class="w-full flex-col space-y-6 mt-4">
<.switch_checkbox
name="reevaluate_automatically"
label="Reevaluate automatically"
checked={@reevaluate_automatically} />
</div>
<div class="mt-8 flex justify-end space-x-2">
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
<button class="button button-blue" type="submit">
Expand All @@ -41,11 +48,20 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
end

@impl true
def handle_event("save", %{"disable_formatting" => disable_formatting}, socket) do
def handle_event(
"save",
%{
"disable_formatting" => disable_formatting,
"reevaluate_automatically" => reevaluate_automatically
},
socket
) do
disable_formatting = disable_formatting == "true"
reevaluate_automatically = reevaluate_automatically == "true"

Session.set_cell_attributes(socket.assigns.session.pid, socket.assigns.cell.id, %{
disable_formatting: disable_formatting
disable_formatting: disable_formatting,
reevaluate_automatically: reevaluate_automatically
})

{:noreply, push_patch(socket, to: socket.assigns.return_to)}
Expand Down
3 changes: 2 additions & 1 deletion test/livebook/live_markdown/export_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
%{
Notebook.Cell.new(:elixir)
| disable_formatting: true,
reevaluate_automatically: true,
source: """
Enum.to_list(1..10)\
"""
Expand Down Expand Up @@ -96,7 +97,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do

$x_{i} + y_{i}$

<!-- livebook:{"disable_formatting":true} -->
<!-- livebook:{"disable_formatting":true,"reevaluate_automatically":true} -->

```elixir
Enum.to_list(1..10)
Expand Down
3 changes: 2 additions & 1 deletion test/livebook/live_markdown/import_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do

$x_{i} + y_{i}$

<!-- livebook:{"disable_formatting": true} -->
<!-- livebook:{"disable_formatting":true,"reevaluate_automatically":true} -->

```elixir
Enum.to_list(1..10)
Expand Down Expand Up @@ -69,6 +69,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
},
%Cell.Elixir{
disable_formatting: true,
reevaluate_automatically: true,
source: """
Enum.to_list(1..10)\
"""
Expand Down
88 changes: 86 additions & 2 deletions test/livebook/session/data_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,29 @@ defmodule Livebook.Session.DataTest do
}, _actions} = Data.apply_operation(data, operation)
end

test "marks child automatically reevaluating cells for evaluation" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_cell_attributes, self(), "c2", %{reevaluate_automatically: true}},
# Evaluate both cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:queue_cell_evaluation, self(), "c2"},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
])

operation = {:delete_cell, self(), "c1"}

assert {:ok,
%{
cell_infos: %{"c2" => %{evaluation_status: :evaluating}}
}, _actions} = Data.apply_operation(data, operation)
end

test "deleting a markdown cell does not change child cell validity" do
data =
data_after_operations!([
Expand Down Expand Up @@ -2073,6 +2096,37 @@ defmodule Livebook.Session.DataTest do
}, []} = Data.apply_operation(data, operation)
end

test "marks child automatically reevaluating cells for evaluation" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
{:set_cell_attributes, self(), "c3", %{reevaluate_automatically: true}},
# Evaluate all cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:queue_cell_evaluation, self(), "c2"},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:queue_cell_evaluation, self(), "c3"},
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
# Queue the first cell again
{:queue_cell_evaluation, self(), "c1"}
])

operation = {:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}

assert {:ok,
%{
cell_infos: %{
"c2" => %{evaluation_status: :evaluating},
"c3" => %{evaluation_status: :queued}
}
}, _actions} = Data.apply_operation(data, operation)
end

test "adds evaluation time to the response" do
data =
data_after_operations!([
Expand Down Expand Up @@ -2838,13 +2892,15 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"}
])

attrs = %{disable_formatting: true}
attrs = %{disable_formatting: true, reevaluate_automatically: true}
operation = {:set_cell_attributes, self(), "c1", attrs}

assert {:ok,
%{
notebook: %{
sections: [%{cells: [%{disable_formatting: true}]}]
sections: [
%{cells: [%{disable_formatting: true, reevaluate_automatically: true}]}
]
}
}, _} = Data.apply_operation(data, operation)
end
Expand Down Expand Up @@ -2944,6 +3000,34 @@ defmodule Livebook.Session.DataTest do
}
}, _} = Data.apply_operation(data, operation)
end

test "setting reevaluate_automatically on stale cell marks it for evaluation" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
{:queue_cell_evaluation, self(), "c2"},
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
])

attrs = %{reevaluate_automatically: true}
operation = {:set_cell_attributes, self(), "c2", attrs}

assert {:ok,
%{
cell_infos: %{
"c1" => %{evaluation_status: :ready},
"c2" => %{evaluation_status: :evaluating}
}
}, _} = Data.apply_operation(data, operation)
end
end

describe "apply_operation/2 given :set_runtime" do
Expand Down