diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index bc28776..b72dcba 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -944,25 +944,50 @@ function KeyGen({ toast }) { function diffLines(a, b) { const aLines = a.split('\n'); const bLines = b.split('\n'); - // Myers diff (simple LCS-based) const m = aLines.length, n = bLines.length; + // LCS dp const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0)); for (let i = m-1; i >= 0; i--) for (let j = n-1; j >= 0; j--) dp[i][j] = aLines[i] === bLines[j] ? dp[i+1][j+1]+1 : Math.max(dp[i+1][j], dp[i][j+1]); - - const result = []; + // Build flat ops + const ops = []; let i = 0, j = 0; while (i < m || j < n) { if (i < m && j < n && aLines[i] === bLines[j]) { - result.push({ type: 'same', text: aLines[i] }); i++; j++; + ops.push({type:'same', a:aLines[i], b:bLines[j]}); i++; j++; } else if (j < n && (i >= m || dp[i][j+1] >= dp[i+1][j])) { - result.push({ type: 'add', text: bLines[j] }); j++; + ops.push({type:'add', a:null, b:bLines[j]}); j++; } else { - result.push({ type: 'del', text: aLines[i] }); i++; + ops.push({type:'del', a:aLines[i], b:null}); i++; } } - return result; + // Pair up consecutive del+add into 'change' rows + const paired = []; + let k = 0; + while (k < ops.length) { + if (ops[k].type==='del' && k+1 < ops.length && ops[k+1].type==='add') { + paired.push({type:'change', a:ops[k].a, b:ops[k+1].b}); k+=2; + } else { + paired.push(ops[k]); k++; + } + } + return paired; +} + +// Compute set of changed line numbers (1-based) for a given side +function changedLineNums(pairs, side) { + const set = new Set(); + let ln = 0; + pairs.forEach(p => { + if (p.type==='same') { ln++; } + else if (p.type==='change') { ln++; set.add(ln); } + else if (p.type==='del' && side==='a') { ln++; set.add(ln); } + else if (p.type==='add' && side==='b') { ln++; set.add(ln); } + else if (p.type==='del' && side==='b') { /* b has no line here */ } + else if (p.type==='add' && side==='a') { /* a has no line here */ } + }); + return set; } function TextDiff({ mobile }) { @@ -986,17 +1011,39 @@ function TextDiff({ mobile }) { e.target.value = ''; } - const added = diff ? diff.filter(d=>d.type==='add').length : 0; - const removed = diff ? diff.filter(d=>d.type==='del').length : 0; + const added = diff ? diff.filter(d=>d.type==='add'||d.type==='change').length : 0; + const removed = diff ? diff.filter(d=>d.type==='del'||d.type==='change').length : 0; const same = diff ? diff.filter(d=>d.type==='same').length : 0; + // Which lines are changed in each textarea (for background highlight) + const changedA = diff ? changedLineNums(diff, 'a') : new Set(); + const changedB = diff ? changedLineNums(diff, 'b') : new Set(); + const ta = { ...S.inp, fontSize:12, fontFamily:"'Courier New',monospace", width:'100%', - boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:1.6 }; + boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:'1.6', padding:'7px 8px' }; + + function LineNumCol({ text, changedSet }) { + const lines = (text||' ').split('\n'); + return ( +