Skip to content

Commit 1760113

Browse files
authored
Implement Tree.RemoveStyle (#171)
1 parent 42fa485 commit 1760113

File tree

12 files changed

+263
-24
lines changed

12 files changed

+263
-24
lines changed

yorkie/proto/yorkie/v1/resources.proto

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ message Operation {
133133
TreePos to = 3;
134134
map<string, string> attributes = 4;
135135
TimeTicket executed_at = 5;
136+
repeated string attributes_to_remove = 6;
136137
}
137138

138139
oneof body {

yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeTest.kt

+29
Original file line numberDiff line numberDiff line change
@@ -2070,6 +2070,35 @@ class JsonTreeTest {
20702070
}
20712071
}
20722072

2073+
@Test
2074+
fun test_sync_content_with_remove_style() {
2075+
withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ ->
2076+
updateAndSync(
2077+
Updater(c1, d1) { root, _ ->
2078+
root.setNewTree(
2079+
"t",
2080+
element("doc") {
2081+
element("p") {
2082+
attr { "italic" to "true" }
2083+
text { "hello" }
2084+
}
2085+
},
2086+
)
2087+
},
2088+
Updater(c2, d2),
2089+
)
2090+
assertTreesXmlEquals("<doc><p italic=\"true\">hello</p></doc>", d1, d2)
2091+
2092+
updateAndSync(
2093+
Updater(c1, d1) { root, _ ->
2094+
root.rootTree().removeStyle(0, 1, listOf("italic"))
2095+
},
2096+
Updater(c2, d2),
2097+
)
2098+
assertTreesXmlEquals("<doc><p>hello</p></doc>", d1, d2)
2099+
}
2100+
}
2101+
20732102
companion object {
20742103

20752104
fun JsonObject.rootTree() = getAs<JsonTree>("t")

yorkie/src/main/kotlin/dev/yorkie/api/OperationConverter.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ internal fun List<PBOperation>.toOperations(): List<Operation> {
106106
parentCreatedAt = it.treeStyle.parentCreatedAt.toTimeTicket(),
107107
fromPos = it.treeStyle.from.toCrdtTreePos(),
108108
toPos = it.treeStyle.to.toCrdtTreePos(),
109-
attributes = it.treeStyle.attributesMap.toMap(),
109+
attributes = it.treeStyle.attributesMap,
110110
executedAt = it.treeStyle.executedAt.toTimeTicket(),
111+
attributesToRemove = it.treeStyle.attributesToRemoveList,
111112
)
112113

113114
else -> throw IllegalArgumentException("unimplemented operation")
@@ -226,9 +227,10 @@ internal fun Operation.toPBOperation(): PBOperation {
226227
from = operation.fromPos.toPBTreePos()
227228
to = operation.toPos.toPBTreePos()
228229
executedAt = operation.executedAt.toPBTimeTicket()
229-
operation.attributes.forEach { (key, value) ->
230+
operation.attributes?.forEach { (key, value) ->
230231
attributes[key] = value
231232
}
233+
operation.attributesToRemove?.forEach { attributesToRemove.add(it) }
232234
}
233235
}
234236
}

yorkie/src/main/kotlin/dev/yorkie/document/crdt/CrdtTree.kt

+33
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,35 @@ internal class CrdtTree(
383383
}
384384
}
385385

