Repository: yorkie-team/yorkie
Pull request #615 (2023-08-18 09:22:39)

Fix treePos calculating logic for text node
<!--  Thanks for sending a pull request! -->

**What this PR does / why we need it**:
Fix `toTreePos` to calculate treePos of text node correctly

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Related to https://github.com/yorkie-team/yorkie-js-sdk/pull/611

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note

```

**Additional documentation**:

<!--
This section can be blank if this pull request does not require a release note.

Please use the following format for linking documentation:
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```

**Checklist**:
- [ ] Added relevant tests or not required
- [ ] Didn't break anything



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8442747c..1969c129 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,20 +6,19 @@ on:
   pull_request:
     branches: [ main ]
 
+env:
+  GO_VERSION: '1.19.2'
+
 jobs:
   build:
     name: build
-    strategy:
-      matrix:
-        go_version:
-          - 1.19.2
     runs-on: ubuntu-latest
     steps:
 
-    - name: Set up Go ${{ matrix.go_version }}
+    - name: Set up Go ${{ env.GO_VERSION }}
       uses: actions/setup-go@v3
       with:
-        go-version: ${{ matrix.go_version }}
+        go-version: ${{ env.GO_VERSION }}
 
     - name: Setup buf action
       uses: bufbuild/buf-setup-action@v1
@@ -51,7 +50,7 @@ jobs:
       run: make bench
 
     - name: Download previous benchmark data
-      uses: actions/cache@v1
+      uses: actions/cache@v3
       with:
         path: ./cache
         key: ${{ runner.os }}-benchmark
@@ -69,6 +68,6 @@ jobs:
         comment-always: true
 
     - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         file: ./coverage.txt
diff --git a/api/converter/from_pb.go b/api/converter/from_pb.go
index 840413be..eb6968f0 100644
--- a/api/converter/from_pb.go
+++ b/api/converter/from_pb.go
@@ -642,12 +642,18 @@ func fromTreeNode(pbNode *api.TreeNode) (*crdt.TreeNode, error) {
 		attrs.Set(k, pbAttr.Value, updatedAt)
 	}
 
