mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-17 21:50:25 +08:00
Fix smart cell indicator when source changes on start (#1851)
This commit is contained in:
parent
744406d1d8
commit
4d46b03cc8
6 changed files with 63 additions and 34 deletions
|
|
@ -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 == []
|
||||||
|
end
|
||||||
|
|
||||||
_ ->
|
@doc """
|
||||||
delta
|
Returns data operations in the order in which they apply.
|
||||||
end
|
"""
|
||||||
|
@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
|
||||||
list
|
ops =
|
||||||
|> Enum.map(&Operation.from_compressed/1)
|
list
|
||||||
|> new()
|
|> Enum.map(&Operation.from_compressed/1)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|
||||||
|
%Delta{ops: ops}
|
||||||
end
|
end
|
||||||
|
|
||||||
defdelegate transform(left, right, priority), to: Transformation
|
defdelegate transform(left, right, priority), to: Transformation
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue