Packages
livebook
0.15.3
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/text/delta/transformation.ex
defmodule Livebook.Text.Delta.Transformation do
# Implementation of the Operational Transformation algorithm 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, that is:
#
# 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, for example, 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.Text.Delta
alias Livebook.Text.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(Delta.operations(left), Delta.operations(right), 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
@doc """
Transforms `index` against `delta`.
The primary usage of this function is to transform text selections.
This transformation maps the position into a matching location in
any text that this delta would be applied to.
Note that OT guarantees documents to converge, however transforming
positions does not provide the same guarantee. In practice, convergence
is achieved most of the time, except certain edge cases where inserts
and deletions happen around the transformed position. For more context
see [this writeup](https://marijnhaverbeke.nl/blog/collaborative-editing-cm.html#position-mapping).
In the implementation, one decision we need to make is whether the
position should shift when an insert happens at that position. Since
either way we do not get convergence in all cases, we can choose any
behaviour and we choose no to shift the position. With this variant
replacements adjacent to the transformed position do not move the
position across the replacement, which is a more intuitive behaviour.
"""
@spec transform_position(Delta.t(), non_neg_integer()) :: non_neg_integer()
def transform_position(delta, index) do
do_transform_position(Delta.operations(delta), index, 0)
end
defp do_transform_position([], index, _offset), do: index
defp do_transform_position(_ops, index, offset) when offset >= index, do: index
defp do_transform_position([{:delete, length} | ops], index, offset) do
index = index - min(length, index - offset)
do_transform_position(ops, index, offset)
end
defp do_transform_position([{:insert, _} = op | ops], index, offset) do
length = Operation.length(op)
do_transform_position(ops, index + length, offset + length)
end
defp do_transform_position([{:retain, length} | ops], index, offset) do
do_transform_position(ops, index, offset + length)
end
end