Side-by-side semantic diff tool with theme support and hookable integration
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: pair removed+added lines onto a single row

+61 -4
+1 -1
VERSION
··· 1 - 0.2.0 1 + 0.3.0
docs/screenshot.png

This is a binary file and will not be displayed.

+54 -3
internal/diff.go
··· 16 16 KindEqual LineKind = iota 17 17 KindAdded // present in "new" 18 18 KindRemoved // present in "old" 19 + KindChanged // paired removal+addition rendered on one row 19 20 ) 20 21 21 22 type DiffLine struct { ··· 26 27 } 27 28 28 29 func ComputeDiff(oldText, newText string) []DiffLine { 30 + return zipChanges(computeRawDiff(oldText, newText)) 31 + } 32 + 33 + // computeRawDiff returns the flat per-line diff without pairing removed/added 34 + // entries. Callers that run further post-processing (e.g. semantic promotion) 35 + // need the unpaired form; the public ComputeDiff wraps this with zipChanges. 36 + func computeRawDiff(oldText, newText string) []DiffLine { 29 37 dmp := diffmatchpatch.New() 30 38 // Use line-level diff so every segment is line-aligned. Character-level 31 39 // DiffMain can split lines in the middle, causing the line-counter to drift ··· 62 70 return lines 63 71 } 64 72 73 + // zipChanges pairs consecutive KindRemoved+KindAdded lines into KindChanged rows 74 + // so a side-by-side renderer can show the removal and its replacement on the 75 + // same visual row, instead of staggering them across two rows with blank space 76 + // opposite each side. 77 + func zipChanges(lines []DiffLine) []DiffLine { 78 + out := make([]DiffLine, 0, len(lines)) 79 + i := 0 80 + for i < len(lines) { 81 + if lines[i].Kind != KindRemoved { 82 + out = append(out, lines[i]) 83 + i++ 84 + continue 85 + } 86 + remStart := i 87 + for i < len(lines) && lines[i].Kind == KindRemoved { 88 + i++ 89 + } 90 + addStart := i 91 + for i < len(lines) && lines[i].Kind == KindAdded { 92 + i++ 93 + } 94 + removed := lines[remStart:addStart] 95 + added := lines[addStart:i] 96 + 97 + n := len(removed) 98 + if len(added) < n { 99 + n = len(added) 100 + } 101 + for k := 0; k < n; k++ { 102 + out = append(out, DiffLine{KindChanged, removed[k].OldLine, added[k].NewLine, removed[k].Text}) 103 + } 104 + for k := n; k < len(removed); k++ { 105 + out = append(out, removed[k]) 106 + } 107 + for k := n; k < len(added); k++ { 108 + out = append(out, added[k]) 109 + } 110 + } 111 + return out 112 + } 113 + 65 114 // ComputeSemanticDiff attempts a tree-sitter token-level diff, falling back to 66 115 // text diff if the language is unrecognised or the file fails to parse. 67 116 // Returns the diff lines and the mode used ("semantic" or "text"). ··· 142 191 143 192 // Start from the text diff (for correct alignment), then promote 144 193 // adjacent (removed, added) pairs that are both semantically unchanged 145 - // into equal lines. 146 - textLines := ComputeDiff(string(oldSrc), string(newSrc)) 147 - return promoteStyleEqual(textLines, changedOldLines, changedNewLines), "semantic" 194 + // into equal lines. Use the unpaired form so promoteStyleEqual can match 195 + // removed/added runs; pair them afterwards for side-by-side display. 196 + textLines := computeRawDiff(string(oldSrc), string(newSrc)) 197 + promoted := promoteStyleEqual(textLines, changedOldLines, changedNewLines) 198 + return zipChanges(promoted), "semantic" 148 199 } 149 200 150 201 // promoteStyleEqual collapses blocks of (KindRemoved*, KindAdded*) lines where
+6
main.go
··· 398 398 case adiff.KindAdded: 399 399 rBgHex = st.AddedBgHex 400 400 leftLine = "" 401 + case adiff.KindChanged: 402 + lBgHex = st.RemovedBgHex 403 + rBgHex = st.AddedBgHex 401 404 } 402 405 403 406 prefix := " " ··· 511 514 case adiff.KindAdded: 512 515 rBgHex = st.AddedBgHex 513 516 leftLine = "" 517 + case adiff.KindChanged: 518 + lBgHex = st.RemovedBgHex 519 + rBgHex = st.AddedBgHex 514 520 } 515 521 516 522 lNum := " "