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.{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())}
@doc """
@ -73,12 +78,7 @@ defmodule Livebook.Delta do
"""
@spec append(t(), Operation.t()) :: t()
def append(delta, op) do
Map.update!(delta, :ops, fn ops ->
ops
|> Enum.reverse()
|> compact(op)
|> Enum.reverse()
end)
Map.update!(delta, :ops, &compact(&1, op))
end
defp compact(ops, {:insert, ""}), do: ops
@ -110,18 +110,23 @@ defmodule Livebook.Delta do
Removes trailing retain operations from the given delta.
"""
@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
case List.last(delta.ops) do
{:retain, _} ->
Map.update!(delta, :ops, fn ops ->
ops |> Enum.reverse() |> tl() |> Enum.reverse()
end)
@doc """
Checks if the delta has no changes.
"""
@spec empty?(t()) :: boolean()
def empty?(delta) do
trim(delta).ops == []
end
_ ->
delta
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
@doc """
@ -130,14 +135,16 @@ defmodule Livebook.Delta do
## 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)
[2, "hey", -3]
"""
@spec to_compressed(t()) :: list(Operation.compressed_t())
def to_compressed(delta) do
Enum.map(delta.ops, &Operation.to_compressed/1)
delta.ops
|> Enum.reverse()
|> Enum.map(&Operation.to_compressed/1)
end
@doc """
@ -145,15 +152,19 @@ defmodule Livebook.Delta do
## Examples
iex> Livebook.Delta.from_compressed([2, "hey", -3])
%Livebook.Delta{ops: [retain: 2, insert: "hey", delete: 3]}
iex> delta = Livebook.Delta.from_compressed([2, "hey", -3])
iex> Livebook.Delta.operations(delta)
[retain: 2, insert: "hey", delete: 3]
"""
@spec from_compressed(list(Operation.compressed_t())) :: t()
def from_compressed(list) do
list
|> Enum.map(&Operation.from_compressed/1)
|> new()
ops =
list
|> Enum.map(&Operation.from_compressed/1)
|> Enum.reverse()
%Delta{ops: ops}
end
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()
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()
end

View file

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

View file

@ -1858,6 +1858,18 @@ defmodule Livebook.Session do
state
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(
state,
_prev_state,

View file

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

View file

@ -944,7 +944,7 @@ defmodule Livebook.SessionTest do
end
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]}]}
session = start_session(notebook: notebook)
@ -964,6 +964,11 @@ defmodule Livebook.SessionTest do
%{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)
send(session.pid, {:runtime_evaluation_response, "setup", {:ok, ""}, @eval_meta})