diff --git a/Dockerfile b/Dockerfile index c938cd7c9..872b90cf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # Stage 1: build binary # Start from the latest golang base image -FROM golang:1.18-buster AS builder +FROM golang:1.21 AS builder # Add Maintainer Info LABEL maintainer="hackerwins " diff --git a/Makefile b/Makefile index f835f821b..28541ba05 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ protoset: ## generate protoset file api/yorkie/v1/*.proto build: ## builds an executable that runs in the current environment - go build -o $(EXECUTABLE) -ldflags "${GO_LDFLAGS}" ./cmd/yorkie + CGO_ENABLED=0 go build -o $(EXECUTABLE) -ldflags "${GO_LDFLAGS}" ./cmd/yorkie build-binaries: ## builds binaries to attach a new release rm -rf binaries diff --git a/go.mod b/go.mod index ac45b4f44..5c7390f9c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/yorkie-team/yorkie -go 1.19 +go 1.21 require ( github.com/go-playground/locales v0.14.0 diff --git a/pkg/document/crdt/tree.go b/pkg/document/crdt/tree.go index b9979c4f7..0172b4f12 100644 --- a/pkg/document/crdt/tree.go +++ b/pkg/document/crdt/tree.go @@ -19,6 +19,7 @@ package crdt import ( "errors" "fmt" + "slices" "strconv" "strings" "unicode/utf16" @@ -587,17 +588,39 @@ 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. + if !node.IsText() && contain == index.OpeningContained { + // TODO(hackerwins): Define more clearly merge-able rules + // between two parents. For now, we only merge two parents are + // both element nodes having text children. + // e.g.

a|b

c|d

->

a|d

