mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 04:54:29 +08:00
* Set up editor and client side delta handling * Synchronize deltas on the server * Document the client code, add more tests * Implement delta on the server, use compact representation when transmitting changes * Simplify transformation implementation and add documentation * Add session and data tests * Add more delta tests * Clean up evaluator tests wait timeout
109 lines
4.4 KiB
Elixir
109 lines
4.4 KiB
Elixir
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
|