From 6c7c74e1d15286fea8d6f3bc5b163e505d523557 Mon Sep 17 00:00:00 2001 From: point Date: Mon, 20 May 2024 18:44:40 +0300 Subject: [PATCH] Type Text WIP --- .../{content_deleted.ex => deleted.ex} | 0 lib/y/content/format.ex | 7 + lib/y/doc.ex | 36 ++ lib/y/type/array/array_tree.ex | 301 +------------ lib/y/type/general_tree.ex | 342 ++++++++++++++ lib/y/type/text.ex | 201 +++++++++ lib/y/type/text/text_position.ex | 8 + lib/y/type/text/tree.ex | 418 ++++++++++++++++++ test/array_test.exs | 23 + test/text_test.exs | 152 +++++++ 10 files changed, 1192 insertions(+), 296 deletions(-) rename lib/y/content/{content_deleted.ex => deleted.ex} (100%) create mode 100644 lib/y/content/format.ex create mode 100644 lib/y/type/general_tree.ex create mode 100644 lib/y/type/text.ex create mode 100644 lib/y/type/text/text_position.ex create mode 100644 lib/y/type/text/tree.ex create mode 100644 test/text_test.exs diff --git a/lib/y/content/content_deleted.ex b/lib/y/content/deleted.ex similarity index 100% rename from lib/y/content/content_deleted.ex rename to lib/y/content/deleted.ex diff --git a/lib/y/content/format.ex b/lib/y/content/format.ex new file mode 100644 index 0000000..7d7537b --- /dev/null +++ b/lib/y/content/format.ex @@ -0,0 +1,7 @@ +defmodule Y.Content.Format do + alias __MODULE__ + defstruct [:key, :value] + + def new(k, v), do: %Format{key: k, value: v} + def to_map(%Format{key: key, value: value}), do: %{key => value} +end diff --git a/lib/y/doc.ex b/lib/y/doc.ex index 09a754c..93ef93d 100644 --- a/lib/y/doc.ex +++ b/lib/y/doc.ex @@ -104,6 +104,19 @@ defmodule Y.Doc do GenServer.call(doc_name, {:get_map, map_name}) end + def get_text(transaction, text_name \\ UUID.uuid4()) + + def get_text(%Transaction{doc: doc} = transaction, text_name) do + case do_get_text(doc, text_name) do + {:ok, text, doc} -> {:ok, text, %{transaction | doc: doc}} + {:error, _} = err -> err + end + end + + def get_text(doc_name, text_name) do + GenServer.call(doc_name, {:get_text, text_name}) + end + def transact(doc_name, f, opts \\ []) do GenServer.call(doc_name, {:transact, f, opts}) end @@ -309,6 +322,13 @@ defmodule Y.Doc do end end + def handle_call({:get_text, name}, _, doc) do + case do_get_text(doc, name) do + {:ok, text, doc} -> {:reply, {:ok, text}, doc} + {:error, _} = err -> {:reply, err, doc} + end + end + def handle_call( {:transact, f, opts}, _, @@ -418,6 +438,22 @@ defmodule Y.Doc do {:ok, map, %Doc{doc | share: Map.put_new(doc.share, name, map)}} end + defp do_get_text(%Doc{share: share} = doc, name) when is_map_key(share, name) do + case share[name] do + %Unknown{} = u -> + map = Y.Type.Text.from_unknown(u) + {:ok, map, %{doc | share: Map.replace(share, name, map)}} + + _ -> + {:error, "Type with the name #{name} has already been added"} + end + end + + defp do_get_text(%Doc{} = doc, name) do + text = Y.Type.Text.new(doc, name) + {:ok, text, %Doc{doc | share: Map.put_new(doc.share, name, text)}} + end + defp do_find_parent(type, child_item) do if Type.impl_for(type) do type diff --git a/lib/y/type/array/array_tree.ex b/lib/y/type/array/array_tree.ex index 19824a1..8031df0 100644 --- a/lib/y/type/array/array_tree.ex +++ b/lib/y/type/array/array_tree.ex @@ -1,64 +1,24 @@ defmodule Y.Type.Array.ArrayTree do alias __MODULE__ - alias FingerTree.EmptyTree - # alias FingerTree.Protocols.Conjable alias Y.Item alias Y.ID - defstruct [:ft] @type t() :: %ArrayTree{ft: FingerTree.t()} + defstruct [:ft] defmodule Meter do @enforce_keys [:highest_clocks, :highest_clocks_with_length, :len] defstruct highest_clocks: %{}, highest_clocks_with_length: %{}, len: 0 end + use Y.Type.GeneralTree, mod: ArrayTree + def new do %ArrayTree{ft: FingerTree.finger_tree(meter_object())} end - def highest_clock_with_length(%ArrayTree{ft: tree}, nil) do - %Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) - - cl - |> Map.values() - |> Enum.max(fn -> 0 end) - end - - def highest_clock_with_length(%ArrayTree{ft: tree}, client_id) do - %Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) - - case Map.fetch(cl, client_id) do - {:ok, clock_len} -> clock_len - _ -> 0 - end - end - - def highest_clock(%ArrayTree{ft: tree}, nil) do - %Meter{highest_clocks: c} = FingerTree.measure(tree) - - c - |> Map.values() - |> Enum.max(fn -> 0 end) - end - - def highest_clock(%ArrayTree{ft: tree}, client_id) do - %Meter{highest_clocks: c} = FingerTree.measure(tree) - - case Map.fetch(c, client_id) do - {:ok, clock} -> clock - _ -> 0 - end - end - - def highest_clock_with_length_by_client_id(%ArrayTree{ft: tree}) do - %Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) - cl - end - - def highest_clock_by_client_id(%ArrayTree{ft: tree}) do - %Meter{highest_clocks: c} = FingerTree.measure(tree) - c + def new(meter_object) do + %ArrayTree{ft: FingerTree.finger_tree(meter_object)} end def put(%ArrayTree{ft: %EmptyTree{} = tree} = array_tree, _index, %Item{} = item) do @@ -149,257 +109,6 @@ defmodule Y.Type.Array.ArrayTree do {:ok, %{array_tree | ft: new_tree}} end - def cons!(%ArrayTree{ft: tree} = array_tree, value), - do: %{array_tree | ft: FingerTree.cons(tree, value)} - - def conj!(%ArrayTree{ft: tree} = array_tree, value), - do: %{array_tree | ft: FingerTree.conj(tree, value)} - - @spec empty?(t()) :: boolean() - def empty?(%ArrayTree{ft: tree}), do: FingerTree.empty?(tree) - - def to_list(%ArrayTree{ft: tree}), do: FingerTree.to_list(tree) - - def find(tree, id, default \\ nil) - - def find(%ArrayTree{ft: %EmptyTree{}}, _id, default), do: default - - def find(%ArrayTree{ft: tree}, id, default) do - {l, v, _} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - case Map.fetch(clocks, id.client) do - {:ok, c} -> c >= id.clock - _ -> false - end - end) - - prev = FingerTree.last(l) - - cond do - v.id.clock == id.clock -> - v - - id.clock > v.id.clock && id.clock <= v.id.clock + Item.content_length(v) -> - v - - prev && id.clock > prev.id.clock && id.clock <= prev.id.clock + Item.content_length(prev) -> - prev - - :otherwise -> - default - end - end - - def replace(%ArrayTree{ft: tree} = array_tree, item, with_items) when is_list(with_items) do - {l, v, r} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - Map.fetch!(clocks, item.id.client) >= item.id.clock - end) - - if v == item do - tree = - with_items - |> Enum.flat_map(&Item.explode/1) - |> Enum.reduce(l, fn item, tree -> FingerTree.conj(tree, item) end) - |> FingerTree.append(r) - - {:ok, %{array_tree | ft: tree}} - else - {:error, "Item not found"} - end - end - - def transform(%ArrayTree{ft: tree} = array_tree, %Item{} = starting_item, acc \\ nil, fun) - when is_function(fun, 2) do - {l, v, r} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - case Map.fetch(clocks, starting_item.id.client) do - {:ok, c} -> c >= starting_item.id.clock - _ -> false - end - end) - - if v == starting_item do - tree = - case do_transform(v, l, r, acc, fun) do - {left_tree, nil} -> - left_tree - - {left_tree, right_tree} -> - FingerTree.append(left_tree, right_tree) - end - - {:ok, %{array_tree | ft: tree}} - else - {:error, "Item not found"} - end - end - - defp do_transform(nil, left_tree, right_tree, _acc, _fun), do: {left_tree, right_tree} - - defp do_transform(_, left_tree, nil, _acc, _fun), - do: {left_tree, nil} - - defp do_transform(%Item{} = item, left_tree, right_tree, acc, fun) do - case fun.(item, acc) do - {%Item{} = new_item, new_acc} -> - do_transform( - FingerTree.first(right_tree), - FingerTree.conj(left_tree, new_item), - FingerTree.rest(right_tree), - new_acc, - fun - ) - - %Item{} = new_item -> - do_transform( - FingerTree.first(right_tree), - FingerTree.conj(left_tree, new_item), - FingerTree.rest(right_tree), - acc, - fun - ) - - nil -> - {FingerTree.conj(left_tree, item), right_tree} - end - end - - def between(%ArrayTree{ft: tree}, %ID{} = left, %ID{} = right) do - {_, v, r} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - case Map.fetch(clocks, left.client) do - {:ok, c} -> c >= left.clock - _ -> false - end - end) - - if v.id == left do - do_between(r, right, [v]) |> Enum.reverse() - else - [] - end - end - - def add_after(%ArrayTree{ft: tree} = at, %Item{} = after_item, %Item{} = item) do - {l, v, r} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - case Map.fetch(clocks, after_item.id.client) do - {:ok, c} -> c >= after_item.id.clock - _ -> false - end - end) - - if v == after_item do - {:ok, - %{ - at - | ft: - l - |> FingerTree.conj(v) - |> then(fn tree -> - Enum.reduce(Item.explode(item), tree, fn item, tree -> - FingerTree.conj(tree, item) - end) - end) - |> FingerTree.append(r) - }} - else - {:error, "Item not found"} - end - end - - def add_before(%ArrayTree{ft: %EmptyTree{} = ft} = at, _, %Item{} = item), - do: - {:ok, - %{ - at - | ft: Enum.reduce(Item.explode(item), ft, fn item, ft -> FingerTree.conj(ft, item) end) - }} - - def add_before(%ArrayTree{ft: tree} = at, %Item{} = before_item, %Item{} = item) do - {l, v, r} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - Map.fetch!(clocks, before_item.id.client) >= before_item.id.clock - end) - - if v == before_item do - {:ok, - %{ - at - | ft: - l - |> then(fn tree -> - Enum.reduce(Item.explode(item), tree, fn item, tree -> - FingerTree.conj(tree, item) - end) - end) - |> FingerTree.conj(v) - |> FingerTree.append(r) - }} - else - {:error, "Item not found"} - end - end - - def next(%ArrayTree{ft: tree}, %Item{} = item) do - {_, v, r} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - case Map.fetch(clocks, item.id.client) do - {:ok, c} -> c >= item.id.clock - _ -> false - end - end) - - if v == item, do: FingerTree.first(r) - end - - def prev(%ArrayTree{ft: tree}, %Item{} = item) do - {l, v, _} = - FingerTree.split(tree, fn %{highest_clocks: clocks} -> - Map.fetch!(clocks, item.id.client) >= item.id.clock - end) - - if v == item, do: FingerTree.last(l) - end - - def first(%ArrayTree{ft: tree}), do: FingerTree.first(tree) - def last(%ArrayTree{ft: tree}), do: FingerTree.last(tree) - def rest(%ArrayTree{ft: tree} = array_tree), do: %{array_tree | ft: FingerTree.rest(tree)} - def butlast(%ArrayTree{ft: tree} = array_tree), do: %{array_tree | ft: FingerTree.butlast(tree)} - - def length(%ArrayTree{ft: tree}) do - %Meter{len: len} = FingerTree.measure(tree) - len - end - - def at(%ArrayTree{ft: %EmptyTree{}}, _index), do: nil - - def at(%ArrayTree{ft: tree} = array_tree, index) do - if index > ArrayTree.length(array_tree) - 1 do - nil - else - {_, v, _} = - FingerTree.split(tree, fn %{len: len} -> - len > index - end) - - v - end - end - - defp do_between(%EmptyTree{}, _, acc), do: acc - - defp do_between(tree, right, acc) do - f = FingerTree.first(tree) - - if f.id == right do - [f | acc] - else - do_between(FingerTree.rest(tree), right, [f | acc]) - end - end - defp meter_object do FingerTree.MeterObject.new( fn %Item{id: id} = item -> diff --git a/lib/y/type/general_tree.ex b/lib/y/type/general_tree.ex new file mode 100644 index 0000000..45a1bf1 --- /dev/null +++ b/lib/y/type/general_tree.ex @@ -0,0 +1,342 @@ +defmodule Y.Type.GeneralTree do + defmacro __using__(opts) do + mod = Keyword.fetch!(opts, :mod) + + quote do + alias FingerTree.EmptyTree + alias Y.Item + alias Y.ID + + def highest_clock_with_length(%unquote(mod){ft: tree}, nil) do + %unquote(mod).Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) + + cl + |> Map.values() + |> Enum.max(fn -> 0 end) + end + + def highest_clock_with_length(%unquote(mod){ft: tree}, client_id) do + %unquote(mod).Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) + + case Map.fetch(cl, client_id) do + {:ok, clock_len} -> clock_len + _ -> 0 + end + end + + def highest_clock(%unquote(mod){ft: tree}, nil) do + %unquote(mod).Meter{highest_clocks: c} = FingerTree.measure(tree) + + c + |> Map.values() + |> Enum.max(fn -> 0 end) + end + + def highest_clock(%unquote(mod){ft: tree}, client_id) do + %unquote(mod).Meter{highest_clocks: c} = FingerTree.measure(tree) + + case Map.fetch(c, client_id) do + {:ok, clock} -> clock + _ -> 0 + end + end + + def highest_clock_with_length_by_client_id(%unquote(mod){ft: tree}) do + %unquote(mod).Meter{highest_clocks_with_length: cl} = FingerTree.measure(tree) + cl + end + + def highest_clock_by_client_id(%unquote(mod){ft: tree}) do + %unquote(mod).Meter{highest_clocks: c} = FingerTree.measure(tree) + c + end + + def cons!(%unquote(mod){ft: tree} = array_tree, value), + do: %{array_tree | ft: FingerTree.cons(tree, value)} + + def conj!(%unquote(mod){ft: tree} = array_tree, value), + do: %{array_tree | ft: FingerTree.conj(tree, value)} + + @spec empty?(t()) :: boolean() + def empty?(%unquote(mod){ft: tree}), do: FingerTree.empty?(tree) + + def to_list(%unquote(mod){ft: tree}), do: FingerTree.to_list(tree) + + def find(tree, id, default \\ nil) + + def find(%unquote(mod){ft: %EmptyTree{}}, _id, default), do: default + + def find(%unquote(mod){ft: tree}, id, default) do + {l, v, _} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + case Map.fetch(clocks, id.client) do + {:ok, c} -> c >= id.clock + _ -> false + end + end) + + prev = FingerTree.last(l) + + cond do + v.id.clock == id.clock -> + v + + id.clock > v.id.clock && id.clock <= v.id.clock + Item.content_length(v) -> + v + + prev && id.clock > prev.id.clock && + id.clock <= prev.id.clock + Item.content_length(prev) -> + prev + + :otherwise -> + default + end + end + + def replace(%unquote(mod){ft: tree} = array_tree, item, with_items) + when is_list(with_items) do + {l, v, r} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + Map.fetch!(clocks, item.id.client) >= item.id.clock + end) + + if v == item do + tree = + with_items + |> Enum.flat_map(&Item.explode/1) + |> Enum.reduce(l, fn item, tree -> FingerTree.conj(tree, item) end) + |> FingerTree.append(r) + + {:ok, %{array_tree | ft: tree}} + else + {:error, "Item not found"} + end + end + + def transform( + %unquote(mod){ft: tree} = array_tree, + %Item{} = starting_item, + acc \\ nil, + fun + ) + when is_function(fun, 2) do + {l, v, r} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + case Map.fetch(clocks, starting_item.id.client) do + {:ok, c} -> c >= starting_item.id.clock + _ -> false + end + end) + + if v == starting_item do + tree = + case do_transform(v, l, r, acc, fun) do + {left_tree, nil} -> + left_tree + + {left_tree, right_tree} -> + FingerTree.append(left_tree, right_tree) + end + + {:ok, %{array_tree | ft: tree}} + else + {:error, "Item not found"} + end + end + + defp do_transform(nil, left_tree, right_tree, _acc, _fun), do: {left_tree, right_tree} + + defp do_transform(_, left_tree, nil, _acc, _fun), + do: {left_tree, nil} + + defp do_transform(%Item{} = item, left_tree, right_tree, acc, fun) do + case fun.(item, acc) do + {%Item{} = new_item, new_acc} -> + do_transform( + FingerTree.first(right_tree), + FingerTree.conj(left_tree, new_item), + FingerTree.rest(right_tree), + new_acc, + fun + ) + + %Item{} = new_item -> + do_transform( + FingerTree.first(right_tree), + FingerTree.conj(left_tree, new_item), + FingerTree.rest(right_tree), + acc, + fun + ) + + nil -> + {FingerTree.conj(left_tree, item), right_tree} + end + end + + def between(%unquote(mod){ft: tree}, %ID{} = left, %ID{} = right) do + {_, v, r} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + case Map.fetch(clocks, left.client) do + {:ok, c} -> c >= left.clock + _ -> false + end + end) + + if v.id == left do + do_between(r, right, [v]) |> Enum.reverse() + else + [] + end + end + + defp do_between(%EmptyTree{}, _, acc), do: acc + + defp do_between(tree, right, acc) do + f = FingerTree.first(tree) + + if f.id == right do + [f | acc] + else + do_between(FingerTree.rest(tree), right, [f | acc]) + end + end + + def add_after(%unquote(mod){ft: tree} = at, %Item{} = after_item, %Item{} = item) do + {l, v, r} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + case Map.fetch(clocks, after_item.id.client) do + {:ok, c} -> c >= after_item.id.clock + _ -> false + end + end) + + if v == after_item do + {:ok, + %{ + at + | ft: + l + |> FingerTree.conj(v) + |> then(fn tree -> + Enum.reduce(Item.explode(item), tree, fn item, tree -> + FingerTree.conj(tree, item) + end) + end) + |> FingerTree.append(r) + }} + else + {:error, "Item not found"} + end + end + + def add_before(%unquote(mod){ft: %EmptyTree{} = ft} = at, _, %Item{} = item), + do: + {:ok, + %{ + at + | ft: + Enum.reduce(Item.explode(item), ft, fn item, ft -> FingerTree.conj(ft, item) end) + }} + + def add_before(%unquote(mod){ft: tree} = at, %Item{} = before_item, %Item{} = item) do + {l, v, r} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + Map.fetch!(clocks, before_item.id.client) >= before_item.id.clock + end) + + if v == before_item do + {:ok, + %{ + at + | ft: + l + |> then(fn tree -> + Enum.reduce(Item.explode(item), tree, fn item, tree -> + FingerTree.conj(tree, item) + end) + end) + |> FingerTree.conj(v) + |> FingerTree.append(r) + }} + else + {:error, "Item not found"} + end + end + + def next(%unquote(mod){ft: tree}, %Item{} = item) do + {_, v, r} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + case Map.fetch(clocks, item.id.client) do + {:ok, c} -> c >= item.id.clock + _ -> false + end + end) + + if v == item, do: FingerTree.first(r) + end + + def prev(%unquote(mod){ft: tree}, %Item{} = item) do + {l, v, _} = + FingerTree.split(tree, fn %{highest_clocks: clocks} -> + Map.fetch!(clocks, item.id.client) >= item.id.clock + end) + + if v == item, do: FingerTree.last(l) + end + + def first(%unquote(mod){ft: tree}), do: FingerTree.first(tree) + def last(%unquote(mod){ft: tree}), do: FingerTree.last(tree) + + def rest(%unquote(mod){ft: tree} = array_tree), + do: %{array_tree | ft: FingerTree.rest(tree)} + + def butlast(%unquote(mod){ft: tree} = array_tree), + do: %{array_tree | ft: FingerTree.butlast(tree)} + + def length(%unquote(mod){ft: tree}) do + %unquote(mod).Meter{len: len} = FingerTree.measure(tree) + len + end + + def at(%unquote(mod){ft: %EmptyTree{}}, _index), do: nil + + def at(%unquote(mod){ft: tree} = array_tree, index) do + if index > unquote(mod).length(array_tree) - 1 do + nil + else + {_, v, _} = + FingerTree.split(tree, fn %{len: len} -> + len > index + end) + + v + end + end + + defoverridable( + highest_clock_with_length: 2, + highest_clock: 2, + highest_clock_with_length_by_client_id: 1, + highest_clock_by_client_id: 1, + cons!: 2, + conj!: 2, + empty?: 1, + to_list: 1, + find: 3, + replace: 3, + transform: 4, + between: 3, + add_after: 3, + add_before: 3, + next: 2, + prev: 2, + first: 1, + last: 1, + rest: 1, + length: 1, + at: 2 + ) + end + end +end diff --git a/lib/y/type/text.ex b/lib/y/type/text.ex new file mode 100644 index 0000000..f0f718e --- /dev/null +++ b/lib/y/type/text.ex @@ -0,0 +1,201 @@ +defmodule Y.Type.Text do + alias __MODULE__ + alias Y.Doc + alias Y.Type + alias Y.Type.Text.Tree + alias Y.Item + alias Y.ID + alias Y.Transaction + + defstruct tree: nil, + doc_name: nil, + name: nil + + def new(%Doc{name: doc_name}, name \\ UUID.uuid4()) do + %Text{doc_name: doc_name, name: name, tree: Tree.new()} + end + + def insert( + %Text{tree: tree} = type_text, + %Transaction{} = transaction, + index, + text, + attributes \\ %{} + ) do + with last_clock = Doc.highest_clock_with_length(transaction), + %Tree{} = new_tree <- + Tree.insert( + tree, + index, + text, + attributes, + type_text.name, + transaction.doc.client_id, + last_clock + ), + new_type_text = %{type_text | tree: new_tree}, + {:ok, new_transaction} <- Transaction.update(transaction, new_type_text) do + {:ok, new_type_text, new_transaction} + end + end + + defdelegate to_list(array), to: Type + defdelegate to_list(array, opts), to: Type + + defimpl Type do + def highest_clock(%Text{tree: tree}, client), do: Tree.highest_clock(tree, client) + + def highest_clock_with_length(%Text{tree: tree}, client), + do: Tree.highest_clock_with_length(tree, client) + + def highest_clock_by_client_id(%Text{tree: tree}), + do: Tree.highest_clock_by_client_id(tree) + + def highest_clock_with_length_by_client_id(%Text{tree: tree}), + do: Tree.highest_clock_with_length_by_client_id(tree) + + def pack(%Text{tree: tree} = text) do + new_tree = + tree + |> Enum.reduce([], fn + e, [] -> + [e] + + e, [%Item{} = head | tail] = acc -> + if Item.mergeable?(head, e) do + [Item.merge!(head, e) | tail] + else + [e | acc] + end + end) + |> Enum.reverse() + |> Enum.into(Tree.new()) + + %{text | tree: new_tree} + end + + def to_list(%Text{tree: tree}, opts \\ []) do + as_items = Keyword.get(opts, :as_items, false) + with_deleted = Keyword.get(opts, :with_deleted, false) + + items = + if with_deleted do + Tree.to_list(tree) + else + Tree.to_list(tree) |> Enum.reject(& &1.deleted?) + end + + if as_items, do: items, else: items |> Enum.map(& &1.content) |> List.flatten() + end + + def find(%Text{tree: tree}, %ID{} = id, default) do + tree |> Tree.find(id, default) + end + + def unsafe_replace( + %Text{tree: tree} = text, + %Item{id: %ID{clock: item_clock}} = item, + with_items + ) + when is_list(with_items) do + [%{id: %ID{clock: f_clock}} | _] = with_items + + with_items_length = + Enum.reduce(with_items, 0, fn i, acc -> acc + Item.content_length(i) end) + + cond do + f_clock != item_clock -> + {:error, "Clocks diverge"} + + Item.content_length(item) != with_items_length -> + {:error, "Total content length of items != length of item to replace"} + + :otherwise -> + case Tree.replace(tree, item, with_items) do + {:ok, new_tree} -> {:ok, %{text | tree: new_tree}} + err -> err + end + end + end + + def between(%Text{tree: tree}, %ID{} = left, %ID{} = right) do + Tree.between(tree, left, right) + end + + def add_after(%Text{tree: tree} = text, %Item{} = after_item, %Item{} = item) do + case Tree.add_after(tree, after_item, item) do + {:ok, new_tree} -> {:ok, %{text | tree: new_tree}} + err -> err + end + end + + def add_before(%Text{tree: tree} = text, %Item{} = before_item, %Item{} = item) do + case Tree.add_before(tree, before_item, item) do + {:ok, tree} -> {:ok, %{text | tree: tree}} + err -> err + end + end + + def next(%Text{tree: tree}, %Item{} = item) do + Tree.next(tree, item) + end + + def prev(%Text{tree: tree}, %Item{} = item) do + Tree.prev(tree, item) + end + + def first(%Text{tree: tree}, _) do + Tree.first(tree) + end + + def last(%Text{tree: tree}, _) do + Tree.last(tree) + end + + # defdelegate delete(text, transaction, id), to: Y.Type.Array, as: :delete_by_id + + def type_ref(_), do: 0 + end + + # defimpl Enumerable do + # def count(_) do + # {:error, __MODULE__} + # end + # + # def member?(_array, _element) do + # {:error, __MODULE__} + # end + # + # def reduce(array, acc, fun) + # + # def reduce(%Text{tree: tree}, {:cont, acc}, fun) do + # Enumerable.reduce(tree, {:cont, acc}, fn + # nil, acc -> + # {:done, acc} + # + # %{deleted?: true}, acc -> + # {:cont, acc} + # + # %{content: content}, acc -> + # Enum.reduce_while(content, {:cont, acc}, fn c, {_, acc} -> + # case fun.(c, acc) do + # {:cont, _acc} = r -> {:cont, r} + # {:halt, _acc} = r -> {:halt, r} + # end + # end) + # end) + # end + # + # def reduce(_array, {:halt, acc}, _fun) do + # {:halted, acc} + # end + # + # def reduce(array, {:suspend, acc}, fun) do + # {:suspended, acc, &reduce(array, &1, fun)} + # end + # + # def slice(_) do + # {:error, __MODULE__} + # end + # end +end diff --git a/lib/y/type/text/text_position.ex b/lib/y/type/text/text_position.ex new file mode 100644 index 0000000..89bac8f --- /dev/null +++ b/lib/y/type/text/text_position.ex @@ -0,0 +1,8 @@ +defmodule Y.Type.Text.TextPosition do + alias __MODULE__ + defstruct left: nil, right: nil, index: 0, attributes: %{} + + def new(left, right, index, attributes) do + %TextPosition{left: left, right: right, index: index, attributes: attributes} + end +end diff --git a/lib/y/type/text/tree.ex b/lib/y/type/text/tree.ex new file mode 100644 index 0000000..933b380 --- /dev/null +++ b/lib/y/type/text/tree.ex @@ -0,0 +1,418 @@ +defmodule Y.Type.Text.Tree do + alias __MODULE__ + alias FingerTree.EmptyTree + alias Y.Content.Format + alias Y.Item + alias Y.ID + + defstruct [:ft] + @type t() :: %Tree{ft: FingerTree.t()} + + defmodule Meter do + @enforce_keys [:highest_clocks, :highest_clocks_with_length, :len] + defstruct highest_clocks: %{}, highest_clocks_with_length: %{}, len: 0 + end + + use Y.Type.GeneralTree, mod: Tree + + def new do + %Tree{ft: FingerTree.finger_tree(meter_object())} + end + + def insert(%Tree{} = tree, index, text, attributes, parent_name, client_id, last_clock) do + %Meter{len: tree_len} = FingerTree.measure(tree.ft) + + ft = + cond do + index == 0 -> + do_insert( + FingerTree.finger_tree(meter_object()), + tree.ft, + text, + attributes, + parent_name, + client_id, + last_clock + ) + + index >= tree_len -> + do_insert( + tree.ft, + FingerTree.finger_tree(meter_object()), + text, + attributes, + parent_name, + client_id, + last_clock + ) + end + + %{tree | ft: ft} + end + + defp do_insert( + left_tree, + right_tree, + text, + attributes, + parent_name, + client_id, + last_clock + ) do + gathered_attributes = gather_attributes(left_tree) + + attributes = + if attributes == %{} do + gathered_attributes + else + attributes + end + + attributes = + Enum.reduce(gathered_attributes, attributes, fn + {k, _}, acc when is_map_key(acc, k) -> acc + {k, _}, acc -> Map.put(acc, k, nil) + end) + + with {left_tree, right_tree, gathered_attributes} <- + minimize_attribute_changes(left_tree, right_tree, gathered_attributes, attributes), + {negated_attrs, left_tree, right_tree, last_clock} <- + insert_attributes( + attributes, + gathered_attributes, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ), + {left_tree, last_clock} <- + do_insert_text(text, left_tree, right_tree, parent_name, client_id, last_clock), + {left_tree, right_tree} <- + insert_negated_attributes( + negated_attrs, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ) do + FingerTree.append(left_tree, right_tree) + end + end + + defp gather_attributes(tree), do: do_gather_attributes(tree, %{}) + + defp do_gather_attributes(%FingerTree.EmptyTree{} = _, acc) do + acc + |> Enum.reject(fn {_, v} -> is_nil(v) end) + |> Enum.into(%{}) + end + + defp do_gather_attributes(tree, acc) do + case FingerTree.first(tree) do + %Item{deleted?: true} -> + do_gather_attributes(FingerTree.rest(tree), acc) + + %Item{content: [%Format{} = format]} -> + do_gather_attributes(FingerTree.rest(tree), Map.merge(acc, Format.to_map(format))) + + _ -> + do_gather_attributes(FingerTree.rest(tree), acc) + end + end + + defp minimize_attribute_changes( + left_tree, + %FingerTree.EmptyTree{} = right_tree, + gathered_attributes, + _attributes + ), + do: {left_tree, right_tree, gathered_attributes} + + defp minimize_attribute_changes(left_tree, right_tree, gathered_attributes, attributes) do + case FingerTree.first(right_tree) do + %Item{deleted?: true} = item -> + minimize_attribute_changes( + FingerTree.conj(left_tree, item), + FingerTree.rest(right_tree), + gathered_attributes, + attributes + ) + + %Item{content: [%Format{key: key, value: value}]} = item -> + if Map.get(attributes, key) == value do + minimize_attribute_changes( + FingerTree.conj(left_tree, item), + FingerTree.rest(right_tree), + if(value == nil, + do: Map.delete(gathered_attributes, key), + else: Map.put(gathered_attributes, key, value) + ), + attributes + ) + else + {left_tree, right_tree, gathered_attributes} + end + + _ -> + {left_tree, right_tree, gathered_attributes} + end + end + + defp insert_attributes( + attributes, + gathered_attributes, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ) do + {_negated_attrs, _left_tree, _right_tree, _last_clock} = + Enum.reduce(attributes, {%{}, left_tree, right_tree, last_clock}, fn {a_k, a_v}, + {negated_attrs, + left_tree, right_tree, + last_clock} -> + current_val = Map.get(gathered_attributes, a_k) + + if current_val == a_v do + {negated_attrs, left_tree, right_tree, last_clock} + else + left_id = + case FingerTree.last(left_tree) do + nil -> nil + %Item{} = item -> Item.last_id(item) + end + + right_id = + case FingerTree.first(right_tree) do + nil -> nil + %Item{id: id} -> id + end + + item = + Item.new( + id: ID.new(client_id, last_clock), + content: [Format.new(a_k, a_v)], + parent_name: parent_name, + origin: left_id, + right_origin: right_id + ) + + left_tree = FingerTree.conj(left_tree, item) + + {Map.put(negated_attrs, a_k, current_val), left_tree, right_tree, + last_clock + Item.content_length(item)} + end + end) + end + + defp do_insert_text(text, left_tree, right_tree, parent_name, client_id, last_clock) do + left_id = + case FingerTree.last(left_tree) do + nil -> nil + %Item{} = item -> Item.last_id(item) + end + + right_id = + case FingerTree.first(right_tree) do + nil -> nil + %Item{id: id} -> id + end + + {_left_tree, _last_clock} = + text + |> String.split("", trim: true) + |> Enum.reduce({left_tree, last_clock}, fn letter, {left_tree, last_clock} -> + item = + Item.new( + id: ID.new(client_id, last_clock), + content: [letter], + parent_name: parent_name, + origin: left_id, + right_origin: right_id + ) + + {FingerTree.conj(left_tree, item), last_clock + Item.content_length(item)} + end) + end + + defp insert_negated_attributes( + negated_attrs, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ) do + case FingerTree.first(right_tree) do + %Item{deleted?: true} = item -> + insert_negated_attributes( + negated_attrs, + FingerTree.conj(left_tree, item), + FingerTree.rest(right_tree), + parent_name, + client_id, + last_clock + ) + + %Item{content: [%Format{key: key, value: value}]} = item -> + if Map.get(negated_attrs, key) == value do + insert_negated_attributes( + Map.delete(negated_attrs, key), + FingerTree.conj(left_tree, item), + FingerTree.rest(right_tree), + parent_name, + client_id, + last_clock + ) + else + do_insert_negated_attributes( + negated_attrs, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ) + end + + _ -> + do_insert_negated_attributes( + negated_attrs, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ) + end + end + + defp do_insert_negated_attributes( + negated_attrs, + left_tree, + right_tree, + parent_name, + client_id, + last_clock + ) do + {left_tree, _last_clock} = + negated_attrs + |> Enum.reduce({left_tree, last_clock}, fn {k, v}, {left_tree, last_clock} -> + left_id = + case FingerTree.last(left_tree) do + nil -> nil + %Item{} = item -> Item.last_id(item) + end + + right_id = + case FingerTree.first(right_tree) do + nil -> nil + %Item{id: id} -> id + end + + item = + Item.new( + id: ID.new(client_id, last_clock), + content: [Format.new(k, v)], + parent_name: parent_name, + origin: left_id, + right_origin: right_id + ) + + {FingerTree.conj(left_tree, item), last_clock + Item.content_length(item)} + end) + + {left_tree, right_tree} + end + + defp meter_object do + FingerTree.MeterObject.new( + fn + %Item{id: id} = item -> + len = + case item do + %Item{content: [%Format{}]} -> 0 + _ -> Item.content_length(item) + end + + %Meter{ + highest_clocks: %{id.client => id.clock}, + highest_clocks_with_length: %{id.client => id.clock + len}, + len: len + } + end, + %Meter{highest_clocks: %{}, highest_clocks_with_length: %{}, len: 0}, + fn %Meter{} = meter1, %Meter{} = meter2 -> + %Meter{ + highest_clocks: + Map.merge(meter1.highest_clocks, meter2.highest_clocks, fn _k, c1, c2 -> + max(c1, c2) + end), + highest_clocks_with_length: + Map.merge( + meter1.highest_clocks_with_length, + meter2.highest_clocks_with_length, + fn _k, c1, c2 -> + max(c1, c2) + end + ), + len: meter1.len + meter2.len + } + end + ) + end + + defimpl Enumerable do + def count(%Tree{} = tree) do + %Meter{len: tree_len} = FingerTree.measure(tree.ft) + {:ok, tree_len} + end + + def member?(_seq, _element) do + {:error, __MODULE__} + end + + def reduce(seq, acc, fun) + + def reduce(%Tree{} = tree, {:cont, acc}, fun) do + case FingerTree.first(tree.ft) do + nil -> {:done, acc} + element -> reduce(%{tree | ft: FingerTree.rest(tree.ft)}, fun.(element, acc), fun) + end + end + + def reduce(_seq, {:halt, acc}, _fun) do + {:halted, acc} + end + + def reduce(seq, {:suspend, acc}, fun) do + {:suspended, acc, &reduce(seq, &1, fun)} + end + + def slice(_) do + {:error, __MODULE__} + end + end + + defimpl Collectable do + def into(%Tree{} = tree) do + collector_fun = fn + %Tree{ft: acc_ft} = acc, {:cont, value} -> + %{acc | ft: FingerTree.conj(acc_ft, value)} + + acc, :done -> + acc + + _acc, :halt -> + :ok + end + + initial_acc = tree + + {initial_acc, collector_fun} + end + end +end diff --git a/test/array_test.exs b/test/array_test.exs index 2e13631..b3e103e 100644 --- a/test/array_test.exs +++ b/test/array_test.exs @@ -487,4 +487,27 @@ defmodule Y.ArrayTest do %Y.Type.Map{} = map = Enum.at(array, 0) assert "new_value" = Y.Type.Map.get(map, "key") end + + test "length + delete" do + {:ok, doc} = Doc.new(name: :array_length) + {:ok, array} = Doc.get_array(doc, "array") + + doc + |> Doc.transact(fn transaction -> + {:ok, array, transaction} = + Array.put(array, transaction, 0, 0) + |> Array.put(1, 1) + |> Array.put_many(2, [2, 3, 4]) + + assert 5 = Array.length(array) + + assert {:ok, array, transaction} = Array.delete(array, transaction, 0) + assert 4 = Array.length(array) + + assert {:ok, array, transaction} = Array.delete(array, transaction, 0, 4) + assert 0 = Array.length(array) + + {:ok, transaction} + end) + end end diff --git a/test/text_test.exs b/test/text_test.exs new file mode 100644 index 0000000..f09f2da --- /dev/null +++ b/test/text_test.exs @@ -0,0 +1,152 @@ +defmodule Y.TextTest do + use ExUnit.Case + alias Y.Doc + alias Y.Type.Text + + test "insert" do + {:ok, doc} = Doc.new(name: :text_insert) + {:ok, _text} = Doc.get_text(doc, "text") + + Doc.transact(doc, fn transaction -> + {:ok, text} = Doc.get(transaction, "text") + {:ok, text, transaction} = Text.insert(text, transaction, 0, "abc", %{bold: true}) + + assert [ + %Y.Item{ + id: %Y.ID{clock: 0}, + length: 1, + content: [%Y.Content.Format{key: :bold, value: true}], + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 1}, + length: 1, + content: ["a"], + origin: %Y.ID{clock: 0}, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 2}, + length: 1, + content: ["b"], + origin: %Y.ID{clock: 0}, + right_origin: nil, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 3}, + length: 1, + content: ["c"], + origin: %Y.ID{clock: 0}, + right_origin: nil, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 4}, + length: 1, + content: [%Y.Content.Format{key: :bold, value: nil}], + origin: %Y.ID{clock: 3}, + right_origin: nil, + parent_name: "text", + deleted?: false + } + ] = Text.to_list(text, as_items: true, with_deleted: true) + + {:ok, text2, transaction} = Text.insert(text, transaction, 0, "d", %{em: true}) + + assert [ + %Y.Item{ + id: %Y.ID{clock: 4}, + length: 1, + content: [%Y.Content.Format{key: :em, value: true}], + origin: nil, + right_origin: %Y.ID{clock: 0}, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 5}, + length: 1, + content: ["d"], + origin: %Y.ID{clock: 4}, + right_origin: %Y.ID{clock: 0}, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 6}, + length: 1, + content: [%Y.Content.Format{key: :em, value: nil}], + origin: %Y.ID{clock: 5}, + right_origin: %Y.ID{clock: 0}, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 0}, + length: 1, + content: [%Y.Content.Format{key: :bold, value: true}], + origin: nil, + right_origin: nil, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 1}, + length: 1, + content: ["a"], + origin: %Y.ID{clock: 0}, + right_origin: nil, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 2}, + length: 1, + content: ["b"], + origin: %Y.ID{clock: 0}, + right_origin: nil, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 3}, + length: 1, + content: ["c"], + origin: %Y.ID{clock: 0}, + right_origin: nil, + parent_name: "text", + deleted?: false + }, + %Y.Item{ + id: %Y.ID{clock: 4}, + length: 1, + content: [%Y.Content.Format{key: :bold, value: nil}], + origin: %Y.ID{clock: 3}, + right_origin: nil, + parent_name: "text", + deleted?: false + } + ] = Text.to_list(text2, as_items: true, with_deleted: true) + + {:ok, text3, transaction} = Text.insert(text, transaction, 0, "d", %{em: true, bold: true}) + + assert [ + %Y.Content.Format{key: :bold, value: true}, + %Y.Content.Format{key: :em, value: true}, + "d", + %Y.Content.Format{key: :em, value: nil}, + "a", + "b", + "c", + %Y.Content.Format{key: :bold, value: nil} + ] = Text.to_list(text3, with_deleted: true) + + {:ok, transaction} + end) + end +end