386+
fun removeStyle(
387+
range: TreePosRange,
388+
attributeToRemove: List<String>,
389+
executedAt: TimeTicket,
390+
): List<TreeChange> {
391+
val (fromParent, fromLeft) = findNodesAndSplitText(range.first, executedAt)
392+
val (toParent, toLeft) = findNodesAndSplitText(range.second, executedAt)
393+
return buildList {
394+
traverseInPosRange(fromParent, fromLeft, toParent, toLeft) { (node, _), _ ->
395+
if (!node.isRemoved && !node.isText && attributeToRemove.isNotEmpty()) {
396+
attributeToRemove.forEach { key ->
397+
node.removeAttribute(key, executedAt)
398+
}
399+
add(
400+
TreeChange(
401+
type = TreeChangeType.RemoveStyle,
402+
from = toIndex(fromParent, fromLeft),
403+
to = toIndex(toParent, toLeft),
404+
fromPath = toPath(fromParent, fromLeft),
405+
toPath = toPath(toParent, toLeft),
406+
actorID = executedAt.actorID,
407+
attributesToRemove = attributeToRemove,
408+
),
409+
)
410+
}
411+
}
412+
}
413+
}
414+
386415
private fun traverseAll(
387416
node: CrdtTreeNode,
388417
depth: Int = 0,
@@ -778,6 +807,10 @@ internal data class CrdtTreeNode private constructor(
778807
_attributes.set(key, value, executedAt)
779808
}
780809

810+
fun removeAttribute(key: String, executedAt: TimeTicket) {
811+
_attributes.remove(key, executedAt)
812+
}
813+
781814
/**
782815
* Marks the node as removed.
783816
*/

yorkie/src/main/kotlin/dev/yorkie/document/crdt/Rht.kt

+50-7
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import dev.yorkie.document.time.TimeTicket.Companion.compareTo
88
* For more details about RHT:
99
* @link http://csl.skku.edu/papers/jpdc11.pdf
1010
*/
11-
internal class Rht : Iterable<Rht.Node> {
11+
internal class Rht : Collection<Rht.Node> {
1212
private val nodeMapByKey = mutableMapOf<String, Node>()
13+
private var numberOfRemovedElements = 0
1314

1415
val nodeKeyValueMap: Map<String, String>
1516
get() {
@@ -25,14 +26,41 @@ internal class Rht : Iterable<Rht.Node> {
2526
) {
2627
val prev = nodeMapByKey[key]
2728
if (prev?.executedAt < executedAt) {
28-
val node = Node(key, value, executedAt)
29+
if (prev?.isRemoved == false) {
30+
numberOfRemovedElements--
31+
}
32+
val node = Node(key, value, executedAt, false)
2933
nodeMapByKey[key] = node
3034
}
3135
}
3236

37+
/**
38+
* Removes the Element of the given [key].
39+
*/
40+
fun remove(key: String, executedAt: TimeTicket): String {
41+
val prev = nodeMapByKey[key]
42+
return when {
43+
prev == null -> {
44+
numberOfRemovedElements++
45+
nodeMapByKey[key] = Node(key, "", executedAt, true)
46+
""
47+
}
48+
49+
prev.executedAt < executedAt -> {
50+
if (!prev.isRemoved) {
51+
numberOfRemovedElements++
52+
}
53+
nodeMapByKey[key] = Node(key, prev.value, executedAt, true)
54+
if (prev.isRemoved) "" else prev.value
55+
}
56+
57+
else -> ""
58+
}
59+
}
60+
3361
operator fun get(key: String): String? = nodeMapByKey[key]?.value
3462

35-
fun has(key: String): Boolean = key in nodeMapByKey
63+
fun has(key: String): Boolean = nodeMapByKey[key]?.isRemoved == false
3664

3765
fun deepCopy(): Rht {
3866
val rht = Rht()
@@ -47,15 +75,23 @@ internal class Rht : Iterable<Rht.Node> {
4775
* Converts the given [Rht] to XML String.
4876
*/
4977
fun toXml(): String {
50-
return nodeKeyValueMap.entries.joinToString(" ") { (key, value) ->
51-
"$key=\"$value\""
52-
}
78+
return nodeMapByKey.filterValues { !it.isRemoved }.entries
79+
.joinToString(" ") { (key, node) ->
80+
"$key=\"${node.value}\""
81+
}
5382
}
5483

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

88+
override val size: Int
89+
get() = nodeMapByKey.size - numberOfRemovedElements
90+
91+
override fun containsAll(elements: Collection<Node>): Boolean = elements.all { contains(it) }
92+
93+
override fun contains(element: Node): Boolean = nodeMapByKey[element.key]?.isRemoved == false
94+
5995
override fun equals(other: Any?): Boolean {
6096
if (other !is Rht) {
6197
return false
@@ -67,5 +103,12 @@ internal class Rht : Iterable<Rht.Node> {
67103
return nodeMapByKey.hashCode()
68104
}
69105

70-
data class Node(val key: String, val value: String, val executedAt: TimeTicket)
106+
override fun isEmpty(): Boolean = size == 0
107+
108+
data class Node(
109+
val key: String,
110+
val value: String,
111+
val executedAt: TimeTicket,
112+
val isRemoved: Boolean,
113+
)
71114
}

yorkie/src/main/kotlin/dev/yorkie/document/crdt/TreeInfo.kt

+2
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@ internal data class TreeChange(
6161
val toPath: List<Int>,
6262
val value: List<TreeNode>? = null,
6363
val attributes: Map<String, String>? = null,
64+
val attributesToRemove: List<String>? = null,
6465
val splitLevel: Int = 0,
6566
)
6667

6768
internal enum class TreeChangeType {
6869
Content,
6970
Style,
71+
RemoveStyle,
7072
}

yorkie/src/main/kotlin/dev/yorkie/document/json/JsonTree.kt

+26-1
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,33 @@ public class JsonTree internal constructor(
7070
target.createdAt,
7171
range.first,
7272
range.second,
73-
attributes.toMap(),
7473
ticket,
74+
attributes.toMap(),
75+
),
76+
)
77+
}
78+
79+
public fun removeStyle(
80+
fromIndex: Int,
81+
toIndex: Int,
82+
attributesToRemove: List<String>,
83+
) {
84+
require(fromIndex <= toIndex) {
85+
"from should be less than or equal to to"
86+
}
87+
88+
val fromPos = target.findPos(fromIndex)
89+
val toPos = target.findPos(toIndex)
90+
val executedAt = context.issueTimeTicket()
91+
target.removeStyle(fromPos to toPos, attributesToRemove, executedAt)
92+
93+
context.push(
94+
TreeStyleOperation(
95+
target.createdAt,
96+
fromPos,
97+
toPos,
98+
executedAt,
99+
attributesToRemove = attributesToRemove,
75100
),
76101
)
77102
}

yorkie/src/main/kotlin/dev/yorkie/document/operation/OperationInfo.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public sealed class OperationInfo {
9292
val from: Int,
9393
val to: Int,
9494
val fromPath: List<Int>,
95-
val attributes: Map<String, String>,
95+
val attributes: Map<String, String> = emptyMap(),
96+
val attributesToRemove: List<String> = emptyList(),
9697
override var path: String = INITIAL_PATH,
9798
) : OperationInfo(), TreeOperationInfo
9899

yorkie/src/main/kotlin/dev/yorkie/document/operation/TreeStyleOperation.kt

+28-10
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ internal data class TreeStyleOperation(
1414
override val parentCreatedAt: TimeTicket,
1515
val fromPos: CrdtTreePos,
1616
val toPos: CrdtTreePos,
17-
val attributes: Map<String, String>,
1817
override var executedAt: TimeTicket,
18+
val attributes: Map<String, String>? = null,
19+
val attributesToRemove: List<String>? = null,
1920
) : Operation() {
2021
override val effectedCreatedAt = parentCreatedAt
2122

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

34-
return changes.map {
35-
TreeStyleOpInfo(
36-
it.from,
37-
it.to,
38-
it.fromPath,
39-
it.attributes.orEmpty(),
40-
root.createPath(parentCreatedAt),
41-
)
34+
return when {
35+
attributes?.isNotEmpty() == true -> {
36+
tree.style(fromPos to toPos, attributes, executedAt).map {
37+
TreeStyleOpInfo(
38+
it.from,
39+
it.to,
40+
it.fromPath,
41+
it.attributes.orEmpty(),
42+
path = root.createPath(parentCreatedAt),
43+
)
44+
}
45+
}
46+
47+
attributesToRemove?.isNotEmpty() == true -> {
48+
tree.removeStyle(fromPos to toPos, attributesToRemove, executedAt).map {
49+
TreeStyleOpInfo(
50+
it.from,
51+
it.to,
52+
it.fromPath,
53+
attributesToRemove = it.attributesToRemove.orEmpty(),
54+
path = root.createPath(parentCreatedAt),
55+
)
56+
}
57+
}
58+
59+
else -> emptyList()
4260
}
4361
}
4462

yorkie/src/test/kotlin/dev/yorkie/api/ConverterTest.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,9 @@ class ConverterTest {
261261
CrdtTreeNodeID(InitialTimeTicket, 10),
262262
CrdtTreeNodeID(InitialTimeTicket, 10),
263263
),
264-
mapOf("a" to "b"),
265264
InitialTimeTicket,
265+
mapOf("a" to "b"),
266+
listOf("a"),
266267
)
267268
val converted = listOf(
268269
addOperation.toPBOperation(),

yorkie/src/test/kotlin/dev/yorkie/document/crdt/RhtTest.kt

+11
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ class RhtTest {
5151
}
5252
}
5353

54+
@Test
55+
fun `should handle remove`() {
56+
target.set(TEST_KEY, TEST_VALUE, TimeTicket.InitialTimeTicket)
57+
assertEquals(TEST_VALUE, target[TEST_KEY])
58+
assertEquals(1, target.size)
59+
60+
target.remove(TEST_KEY, TimeTicket.MaxTimeTicket)
61+
assertFalse(target.has(TEST_KEY))
62+
assertTrue(target.isEmpty())
63+
}
64+
5465
private fun Rht.toTestString(): String {
5566
return nodeKeyValueMap.entries.joinToString("") { "${it.key}:${it.value}" }
5667
}

0 commit comments

Comments
 (0)