+ if !fromParent.Value.IndexTreeNode.HasTextChild() || + !toParent.Value.IndexTreeNode.HasTextChild() { + return + } + + 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 @@ -618,7 +641,7 @@ func (t *Tree) Edit(from, to *TreePos, if latestCreatedAt == nil || createdAt.After(latestCreatedAt) { createdAtMapByActor[actorIDHex] = createdAt } - toBeRemoved = append(toBeRemoved, node) + toBeRemoveds = append(toBeRemoveds, node) } }) @@ -626,12 +649,18 @@ func (t *Tree) Edit(from, to *TreePos, 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 diff --git a/pkg/document/crdt/tree_test.go b/pkg/document/crdt/tree_test.go index c79eee92c..2c0a265cc 100644 --- a/pkg/document/crdt/tree_test.go +++ b/pkg/document/crdt/tree_test.go @@ -210,7 +210,7 @@ func TestTree(t *testing.T) { assert.Equal(t, 2, node.Children[0].Size) assert.Equal(t, 2, node.Children[0].Children[0].Size) - // 02. Delete b from the first paragraph. + // 02. Delete b from the second paragraph. // 0 1 2 3 4 5 6 7 //

a

c d

_, err = tree.EditByIndex(2, 3, nil, nil, helper.IssueTime(ctx)) @@ -244,27 +244,61 @@ func TestTree(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "

ab

cd

", tree.ToXML()) - // 02. delete b, c and first paragraph. + // 02. delete b, c and the second paragraph. // 0 1 2 3 4 //

a d

_, err = tree.EditByIndex(2, 6, nil, nil, helper.IssueTime(ctx)) assert.NoError(t, err) - assert.Equal(t, "

a

d

", tree.ToXML()) + assert.Equal(t, "

ad

", tree.ToXML()) - // TODO(sejongk): Use the below assertions after implementing Tree.Move. - // assert.Equal(t, "

ad

", 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, "

@ad

", tree.ToXML()) + }) + + t.Run("delete nodes between element nodes in different levels test", func(t *testing.T) { + // 01. Create a tree with 2 paragraphs. + // 0 1 2 3 4 5 6 7 8 9 10 + //

a b

c d

- // 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) + ctx := helper.TextChangeContext(helper.TestRoot()) + tree := crdt.NewTree(crdt.NewTreeNode(helper.IssuePos(ctx), "root", nil), helper.IssueTime(ctx)) + _, err := tree.EditByIndex(0, 0, nil, []*crdt.TreeNode{ + crdt.NewTreeNode(helper.IssuePos(ctx), "p", nil), + }, helper.IssueTime(ctx)) + assert.NoError(t, err) + _, err = tree.EditByIndex(1, 1, nil, []*crdt.TreeNode{ + crdt.NewTreeNode(helper.IssuePos(ctx), "b", nil), + }, helper.IssueTime(ctx)) + assert.NoError(t, err) + _, err = tree.EditByIndex(2, 2, nil, []*crdt.TreeNode{ + crdt.NewTreeNode(helper.IssuePos(ctx), "text", nil, "ab"), + }, helper.IssueTime(ctx)) + assert.NoError(t, err) + _, err = tree.EditByIndex(6, 6, nil, []*crdt.TreeNode{ + crdt.NewTreeNode(helper.IssuePos(ctx), "p", nil), + }, helper.IssueTime(ctx)) + assert.NoError(t, err) + _, err = tree.EditByIndex(7, 7, nil, []*crdt.TreeNode{ + crdt.NewTreeNode(helper.IssuePos(ctx), "text", nil, "cd"), + }, helper.IssueTime(ctx)) + assert.NoError(t, err) + assert.Equal(t, "

ab

cd

", tree.ToXML()) - // // 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, "

@ad

", tree.ToXML()) + // 02. delete b, c and the second paragraph. + // 0 1 2 3 4 5 + //

a d + _, err = tree.EditByIndex(3, 8, nil, nil, helper.IssueTime(ctx)) + assert.NoError(t, err) + assert.Equal(t, "

ad

", tree.ToXML()) }) t.Run("style node with element attributes test", func(t *testing.T) { @@ -403,9 +437,6 @@ func TestTree(t *testing.T) { _, err = tree.EditByIndex(2, 18, nil, nil, helper.IssueTime(ctx)) assert.NoError(t, err) - assert.Equal(t, "

a

f

", tree.ToXML()) - - // TODO(sejongk): Use the below assertion after implementing Tree.Move. - // assert.Equal(t, "

af

", tree.ToXML()) + assert.Equal(t, "

af

", tree.ToXML()) }) } diff --git a/pkg/index/tree.go b/pkg/index/tree.go index 2f10154df..f968a4f2d 100644 --- a/pkg/index/tree.go +++ b/pkg/index/tree.go @@ -569,8 +569,8 @@ func (n *Node[V]) InsertAfter(newNode, referenceNode *Node[V]) error { return nil } -// hasTextChild returns true if the node has a text child. -func (n *Node[V]) hasTextChild() bool { +// HasTextChild returns true if the node has a text child. +func (n *Node[V]) HasTextChild() bool { for _, child := range n.Children() { if child.IsText() { return true @@ -765,7 +765,7 @@ func (t *Tree[V]) PathToTreePos(path []int) (*TreePos[V], error) { } } - if node.hasTextChild() { + if node.HasTextChild() { return findTextPos(node, path[len(path)-1]) } if len(node.Children()) < path[len(path)-1] { diff --git a/test/integration/tree_test.go b/test/integration/tree_test.go index ea62d13e9..533b94acf 100644 --- a/test/integration/tree_test.go +++ b/test/integration/tree_test.go @@ -280,18 +280,20 @@ func TestTree(t *testing.T) { assert.Equal(t, "

aXb!cd

aq

", 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, "

aXb!cd

aqB

", 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) @@ -428,7 +430,7 @@ func TestTree(t *testing.T) { Value: "d", }) assert.Equal(t, "

abcd

", root.GetTree("t").ToXML()) - + return nil }) assert.NoError(t, err) @@ -454,7 +456,7 @@ func TestTree(t *testing.T) { Children: []json.TreeNode{{Type: "text", Value: "fg"}}, }) assert.Equal(t, "

ab

cd

fg
", root.GetTree("t").ToXML()) - + return nil }) assert.NoError(t, err) @@ -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, "

ab

", 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) @@ -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, "

ab

", 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) @@ -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, "

ab

", 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) @@ -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, "

ab

", 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) @@ -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, "

ab

", 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) @@ -688,16 +700,12 @@ func TestTree(t *testing.T) { assert.Equal(t, `

ab

cd

`, root.GetTree("t").ToXML()) root.GetTree("t").Edit(2, 6, nil) - assert.Equal(t, `

a

d

`, root.GetTree("t").ToXML()) - // TODO(sejongk): Use the below assertion after implementing Tree.Move. - // assert.Equal(t, `

ad

`, root.GetTree("t").ToXML()) + assert.Equal(t, `

ad

`, root.GetTree("t").ToXML()) return nil }) assert.NoError(t, err) - assert.Equal(t, `

a

d

`, doc.Root().GetTree("t").ToXML()) - // TODO(sejongk): Use the below assertion after implementing Tree.Move. - // assert.Equal(t, `

ad

`, doc.Root().GetTree("t").ToXML()) + assert.Equal(t, `

ad

`, doc.Root().GetTree("t").ToXML()) }) t.Run("set attributes test", func(t *testing.T) { @@ -2094,7 +2102,7 @@ func TestTree(t *testing.T) { syncClientsThenAssertEqual(t, []clientAndDocPair{{c1, d1}, {c2, d2}}) assert.Equal(t, "

A

", 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))