Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Tree.RemoveStyle #748

Merged
merged 12 commits into from
Jan 19, 2024
10 changes: 10 additions & 0 deletions api/converter/from_pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,16 @@ func fromTreeStyle(pbTreeStyle *api.Operation_TreeStyle) (*operations.TreeStyle,
return nil, err
}

if len(pbTreeStyle.AttributesToRemove) > 0 {
return operations.NewTreeStyleRemove(
parentCreatedAt,
from,
to,
pbTreeStyle.AttributesToRemove,
executedAt,
), nil
}

return operations.NewTreeStyle(
parentCreatedAt,
from,
Expand Down
11 changes: 6 additions & 5 deletions api/converter/to_pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,12 @@ func toTreeEdit(e *operations.TreeEdit) (*api.Operation_TreeEdit_, error) {
func toTreeStyle(style *operations.TreeStyle) (*api.Operation_TreeStyle_, error) {
return &api.Operation_TreeStyle_{
TreeStyle: &api.Operation_TreeStyle{
ParentCreatedAt: ToTimeTicket(style.ParentCreatedAt()),
From: toTreePos(style.FromPos()),
To: toTreePos(style.ToPos()),
Attributes: style.Attributes(),
ExecutedAt: ToTimeTicket(style.ExecutedAt()),
ParentCreatedAt: ToTimeTicket(style.ParentCreatedAt()),
From: toTreePos(style.FromPos()),
To: toTreePos(style.ToPos()),
Attributes: style.Attributes(),
ExecutedAt: ToTimeTicket(style.ExecutedAt()),
AttributesToRemove: style.AttributesToRemove(),
},
}, nil
}
Expand Down
730 changes: 371 additions & 359 deletions api/yorkie/v1/resources.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions api/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
10 changes: 9 additions & 1 deletion pkg/document/crdt/rht.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ func (rht *RHT) Set(k, v string, executedAt *time.Ticket) {

// Remove removes the Element of the given key.
func (rht *RHT) Remove(k string, executedAt *time.Ticket) string {
if node, ok := rht.nodeMapByKey[k]; ok && executedAt.After(node.updatedAt) {
if node, ok := rht.nodeMapByKey[k]; !ok || executedAt.After(node.updatedAt) {
// NOTE(justiceHui): Even if key is not existed, we must set flag `isRemoved` for concurrency
if node == nil {
rht.numberOfRemovedElement++
newNode := newRHTNode(k, ``, executedAt, true)
rht.nodeMapByKey[k] = newNode
return ""
}

alreadyRemoved := node.isRemoved
if !alreadyRemoved {
rht.numberOfRemovedElement++
Expand Down
32 changes: 32 additions & 0 deletions pkg/document/crdt/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,38 @@ func (t *Tree) Style(from, to *TreePos, attributes map[string]string, editedAt *
return nil
}

// RemoveStyle removes the given attributes of the given range.
func (t *Tree) RemoveStyle(from, to *TreePos, attributesToRemove []string, editedAt *time.Ticket) error {
// 01. split text nodes at the given range if needed.
fromParent, fromLeft, err := t.FindTreeNodesWithSplitText(from, editedAt)
if err != nil {
return err
}
toParent, toLeft, err := t.FindTreeNodesWithSplitText(to, editedAt)
if err != nil {
return err
}

err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft,
func(token index.TreeToken[*TreeNode], _ bool) {
node := token.Node
// NOTE(justiceHui): Even if key is not existed, we must set flag `isRemoved` for concurrency
if !node.IsRemoved() && !node.IsText() {
if node.Attrs == nil {
node.Attrs = NewRHT()
}
for _, value := range attributesToRemove {
node.Attrs.Remove(value, editedAt)
hackerwins marked this conversation as resolved.
Show resolved Hide resolved
}
}
})
if err != nil {
return err
}

return nil
}

// FindTreeNodesWithSplitText finds TreeNode of the given crdt.TreePos and
// splits the text node if the position is in the middle of the text node.
// crdt.TreePos is a position in the CRDT perspective. This is different
Expand Down
39 changes: 39 additions & 0 deletions pkg/document/json/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ func (t *Tree) Style(fromIdx, toIdx int, attributes map[string]string) bool {
panic("from should be less than or equal to to")
}

if len(attributes) == 0 {
return true
}

fromPos, err := t.Tree.FindPos(fromIdx)
if err != nil {
panic(err)
Expand All @@ -223,6 +227,41 @@ func (t *Tree) Style(fromIdx, toIdx int, attributes map[string]string) bool {
return true
}

// RemoveStyle sets the attributes to the elements of the given range.
func (t *Tree) RemoveStyle(fromIdx, toIdx int, attributesToRemove []string) bool {
hackerwins marked this conversation as resolved.
Show resolved Hide resolved
if fromIdx > toIdx {
panic("from should be less than or equal to to")
}

if len(attributesToRemove) == 0 {
return true
}

fromPos, err := t.Tree.FindPos(fromIdx)
if err != nil {
panic(err)
}
toPos, err := t.Tree.FindPos(toIdx)
if err != nil {
panic(err)
}

ticket := t.context.IssueTimeTicket()
if err := t.Tree.RemoveStyle(fromPos, toPos, attributesToRemove, ticket); err != nil {
panic(err)
}

t.context.Push(operations.NewTreeStyleRemove(
t.CreatedAt(),
fromPos,
toPos,
attributesToRemove,
ticket,
))

return true
}

// Len returns the length of this tree.
func (t *Tree) Len() int {
return t.IndexTree.Root().Len()
Expand Down
45 changes: 38 additions & 7 deletions pkg/document/operations/tree_style.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ type TreeStyle struct {
// toPos represents the end point of the editing range.
to *crdt.TreePos

// attributes represents the tree style.
// attributes represents the tree style to be added.
attributes map[string]string

// attributesToRemove represents the tree style to be removed.
attributesToRemove []string

// executedAt is the time the operation was executed.
executedAt *time.Ticket
}
Expand All @@ -48,11 +51,30 @@ func NewTreeStyle(
executedAt *time.Ticket,
) *TreeStyle {
return &TreeStyle{
parentCreatedAt: parentCreatedAt,
from: from,
to: to,
attributes: attributes,
executedAt: executedAt,
parentCreatedAt: parentCreatedAt,
from: from,
to: to,
attributes: attributes,
attributesToRemove: []string{},
executedAt: executedAt,
}
}

// NewTreeStyleRemove creates a new instance of TreeStyle.
func NewTreeStyleRemove(
parentCreatedAt *time.Ticket,
from *crdt.TreePos,
to *crdt.TreePos,
attributesToRemove []string,
executedAt *time.Ticket,
) *TreeStyle {
return &TreeStyle{
parentCreatedAt: parentCreatedAt,
from: from,
to: to,
attributes: map[string]string{},
attributesToRemove: attributesToRemove,
executedAt: executedAt,
}
}

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

return obj.Style(e.from, e.to, e.attributes, e.executedAt)
if len(e.attributes) > 0 {
return obj.Style(e.from, e.to, e.attributes, e.executedAt)
}

return obj.RemoveStyle(e.from, e.to, e.attributesToRemove, e.executedAt)
}

// FromPos returns the start point of the editing range.
Expand Down Expand Up @@ -96,3 +122,8 @@ func (e *TreeStyle) ParentCreatedAt() *time.Ticket {
func (e *TreeStyle) Attributes() map[string]string {
return e.attributes
}

// AttributesToRemove returns the content of Style.
func (e *TreeStyle) AttributesToRemove() []string {
return e.attributesToRemove
}
31 changes: 31 additions & 0 deletions test/integration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,37 @@ func syncClientsThenAssertEqual(t *testing.T, pairs []clientAndDocPair) {
}
}

func syncClientsThenCheckEqual(t *testing.T, pairs []clientAndDocPair) bool {
assert.True(t, len(pairs) > 1)
ctx := context.Background()
// Save own changes and get previous changes.
for i, pair := range pairs {
fmt.Printf("before d%d: %s\n", i+1, pair.doc.Marshal())
err := pair.cli.Sync(ctx)
assert.NoError(t, err)
}

// Get last client changes.
// Last client get all precede changes in above loop.
for _, pair := range pairs[:len(pairs)-1] {
err := pair.cli.Sync(ctx)
assert.NoError(t, err)
}

// Assert start.
expected := pairs[0].doc.Marshal()
fmt.Printf("after d1: %s\n", expected)
for i, pair := range pairs[1:] {
v := pair.doc.Marshal()
fmt.Printf("after d%d: %s\n", i+2, v)
if expected != v {
return false
}
}

return true
}

// activeClients creates and activates the given number of clients.
func activeClients(t *testing.T, n int) (clients []*client.Client) {
for i := 0; i < n; i++ {
Expand Down