Skip to content

Commit

Permalink
Implement merge elements in Tree.Edit
Browse files Browse the repository at this point in the history
  • Loading branch information
hackerwins committed Nov 3, 2023
1 parent 6521e75 commit 38caea6
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 103 deletions.
35 changes: 29 additions & 6 deletions pkg/document/crdt/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package crdt
import (
"errors"
"fmt"
"slices"
"strconv"
"strings"
"unicode/utf16"
Expand Down Expand Up @@ -587,17 +588,33 @@ func (t *Tree) Edit(from, to *TreePos,
}

// 02. remove the nodes and update index tree.
var toBeRemoveds []*TreeNode
var toBeMovedToFromParents []*TreeNode
createdAtMapByActor := make(map[string]*time.Ticket)
var toBeRemoved []*TreeNode

err = t.traverseInPosRange(fromParent.Value, fromLeft.Value, toParent.Value, toLeft.Value,
func(node *TreeNode, contain index.TagContained) {
// If node is a element node and half-contained in the range,
// it should not be removed.
if !node.IsText() && contain != index.AllContained {
// NOTE(hackerwins): If the node overlaps as a closing tag with the
// range, then we need to keep it.
if !node.IsText() && contain == index.ClosingContained {
return
}

// NOTE(hackerwins): If the node overlaps as an opening tag with the
// range then we need to move the remaining children to fromParent.
// TODO(hackerwins): Define more clearly merge-able rules between fromParent
// and toParent. For now, if fromParent and toParent are the same
// type, then we can merge them.
if !node.IsText() && contain == index.OpeningContained && fromParent.Type == toParent.Type {
for _, child := range node.IndexTreeNode.Children() {
if slices.Contains(toBeRemoveds, child.Value) {
continue
}

toBeMovedToFromParents = append(toBeMovedToFromParents, child.Value)
}
}

actorIDHex := node.ID.CreatedAt.ActorIDHex()

var latestCreatedAt *time.Ticket
Expand All @@ -618,20 +635,26 @@ func (t *Tree) Edit(from, to *TreePos,
if latestCreatedAt == nil || createdAt.After(latestCreatedAt) {
createdAtMapByActor[actorIDHex] = createdAt
}
toBeRemoved = append(toBeRemoved, node)
toBeRemoveds = append(toBeRemoveds, node)
}

})
if err != nil {
return nil, err
}

for _, node := range toBeRemoved {
for _, node := range toBeRemoveds {
if node.remove(editedAt) {
t.removedNodeMap[node.ID.toIDString()] = node
}
}

for _, node := range toBeMovedToFromParents {
if err := fromParent.Append(node.IndexTreeNode); err != nil {
return nil, err
}
}

// 03. insert the given node at the given position.
if len(contents) != 0 {
leftInChildren := fromLeft
Expand Down
34 changes: 14 additions & 20 deletions pkg/document/crdt/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,22 +249,19 @@ func TestTree(t *testing.T) {
// <root> <p> a d </p> </root>
_, err = tree.EditByIndex(2, 6, nil, nil, helper.IssueTime(ctx))
assert.NoError(t, err)
assert.Equal(t, "<root><p>a</p><p>d</p></root>", tree.ToXML())

// TODO(sejongk): Use the below assertions after implementing Tree.Move.
// assert.Equal(t, "<root><p>ad</p></root>", tree.ToXML())

// node := tree.ToTreeNodeForTest()
// assert.Equal(t, 4, node.Size)
// assert.Equal(t, 2, node.Children[0].Size)
// assert.Equal(t, 1, node.Children[0].Children[0].Size)
// assert.Equal(t, 1, node.Children[0].Children[1].Size)

// // 03. insert a new text node at the start of the first paragraph.
// _, err = tree.EditByIndex(1, 1, nil, []*crdt.TreeNode{crdt.NewTreeNode(helper.IssuePos(ctx),
// "text", nil, "@")}, helper.IssueTime(ctx))
// assert.NoError(t, err)
// assert.Equal(t, "<root><p>@ad</p></root>", tree.ToXML())
assert.Equal(t, "<root><p>ad</p></root>", tree.ToXML())

node := tree.ToTreeNodeForTest()
assert.Equal(t, 4, node.Size)
assert.Equal(t, 2, node.Children[0].Size)
assert.Equal(t, 1, node.Children[0].Children[0].Size)
assert.Equal(t, 1, node.Children[0].Children[1].Size)

// 03. insert a new text node at the start of the first paragraph.
_, err = tree.EditByIndex(1, 1, nil, []*crdt.TreeNode{crdt.NewTreeNode(helper.IssuePos(ctx),
"text", nil, "@")}, helper.IssueTime(ctx))
assert.NoError(t, err)
assert.Equal(t, "<root><p>@ad</p></root>", tree.ToXML())
})

t.Run("style node with element attributes test", func(t *testing.T) {
Expand Down Expand Up @@ -403,9 +400,6 @@ func TestTree(t *testing.T) {

_, err = tree.EditByIndex(2, 18, nil, nil, helper.IssueTime(ctx))
assert.NoError(t, err)
assert.Equal(t, "<root><p>a</p><p>f</p></root>", tree.ToXML())

// TODO(sejongk): Use the below assertion after implementing Tree.Move.
// assert.Equal(t, "<root><p>af</p></root>", tree.ToXML())
assert.Equal(t, "<root><p>af</p></root>", tree.ToXML())
})
}
162 changes: 85 additions & 77 deletions test/integration/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,18 +280,20 @@ func TestTree(t *testing.T) {
assert.Equal(t, "<doc><tc><p><tn>aXb!</tn><tn>cd</tn></p><p><tn>aq</tn></p></tc></doc>", root.GetTree("t").ToXML())

root.GetTree("t").EditByPath([]int{0, 1, 0, 2}, []int{0, 1, 0, 2}, &json.TreeNode{
Type: "text",
Type: "text",
Value: "B",
})
assert.Equal(t, "<doc><tc><p><tn>aXb!</tn><tn>cd</tn></p><p><tn>aqB</tn></p></tc></doc>", root.GetTree("t").ToXML())

assert.Panics(t, func() {doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").EditByPath([]int{0, 0, 4}, []int{0, 0, 4}, &json.TreeNode{
Type: "tn",
Children: []json.TreeNode{},

assert.Panics(t, func() {
doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").EditByPath([]int{0, 0, 4}, []int{0, 0, 4}, &json.TreeNode{
Type: "tn",
Children: []json.TreeNode{},
})
return nil
})
return nil
})}, index.ErrUnreachablePath)
}, index.ErrUnreachablePath)
return nil
})
assert.NoError(t, err)
Expand Down Expand Up @@ -428,7 +430,7 @@ func TestTree(t *testing.T) {
Value: "d",
})
assert.Equal(t, "<doc><p>abcd</p></doc>", root.GetTree("t").ToXML())

