Fix smart cell indicator when source changes on start (#1851)

This commit is contained in:
Jonatan Kłosko 2023-04-08 09:49:34 +01:00 committed by GitHub
parent 744406d1d8
commit 4d46b03cc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 34 deletions

View file

@ -24,6 +24,11 @@ defmodule Livebook.Delta do
alias Livebook.Delta alias Livebook.Delta
alias Livebook.Delta.{Operation, Transformation} alias Livebook.Delta.{Operation, Transformation}
@typedoc """
Delta carries a list of consecutive operations.
Note that we keep the operations in reversed order for efficiency.
"""
@type t :: %Delta{ops: list(Operation.t())} @type t :: %Delta{ops: list(Operation.t())}
@doc """ @doc """
@ -73,12 +78,7 @@ defmodule Livebook.Delta do
""" """
@spec append(t(), Operation.t()) :: t() @spec append(t(), Operation.t()) :: t()
def append(delta, op) do def append(delta, op) do
Map.update!(delta, :ops, fn ops -> Map.update!(delta, :ops, &compact(&1, op))
ops
|> Enum.reverse()
|> compact(op)
|> Enum.reverse()
end)
end end
defp compact(ops, {:insert, ""}), do: ops defp compact(ops, {:insert, ""}), do: ops
@ -110,18 +110,23 @@ defmodule Livebook.Delta do
Removes trailing retain operations from the given delta. Removes trailing retain operations from the given delta.
""" """
@spec trim(t()) :: t() @spec trim(t()) :: t()
def trim(%Delta{ops: []} = delta), do: delta def trim(%Delta{ops: [{:retain, _} | ops]} = delta), do: %{delta | ops: ops}
def trim(delta), do: delta
def trim(delta) do @doc """
case List.last(delta.ops) do Checks if the delta has no changes.
{:retain, _} -> """
Map.update!(delta, :ops, fn ops -> @spec empty?(t()) :: boolean()
ops |> Enum.reverse() |> tl() |> Enum.reverse() def empty?(delta) do
end) trim(delta).ops == []
_ ->
delta
end end
@doc """
Returns data operations in the order in which they apply.
"""
@spec operations(t()) :: list(Operation.t())
def operations(delta) do
Enum.reverse(delta.ops)
end end
@doc """ @doc """
@ -130,14 +135,16 @@ defmodule Livebook.Delta do
## Examples ## Examples
iex> delta = %Livebook.Delta{ops: [retain: 2, insert: "hey", delete: 3]} iex> delta = Delta.new([retain: 2, insert: "hey", delete: 3])
iex> Livebook.Delta.to_compressed(delta) iex> Livebook.Delta.to_compressed(delta)
[2, "hey", -3] [2, "hey", -3]
""" """
@spec to_compressed(t()) :: list(Operation.compressed_t()) @spec to_compressed(t()) :: list(Operation.compressed_t())
def to_compressed(delta) do def to_compressed(delta) do
Enum.map(delta.ops, &Operation.to_compressed/1) delta.ops
|> Enum.reverse()
|> Enum.map(&Operation.to_compressed/1)
end end
@doc """ @doc """
@ -145,15 +152,19 @@ defmodule Livebook.Delta do
## Examples ## Examples
iex> Livebook.Delta.from_compressed([2, "hey", -3]) iex> delta = Livebook.Delta.from_compressed([2, "hey", -3])
%Livebook.Delta{ops: [retain: 2, insert: "hey", delete: 3]} iex> Livebook.Delta.operations(delta)
[retain: 2, insert: "hey", delete: 3]
""" """
@spec from_compressed(list(Operation.compressed_t())) :: t() @spec from_compressed(list(Operation.compressed_t())) :: t()
def from_compressed(list) do def from_compressed(list) do
ops =
list list
|> Enum.map(&Operation.from_compressed/1) |> Enum.map(&Operation.from_compressed/1)
|> new() |> Enum.reverse()
%Delta{ops: ops}
end end
defdelegate transform(left, right, priority), to: Transformation defdelegate transform(left, right, priority), to: Transformation

View file

@ -36,7 +36,7 @@ defmodule Livebook.Delta.Transformation do
""" """
@spec transform(Delta.t(), Delta.t(), priority()) :: Delta.t() @spec transform(Delta.t(), Delta.t(), priority()) :: Delta.t()
def transform(left, right, priority) do def transform(left, right, priority) do
do_transform(left.ops, right.ops, priority, Delta.new()) do_transform(Delta.operations(left), Delta.operations(right), priority, Delta.new())
|> Delta.trim() |> Delta.trim()
end end

View file

@ -20,7 +20,8 @@ defmodule Livebook.JSInterop do
def apply_delta_to_string(delta, string) do def apply_delta_to_string(delta, string) do
code_units = string_to_utf16_code_units(string) code_units = string_to_utf16_code_units(string)
delta.ops delta
|> Delta.operations()
|> apply_to_code_units(code_units) |> apply_to_code_units(code_units)
|> utf16_code_units_to_string() |> utf16_code_units_to_string()
end end

View file

@ -1858,6 +1858,18 @@ defmodule Livebook.Session do
state state
end end
defp after_operation(
state,
_prev_state,
{:smart_cell_started, _client_id, cell_id, delta, _chunks, _js_view, _editor}
) do
unless Delta.empty?(delta) do
hydrate_cell_source_digest(state, cell_id, :primary)
end
state
end
defp after_operation( defp after_operation(
state, state,
_prev_state, _prev_state,

View file

@ -8,39 +8,39 @@ defmodule Livebook.DeltaTest do
describe "append/2" do describe "append/2" do
test "ignores empty operations" do test "ignores empty operations" do
assert Delta.append(Delta.new(), {:insert, ""}) == %Delta{ops: []} assert Delta.new() |> Delta.append({:insert, ""}) |> Delta.operations() == []
assert Delta.append(Delta.new(), {:retain, 0}) == %Delta{ops: []} assert Delta.new() |> Delta.append({:retain, 0}) |> Delta.operations() == []
assert Delta.append(Delta.new(), {:delete, 0}) == %Delta{ops: []} assert Delta.new() |> Delta.append({:delete, 0}) |> Delta.operations() == []
end end
test "given empty delta just appends the operation" do test "given empty delta just appends the operation" do
delta = Delta.new() delta = Delta.new()
op = Operation.insert("cats") op = Operation.insert("cats")
assert Delta.append(delta, op) == %Delta{ops: [insert: "cats"]} assert delta |> Delta.append(op) |> Delta.operations() == [insert: "cats"]
end end
test "merges consecutive inserts" do test "merges consecutive inserts" do
delta = Delta.new() |> Delta.insert("cats") delta = Delta.new() |> Delta.insert("cats")
op = Operation.insert(" rule") op = Operation.insert(" rule")
assert Delta.append(delta, op) == %Delta{ops: [insert: "cats rule"]} assert delta |> Delta.append(op) |> Delta.operations() == [insert: "cats rule"]
end end
test "merges consecutive retains" do test "merges consecutive retains" do
delta = Delta.new() |> Delta.retain(2) delta = Delta.new() |> Delta.retain(2)
op = Operation.retain(2) op = Operation.retain(2)
assert Delta.append(delta, op) == %Delta{ops: [retain: 4]} assert delta |> Delta.append(op) |> Delta.operations() == [retain: 4]
end end
test "merges consecutive delete" do test "merges consecutive delete" do
delta = Delta.new() |> Delta.delete(2) delta = Delta.new() |> Delta.delete(2)
op = Operation.delete(2) op = Operation.delete(2)
assert Delta.append(delta, op) == %Delta{ops: [delete: 4]} assert delta |> Delta.append(op) |> Delta.operations() == [delete: 4]
end end
test "given insert appended after delete, swaps the operations" do test "given insert appended after delete, swaps the operations" do
delta = Delta.new() |> Delta.delete(2) delta = Delta.new() |> Delta.delete(2)
op = Operation.insert("cats") op = Operation.insert("cats")
assert Delta.append(delta, op) == %Delta{ops: [insert: "cats", delete: 2]} assert delta |> Delta.append(op) |> Delta.operations() == [insert: "cats", delete: 2]
end end
end end
end end

View file

@ -944,7 +944,7 @@ defmodule Livebook.SessionTest do
end end
test "pings the smart cell before evaluation to await all incoming messages" do test "pings the smart cell before evaluation to await all incoming messages" do
smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: "1"} smart_cell = %{Notebook.Cell.new(:smart) | kind: "text", source: ""}
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]} notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
session = start_session(notebook: notebook) session = start_session(notebook: notebook)
@ -964,6 +964,11 @@ defmodule Livebook.SessionTest do
%{source: "1", js_view: %{pid: self(), ref: "ref"}, editor: nil}} %{source: "1", js_view: %{pid: self(), ref: "ref"}, editor: nil}}
) )
# Sends digest to clients when the source is different
cell_id = smart_cell.id
new_digest = :erlang.md5("1")
assert_receive {:hydrate_cell_source_digest, ^cell_id, :primary, ^new_digest}
Session.queue_cell_evaluation(session.pid, smart_cell.id) Session.queue_cell_evaluation(session.pid, smart_cell.id)
send(session.pid, {:runtime_evaluation_response, "setup", {:ok, ""}, @eval_meta}) send(session.pid, {:runtime_evaluation_response, "setup", {:ok, ""}, @eval_meta})