Skip to content
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
25 changes: 2 additions & 23 deletions internal/actions/chawathe.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,9 @@ func (s *chawatheState) generate() *EditScript {
Node: w.orig,
Parent: z.orig,
Position: k,
Subtree: len(w.orig.Children) > 0,
})

s.addDescendantMoves(w)

oldk := w.ChildIndex()
if oldk >= 0 {
w.parent.children = slices.Delete(w.parent.children, oldk, oldk+1)
Expand Down Expand Up @@ -279,10 +278,9 @@ func (s *chawatheState) alignChildren(w *cnode, x *treesitter.ASTNode) {
Node: a.orig,
Parent: w.orig,
Position: k,
Subtree: len(a.orig.Children) > 0,
})

s.addDescendantMoves(a)

insertChild(w, a, k)

s.srcInOrder[a] = true
Expand Down Expand Up @@ -360,22 +358,3 @@ func insertChild(parent, child *cnode, k int) {
k = max(0, min(k, len(parent.children)))
parent.children = slices.Insert(parent.children, k, child)
}

func (s *chawatheState) addDescendantMoves(n *cnode) {
var traverse func(curr *cnode)
traverse = func(curr *cnode) {
for _, child := range curr.children {
if dst, ok := s.cpySrcToDst[child]; ok {
pos := max(0, dst.ChildIndex())
s.script.Add(Action{
Type: Move,
Node: child.orig,
Parent: dst.Parent,
Position: pos,
})
}
traverse(child)
}
}
traverse(n)
}
23 changes: 22 additions & 1 deletion internal/engine/top-down.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type scoredPair struct {
pair [2]*treesitter.ASTNode
dice float64
ancSim int
lineageSim int
nameMatched bool
mismatched bool
}
Expand Down Expand Up @@ -114,10 +115,12 @@ func TopDown(

di := Dice(t1.Parent, t2.Parent, m.Src())
si := AncestorNameSimilarity(t1, t2)
li := parentLineageSimilarity(t1, t2)
scored = append(scored, scoredPair{
pair: pair,
dice: di,
ancSim: si,
lineageSim: li,
nameMatched: nameMatched,
mismatched: mismatched,
})
Expand All @@ -133,7 +136,10 @@ func TopDown(
if scored[i].ancSim != scored[j].ancSim {
return scored[i].ancSim > scored[j].ancSim
}
return scored[i].dice > scored[j].dice
if scored[i].dice != scored[j].dice {
return scored[i].dice > scored[j].dice
}
return scored[i].lineageSim > scored[j].lineageSim
})

for len(scored) > 0 {
Expand Down Expand Up @@ -218,3 +224,18 @@ func openUnmatched(
}
}
}

// Checks if the immediate parent and grandparent node types match to help break
// ties when identical subtrees appear in different parts of the file.
func parentLineageSimilarity(t1, t2 *treesitter.ASTNode) int {
score := 0
p1, p2 := t1.Parent, t2.Parent
if p1 != nil && p2 != nil && p1.Type == p2.Type {
score += 2
gp1, gp2 := p1.Parent, p2.Parent
if gp1 != nil && gp2 != nil && gp1.Type == gp2.Type {
score += 1
}
}
return score
}
59 changes: 25 additions & 34 deletions internal/engine/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,48 +238,39 @@ func AncestorNameSimilarity(t1, t2 *treesitter.ASTNode) int {
return typ == "identifier" || typ == "field_identifier" || typ == "type_identifier" || typ == "name"
}

labels1 := make(map[string]bool)
curr := t1.Parent
for curr != nil {
if name := getDeclarationName(curr); name != "" {
labels1[name] = true
}
for _, child := range curr.Children {
if child.Label != "" && isID(r1, child.Type) {
labels1[child.Label] = true
collectLabels := func(t *treesitter.ASTNode, r *treesitter.Rules) map[string]bool {
labels := make(map[string]bool, 8)
curr := t.Parent
for curr != nil {
if name := getDeclarationName(curr); name != "" {
labels[name] = true
}
if child.IsScaffolding() {
for _, sub := range child.Children {
if sub.Label != "" && isID(r1, sub.Type) {
labels1[sub.Label] = true
}
}
}
}
curr = curr.Parent
}

labels2 := make(map[string]bool)
curr = t2.Parent
for curr != nil {
if name := getDeclarationName(curr); name != "" {
labels2[name] = true
}
for _, child := range curr.Children {
if child.Label != "" && isID(r2, child.Type) {
labels2[child.Label] = true
if key := getKeyLabel(curr); key != "" {
labels[key] = true
}
if child.IsScaffolding() {
for _, sub := range child.Children {
if sub.Label != "" && isID(r2, sub.Type) {
labels2[sub.Label] = true
for _, child := range curr.Children {
if child.Label != "" && isID(r, child.Type) {
labels[child.Label] = true
}
if child.IsScaffolding() {
for _, sub := range child.Children {
if sub.Label != "" && isID(r, sub.Type) {
labels[sub.Label] = true
}
if key := getKeyLabel(sub); key != "" {
labels[key] = true
}
}
}
}
curr = curr.Parent
}
curr = curr.Parent
return labels
}

labels1 := collectLabels(t1, r1)
labels2 := collectLabels(t2, r2)

overlap := 0
for l := range labels2 {
if labels1[l] {
Expand Down
13 changes: 13 additions & 0 deletions internal/engine/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ func TestAncestorNameSimilarity(t *testing.T) {
}
}

func TestAncestorNameSimilarityPairKey(t *testing.T) {
// Ancestor pair keys (like in JSON/YAML) should contribute to similarity overlap.
pair1 := testutil.Node("pair", "", testutil.Leaf("string", "\"priority\""), testutil.Node("object", "", testutil.Leaf("string", "min")))
pair2 := testutil.Node("pair", "", testutil.Leaf("string", "\"priority\""), testutil.Node("object", "", testutil.Leaf("string", "max")))
leaf1 := pair1.Children[1].Children[0]
leaf2 := pair2.Children[1].Children[0]

overlap := AncestorNameSimilarity(leaf1, leaf2)
if overlap != 1 {
t.Errorf("expected overlap=1 for pair key 'priority', got %d", overlap)
}
}

func TestAncestorNameSimilarityNil(t *testing.T) {
if AncestorNameSimilarity(nil, testutil.Leaf("id", "x")) != 0 {
t.Error("nil input should return 0")
Expand Down
Loading
Loading