-	return crdt.NewTreeNode(
+	node := crdt.NewTreeNode(
 		id,
 		pbNode.Type,
 		attrs,
 		pbNode.Value,
-	), nil
+	)
+	node.RemovedAt, err = fromTimeTicket(pbNode.RemovedAt)
+	if err != nil {
+		return nil, err
+	}
+
+	return node, nil
 }
 
 func fromTreePos(pbPos *api.TreePos) (*crdt.TreePos, error) {
diff --git a/pkg/document/crdt/tree.go b/pkg/document/crdt/tree.go
index a29d26d5..6cb7ac4c 100644
--- a/pkg/document/crdt/tree.go
+++ b/pkg/document/crdt/tree.go
@@ -537,26 +537,10 @@ func (t *Tree) FindPos(offset int) (*TreePos, error) {
 	var leftSibling *TreeNode
 
 	if node.IsText() {
-		if node.Parent.Children(true)[0] == node && offset == 0 {
+		if node.Parent.Children(false)[0] == node && offset == 0 {
 			leftSibling = node.Parent.Value
 		} else {
 			leftSibling = node.Value
-			absOffset := node.Value.ID.Offset
-			split, err := node.Value.Split(offset, absOffset)
-			if err != nil {
-				return nil, err
-			}
-
-			if split != nil {
-				split.InsPrev = node.Value
-				t.NodeMapByID.Put(split.ID, split)
-
-				if node.Value.InsNext != nil {
-					node.Value.InsNext.InsPrev = split
-					split.InsNext = node.Value.InsNext
-				}
-				node.Value.InsNext = split
-			}
 		}
 		node = node.Parent
 	} else {
@@ -640,7 +624,7 @@ func (t *Tree) Edit(from, to *TreePos,
 				index.TraverseNode(node.IndexTreeNode, func(node *index.Node[*TreeNode], depth int) {
 					if node.Value.remove(editedAt, time.MaxTicket) {
 						// TODO(sejongk): Refactor the repeated code.
-						latestCreatedAt = latestCreatedAtMapByActor[actorIDHex]
+						latestCreatedAt = createdAtMapByActor[actorIDHex]
 						createdAt := node.Value.ID.CreatedAt
 						if latestCreatedAt == nil || createdAt.After(latestCreatedAt) {
 							createdAtMapByActor[actorIDHex] = createdAt
@@ -680,7 +664,7 @@ func (t *Tree) Edit(from, to *TreePos,
 				if fromParent.Value.IsRemoved() {
 					actorIDHex := node.Value.ID.CreatedAt.ActorIDHex()
 					if node.Value.remove(editedAt, time.MaxTicket) {
-						latestCreatedAt := latestCreatedAtMapByActor[actorIDHex]
+						latestCreatedAt := createdAtMapByActor[actorIDHex]
 						createdAt := node.Value.ID.CreatedAt
 						if latestCreatedAt == nil || createdAt.After(latestCreatedAt) {
 							createdAtMapByActor[actorIDHex] = createdAt
@@ -813,54 +797,81 @@ func (t *Tree) findTreeNodesWithSplitText(pos *TreePos, editedAt *time.Ticket) (
 }
 
 // toTreePos converts the given crdt.TreePos to local index.TreePos<CRDTTreeNode>.
-func (t *Tree) toTreePos(pos *TreePos) *index.TreePos[*TreeNode] {
+func (t *Tree) toTreePos(pos *TreePos) (*index.TreePos[*TreeNode], error) {
 	if pos.ParentID == nil || pos.LeftSiblingID == nil {
-		return nil
+		return nil, nil
 	}
 
 	parentNode, leftSiblingNode := t.toTreeNodes(pos)
 	if parentNode == nil || leftSiblingNode == nil {
-		return nil
+		return nil, nil
 	}
 
 	var treePos *index.TreePos[*TreeNode]
 
-	if parentNode == leftSiblingNode {
-		treePos = &index.TreePos[*TreeNode]{
-			Node:   leftSiblingNode.IndexTreeNode,
-			Offset: 0,
-		}
-	} else {
-		leftSiblingOffset, err := parentNode.IndexTreeNode.FindOffset(leftSiblingNode.IndexTreeNode)
-		if err != nil {
-			return nil
+	if parentNode.IsRemoved() {
+		// If parentNode is removed, treePos is the position of its least alive ancestor.
+		var childNode *TreeNode
+		for parentNode.IsRemoved() {
+			childNode = parentNode
+			parentNode = childNode.IndexTreeNode.Parent.Value
 		}
 
-		offset := leftSiblingOffset + 1
-		if leftSiblingNode.IsText() {
-			offset, err = t.IndexTree.LeftSiblingsSize(parentNode.IndexTreeNode, offset)
-			if err != nil {
-				return nil // NOTE(sejongk): should return error instead?
-			}
+		childOffset, err := parentNode.IndexTreeNode.FindOffset(childNode.IndexTreeNode)
+		if err != nil {
+			return nil, nil
 		}
 
 		treePos = &index.TreePos[*TreeNode]{
 			Node:   parentNode.IndexTreeNode,
-			Offset: offset,
+			Offset: childOffset,
 		}
+	} else {
+		if parentNode == leftSiblingNode {
+			treePos = &index.TreePos[*TreeNode]{
+				Node:   leftSiblingNode.IndexTreeNode,
+				Offset: 0,
+			}
+		} else {
+			// Find the closest existing leftSibling node.
+			offset, err := parentNode.IndexTreeNode.FindOffset(leftSiblingNode.IndexTreeNode)
+			if err != nil {
+				return nil, nil
+			}
+
+			if !leftSiblingNode.IsRemoved() {
+				if leftSiblingNode.IsText() {
+					treePos = &index.TreePos[*TreeNode]{
+						Node:   leftSiblingNode.IndexTreeNode,
+						Offset: leftSiblingNode.IndexTreeNode.PaddedLength(),
+					}
+					return treePos, nil
+				}
+				offset++
+			}
 
+			treePos = &index.TreePos[*TreeNode]{
+				Node:   parentNode.IndexTreeNode,
+				Offset: offset,
+			}
+
+		}
 	}
 
-	return treePos
+	return treePos, nil
 }
 
-// toIndex converts the given CRDTTreePos to the index of the tree.
-func (t *Tree) toIndex(pos *TreePos) (int, error) {
-	treePos := t.toTreePos(pos)
+// ToIndex converts the given CRDTTreePos to the index of the tree.
+func (t *Tree) ToIndex(pos *TreePos) (int, error) {
+	treePos, err := t.toTreePos(pos)
 	if treePos == nil {
 		return -1, nil
 	}
 
+	if err != nil {
+		return 0, err
+	}
+
 	idx, err := t.IndexTree.IndexOf(treePos)
 	if err != nil {
 		return 0, err
diff --git a/pkg/index/tree.go b/pkg/index/tree.go
index d2730db1..b8af592f 100644
--- a/pkg/index/tree.go
+++ b/pkg/index/tree.go
@@ -362,28 +362,17 @@ func (n *Node[V]) FindOffset(node *Node[V]) (int, error) {
 
 	// If nodes are removed, the offset of the removed node is the number of
 	// nodes before the node excluding the removed nodes.
-	if node.Value.IsRemoved() {
-		refined := 0
-		for _, child := range n.Children(true) {
-			if child == node {
-				if refined == 0 {
-					return 0, nil
-				}
-				return refined - 1, nil
-			}
-			if !node.Value.IsRemoved() {
-				refined++
-			}
-		}
-	}
-
-	for i, child := range n.Children() {
+	offset := 0
+	for _, child := range n.Children(true) {
 		if child == node {
-			return i, nil
+			return offset, nil
+		}
+		if !child.Value.IsRemoved() {
+			offset++
 		}
 	}
 
-	return -1, nil
+	return -1, ErrChildNotFound
 }
 
 // IsAncestorOf returns true if the node is an ancestor of the given node.


Test patch:
diff --git a/pkg/document/crdt/tree_test.go b/pkg/document/crdt/tree_test.go
index c15da851..394f6c49 100644
--- a/pkg/document/crdt/tree_test.go
+++ b/pkg/document/crdt/tree_test.go
@@ -300,4 +300,49 @@ func TestTree(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, `<root><p style="italic" weight="bold">ab</p><p style="italic">cd</p></root>`, tree.ToXML())
 	})
+
+	t.Run("can find the closest TreePos when parentNode or leftSiblingNode does not exist", func(t *testing.T) {
+		root := helper.TestRoot()
+		ctx := helper.TextChangeContext(root)
+		//       0
+		// <root> </root>
+		tree := crdt.NewTree(crdt.NewTreeNode(helper.IssuePos(ctx), "r", nil), helper.IssueTime(ctx))
+		assert.Equal(t, 0, tree.Root().Len())
+		assert.Equal(t, "<r></r>", tree.ToXML())
+
+		//       0   1 2 3    4
+		// <root> <p> a b </p> </root>
+		pNode := crdt.NewTreeNode(helper.IssuePos(ctx), "p", nil)
+		textNode := crdt.NewTreeNode(helper.IssuePos(ctx), "text", nil, "ab")
+
+		_, err := tree.EditByIndex(0, 0, nil, []*crdt.TreeNode{pNode}, helper.IssueTime(ctx))
+		assert.NoError(t, err)
+		_, err = tree.EditByIndex(1, 1, nil, []*crdt.TreeNode{textNode}, helper.IssueTime(ctx))
+		assert.NoError(t, err)
+		assert.Equal(t, "<r><p>ab</p></r>", tree.ToXML())
+
+		// Find the closest index.TreePos when leftSiblingNode in crdt.TreePos is removed.
+		//       0   1    2
+		// <root> <p> </p> </root>
+		_, err = tree.EditByIndex(1, 3, nil, nil, helper.IssueTime(ctx))
+		assert.NoError(t, err)
+		assert.Equal(t, "<r><p></p></r>", tree.ToXML())
+
+		treePos := crdt.NewTreePos(pNode.ID, textNode.ID)
+		idx, err := tree.ToIndex(treePos)
+		assert.NoError(t, err)
+		assert.Equal(t, 1, idx)
+
+		// Find the closest index.TreePos when parentNode in crdt.TreePos is removed.
+		//       0
+		// <root> </root>
+		_, err = tree.EditByIndex(0, 2, nil, nil, helper.IssueTime(ctx))
+		assert.NoError(t, err)
+		assert.Equal(t, "<r></r>", tree.ToXML())
+
+		treePos = crdt.NewTreePos(pNode.ID, textNode.ID)
+		idx, err = tree.ToIndex(treePos)
+		assert.NoError(t, err)
+		assert.Equal(t, 0, idx)
+	})
 }
diff --git a/test/integration/tree_test.go b/test/integration/tree_test.go
index a772fa57..a6d68219 100644
--- a/test/integration/tree_test.go
+++ b/test/integration/tree_test.go
@@ -27,6 +27,7 @@ import (
 	"github.com/yorkie-team/yorkie/pkg/document"
 	"github.com/yorkie-team/yorkie/pkg/document/json"
 	"github.com/yorkie-team/yorkie/pkg/document/presence"
+	"github.com/yorkie-team/yorkie/pkg/index"
 	"github.com/yorkie-team/yorkie/test/helper"
 )
 
@@ -224,7 +225,7 @@ func TestTree(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("edit its content with path", func(t *testing.T) {
+	t.Run("edit content with path test", func(t *testing.T) {
 		doc := document.New(helper.TestDocKey(t))
 		err := doc.Update(func(root *json.Object, p *presence.Presence) error {
 			root.SetNewTree("t", &json.TreeNode{
@@ -255,10 +256,112 @@ func TestTree(t *testing.T) {
 			assert.Equal(t, "<doc><tc><p><tn>aXb!</tn></p></tc></doc>", root.GetTree("t").ToXML())
 
 			root.GetTree("t").EditByPath([]int{0, 0, 1}, []int{0, 0, 1}, &json.TreeNode{
-				Type:     "tn",
-				Children: []json.TreeNode{},
+				Type: "tn",
+				Children: []json.TreeNode{{
+					Type: "text", Value: "cd",
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn>aXb!</tn><tn>cd</tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 1}, []int{0, 1}, &json.TreeNode{
+				Type: "p",
+				Children: []json.TreeNode{{
+					Type: "tn", Children: []json.TreeNode{{
+						Type: "text", Value: "q",
+					}},
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn>aXb!</tn><tn>cd</tn></p><p><tn>q</tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 1, 0, 0}, []int{0, 1, 0, 0}, &json.TreeNode{
+				Type: "text",
+				Value: "a",
+			})
+			assert.Equal(t, "<doc><tc><p><tn>aXb!</tn><tn>cd</tn></p><p><tn>aq</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{},
+				})
+				return nil
+			 })}, index.ErrUnreachablePath)			
+			return nil
+		})
+		assert.NoError(t, err)
+	})
+
+	t.Run("edit content with path test 2", func(t *testing.T) {
+		doc := document.New(helper.TestDocKey(t))
+		err := doc.Update(func(root *json.Object, p *presence.Presence) error {
+			root.SetNewTree("t", &json.TreeNode{
+				Type: "doc",
+				Children: []json.TreeNode{{
+					Type: "tc",
+					Children: []json.TreeNode{{
+						Type: "p", Children: []json.TreeNode{{
+							Type: "tn", Children: []json.TreeNode{},
+						}},
+					}},
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn></tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 0, 0, 0}, []int{0, 0, 0, 0}, &json.TreeNode{
+				Type: "text",
+				Value: "a",
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 1}, []int{0, 1}, &json.TreeNode{
+				Type: "p",
+				Children: []json.TreeNode{{
+					Type: "tn", Children: []json.TreeNode{},
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn></tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 1, 0, 0}, []int{0, 1, 0, 0}, &json.TreeNode{
+				Type: "text",
+				Value: "b",
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn>b</tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 2}, []int{0, 2}, &json.TreeNode{
+				Type: "p",
+				Children: []json.TreeNode{{
+					Type: "tn", Children: []json.TreeNode{},
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn>b</tn></p><p><tn></tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 2, 0, 0}, []int{0, 2, 0, 0}, &json.TreeNode{
+				Type: "text",
+				Value: "c",
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn>b</tn></p><p><tn>c</tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 3}, []int{0, 3}, &json.TreeNode{
+				Type: "p",
+				Children: []json.TreeNode{{
+					Type: "tn", Children: []json.TreeNode{},
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn>b</tn></p><p><tn>c</tn></p><p><tn></tn></p></tc></doc>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").EditByPath([]int{0, 3, 0, 0}, []int{0, 3, 0, 0}, &json.TreeNode{
+				Type: "text",
+				Value: "d",
 			})
-			assert.Equal(t, "<doc><tc><p><tn>aXb!</tn><tn></tn></p></tc></doc>", root.GetTree("t").ToXML())
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn>b</tn></p><p><tn>c</tn></p><p><tn>d</tn></p></tc></doc>", root.GetTree("t").ToXML())
+			
+			root.GetTree("t").EditByPath([]int{0, 3}, []int{0, 3}, &json.TreeNode{
+				Type: "p",
+				Children: []json.TreeNode{{
+					Type: "tn", Children: []json.TreeNode{},
+				}},
+			})
+			assert.Equal(t, "<doc><tc><p><tn>a</tn></p><p><tn>b</tn></p><p><tn>c</tn></p><p><tn></tn></p><p><tn>d</tn></p></tc></doc>", root.GetTree("t").ToXML())
 			return nil
 		})
 		assert.NoError(t, err)
@@ -401,6 +504,7 @@ 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())
 	})
 
+	// Concurrent editing, overlapping range test
 	t.Run("concurrently delete overlapping elements test", func(t *testing.T) {
 		ctx := context.Background()
 		d1 := document.New(helper.TestDocKey(t))
@@ -479,6 +583,7 @@ func TestTree(t *testing.T) {
 		assert.Equal(t, "<root><p></p></root>", d1.Root().GetTree("t").ToXML())
 	})
 
+	// Concurrent editing, contained range test
 	t.Run("concurrently insert and delete contained elements of the same depth test", func(t *testing.T) {
 		ctx := context.Background()
 		d1 := document.New(helper.TestDocKey(t))
@@ -788,6 +893,7 @@ func TestTree(t *testing.T) {
 		assert.Equal(t, "<root></root>", d1.Root().GetTree("t").ToXML())
 	})
 
+	// Concurrent editing, side by side range test
 	t.Run("concurrently insert side by side elements (left) test", func(t *testing.T) {
 		ctx := context.Background()
 		d1 := document.New(helper.TestDocKey(t))
@@ -1340,6 +1446,7 @@ func TestTree(t *testing.T) {
 		assert.Equal(t, "<root><p>12</p></root>", d1.Root().GetTree("t").ToXML())
 	})
 
+	// Concurrent editing, complex cases test
 	t.Run("delete text content anchored to another concurrently test", func(t *testing.T) {
 		ctx := context.Background()
 		d1 := document.New(helper.TestDocKey(t))
@@ -1756,4 +1863,92 @@ 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())
 	})
+
+	t.Run("Can delete very first text when there is tombstone in front of target text", func(t *testing.T) {
+		doc := document.New(helper.TestDocKey(t))
+		err := doc.Update(func(root *json.Object, p *presence.Presence) error {
+			// 01. Create a tree and insert a paragraph.
+			root.SetNewTree("t").Edit(0, 0, &json.TreeNode{
+				Type:     "p",
+				Children: []json.TreeNode{{Type: "text", Value: "abcdefghi"}}})
+			assert.Equal(t, "<root><p>abcdefghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(1, 1, &json.TreeNode{
+				Type:  "text",
+				Value: "12345",
+			})
+			assert.Equal(t, "<root><p>12345abcdefghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(2, 5)
+			assert.Equal(t, "<root><p>15abcdefghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(3, 5)
+			assert.Equal(t, "<root><p>15cdefghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(2, 4)
+			assert.Equal(t, "<root><p>1defghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(1, 3)
+			assert.Equal(t, "<root><p>efghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(1, 2)
+			assert.Equal(t, "<root><p>fghi</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(2, 5)
+			assert.Equal(t, "<root><p>f</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(1, 2)
+			assert.Equal(t, "<root><p></p></root>", root.GetTree("t").ToXML())
+
+			return nil
+		})
+		assert.NoError(t, err)
+	})
+
+	t.Run("Can delete node when there is more than one text node in front which has size bigger than 1", func(t *testing.T) {
+		doc := document.New(helper.TestDocKey(t))
+		err := doc.Update(func(root *json.Object, p *presence.Presence) error {
+			// 01. Create a tree and insert a paragraph.
+			root.SetNewTree("t").Edit(0, 0, &json.TreeNode{
+				Type:     "p",
+				Children: []json.TreeNode{{Type: "text", Value: "abcde"}}})
+			assert.Equal(t, "<root><p>abcde</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(6, 6, &json.TreeNode{
+				Type:  "text",
+				Value: "f",
+			})
+			assert.Equal(t, "<root><p>abcdef</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(7, 7, &json.TreeNode{
+				Type:  "text",
+				Value: "g",
+			})
+			assert.Equal(t, "<root><p>abcdefg</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(7, 8)
+			assert.Equal(t, "<root><p>abcdef</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(6, 7)
+			assert.Equal(t, "<root><p>abcde</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(5, 6)
+			assert.Equal(t, "<root><p>abcd</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(4, 5)
+			assert.Equal(t, "<root><p>abc</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(3, 4)
+			assert.Equal(t, "<root><p>ab</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(2, 3)
+			assert.Equal(t, "<root><p>a</p></root>", root.GetTree("t").ToXML())
+
+			root.GetTree("t").Edit(1, 2)
+			assert.Equal(t, "<root><p></p></root>", root.GetTree("t").ToXML())
+
+			return nil
+		})
+		assert.NoError(t, err)
+	})
 }