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
28 changes: 28 additions & 0 deletions pkg/document/crdt/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,34 @@ 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
if !node.IsRemoved() && !node.IsText() && len(attributesToRemove) > 0 && node.Attrs != nil {
hackerwins marked this conversation as resolved.
Show resolved Hide resolved
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
}
112 changes: 112 additions & 0 deletions test/integration/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,118 @@ func TestTree(t *testing.T) {
assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}],"attributes":{"bold":"true"}},{"type":"p","children":[{"type":"text","value":"cd"}],"attributes":{"italic":"true"}}]}`, d2.Root().GetTree("t").Marshal())
})

t.Run("remove attributes test", func(t *testing.T) {
ctx := context.Background()
d1 := document.New(helper.TestDocKey(t))
assert.NoError(t, c1.Attach(ctx, d1))

assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
root.SetNewTree("t", &json.TreeNode{
Type: "root",
Children: []json.TreeNode{
{Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ab"}}},
{Type: "p", Attributes: map[string]string{"italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: "cd"}}},
},
})
return nil
}))
assert.NoError(t, c1.Sync(ctx))
assert.Equal(t, `<root><p>ab</p><p italic="true">cd</p></root>`, d1.Root().GetTree("t").ToXML())

assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").RemoveStyle(4, 8, []string{"italic"})
return nil
}))

assert.NoError(t, c1.Sync(ctx))
d2 := document.New(helper.TestDocKey(t))
assert.NoError(t, c2.Attach(ctx, d2))

assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d1.Root().GetTree("t").ToXML())
assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d2.Root().GetTree("t").ToXML())

assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d1.Root().GetTree("t").Marshal())
assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d2.Root().GetTree("t").Marshal())

// remove not exist style
assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").RemoveStyle(4, 8, []string{"bold"})
return nil
}))

assert.NoError(t, c1.Sync(ctx))
assert.NoError(t, c2.Sync(ctx))

assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d1.Root().GetTree("t").ToXML())
assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d2.Root().GetTree("t").ToXML())

assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d1.Root().GetTree("t").Marshal())
assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d2.Root().GetTree("t").Marshal())
})

t.Run("set/remove style without any attributes", func(t *testing.T) {
ctx := context.Background()
d1 := document.New(helper.TestDocKey(t))
assert.NoError(t, c1.Attach(ctx, d1))

assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
root.SetNewTree("t", &json.TreeNode{
Type: "root",
Children: []json.TreeNode{
{Type: "p", Children: []json.TreeNode{{Type: "text", Value: "ab"}}},
{Type: "p", Attributes: map[string]string{"italic": "true"}, Children: []json.TreeNode{{Type: "text", Value: "cd"}}},
},
})
return nil
}))
assert.NoError(t, c1.Sync(ctx))
assert.Equal(t, `<root><p>ab</p><p italic="true">cd</p></root>`, d1.Root().GetTree("t").ToXML())

assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
// NOTE(sejongk): 0, 4 -> 0,1 / 3,4
root.GetTree("t").Style(0, 4, map[string]string{})
return nil
}))

assert.NoError(t, c1.Sync(ctx))
d2 := document.New(helper.TestDocKey(t))
assert.NoError(t, c2.Attach(ctx, d2))

assert.Equal(t, `<root><p>ab</p><p italic="true">cd</p></root>`, d1.Root().GetTree("t").ToXML())
assert.Equal(t, `<root><p>ab</p><p italic="true">cd</p></root>`, d2.Root().GetTree("t").ToXML())

assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}],"attributes":{"italic":"true"}}]}`, d1.Root().GetTree("t").Marshal())
assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}],"attributes":{"italic":"true"}}]}`, d2.Root().GetTree("t").Marshal())

assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").RemoveStyle(4, 8, []string{"italic"})
return nil
}))

assert.NoError(t, c1.Sync(ctx))
assert.NoError(t, c2.Sync(ctx))

assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d1.Root().GetTree("t").ToXML())
assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d2.Root().GetTree("t").ToXML())

assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d1.Root().GetTree("t").Marshal())
assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d2.Root().GetTree("t").Marshal())

assert.NoError(t, d1.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").RemoveStyle(4, 8, []string{})
return nil
}))

assert.NoError(t, c1.Sync(ctx))
assert.NoError(t, c2.Sync(ctx))

assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d1.Root().GetTree("t").ToXML())
assert.Equal(t, `<root><p>ab</p><p>cd</p></root>`, d2.Root().GetTree("t").ToXML())

assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d1.Root().GetTree("t").Marshal())
assert.Equal(t, `{"type":"root","children":[{"type":"p","children":[{"type":"text","value":"ab"}]},{"type":"p","children":[{"type":"text","value":"cd"}]}]}`, d2.Root().GetTree("t").Marshal())
})

// Concurrent editing, overlapping range test
t.Run("concurrently delete overlapping elements test", func(t *testing.T) {
ctx := context.Background()
Expand Down