Skip to content

Commit

Permalink
Implement Tree.RemoveStyle
Browse files Browse the repository at this point in the history
  • Loading branch information
7hong13 committed Apr 15, 2024
1 parent 95a2fee commit a84dd56
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 24 deletions.
1 change: 1 addition & 0 deletions yorkie/proto/yorkie/v1/resources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ message Operation {
TreePos to = 3;
map<string, string> attributes = 4;
TimeTicket executed_at = 5;
repeated string attributes_to_remove = 6;
}

oneof body {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,35 @@ class JsonTreeTest {
}
}

@Test
fun test_sync_content_with_remove_style() {
withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ ->
updateAndSync(
Updater(c1, d1) { root, _ ->
root.setNewTree(
"t",
element("doc") {
element("p") {
attr { "italic" to "true" }
text { "hello" }
}
},
)
},
Updater(c2, d2),
)
assertTreesXmlEquals("<doc><p italic=\"true\">hello</p></doc>", d1, d2)

updateAndSync(
Updater(c1, d1) { root, _ ->
root.rootTree().removeStyle(0, 1, listOf("italic"))
},
Updater(c2, d2),
)
assertTreesXmlEquals("<doc><p>hello</p></doc>", d1, d2)
}
}

companion object {

fun JsonObject.rootTree() = getAs<JsonTree>("t")
Expand Down
6 changes: 4 additions & 2 deletions yorkie/src/main/kotlin/dev/yorkie/api/OperationConverter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ internal fun List<PBOperation>.toOperations(): List<Operation> {
parentCreatedAt = it.treeStyle.parentCreatedAt.toTimeTicket(),
fromPos = it.treeStyle.from.toCrdtTreePos(),
toPos = it.treeStyle.to.toCrdtTreePos(),
attributes = it.treeStyle.attributesMap.toMap(),
attributes = it.treeStyle.attributesMap,
executedAt = it.treeStyle.executedAt.toTimeTicket(),
attributesToRemove = it.treeStyle.attributesToRemoveList,
)

else -> throw IllegalArgumentException("unimplemented operation")
Expand Down Expand Up @@ -226,9 +227,10 @@ internal fun Operation.toPBOperation(): PBOperation {
from = operation.fromPos.toPBTreePos()
to = operation.toPos.toPBTreePos()
executedAt = operation.executedAt.toPBTimeTicket()
operation.attributes.forEach { (key, value) ->
operation.attributes?.forEach { (key, value) ->
attributes[key] = value
}
operation.attributesToRemove?.forEach { attributesToRemove.add(it) }
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,35 @@ internal class CrdtTree(
}
}

fun removeStyle(
range: TreePosRange,
attributeToRemove: List<String>,
executedAt: TimeTicket,
): List<TreeChange> {
val (fromParent, fromLeft) = findNodesAndSplitText(range.first, executedAt)
val (toParent, toLeft) = findNodesAndSplitText(range.second, executedAt)
return buildList {
traverseInPosRange(fromParent, fromLeft, toParent, toLeft) { (node, _), _ ->
if (!node.isRemoved && !node.isText && attributeToRemove.isNotEmpty()) {
attributeToRemove.forEach { key ->
node.removeAttribute(key, executedAt)
}
add(
TreeChange(
type = TreeChangeType.RemoveStyle,
from = toIndex(fromParent, fromLeft),
to = toIndex(toParent, toLeft),
fromPath = toPath(fromParent, fromLeft),
toPath = toPath(toParent, toLeft),
actorID = executedAt.actorID,
attributesToRemove = attributeToRemove,
),
)
}
}
}
}

private fun traverseAll(
node: CrdtTreeNode,
depth: Int = 0,
Expand Down Expand Up @@ -772,6 +801,10 @@ internal data class CrdtTreeNode private constructor(
_attributes.set(key, value, executedAt)
}

fun removeAttribute(key: String, executedAt: TimeTicket) {
_attributes.remove(key, executedAt)
}

/**
* Marks the node as removed.
*/
Expand Down
57 changes: 50 additions & 7 deletions yorkie/src/main/kotlin/dev/yorkie/document/crdt/Rht.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import dev.yorkie.document.time.TimeTicket.Companion.compareTo
* For more details about RHT:
* @link http://csl.skku.edu/papers/jpdc11.pdf
*/
internal class Rht : Iterable<Rht.Node> {
internal class Rht : Collection<Rht.Node> {
private val nodeMapByKey = mutableMapOf<String, Node>()
private var numberOfRemovedElements = 0

val nodeKeyValueMap: Map<String, String>
get() {
Expand All @@ -25,14 +26,41 @@ internal class Rht : Iterable<Rht.Node> {
) {
val prev = nodeMapByKey[key]
if (prev?.executedAt < executedAt) {
val node = Node(key, value, executedAt)
if (prev?.isRemoved == false) {
numberOfRemovedElements--
}
val node = Node(key, value, executedAt, false)
nodeMapByKey[key] = node
}
}

/**
* Removes the Element of the given [key].
*/
fun remove(key: String, executedAt: TimeTicket): String {
val prev = nodeMapByKey[key]
return when {
prev == null -> {
numberOfRemovedElements++
nodeMapByKey[key] = Node(key, "", executedAt, true)
""
}

prev.executedAt < executedAt -> {
if (!prev.isRemoved) {
numberOfRemovedElements++
}
nodeMapByKey[key] = Node(key, prev.value, executedAt, true)
if (prev.isRemoved) "" else prev.value
}

else -> ""
}
}

operator fun get(key: String): String? = nodeMapByKey[key]?.value

fun has(key: String): Boolean = key in nodeMapByKey
fun has(key: String): Boolean = nodeMapByKey[key]?.isRemoved == false

fun deepCopy(): Rht {
val rht = Rht()
Expand All @@ -47,15 +75,23 @@ internal class Rht : Iterable<Rht.Node> {
* Converts the given [Rht] to XML String.
*/
fun toXml(): String {
return nodeKeyValueMap.entries.joinToString(" ") { (key, value) ->
"$key=\"$value\""
}
return nodeMapByKey.filterValues { !it.isRemoved }.entries
.joinToString(" ") { (key, node) ->
"$key=\"${node.value}\""
}
}

override fun iterator(): Iterator<Node> {
return nodeMapByKey.values.iterator()
}

override val size: Int
get() = nodeMapByKey.size - numberOfRemovedElements

override fun containsAll(elements: Collection<Node>): Boolean = elements.all { contains(it) }

override fun contains(element: Node): Boolean = nodeMapByKey[element.key]?.isRemoved == false

override fun equals(other: Any?): Boolean {
if (other !is Rht) {
return false
Expand All @@ -67,5 +103,12 @@ internal class Rht : Iterable<Rht.Node> {
return nodeMapByKey.hashCode()
}

data class Node(val key: String, val value: String, val executedAt: TimeTicket)
override fun isEmpty(): Boolean = size == 0

data class Node(
val key: String,
val value: String,
val executedAt: TimeTicket,
val isRemoved: Boolean,
)
}
2 changes: 2 additions & 0 deletions yorkie/src/main/kotlin/dev/yorkie/document/crdt/TreeInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ internal data class TreeChange(
val toPath: List<Int>,
val value: List<TreeNode>? = null,
val attributes: Map<String, String>? = null,
val attributesToRemove: List<String>? = null,
val splitLevel: Int = 0,
)

internal enum class TreeChangeType {
Content,
Style,
RemoveStyle,
}
27 changes: 26 additions & 1 deletion yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,33 @@ public class JsonTree internal constructor(
target.createdAt,
range.first,
range.second,
attributes.toMap(),
ticket,
attributes.toMap(),
),
)
}

public fun removeStyle(
fromIndex: Int,
toIndex: Int,
attributesToRemove: List<String>,
) {
require(fromIndex <= toIndex) {
"from should be less than or equal to to"
}

val fromPos = target.findPos(fromIndex)
val toPos = target.findPos(toIndex)
val executedAt = context.issueTimeTicket()
target.removeStyle(fromPos to toPos, attributesToRemove, executedAt)

context.push(
TreeStyleOperation(
target.createdAt,
fromPos,
toPos,
executedAt,
attributesToRemove = attributesToRemove,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public sealed class OperationInfo {
val from: Int,
val to: Int,
val fromPath: List<Int>,
val attributes: Map<String, String>,
val attributes: Map<String, String> = emptyMap(),
val attributesToRemove: List<String> = emptyList(),
override var path: String = INITIAL_PATH,
) : OperationInfo(), TreeOperationInfo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ internal data class TreeStyleOperation(
override val parentCreatedAt: TimeTicket,
val fromPos: CrdtTreePos,
val toPos: CrdtTreePos,
val attributes: Map<String, String>,
override var executedAt: TimeTicket,
val attributes: Map<String, String>? = null,
val attributesToRemove: List<String>? = null,
) : Operation() {
override val effectedCreatedAt = parentCreatedAt

Expand All @@ -29,16 +30,33 @@ internal data class TreeStyleOperation(
YorkieLogger.e(TAG, "fail to execute, only Tree can execute edit")
return emptyList()
}
val changes = tree.style(fromPos to toPos, attributes.toMap(), executedAt)

return changes.map {
TreeStyleOpInfo(
it.from,
it.to,
it.fromPath,
it.attributes.orEmpty(),
root.createPath(parentCreatedAt),
)
return when {
attributes?.isNotEmpty() == true -> {
tree.style(fromPos to toPos, attributes, executedAt).map {
TreeStyleOpInfo(
it.from,
it.to,
it.fromPath,
it.attributes.orEmpty(),
path = root.createPath(parentCreatedAt),
)
}
}

attributesToRemove?.isNotEmpty() == true -> {
tree.removeStyle(fromPos to toPos, attributesToRemove, executedAt).map {
TreeStyleOpInfo(
it.from,
it.to,
it.fromPath,
attributesToRemove = it.attributesToRemove.orEmpty(),
path = root.createPath(parentCreatedAt),
)
}
}

else -> emptyList()
}
}

Expand Down
3 changes: 2 additions & 1 deletion yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,9 @@ class ConverterTest {
CrdtTreeNodeID(InitialTimeTicket, 10),
CrdtTreeNodeID(InitialTimeTicket, 10),
),
mapOf("a" to "b"),
InitialTimeTicket,
mapOf("a" to "b"),
listOf("a"),
)
val converted = listOf(
addOperation.toPBOperation(),
Expand Down
11 changes: 11 additions & 0 deletions yorkie/src/test/kotlin/dev/yorkie/document/crdt/RhtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ class RhtTest {
}
}

@Test
fun `should handle remove`() {
target.set(TEST_KEY, TEST_VALUE, TimeTicket.InitialTimeTicket)
assertEquals(TEST_VALUE, target[TEST_KEY])
assertEquals(1, target.size)

target.remove(TEST_KEY, TimeTicket.MaxTimeTicket)
assertFalse(target.has(TEST_KEY))
assertTrue(target.isEmpty())
}

private fun Rht.toTestString(): String {
return nodeKeyValueMap.entries.joinToString("") { "${it.key}:${it.value}" }
}
Expand Down
Loading

0 comments on commit a84dd56

Please sign in to comment.