return nil
})
assert.NoError(t, err)
Expand All @@ -454,7 +456,7 @@ func TestTree(t *testing.T) {
Children: []json.TreeNode{{Type: "text", Value: "fg"}},
})
assert.Equal(t, "<doc><p>ab</p><p>cd</p><i>fg</i></doc>", root.GetTree("t").ToXML())

return nil
})
assert.NoError(t, err)
Expand Down Expand Up @@ -514,22 +516,24 @@ func TestTree(t *testing.T) {
Children: []json.TreeNode{{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Type: "text",
Value: "ab",
}},
}},
})
assert.Equal(t, "<doc><p>ab</p></doc>", root.GetTree("t").ToXML())

assert.Panics(t, func() {doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "text",
Value: "c"}, &json.TreeNode{
Type: "text",
Value: "",
assert.Panics(t, func() {
doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "text",
Value: "c"}, &json.TreeNode{
Type: "text",
Value: "",
})
return nil
})
return nil
})}, json.ErrEmptyTextNode)
}, json.ErrEmptyTextNode)
return nil
})
assert.NoError(t, err)
Expand All @@ -543,22 +547,24 @@ func TestTree(t *testing.T) {
Children: []json.TreeNode{{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Type: "text",
Value: "ab",
}},
}},
})
assert.Equal(t, "<doc><p>ab</p></doc>", root.GetTree("t").ToXML())

