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

Introduce TreeToken and tokensBetween to Tree #747

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 25 additions & 21 deletions pkg/document/crdt/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,11 @@ func (n *TreeNode) remove(removedAt *time.Ticket) bool {
if n.RemovedAt == nil || n.RemovedAt.Compare(removedAt) > 0 {
n.RemovedAt = removedAt
if justRemoved {
n.Index.UpdateAncestorsSize()
if n.Index.Parent.Value.RemovedAt == nil {
n.Index.UpdateAncestorsSize()
} else {
n.Index.Parent.Length -= n.Index.PaddedLength()
}
}
return true
}
Expand Down Expand Up @@ -649,8 +653,10 @@ func (t *Tree) Edit(

// 03. Merge: move the nodes that are marked as moved.
for _, node := range toBeMovedToFromParents {
if err := fromParent.Append(node); err != nil {
return nil, err
if node.RemovedAt == nil {
if err := fromParent.Append(node); err != nil {
return nil, err
}
}
}

Expand Down Expand Up @@ -717,16 +723,11 @@ func (t *Tree) collectBetween(
if err := t.traverseInPosRange(
fromParent, fromLeft,
toParent, toLeft,
func(node *TreeNode, contain index.TagContained) {
// 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
func(token index.TreeToken[*TreeNode], ended bool) {
node, tokenType := token.Node, token.TokenType
// NOTE(hackerwins): If the node overlaps as a start token with the
// range then we need to move the remaining children to fromParent.
if !node.IsText() && contain == index.OpeningContained {
if tokenType == index.Start && !ended {
// 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.
Expand All @@ -737,10 +738,6 @@ func (t *Tree) collectBetween(
// }

for _, child := range node.Index.Children() {
if slices.Contains(toBeRemoveds, child.Value) {
continue
}

toBeMovedToFromParents = append(toBeMovedToFromParents, child.Value)
}
}
Expand All @@ -759,13 +756,19 @@ func (t *Tree) collectBetween(
}
}

if node.canDelete(editedAt, latestCreatedAt) {
// NOTE(sejongk): If the node is removable or its parent is going to
// be removed, then this node should be removed.
if node.canDelete(editedAt, latestCreatedAt) || slices.Contains(toBeRemoveds, node.Index.Parent.Value) {
latestCreatedAt = createdAtMapByActor[actorIDHex]
createdAt := node.ID.CreatedAt
if latestCreatedAt == nil || createdAt.After(latestCreatedAt) {
createdAtMapByActor[actorIDHex] = createdAt
}
toBeRemoveds = append(toBeRemoveds, node)
// NOTE(hackerwins): If the node overlaps as an end token with the
// range then we need to keep the node.
if tokenType == index.Text || tokenType == index.Start {
toBeRemoveds = append(toBeRemoveds, node)
}
}
},
); err != nil {
Expand Down Expand Up @@ -811,7 +814,7 @@ func (t *Tree) split(
}

func (t *Tree) traverseInPosRange(fromParent, fromLeft, toParent, toLeft *TreeNode,
callback func(node *TreeNode, contain index.TagContained),
callback func(token index.TreeToken[*TreeNode], ended bool),
) error {
fromIdx, err := t.ToIndex(fromParent, fromLeft)
if err != nil {
Expand All @@ -822,7 +825,7 @@ func (t *Tree) traverseInPosRange(fromParent, fromLeft, toParent, toLeft *TreeNo
return err
}

return t.IndexTree.NodesBetween(fromIdx, toIdx, callback)
return t.IndexTree.TokensBetween(fromIdx, toIdx, callback)
}

// StyleByIndex applies the given attributes of the given range.
Expand Down Expand Up @@ -854,7 +857,8 @@ func (t *Tree) Style(from, to *TreePos, attributes map[string]string, editedAt *
}

err = t.traverseInPosRange(fromParent, fromLeft, toParent, toLeft,
func(node *TreeNode, contain index.TagContained) {
func(token index.TreeToken[*TreeNode], _ bool) {
node := token.Node
if !node.IsRemoved() && !node.IsText() && len(attributes) > 0 {
if node.Attrs == nil {
node.Attrs = NewRHT()
Expand Down
80 changes: 46 additions & 34 deletions pkg/index/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,37 +119,47 @@ func postorderTraversal[V Value](node *Node[V], callback func(node *Node[V], dep
callback(node, depth)
}

// TagContained represents whether the opening or closing tag of a element is selected.
type TagContained int
// TokenType represents the type of the selected token.
type TokenType int

const (
// AllContained represents that both opening and closing tag of a element are selected.
AllContained TagContained = 1 + iota
// OpeningContained represents that only the opening tag is selected.
OpeningContained
// ClosingContained represents that only the closing tag is selected.
ClosingContained
// Start represents that the start token type.
Start TokenType = 1 + iota
// End represents that the end token type.
End
// Text represents that the text token type.
Text
)

// ToString returns the string of TagContain.
func (c TagContained) ToString() string {
// ToString returns the string of TokenType.
func (c TokenType) ToString() string {
var str string
switch c {
case AllContained:
str = "All"
case OpeningContained:
str = "Opening"
case ClosingContained:
str = "Closing"
case Start:
str = "Start"
case End:
str = "End"
case Text:
str = "Text"
}
return str
}

// nodesBetween iterates the nodes between the given range.
// TreeToken represents a token in XML-like string.
type TreeToken[V Value] struct {
Node V
TokenType TokenType
}

// tokensBetween iterates the tokens between the given range.
//
// For example, if the tree is <p><i>abc</i></p>, the tokens are
// [p, Start], [i, Start], [abc, Text], [i, End], [p, End].
//
// If the given range is collapsed, the callback is not called.
// It traverses the tree with postorder traversal.
// It traverses the tree based on the concept of token.
// NOTE(sejongk): Nodes should not be removed in callback, because it leads wrong behaviors.
func nodesBetween[V Value](root *Node[V], from, to int, callback func(node V, contain TagContained)) error {
func tokensBetween[V Value](root *Node[V], from, to int, callback func(token TreeToken[V], ended bool)) error {
if from > to {
return fmt.Errorf("from cannot be greater than to %d > %d", from, to)
}
Expand Down Expand Up @@ -178,25 +188,27 @@ func nodesBetween[V Value](root *Node[V], from, to int, callback func(node V, co
toChild = to - pos
}

if err := nodesBetween(
startContained := !child.IsText() && fromChild < 0
endContained := !child.IsText() && toChild > child.Length
if child.IsText() || startContained {
var tokenType TokenType
if child.IsText() {
tokenType = Text
} else {
tokenType = Start
}
callback(TreeToken[V]{child.Value, tokenType}, endContained)
}
if err := tokensBetween(
child,
int(math.Max(0, float64(fromChild))),
int(math.Min(float64(toChild), float64(child.Length))),
callback,
); err != nil {
return err
}

if fromChild < 0 || toChild > child.Length || child.IsText() {
var contain TagContained
if (fromChild < 0 && toChild > child.Length) || child.IsText() {
contain = AllContained
} else if fromChild < 0 {
contain = OpeningContained
} else {
contain = ClosingContained
}
callback(child.Value, contain)
if endContained {
callback(TreeToken[V]{child.Value, End}, endContained)
}
}
pos += child.PaddedLength()
Expand Down Expand Up @@ -603,9 +615,9 @@ func (n *Node[V]) OffsetOfChild(node *Node[V]) int {
return -1
}

// NodesBetween returns the nodes between the given range.
func (t *Tree[V]) NodesBetween(from int, to int, callback func(node V, contain TagContained)) error {
return nodesBetween(t.root, from, to, callback)
// TokensBetween returns the tokens between the given range.
func (t *Tree[V]) TokensBetween(from int, to int, callback func(token TreeToken[V], ended bool)) error {
return tokensBetween(t.root, from, to, callback)
}

// TreePos is the position of a node in the tree.
Expand Down
52 changes: 26 additions & 26 deletions pkg/index/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ func TestIndexTree(t *testing.T) {
assert.Equal(t, 0, pos.Offset)
assert.NoError(t, err)
pos, err = tree.FindTreePos(1)
assert.Equal(t, "text.hello", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "hello", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)
assert.NoError(t, err)
pos, err = tree.FindTreePos(6)
assert.Equal(t, "text.hello", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "hello", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 5, pos.Offset)
assert.NoError(t, err)
pos, err = tree.FindTreePos(6, false)
Expand All @@ -59,11 +59,11 @@ func TestIndexTree(t *testing.T) {
assert.Equal(t, 1, pos.Offset)
assert.NoError(t, err)
pos, err = tree.FindTreePos(8)
assert.Equal(t, "text.world", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "world", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)
assert.NoError(t, err)
pos, err = tree.FindTreePos(13)
assert.Equal(t, "text.world", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "world", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 5, pos.Offset)
assert.NoError(t, err)
pos, err = tree.FindTreePos(14)
Expand Down Expand Up @@ -146,12 +146,12 @@ func TestIndexTree(t *testing.T) {
treePosCD, _ := tree.FindTreePos(7, true)
nodeCD := treePosCD.Node

assert.Equal(t, "text.ab", helper.ToDiagnostic(nodeAB.Value))
assert.Equal(t, "text.cd", helper.ToDiagnostic(nodeCD.Value))
assert.Equal(t, "ab", helper.ToDiagnostic(nodeAB.Value))
assert.Equal(t, "cd", helper.ToDiagnostic(nodeCD.Value))
assert.Equal(t, "p", tree.FindCommonAncestor(nodeAB, nodeCD).Type())
})

t.Run("traverse nodes between two given positions test", func(t *testing.T) {
t.Run("traverse tokens between two given positions test", func(t *testing.T) {
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13
// <root> <p> a b </p> <p> c d e </p> <p> f g </p> </root>
tree := helper.BuildIndexTree(&json.TreeNode{
Expand All @@ -169,12 +169,12 @@ func TestIndexTree(t *testing.T) {
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13
// `<root> <p> a b </p> <p> c d e </p> <p> f g </p> </root>`

helper.NodesBetweenEqual(t, tree, 2, 11, []string{"text.b:All", "p:Closing",
"text.cde:All", "p:All", "text.fg:All", "p:Opening"})
helper.NodesBetweenEqual(t, tree, 2, 6, []string{"text.b:All", "p:Closing", "text.cde:All", "p:Opening"})
helper.NodesBetweenEqual(t, tree, 0, 1, []string{"p:Opening"})
helper.NodesBetweenEqual(t, tree, 3, 4, []string{"p:Closing"})
helper.NodesBetweenEqual(t, tree, 3, 5, []string{"p:Closing", "p:Opening"})
helper.TokensEqualBetween(t, tree, 2, 11, []string{"b:Text", "p:End", "p:Start", "cde:Text",
"p:End", "p:Start", "fg:Text"})
helper.TokensEqualBetween(t, tree, 2, 6, []string{"b:Text", "p:End", "p:Start", "cde:Text"})
helper.TokensEqualBetween(t, tree, 0, 1, []string{"p:Start"})
helper.TokensEqualBetween(t, tree, 3, 4, []string{"p:End"})
helper.TokensEqualBetween(t, tree, 3, 5, []string{"p:End", "p:Start"})
})

t.Run("find index of the given node test", func(t *testing.T) {
Expand Down Expand Up @@ -203,15 +203,15 @@ func TestIndexTree(t *testing.T) {

pos, posErr = tree.FindTreePos(1, true)
assert.NoError(t, posErr)
assert.Equal(t, "text.a", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "a", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)
index, indexErr = tree.IndexOf(pos)
assert.NoError(t, indexErr)
assert.Equal(t, 1, index)

pos, posErr = tree.FindTreePos(3, true)
assert.NoError(t, posErr)
assert.Equal(t, "text.b", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "b", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 1, pos.Offset)
index, indexErr = tree.IndexOf(pos)
assert.NoError(t, indexErr)
Expand All @@ -227,7 +227,7 @@ func TestIndexTree(t *testing.T) {

pos, posErr = tree.FindTreePos(10, true)
assert.NoError(t, posErr)
assert.Equal(t, "text.fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)
index, indexErr = tree.IndexOf(pos)
assert.NoError(t, indexErr)
Expand Down Expand Up @@ -257,17 +257,17 @@ func TestIndexTree(t *testing.T) {

pos, err = tree.PathToTreePos([]int{0, 0})
assert.NoError(t, err)
assert.Equal(t, "text.a", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "a", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)

pos, err = tree.PathToTreePos([]int{0, 1})
assert.NoError(t, err)
assert.Equal(t, "text.a", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "a", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 1, pos.Offset)

pos, err = tree.PathToTreePos([]int{0, 2})
assert.NoError(t, err)
assert.Equal(t, "text.b", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "b", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 1, pos.Offset)

pos, err = tree.PathToTreePos([]int{1})
Expand All @@ -277,22 +277,22 @@ func TestIndexTree(t *testing.T) {

pos, err = tree.PathToTreePos([]int{1, 0})
assert.NoError(t, err)
assert.Equal(t, "text.cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)

pos, err = tree.PathToTreePos([]int{1, 1})
assert.NoError(t, err)
assert.Equal(t, "text.cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 1, pos.Offset)

pos, err = tree.PathToTreePos([]int{1, 2})
assert.NoError(t, err)
assert.Equal(t, "text.cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 2, pos.Offset)

pos, err = tree.PathToTreePos([]int{1, 3})
assert.NoError(t, err)
assert.Equal(t, "text.cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "cde", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 3, pos.Offset)

pos, err = tree.PathToTreePos([]int{2})
Expand All @@ -302,17 +302,17 @@ func TestIndexTree(t *testing.T) {

pos, err = tree.PathToTreePos([]int{2, 0})
assert.NoError(t, err)
assert.Equal(t, "text.fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 0, pos.Offset)

pos, err = tree.PathToTreePos([]int{2, 1})
assert.NoError(t, err)
assert.Equal(t, "text.fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 1, pos.Offset)

pos, err = tree.PathToTreePos([]int{2, 2})
assert.NoError(t, err)
assert.Equal(t, "text.fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, "fg", helper.ToDiagnostic(pos.Node.Value))
assert.Equal(t, 2, pos.Offset)

pos, err = tree.PathToTreePos([]int{3})
Expand Down
Loading
Loading