Skip to content

Commit 8def590

Browse files
justiceHuihackerwins
authored andcommitted
Implement Tree.RemoveStyle (#748)
This commit added tests for Tree behavior in concurrency scenarios: Styles by 2 users (4 * 6 * 6 = 144 cases): - Ranges(4) - A = B - A contains B - A and B are intersecting - not intersecting - Operations for both A and B(6) - `RemoveStyle({'bold'})` - `Style({'bold': 'aa'})` - `Style({'bold': 'bb'})` - `RemoveStyle({'italic'})` - `Style({'italic': 'aa'})` - `Style({'italic': 'bb'})` Edit and Style (6 * 5 * 2 = 60 cases): - Ranges(6) - A = B - A contains B - B contains A - A and B are intersecting - B is next to A - A is next to B. - Operations for A(5) - Insert front of range - Insert back of range - Insert middle of range - Delete - Change - Operations for B(2) - `RemoveStyle({'bold'})` - `Style({'bold': 'aa'})`
1 parent 65712ec commit 8def590

File tree

11 files changed

+887
-386
lines changed

11 files changed

+887
-386
lines changed

api/converter/from_pb.go

+10
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,16 @@ func fromTreeStyle(pbTreeStyle *api.Operation_TreeStyle) (*operations.TreeStyle,
511511
return nil, err
512512
}
513513

514+
if len(pbTreeStyle.AttributesToRemove) > 0 {
515+
return operations.NewTreeStyleRemove(
516+
parentCreatedAt,
517+
from,
518+
to,
519+
pbTreeStyle.AttributesToRemove,
520+
executedAt,
521+
), nil
522+
}
523+
514524
return operations.NewTreeStyle(
515525
parentCreatedAt,
516526
from,

api/converter/to_pb.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,12 @@ func toTreeEdit(e *operations.TreeEdit) (*api.Operation_TreeEdit_, error) {
364364
func toTreeStyle(style *operations.TreeStyle) (*api.Operation_TreeStyle_, error) {
365365
return &api.Operation_TreeStyle_{
366366
TreeStyle: &api.Operation_TreeStyle{
367-
ParentCreatedAt: ToTimeTicket(style.ParentCreatedAt()),
368-
From: toTreePos(style.FromPos()),
369-
To: toTreePos(style.ToPos()),
370-
Attributes: style.Attributes(),
371-
ExecutedAt: ToTimeTicket(style.ExecutedAt()),
367+
ParentCreatedAt: ToTimeTicket(style.ParentCreatedAt()),
368+
From: toTreePos(style.FromPos()),
369+
To: toTreePos(style.ToPos()),
370+
Attributes: style.Attributes(),
371+
ExecutedAt: ToTimeTicket(style.ExecutedAt()),
372+
AttributesToRemove: style.AttributesToRemove(),
372373
},
373374
}, nil
374375
}

api/yorkie/v1/resources.pb.go

+371-359
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/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 {

pkg/document/crdt/rht.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,15 @@ func (rht *RHT) Set(k, v string, executedAt *time.Ticket) {
106106

107107
// Remove removes the Element of the given key.
108108
func (rht *RHT) Remove(k string, executedAt *time.Ticket) string {
109-
if node, ok := rht.nodeMapByKey[k]; ok && executedAt.After(node.updatedAt) {
109+
if node, ok := rht.nodeMapByKey[k]; !ok || executedAt.After(node.updatedAt) {
110+
// NOTE(justiceHui): Even if key is not existed, we must set flag `isRemoved` for concurrency
111+
if node == nil {
112+
rht.numberOfRemovedElement++
113+
newNode := newRHTNode(k, ``, executedAt, true)
114+
rht.nodeMapByKey[k] = newNode
115+
return ""
116+
}
117+
110118
alreadyRemoved := node.isRemoved
111119
if !alreadyRemoved {
112120
rht.numberOfRemovedElement++

pkg/document/crdt/tree.go

+32
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,38 @@ func (t *Tree) Style(from, to *TreePos, attributes map[string]string, editedAt *
876876
return nil
877877
}
878878

879+
// RemoveStyle removes the given attributes of the given range.
880+
func (t *Tree) RemoveStyle(from, to *TreePos, attributesToRemove []string, editedAt *time.Ticket) error {
881+
// 01. split text nodes at the given range if needed.
882+
fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt)
883+
if err != nil {
884+
return err
885+
}
886+
toParent, toLeft, err := t.FindTreeNodesWithSplitText(to, editedAt)
887+
if err != nil {
888+
return err
889+
}
890+
891+
err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft,
892+
func(token index.TreeToken[*TreeNode], _ bool) {
893+
node := token.Node
894+
// NOTE(justiceHui): Even if key is not existed, we must set flag `isRemoved` for concurrency
895+
if !node.IsRemoved() && !node.IsText() {
896+
if node.Attrs == nil {
897+
node.Attrs = NewRHT()
898+
}
899+
for _, value := range attributesToRemove {
900+
node.Attrs.Remove(value, editedAt)
901+
}
902+
}
903+
})
904+
if err != nil {
905+
return err
906+
}
907+
908+
return nil
909+
}
910+
879911
// FindTreeNodesWithSplitText finds TreeNode of the given crdt.TreePos and
880912
// splits the text node if the position is in the middle of the text node.
881913
// crdt.TreePos is a position in the CRDT perspective. This is different

pkg/document/json/tree.go

+39
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ func (t *Tree) Style(fromIdx, toIdx int, attributes map[string]string) bool {
207207
panic("from should be less than or equal to to")
208208
}
209209

210+
if len(attributes) == 0 {
211+
return true
212+
}
213+
210214
fromPos, err := t.Tree.FindPos(fromIdx)
211215
if err != nil {
212216
panic(err)
@@ -232,6 +236,41 @@ func (t *Tree) Style(fromIdx, toIdx int, attributes map[string]string) bool {
232236
return true
233237
}
234238

239+
// RemoveStyle sets the attributes to the elements of the given range.
240+
func (t *Tree) RemoveStyle(fromIdx, toIdx int, attributesToRemove []string) bool {
241+
if fromIdx > toIdx {
242+
panic("from should be less than or equal to to")
243+
}
244+
245+
if len(attributesToRemove) == 0 {
246+
return true
247+
}
248+
249+
fromPos, err := t.Tree.FindPos(fromIdx)
250+
if err != nil {
251+
panic(err)
252+
}
253+
toPos, err := t.Tree.FindPos(toIdx)
254+
if err != nil {
255+
panic(err)
256+
}
257+
258+
ticket := t.context.IssueTimeTicket()
259+
if err := t.Tree.RemoveStyle(fromPos, toPos, attributesToRemove, ticket); err != nil {
260+
panic(err)
261+
}
262+
263+
t.context.Push(operations.NewTreeStyleRemove(
264+
t.CreatedAt(),
265+
fromPos,
266+
toPos,
267+
attributesToRemove,
268+
ticket,
269+
))
270+
271+
return true
272+
}
273+
235274
// Len returns the length of this tree.
236275
func (t *Tree) Len() int {
237276
return t.IndexTree.Root().Len()

pkg/document/operations/tree_style.go

+38-7
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ type TreeStyle struct {
3232
// toPos represents the end point of the editing range.
3333
to *crdt.TreePos
3434

35-
// attributes represents the tree style.
35+
// attributes represents the tree style to be added.
3636
attributes map[string]string
3737

38+
// attributesToRemove represents the tree style to be removed.
39+
attributesToRemove []string
40+
3841
// executedAt is the time the operation was executed.
3942
executedAt *time.Ticket
4043
}
@@ -48,11 +51,30 @@ func NewTreeStyle(
4851
executedAt *time.Ticket,
4952
) *TreeStyle {
5053
return &TreeStyle{
51-
parentCreatedAt: parentCreatedAt,
52-
from: from,
53-
to: to,
54-
attributes: attributes,
55-
executedAt: executedAt,
54+
parentCreatedAt: parentCreatedAt,
55+
from: from,
56+
to: to,
57+
attributes: attributes,
58+
attributesToRemove: []string{},
59+
executedAt: executedAt,
60+
}
61+
}
62+
63+
// NewTreeStyleRemove creates a new instance of TreeStyle.
64+
func NewTreeStyleRemove(
65+
parentCreatedAt *time.Ticket,
66+
from *crdt.TreePos,
67+
to *crdt.TreePos,
68+
attributesToRemove []string,
69+
executedAt *time.Ticket,
70+
) *TreeStyle {
71+
return &TreeStyle{
72+
parentCreatedAt: parentCreatedAt,
73+
from: from,
74+
to: to,
75+
attributes: map[string]string{},
76+
attributesToRemove: attributesToRemove,
77+
executedAt: executedAt,
5678
}
5779
}
5880

@@ -64,7 +86,11 @@ func (e *TreeStyle) Execute(root *crdt.Root) error {
6486
return ErrNotApplicableDataType
6587
}
6688

67-
return obj.Style(e.from, e.to, e.attributes, e.executedAt)
89+
if len(e.attributes) > 0 {
90+
return obj.Style(e.from, e.to, e.attributes, e.executedAt)
91+
}
92+
93+
return obj.RemoveStyle(e.from, e.to, e.attributesToRemove, e.executedAt)
6894
}
6995

7096
// FromPos returns the start point of the editing range.
@@ -96,3 +122,8 @@ func (e *TreeStyle) ParentCreatedAt() *time.Ticket {
96122
func (e *TreeStyle) Attributes() map[string]string {
97123
return e.attributes
98124
}
125+
126+
// AttributesToRemove returns the content of Style.
127+
func (e *TreeStyle) AttributesToRemove() []string {
128+
return e.attributesToRemove
129+
}

test/integration/main_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,37 @@ func syncClientsThenAssertEqual(t *testing.T, pairs []clientAndDocPair) {
8888
}
8989
}
9090

91+
func syncClientsThenCheckEqual(t *testing.T, pairs []clientAndDocPair) bool {
92+
assert.True(t, len(pairs) > 1)
93+
ctx := context.Background()
94+
// Save own changes and get previous changes.
95+
for i, pair := range pairs {
96+
fmt.Printf("before d%d: %s\n", i+1, pair.doc.Marshal())
97+
err := pair.cli.Sync(ctx)
98+
assert.NoError(t, err)
99+
}
100+
101+
// Get last client changes.
102+
// Last client get all precede changes in above loop.
103+
for _, pair := range pairs[:len(pairs)-1] {
104+
err := pair.cli.Sync(ctx)
105+
assert.NoError(t, err)
106+
}
107+
108+
// Assert start.
109+
expected := pairs[0].doc.Marshal()
110+
fmt.Printf("after d1: %s\n", expected)
111+
for i, pair := range pairs[1:] {
112+
v := pair.doc.Marshal()
113+
fmt.Printf("after d%d: %s\n", i+2, v)
114+
if expected != v {
115+
return false
116+
}
117+
}
118+
119+
return true
120+
}
121+
91122
// activeClients creates and activates the given number of clients.
92123
func activeClients(t *testing.T, n int) (clients []*client.Client) {
93124
for i := 0; i < n; i++ {

0 commit comments

Comments
 (0)