diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac1c2ec..7190997 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] permissions: contents: read diff --git a/cmd/diff_test.go b/cmd/diff_test.go index 01cb212..ab0b625 100644 --- a/cmd/diff_test.go +++ b/cmd/diff_test.go @@ -52,3 +52,23 @@ func TestComputeDiffWithDevNull(t *testing.T) { t.Fatal("expected non-nil diff result and envelope for deleted file") } } + +func TestComputeDiffUnsupportedLanguage(t *testing.T) { + dir := t.TempDir() + fileA := dir + "/a.unknown" + fileB := dir + "/b.unknown" + + _ = os.WriteFile(fileA, []byte("line 1\nline 2\nline 3\n"), 0o644) + _ = os.WriteFile(fileB, []byte("line 1\nline 2 modified\nline 3\nline 4\n"), 0o644) + + res, err := computeDiff(fileA, fileB) + if err != nil { + t.Fatalf("computeDiff failed for unsupported files: %v", err) + } + if res == nil || res.Envelope == nil { + t.Fatal("expected non-nil result and envelope for unsupported file diff") + } + if len(res.Envelope.Actions) == 0 { + t.Error("expected fallback line diff actions for unsupported files, got 0") + } +} diff --git a/cmd/pipeline.go b/cmd/pipeline.go index 28062dc..35244f2 100644 --- a/cmd/pipeline.go +++ b/cmd/pipeline.go @@ -34,8 +34,15 @@ func computeDiff(fileA, fileB string) (*diffResult, error) { langA, _ := treesitter.DetectLanguage(fileA) langB, _ := treesitter.DetectLanguage(fileB) + // Fall back to line diff if tree-sitter can't parse the file. if langA == nil && langB == nil { - return nil, fmt.Errorf("unsupported language for files: %s, %s", fileA, fileB) + return &diffResult{ + SrcBytes: srcBytes, + DstBytes: dstBytes, + SrcFile: fileA, + DstFile: fileB, + Envelope: serialize.BuildLineDiffEnvelope(srcBytes, dstBytes), + }, nil } if langA == nil { @@ -45,16 +52,19 @@ func computeDiff(fileA, fileB string) (*diffResult, error) { langB = langA } - srcAST, err := treesitter.ParseWithLanguage(srcBytes, langA) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", fileA, err) - } - dstAST, err := treesitter.ParseWithLanguage(dstBytes, langB) - if err != nil { - return nil, fmt.Errorf("parsing %s: %w", fileB, err) + srcAST, _ := treesitter.ParseWithLanguage(srcBytes, langA) + dstAST, _ := treesitter.ParseWithLanguage(dstBytes, langB) + if srcAST == nil || dstAST == nil { + return &diffResult{ + SrcBytes: srcBytes, + DstBytes: dstBytes, + SrcFile: fileA, + DstFile: fileB, + Envelope: serialize.BuildLineDiffEnvelope(srcBytes, dstBytes), + }, nil } - matchResult := engine.Match(srcAST, dstAST) + matchResult := engine.Match(srcAST, dstAST, srcBytes, dstBytes) es := actions.GenerateEditScript(srcAST, dstAST, matchResult.Mappings) es = postprocess.Run(es, matchResult.Mappings, srcAST, dstAST) diff --git a/internal/engine/declaration_test.go b/internal/engine/declaration_test.go index 7c52d65..5e1574f 100644 --- a/internal/engine/declaration_test.go +++ b/internal/engine/declaration_test.go @@ -271,7 +271,7 @@ func TestMatchDeclarationIntegration(t *testing.T) { ), ) - r := Match(src, dst) + r := Match(src, dst, nil, nil) srcFn := src.Children[0] dstFn := dst.Children[0] if !r.Mappings.Has(srcFn) { diff --git a/internal/engine/linediff.go b/internal/engine/linediff.go new file mode 100644 index 0000000..0ecdf5f --- /dev/null +++ b/internal/engine/linediff.go @@ -0,0 +1,60 @@ +package engine + +// LineDiff matches line indices from linesA to linesB using trimmed LCS. +// Returns a map from A's line index to B's line index. +func LineDiff(linesA, linesB []string) map[int]int { + matchedA := make(map[int]int) + m, n := len(linesA), len(linesB) + + // Trim common prefix + start := 0 + for start < m && start < n && linesA[start] == linesB[start] { + matchedA[start] = start + start++ + } + + // Trim common suffix + endA, endB := m-1, n-1 + for endA >= start && endB >= start && linesA[endA] == linesB[endB] { + matchedA[endA] = endB + endA-- + endB-- + } + + if start > endA || start > endB { + return matchedA + } + + // 1D DP table for the remaining middle window + subA := linesA[start : endA+1] + subB := linesB[start : endB+1] + lenA, lenB := len(subA), len(subB) + stride := lenB + 1 + + dp := make([]int, (lenA+1)*stride) + for i := 1; i <= lenA; i++ { + for j := 1; j <= lenB; j++ { + if subA[i-1] == subB[j-1] { + dp[i*stride+j] = dp[(i-1)*stride+(j-1)] + 1 + } else { + dp[i*stride+j] = max(dp[(i-1)*stride+j], dp[i*stride+(j-1)]) + } + } + } + + // Backtrack to find matching lines + i, j := lenA, lenB + for i > 0 && j > 0 { + if subA[i-1] == subB[j-1] { + matchedA[start+i-1] = start + j - 1 + i-- + j-- + } else if dp[(i-1)*stride+j] >= dp[i*stride+(j-1)] { + i-- + } else { + j-- + } + } + + return matchedA +} diff --git a/internal/engine/linediff_test.go b/internal/engine/linediff_test.go new file mode 100644 index 0000000..7f97be9 --- /dev/null +++ b/internal/engine/linediff_test.go @@ -0,0 +1,55 @@ +package engine + +import ( + "reflect" + "testing" +) + +func TestLineDiff(t *testing.T) { + tests := []struct { + name string + linesA []string + linesB []string + expected map[int]int + }{ + { + name: "basic match with insertions and deletions", + linesA: []string{"a", "b", "c", "d"}, + linesB: []string{"a", "x", "c", "y", "d"}, + expected: map[int]int{0: 0, 2: 2, 3: 4}, + }, + { + name: "identical files", + linesA: []string{"one", "two", "three"}, + linesB: []string{"one", "two", "three"}, + expected: map[int]int{0: 0, 1: 1, 2: 2}, + }, + { + name: "completely different files", + linesA: []string{"a", "b"}, + linesB: []string{"x", "y"}, + expected: map[int]int{}, + }, + { + name: "empty inputs", + linesA: []string{}, + linesB: []string{}, + expected: map[int]int{}, + }, + { + name: "prefix and suffix trim with middle changes", + linesA: []string{"head", "mid_old_1", "mid_old_2", "tail"}, + linesB: []string{"head", "mid_new_1", "tail"}, + expected: map[int]int{0: 0, 3: 2}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := LineDiff(tt.linesA, tt.linesB) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("LineDiff() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/internal/engine/matcher.go b/internal/engine/matcher.go index d885d0b..0e3ecbf 100644 --- a/internal/engine/matcher.go +++ b/internal/engine/matcher.go @@ -13,14 +13,20 @@ type MatchResult struct { Mappings *Mapping } -func Match(t1, t2 *treesitter.ASTNode) *MatchResult { +func Match(t1, t2 *treesitter.ASTNode, srcA, srcB []byte) *MatchResult { + mappings := NewMapping() + + part := NewLinePartition(srcA, srcB) + minHeight := 2 minDice := 0.5 - mappings := TopDown(t1, t2, minHeight) + + // Match AST nodes top-down, by declaration, and bottom-up using line partitioning. + TopDown(t1, t2, minHeight, mappings, part) matchDeclarations(t1, t2, mappings) BottomUp(t1, t2, mappings, minDice) - MatchUnmatchedLeaves(t1, t2, mappings) + MatchUnmatchedLeaves(t1, t2, mappings, part) if !mappings.Has(t1) && !mappings.HasDst(t2) { mappings.Add(t1, t2) @@ -34,7 +40,7 @@ func Match(t1, t2 *treesitter.ASTNode) *MatchResult { // MatchUnmatchedLeaves pairs unmatched leaf nodes of the same type and label using // parent Dice similarity and positional scores to break ties. Leaves under unmatched // parents are skipped since they belong to deleted or inserted blocks. -func MatchUnmatchedLeaves(t1Root, t2Root *treesitter.ASTNode, m *Mapping) { +func MatchUnmatchedLeaves(t1Root, t2Root *treesitter.ASTNode, m *Mapping, part *LinePartition) { t2Nodes := PostOrder(t2Root) for _, t1 := range PostOrder(t1Root) { if m.Has(t1) || len(t1.Children) > 0 || t1.Label == "" { @@ -59,6 +65,9 @@ func MatchUnmatchedLeaves(t1Root, t2Root *treesitter.ASTNode, m *Mapping) { if m.HasDst(t2) || t2.Type != t1.Type || t2.Label != t1.Label || len(t2.Children) > 0 { continue } + if part != nil && !part.CanMatch(t1, t2) { + continue + } // No matched parent on destination leaf -> it belongs to an inserted subtree. if t2.Parent != nil && !m.HasDst(t2.Parent) { diff --git a/internal/engine/matcher_test.go b/internal/engine/matcher_test.go index afa5e51..d535abe 100644 --- a/internal/engine/matcher_test.go +++ b/internal/engine/matcher_test.go @@ -22,7 +22,7 @@ func TestMatchIdenticalTrees(t *testing.T) { ), ) - r := Match(src, dst) + r := Match(src, dst, nil, nil) if r == nil || r.Mappings == nil { t.Fatal("Match returned nil") } @@ -40,7 +40,7 @@ func TestMatchDifferentLeafLabels(t *testing.T) { src := testutil.Node("func", "main", testutil.Leaf("id", "x")) dst := testutil.Node("func", "main", testutil.Leaf("id", "y")) - r := Match(src, dst) + r := Match(src, dst, nil, nil) if r == nil { t.Fatal("Match returned nil") return @@ -55,7 +55,7 @@ func TestMatchRootAlwaysMapped(t *testing.T) { src := testutil.Node("func", "a", testutil.Leaf("id", "x")) dst := testutil.Node("func", "b", testutil.Leaf("str", "hello")) - r := Match(src, dst) + r := Match(src, dst, nil, nil) if !r.Mappings.Has(src) { t.Error("src root should always be mapped") } @@ -67,7 +67,7 @@ func TestMatchRootAlwaysMapped(t *testing.T) { func TestMatchSingleLeaves(t *testing.T) { src := testutil.Leaf("id", "x") dst := testutil.Leaf("id", "x") - r := Match(src, dst) + r := Match(src, dst, nil, nil) if !r.Mappings.Has(src) || r.Mappings.Src()[src] != dst { t.Error("single identical leaves should be mapped") } @@ -80,7 +80,7 @@ func TestMatchPairsPreOrder(t *testing.T) { src := testutil.Node("block", "", c1, c2) dst := testutil.Node("block", "", testutil.Leaf("id", "x"), testutil.Leaf("id", "y")) - r := Match(src, dst) + r := Match(src, dst, nil, nil) if len(r.Mappings.Pairs) < 3 { t.Fatalf("expected at least 3 pairs, got %d", len(r.Mappings.Pairs)) } @@ -115,7 +115,8 @@ func TestTopDownUnambiguous(t *testing.T) { testutil.Node("call", "", testutil.Leaf("id", "f")), ) - m := TopDown(src, dst, 2) + m := NewMapping() + TopDown(src, dst, 2, m, nil) srcCall := src.Children[0] if !m.Has(srcCall) { t.Error("unambiguous isomorphic subtree should be mapped by TopDown") diff --git a/internal/engine/partition.go b/internal/engine/partition.go new file mode 100644 index 0000000..42da6a6 --- /dev/null +++ b/internal/engine/partition.go @@ -0,0 +1,84 @@ +package engine + +import ( + "strings" + + "github.com/HarshK97/diffmantic/internal/treesitter" +) + +// LinePartition partitions AST nodes into untouched lines (Group 1) and edited +// lines (Group 2) using line-level LCS matching ("Beyond GumTree"). This stops +// untouched lines from matching against edited regions across the file. +type LinePartition struct { + matchedA map[int]int // rowA -> rowB + matchedB map[int]int // rowB -> rowA +} + +func NewLinePartition(srcA, srcB []byte) *LinePartition { + linesA := strings.Split(string(srcA), "\n") + linesB := strings.Split(string(srcB), "\n") + mA := LineDiff(linesA, linesB) + mB := make(map[int]int, len(mA)) + for rA, rB := range mA { + mB[rB] = rA + } + return &LinePartition{matchedA: mA, matchedB: mB} +} + +// IsGroup1A returns true if all lines spanned by n1 in file A are non-edited lines. +func (p *LinePartition) IsGroup1A(n1 *treesitter.ASTNode) (bool, int) { + if p == nil { + return false, -1 + } + return p.isGroup1(p.matchedA, n1) +} + +// IsGroup1B returns true if all lines spanned by n2 in file B are non-edited lines. +func (p *LinePartition) IsGroup1B(n2 *treesitter.ASTNode) (bool, int) { + if p == nil { + return false, -1 + } + return p.isGroup1(p.matchedB, n2) +} + +func (p *LinePartition) isGroup1(matched map[int]int, n *treesitter.ASTNode) (bool, int) { + if len(matched) == 0 || n == nil { + return false, -1 + } + startRow := int(n.StartRow) + endRow := int(n.EndRow) + if startRow < 0 { + return false, -1 + } + startOther, ok := matched[startRow] + if !ok { + return false, -1 + } + for r := startRow; r <= endRow; r++ { + m, ok := matched[r] + if !ok || m != startOther+(r-startRow) { + return false, -1 + } + } + return true, startOther +} + +// CanMatch checks if two nodes are allowed to pair up: +// - Untouched nodes (Group 1) only match untouched nodes on corresponding lines. +// - Edited nodes (Group 2) only match other edited nodes. +// - Cross-matching between untouched and edited nodes is forbidden. +func (p *LinePartition) CanMatch(n1, n2 *treesitter.ASTNode) bool { + if p == nil || n1 == nil || n2 == nil { + return true + } + isG1A, startB := p.IsGroup1A(n1) + isG1B, startA := p.IsGroup1B(n2) + + if isG1A && isG1B { + return int(n2.StartRow) == startB && int(n1.StartRow) == startA + } + if isG1A != isG1B { + return false + } + return true +} diff --git a/internal/engine/partition_test.go b/internal/engine/partition_test.go new file mode 100644 index 0000000..270735e --- /dev/null +++ b/internal/engine/partition_test.go @@ -0,0 +1,69 @@ +package engine + +import ( + "testing" + + "github.com/HarshK97/diffmantic/internal/treesitter" +) + +func TestLinePartition(t *testing.T) { + t.Run("Identical lines classification and CanMatch", func(t *testing.T) { + srcA := []byte("func main() {\n\tx := 1\n}\n") + srcB := []byte("func main() {\n\tx := 1\n}\n") + + part := NewLinePartition(srcA, srcB) + + n1 := &treesitter.ASTNode{StartRow: 0, EndRow: 2} + n2 := &treesitter.ASTNode{StartRow: 0, EndRow: 2} + + if !part.CanMatch(n1, n2) { + t.Errorf("expected identical subtrees on matching lines to be allowed") + } + }) + + t.Run("Forbids cross-matching between Group 1 (non-edited) and Group 2 (edited)", func(t *testing.T) { + srcA := []byte("func main() {\n\t// untouched\n\tdeletedLine()\n}\n") + srcB := []byte("func main() {\n\t// untouched\n\tinsertedLine()\n}\n") + + part := NewLinePartition(srcA, srcB) + + n1 := &treesitter.ASTNode{StartRow: 1, EndRow: 1} + n2 := &treesitter.ASTNode{StartRow: 2, EndRow: 2} + + if part.CanMatch(n1, n2) { + t.Errorf("expected CanMatch to return false between Group 1 (non-edited) and Group 2 (edited) lines") + } + }) + + t.Run("Allows matching between Group 2 (edited) and Group 2 (edited) nodes", func(t *testing.T) { + srcA := []byte("func oldFunc() {\n\tdeletedCode()\n}\n") + srcB := []byte("func newFunc() {\n\tinsertedCode()\n}\n") + + part := NewLinePartition(srcA, srcB) + + n1 := &treesitter.ASTNode{StartRow: 1, EndRow: 1} + n2 := &treesitter.ASTNode{StartRow: 1, EndRow: 1} + + if !part.CanMatch(n1, n2) { + t.Errorf("expected CanMatch to return true between two Group 2 (edited) nodes") + } + }) + + t.Run("Forbids Group 1 matching on non-corresponding line offsets", func(t *testing.T) { + srcA := []byte("lineA0\nlineA1\nlineA2\n") + srcB := []byte("header\nlineA0\nlineA1\nlineA2\n") + + part := NewLinePartition(srcA, srcB) + + n1 := &treesitter.ASTNode{StartRow: 0, EndRow: 0} + n2Correct := &treesitter.ASTNode{StartRow: 1, EndRow: 1} + n2Wrong := &treesitter.ASTNode{StartRow: 2, EndRow: 2} + + if !part.CanMatch(n1, n2Correct) { + t.Errorf("expected CanMatch to return true for mapped line offsets") + } + if part.CanMatch(n1, n2Wrong) { + t.Errorf("expected CanMatch to return false for non-corresponding Group 1 lines") + } + }) +} diff --git a/internal/engine/top-down.go b/internal/engine/top-down.go index c8399b0..49dfd50 100644 --- a/internal/engine/top-down.go +++ b/internal/engine/top-down.go @@ -11,11 +11,12 @@ import ( func TopDown( t1Root, t2Root *treesitter.ASTNode, minHeight int, -) *Mapping { + m *Mapping, + part *LinePartition, +) { l1 := newPriorityList() l2 := newPriorityList() var A [][2]*treesitter.ASTNode // canditdate mappings - m := NewMapping() // final mapping l1.Push(t1Root) l2.Push(t2Root) @@ -37,6 +38,9 @@ func TopDown( for _, t1 := range H1 { for _, t2 := range H2 { + if part != nil && !part.CanMatch(t1, t2) { + continue + } if Isomorphic(t1, t2) { ambiguous := false @@ -124,8 +128,6 @@ func TopDown( return p[0] == t1 || p[1] == t2 }) } - - return m } type priorityList struct { diff --git a/internal/engine/topdown_test.go b/internal/engine/topdown_test.go index f6acdfe..eef87c5 100644 --- a/internal/engine/topdown_test.go +++ b/internal/engine/topdown_test.go @@ -21,7 +21,8 @@ func TestTopDown(t *testing.T) { ), ) - m := TopDown(src, dst, 1) + m := NewMapping() + TopDown(src, dst, 1, m, nil) for _, n := range PreOrder(src) { if !m.Has(n) { @@ -43,7 +44,8 @@ func TestTopDown(t *testing.T) { testutil.Node("block", "", dstLeaf1, dstLeaf2), ) - m := TopDown(src, dst, 1) + m := NewMapping() + TopDown(src, dst, 1, m, nil) // TopDown matches isomorphic subtrees like srcLeaf1. if !m.Has(srcLeaf1) { @@ -62,7 +64,8 @@ func TestTopDown(t *testing.T) { dstLeaf2 := testutil.Leaf("id", "y") dst := testutil.Node("block", "", dstLeaf1, dstLeaf2) - m := TopDown(src, dst, 1) + m := NewMapping() + TopDown(src, dst, 1, m, nil) if !m.Has(srcLeaf) { t.Errorf("expected original leaf to be mapped") @@ -79,7 +82,8 @@ func TestTopDown(t *testing.T) { dstSub := testutil.Node("call", "", testutil.Leaf("id", "f")) dst := testutil.Node("func", "b", dstSub) - m := TopDown(src, dst, 1) + m := NewMapping() + TopDown(src, dst, 1, m, nil) if !m.Has(srcSub) { t.Errorf("expected identical subtree to be mapped") @@ -120,7 +124,8 @@ func TestBottomUp(t *testing.T) { dstBlock := testutil.Node("block", "", dstLeaf1, dstLeaf2) dstRoot := testutil.Node("func", "main", dstBlock) - m := TopDown(srcRoot, dstRoot, 1) + m := NewMapping() + TopDown(srcRoot, dstRoot, 1, m, nil) BottomUp(srcRoot, dstRoot, m, 0.5) if !m.Has(srcRoot) { @@ -138,7 +143,8 @@ func TestBottomUp(t *testing.T) { dstLeaf := testutil.Leaf("str", "hello") dst := testutil.Node("func", "b", dstLeaf) - m := TopDown(src, dst, 1) + m := NewMapping() + TopDown(src, dst, 1, m, nil) BottomUp(src, dst, m, 0.5) // BottomUp might map the root as a fallback, but leaves shouldn't map. diff --git a/internal/serialize/json.go b/internal/serialize/json.go index 1f4b91e..06c63c4 100644 --- a/internal/serialize/json.go +++ b/internal/serialize/json.go @@ -55,6 +55,64 @@ type NodeRef struct { EndByte uint32 `json:"end_byte"` } +// BuildLineDiffEnvelope creates a line-level diff envelope when tree-sitter can't parse a file. +func BuildLineDiffEnvelope(srcBytes, dstBytes []byte) *Envelope { + alignment := AlignLines(srcBytes, dstBytes, nil, nil, nil, nil) + + buildLineOffsets := func(data []byte) []uint32 { + offsets := []uint32{0} + for i, b := range data { + if b == '\n' { + offsets = append(offsets, uint32(i+1)) + } + } + return offsets + } + + offsetsSrc := buildLineOffsets(srcBytes) + offsetsDst := buildLineOffsets(dstBytes) + + getBounds := func(lineIdx int, offsets []uint32, maxLen int) (uint32, uint32) { + end := uint32(maxLen) + if lineIdx+1 < len(offsets) { + end = offsets[lineIdx+1] + } + return offsets[lineIdx], end + } + + var actionsList []Action + + for _, pair := range alignment { + if pair.RightLine == -1 && pair.LeftLine != -1 { + start, end := getBounds(pair.LeftLine, offsetsSrc, len(srcBytes)) + actionsList = append(actionsList, Action{ + Action: "delete", + Node: &NodeRef{ + Tree: "before", + StartByte: start, + EndByte: end, + }, + }) + } else if pair.LeftLine == -1 && pair.RightLine != -1 { + start, end := getBounds(pair.RightLine, offsetsDst, len(dstBytes)) + actionsList = append(actionsList, Action{ + Action: "insert", + Node: &NodeRef{ + Tree: "after", + StartByte: start, + EndByte: end, + }, + }) + } + } + + return &Envelope{ + Version: SchemaVersion, + Actions: actionsList, + LineAlignment: alignment, + } +} + // BuildEnvelope bundles the edit script, AST mappings, and metadata into a unified envelope. func BuildEnvelope(es *actions.EditScript, ms *engine.Mapping, srcRoot, dstRoot *treesitter.ASTNode, srcBytes, dstBytes []byte) (*Envelope, error) { if es == nil { diff --git a/internal/tui/model.go b/internal/tui/model.go index 4206cc7..179a5dd 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -376,72 +376,18 @@ func hasConflictMarkers(data []byte) bool { return bytes.Contains(data, []byte("<<<<<<<")) || bytes.Contains(data, []byte("=======")) || bytes.Contains(data, []byte(">>>>>>>")) } -func generateLineDiff(srcBytes, dstBytes []byte) *serialize.Envelope { - alignment := serialize.AlignLines(srcBytes, dstBytes, nil, nil, nil, nil) - - buildLineOffsets := func(data []byte) []uint32 { - offsets := []uint32{0} - for i, b := range data { - if b == '\n' { - offsets = append(offsets, uint32(i+1)) - } - } - return offsets - } - - offsetsSrc := buildLineOffsets(srcBytes) - offsetsDst := buildLineOffsets(dstBytes) - - getBounds := func(lineIdx int, offsets []uint32, maxLen int) (uint32, uint32) { - end := uint32(maxLen) - if lineIdx+1 < len(offsets) { - end = offsets[lineIdx+1] - } - return offsets[lineIdx], end - } - - var actions []serialize.Action - - for _, pair := range alignment { - if pair.RightLine == -1 && pair.LeftLine != -1 { - start, end := getBounds(pair.LeftLine, offsetsSrc, len(srcBytes)) - actions = append(actions, serialize.Action{ - Action: "delete", - Node: &serialize.NodeRef{ - StartByte: start, - EndByte: end, - }, - }) - } else if pair.LeftLine == -1 && pair.RightLine != -1 { - start, end := getBounds(pair.RightLine, offsetsDst, len(dstBytes)) - actions = append(actions, serialize.Action{ - Action: "insert", - Node: &serialize.NodeRef{ - StartByte: start, - EndByte: end, - }, - }) - } - } - - return &serialize.Envelope{ - Actions: actions, - LineAlignment: alignment, - } -} - func computeBytesDiff(srcBytes, dstBytes []byte, srcFile, dstFile string, isConflict bool) (*serialize.Envelope, error) { if isConflict || hasConflictMarkers(srcBytes) || hasConflictMarkers(dstBytes) { - return generateLineDiff(srcBytes, dstBytes), nil + return serialize.BuildLineDiffEnvelope(srcBytes, dstBytes), nil } srcAST, _ := treesitter.Parse(srcBytes, srcFile) dstAST, _ := treesitter.Parse(dstBytes, dstFile) if srcAST == nil || dstAST == nil { - return generateLineDiff(srcBytes, dstBytes), nil + return serialize.BuildLineDiffEnvelope(srcBytes, dstBytes), nil } - matchResult := engine.Match(srcAST, dstAST) + matchResult := engine.Match(srcAST, dstAST, srcBytes, dstBytes) es := actions.GenerateEditScript(srcAST, dstAST, matchResult.Mappings) es = postprocess.Run(es, matchResult.Mappings, srcAST, dstAST) diff --git a/tests/integration/pipeline_bench_test.go b/tests/integration/pipeline_bench_test.go index 44fa6e2..37586bc 100644 --- a/tests/integration/pipeline_bench_test.go +++ b/tests/integration/pipeline_bench_test.go @@ -19,7 +19,7 @@ func BenchmarkPipeline(b *testing.B) { astA := mustParse(b, f.OldSrc, f.OldPath) astB := mustParse(b, f.NewSrc, f.NewPath) - result := engine.Match(astA, astB) + result := engine.Match(astA, astB, f.OldSrc, f.NewSrc) es := actions.GenerateEditScript(astA, astB, result.Mappings) es = postprocess.Run(es, result.Mappings, astA, astB) @@ -54,7 +54,7 @@ func BenchmarkMatch(b *testing.B) { for b.Loop() { astA := mustParse(b, f.OldSrc, f.OldPath) astB := mustParse(b, f.NewSrc, f.NewPath) - engine.Match(astA, astB) + engine.Match(astA, astB, f.OldSrc, f.NewSrc) } }) } @@ -69,7 +69,7 @@ func BenchmarkEditScript(b *testing.B) { for b.Loop() { astA := mustParse(b, f.OldSrc, f.OldPath) astB := mustParse(b, f.NewSrc, f.NewPath) - result := engine.Match(astA, astB) + result := engine.Match(astA, astB, f.OldSrc, f.NewSrc) actions.GenerateEditScript(astA, astB, result.Mappings) } }) @@ -85,7 +85,7 @@ func BenchmarkPostprocess(b *testing.B) { for b.Loop() { astA := mustParse(b, f.OldSrc, f.OldPath) astB := mustParse(b, f.NewSrc, f.NewPath) - result := engine.Match(astA, astB) + result := engine.Match(astA, astB, f.OldSrc, f.NewSrc) es := actions.GenerateEditScript(astA, astB, result.Mappings) postprocess.Run(es, result.Mappings, astA, astB) } @@ -102,7 +102,7 @@ func BenchmarkSerialize(b *testing.B) { for b.Loop() { astA := mustParse(b, f.OldSrc, f.OldPath) astB := mustParse(b, f.NewSrc, f.NewPath) - result := engine.Match(astA, astB) + result := engine.Match(astA, astB, f.OldSrc, f.NewSrc) es := actions.GenerateEditScript(astA, astB, result.Mappings) es = postprocess.Run(es, result.Mappings, astA, astB) if _, err := serialize.Marshal(es, result.Mappings, astA, astB, f.OldSrc, f.NewSrc); err != nil { diff --git a/tests/integration/pipeline_test.go b/tests/integration/pipeline_test.go index b17c578..3913f72 100644 --- a/tests/integration/pipeline_test.go +++ b/tests/integration/pipeline_test.go @@ -121,7 +121,7 @@ func runPipeline(t *testing.T, f fixture) pipelineResult { t.Fatalf("parsing %s: %v", f.NewPath, err) } - result := engine.Match(astA, astB) + result := engine.Match(astA, astB, f.OldSrc, f.NewSrc) es := actions.GenerateEditScript(astA, astB, result.Mappings) es = postprocess.Run(es, result.Mappings, astA, astB)