assert.Panics(t, func() {doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{}}, &json.TreeNode{
Type: "text",
Value: "d",
assert.Panics(t, func() {
doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{}}, &json.TreeNode{
Type: "text",
Value: "d",
})
return nil
})
return nil
})}, json.ErrMixedNodeType)
}, json.ErrMixedNodeType)
return nil
})
assert.NoError(t, err)
Expand All @@ -572,28 +578,30 @@ func TestTree(t *testing.T) {
Children: []json.TreeNode{{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Type: "text",
Value: "ab",
}},
}},
})
assert.Equal(t, "<doc><p>ab</p></doc>", root.GetTree("t").ToXML())

assert.Panics(t, func() {doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "c",
},{
Type: "text",
Value: "",
}}}, &json.TreeNode{
Type: "text",
Value: "d",
assert.Panics(t, func() {
doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "c",
}, {
Type: "text",
Value: "",
}}}, &json.TreeNode{
Type: "text",
Value: "d",
})
return nil
})
return nil
})}, json.ErrMixedNodeType)
}, json.ErrMixedNodeType)
return nil
})
assert.NoError(t, err)
Expand All @@ -607,28 +615,30 @@ func TestTree(t *testing.T) {
Children: []json.TreeNode{{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Type: "text",
Value: "ab",
}},
}},
})
assert.Equal(t, "<doc><p>ab</p></doc>", root.GetTree("t").ToXML())

assert.Panics(t, func() {doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "c",
}}}, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "",
}},
assert.Panics(t, func() {
doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "c",
}}}, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "",
}},
})
return nil
})
return nil
})}, json.ErrEmptyTextNode)
}, json.ErrEmptyTextNode)
return nil
})
assert.NoError(t, err)
Expand All @@ -642,26 +652,28 @@ func TestTree(t *testing.T) {
Children: []json.TreeNode{{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Type: "text",
Value: "ab",
}},
}},
})
assert.Equal(t, "<doc><p>ab</p></doc>", root.GetTree("t").ToXML())

assert.Panics(t, func() {doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "text",
Value: "d",
assert.Panics(t, func() {
doc.Update(func(root *json.Object, p *presence.Presence) error {
root.GetTree("t").Edit(3, 3, &json.TreeNode{
Type: "text",
Value: "d",
}, &json.TreeNode{
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "c",
}},
Type: "p",
Children: []json.TreeNode{{
Type: "text",
Value: "c",
}},
})
return nil
})
return nil
})}, json.ErrMixedNodeType)
}, json.ErrMixedNodeType)
return nil
})
assert.NoError(t, err)
Expand All @@ -688,16 +700,12 @@ func TestTree(t *testing.T) {
assert.Equal(t, `<doc><p bold="true">ab</p><p italic="true">cd</p></doc>`, root.GetTree("t").ToXML())

root.GetTree("t").Edit(2, 6, nil)
assert.Equal(t, `<doc><p bold="true">a</p><p italic="true">d</p></doc>`, root.GetTree("t").ToXML())
// TODO(sejongk): Use the below assertion after implementing Tree.Move.
// assert.Equal(t, `<doc><p italic="true">ad</p></doc>`, root.GetTree("t").ToXML())
assert.Equal(t, `<doc><p bold="true">ad</p></doc>`, root.GetTree("t").ToXML())

return nil
})
assert.NoError(t, err)
assert.Equal(t, `<doc><p bold="true">a</p><p italic="true">d</p></doc>`, doc.Root().GetTree("t").ToXML())
// TODO(sejongk): Use the below assertion after implementing Tree.Move.
// assert.Equal(t, `<doc><p italic="true">ad</p></doc>`, doc.Root().GetTree("t").ToXML())
assert.Equal(t, `<doc><p bold="true">ad</p></doc>`, doc.Root().GetTree("t").ToXML())
})

t.Run("set attributes test", func(t *testing.T) {
Expand Down Expand Up @@ -2094,7 +2102,7 @@ func TestTree(t *testing.T) {
syncClientsThenAssertEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}})
assert.Equal(t, "<root><p>A</p></root>", d1.Root().GetTree("t").ToXML())
})

// Edge cases test
t.Run("delete very first text when there is tombstone in front of target text test", func(t *testing.T) {
doc := document.New(helper.TestDocKey(t))
Expand Down

0 comments on commit 38caea6

Please sign in to comment.