Packages
livebook
0.4.1
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/delta/transformation.ex
defmodule Livebook.Delta.Transformation do
@moduledoc false
# Implementation of the Operational Transformation concept for deltas.
#
# The transformation allows for conflict resolution in concurrent editing.
# Consider delta `Oa` and delta `Ob` that occurred at the same time against the same text state `S`.
# The resulting new text states are `S ∘ Oa` and `S ∘ Ob` respectively.
# Now for each text state we would like to apply the other delta,
# so that both texts converge to the same state, i.e.:
#
# `S ∘ Oa ∘ transform(Oa, Ob) = S ∘ Ob ∘ transform(Ob, Oa)`
#
# That's the high-level idea.
# To actually achieve convergence we have to introduce a linear order of operations.
# This way we can resolve conflicts - e.g. if two deltas insert a text at the same
# position, we have to unambiguously determine which takes precedence.
# A reasonable solution is to have a server process where all
# the clients send deltas, as it naturally imposes the necessary ordering.
alias Livebook.Delta
alias Livebook.Delta.Operation
@type priority :: :left | :right
@doc """
Transforms `right` delta against the `left` delta.
Assuming both deltas represent changes applied to the same
document state, this operation results in modified `right` delta
that represents effectively the same changes (preserved intent),
but works on the document with `left` delta already applied.
The `priority` indicates which delta is considered to have
happened first and is used for conflict resolution.
"""
@spec transform(Delta.t(), Delta.t(), priority()) :: Delta.t()
def transform(left, right, priority) do
do_transform(left.ops, right.ops, priority, Delta.new())
|> Delta.trim()
end
defp do_transform(_ops_a, [] = _ops_b, _priority, result) do
result
end
defp do_transform([], ops_b, _priority, result) do
Enum.reduce(ops_b, result, &Delta.append(&2, &1))
end
defp do_transform([{:insert, _} | _] = ops_a, [{:insert, _} | _] = ops_b, :left, result) do
[ins_a | remainder_a] = ops_a
retain = make_retain(ins_a)
do_transform(remainder_a, ops_b, :left, Delta.append(result, retain))
end
defp do_transform([{:insert, _} | _] = ops_a, [{:insert, _} | _] = ops_b, :right, result) do
[ins_b | remainder_b] = ops_b
do_transform(ops_a, remainder_b, :right, Delta.append(result, ins_b))
end
defp do_transform([{:insert, _} | _] = ops_a, [{:retain, _} | _] = ops_b, priority, result) do
[ins_a | remainder_a] = ops_a
retain = make_retain(ins_a)
do_transform(remainder_a, ops_b, priority, Delta.append(result, retain))
end
defp do_transform([{:insert, _} | _] = ops_a, [{:delete, _} | _] = ops_b, priority, result) do
[ins_a | remainder_a] = ops_a
retain = make_retain(ins_a)
do_transform(remainder_a, ops_b, priority, Delta.append(result, retain))
end
defp do_transform([{:delete, _} | _] = ops_a, [{:insert, _} | _] = ops_b, priority, result) do
[ins_b | remainder_b] = ops_b
do_transform(ops_a, remainder_b, priority, Delta.append(result, ins_b))
end
defp do_transform([{:delete, _} | _] = ops_a, [{:retain, _} | _] = ops_b, priority, result) do
{[_del_a | remainder_a], [_ret_b | remainder_b]} = Operation.align_heads(ops_a, ops_b)
do_transform(remainder_a, remainder_b, priority, result)
end
defp do_transform([{:delete, _} | _] = ops_a, [{:delete, _} | _] = ops_b, priority, result) do
{[_del_a | remainder_a], [_del_b | remainder_b]} = Operation.align_heads(ops_a, ops_b)
do_transform(remainder_a, remainder_b, priority, result)
end
defp do_transform([{:retain, _} | _] = ops_a, [{:insert, _} | _] = ops_b, priority, result) do
[ins_b | remainder_b] = ops_b
do_transform(ops_a, remainder_b, priority, Delta.append(result, ins_b))
end
defp do_transform([{:retain, _} | _] = ops_a, [{:retain, _} | _] = ops_b, priority, result) do
{[ret | remainder_a], [ret | remainder_b]} = Operation.align_heads(ops_a, ops_b)
do_transform(remainder_a, remainder_b, priority, Delta.append(result, ret))
end
defp do_transform([{:retain, _} | _] = ops_a, [{:delete, _} | _] = ops_b, priority, result) do
{[_ret_a | remainder_a], [del_b | remainder_b]} = Operation.align_heads(ops_a, ops_b)
do_transform(remainder_a, remainder_b, priority, Delta.append(result, del_b))
end
defp make_retain(op) do
op
|> Operation.length()
|> Operation.retain()
end
end