DevTools TextDiff: Unified Diff Format mit gepaarten Zeilennummern

This commit is contained in:
2026-06-01 11:41:21 +02:00
parent 9995571573
commit 3f63a6dcd7

View File

@@ -945,47 +945,37 @@ function diffLines(a, b) {
const aLines = a.split('\n');
const bLines = b.split('\n');
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]);
// Build flat ops
const ops = [];
let i = 0, j = 0;
// Build rows: same / del / add — consecutive del+add stay paired (same display line number)
const rows = []; // {type, lineA, lineB, text}
let i = 0, j = 0, lnA = 0, lnB = 0;
while (i < m || j < n) {
if (i < m && j < n && aLines[i] === bLines[j]) {
ops.push({type:'same', a:aLines[i], b:bLines[j]}); i++; j++;
lnA++; lnB++;
rows.push({type:'same', ln: lnA, text: aLines[i]});
i++; j++;
} else if (j < n && (i >= m || dp[i][j+1] >= dp[i+1][j])) {
ops.push({type:'add', a:null, b:bLines[j]}); j++;
lnB++;
rows.push({type:'add', ln: lnB, text: bLines[j]});
j++;
} else {
ops.push({type:'del', a:aLines[i], b:null}); i++;
lnA++;
rows.push({type:'del', ln: lnA, text: aLines[i]});
i++;
}
}
// 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;
return rows;
}
// Compute set of changed line numbers (1-based) for a given side
function changedLineNums(pairs, side) {
// Which line numbers (1-based) in text A/B are changed
function changedLineNums(rows, 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 */ }
rows.forEach(r => {
if (r.type === 'del' && side === 'a') set.add(r.ln);
if (r.type === 'add' && side === 'b') set.add(r.ln);
});
return set;
}
@@ -1011,11 +1001,10 @@ function TextDiff({ mobile }) {
e.target.value = '';
}
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 added = diff ? diff.filter(d=>d.type==='add').length : 0;
const removed = diff ? diff.filter(d=>d.type==='del').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();
@@ -1023,14 +1012,13 @@ function TextDiff({ mobile }) {
boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:'1.6', padding:'7px 8px' };
function LineNumCol({ text, changedSet }) {
const lines = (text||' ').split('\n');
return (
<div style={{
padding:'7px 6px', minWidth:36, textAlign:'right', userSelect:'none',
fontFamily:"'Courier New',monospace", fontSize:12, lineHeight:'1.6',
background:'rgba(0,0,0,0.2)', borderRight:'1px solid rgba(255,255,255,0.07)',
whiteSpace:'pre', flexShrink:0}}>
{lines.map((_, i) => (
{(text||' ').split('\n').map((_,i) => (
<div key={i} style={{
color: changedSet.has(i+1) ? '#ff6b9d' : 'rgba(255,255,255,0.25)',
background: changedSet.has(i+1) ? 'rgba(255,107,157,0.12)' : 'transparent',
@@ -1073,9 +1061,9 @@ function TextDiff({ mobile }) {
{!!diff && (
<div style={{display:'flex', gap:10, alignItems:'center', flexWrap:'wrap'}}>
<span style={{background:'rgba(255,107,157,0.15)', color:'#ff6b9d', fontFamily:'monospace',
fontSize:11, padding:'4px 10px', borderRadius:6}}> {removed} entfernt/geändert</span>
fontSize:11, padding:'4px 10px', borderRadius:6}}> {removed} entfernt</span>
<span style={{background:'rgba(78,205,196,0.15)', color:'#4ecdc4', fontFamily:'monospace',
fontSize:11, padding:'4px 10px', borderRadius:6}}>+ {added} hinzugefügt/geändert</span>
fontSize:11, padding:'4px 10px', borderRadius:6}}>+ {added} hinzugefügt</span>
<span style={{background:'rgba(255,255,255,0.05)', color:'rgba(255,255,255,0.45)', fontFamily:'monospace',
fontSize:11, padding:'4px 10px', borderRadius:6}}>= {same} gleich</span>
</div>
@@ -1085,73 +1073,29 @@ function TextDiff({ mobile }) {
{!!diff && (
<div style={{borderRadius:9, border:'1px solid rgba(255,255,255,0.08)',
overflow:'auto', maxHeight:520, background:'rgba(0,0,0,0.2)'}}>
{/* Header */}
<div style={{display:'grid', gridTemplateColumns:'36px 16px 1fr 36px 16px 1fr',
borderBottom:'1px solid rgba(255,255,255,0.1)', background:'rgba(255,255,255,0.04)'}}>
<div style={{gridColumn:'1/4', padding:'5px 10px', color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10, borderRight:'1px solid rgba(255,255,255,0.08)'}}>Text A (Original)</div>
<div style={{gridColumn:'4/7', padding:'5px 10px', color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10}}>Text B (Neu)</div>
</div>
<table style={{width:'100%', borderCollapse:'collapse', fontFamily:"'Courier New',monospace", fontSize:12, tableLayout:'fixed'}}>
<colgroup>
<col style={{width:36}}/><col style={{width:16}}/><col/>
<col style={{width:36}}/><col style={{width:16}}/><col/>
</colgroup>
<table style={{width:'100%', borderCollapse:'collapse', fontFamily:"'Courier New',monospace", fontSize:12}}>
<tbody>
{(()=>{
let lnA=0, lnB=0;
return diff.map((d,i)=>{
const isChange = d.type==='change';
const isDel = d.type==='del';
const isAdd = d.type==='add';
const isSame = d.type==='same';
if (isSame) { lnA++; lnB++; }
if (isChange) { lnA++; lnB++; }
if (isDel) lnA++;
if (isAdd) lnB++;
const bgA = (isDel||isChange) ? 'rgba(255,107,157,0.1)' : 'transparent';
const bgB = (isAdd||isChange) ? 'rgba(78,205,196,0.1)' : 'transparent';
const colA = isDel||isChange ? '#ffb3cc' : 'rgba(255,255,255,0.65)';
const colB = isAdd||isChange ? '#a8f0eb' : 'rgba(255,255,255,0.65)';
const symA = isDel?'': isChange?'~':'';
const symB = isAdd?'+': isChange?'~':'';
const symColA = isDel?'#ff6b9d': isChange?'#ffe66d':'rgba(255,255,255,0.15)';
const symColB = isAdd?'#4ecdc4': isChange?'#ffe66d':'rgba(255,255,255,0.15)';
return (
<tr key={i} style={{borderBottom:'1px solid rgba(255,255,255,0.03)'}}>
{/* Side A */}
<td style={{background:bgA, textAlign:'right', padding:'2px 6px', color:'rgba(255,255,255,0.2)',
borderRight:'1px solid rgba(255,255,255,0.05)', userSelect:'none', fontSize:11}}>
{(isDel||isChange||isSame) ? lnA : ''}
{diff.map((d,i) => (
<tr key={i} style={{
background: d.type==='add' ? 'rgba(78,205,196,0.1)' : d.type==='del' ? 'rgba(255,107,157,0.1)' : 'transparent',
borderBottom:'1px solid rgba(255,255,255,0.03)'}}>
<td style={{width:36, textAlign:'right', padding:'2px 8px', userSelect:'none',
color:'rgba(255,255,255,0.2)', fontSize:11,
borderRight:'1px solid rgba(255,255,255,0.05)'}}>
{d.ln}
</td>
<td style={{background:bgA, textAlign:'center', padding:'2px 3px', color:symColA,
fontWeight:700, fontSize:13, userSelect:'none',
borderRight:'1px solid rgba(255,255,255,0.06)'}}>
{symA}
<td style={{width:20, textAlign:'center', padding:'2px 4px', userSelect:'none',
color: d.type==='add'?'#4ecdc4': d.type==='del'?'#ff6b9d':'rgba(255,255,255,0.15)',
fontWeight:700, fontSize:14,
borderRight:'1px solid rgba(255,255,255,0.05)'}}>
{d.type==='add'?'+': d.type==='del'?'':''}
</td>
<td style={{background:bgA, padding:'3px 8px', whiteSpace:'pre-wrap', wordBreak:'break-all',
color:colA, borderRight:'2px solid rgba(255,255,255,0.06)'}}>
{(isDel||isChange||isSame) ? (d.a ?? d.text ?? ' ') : ''}
</td>
{/* Side B */}
<td style={{background:bgB, textAlign:'right', padding:'2px 6px', color:'rgba(255,255,255,0.2)',
borderRight:'1px solid rgba(255,255,255,0.05)', userSelect:'none', fontSize:11}}>
{(isAdd||isChange||isSame) ? lnB : ''}
</td>
<td style={{background:bgB, textAlign:'center', padding:'2px 3px', color:symColB,
fontWeight:700, fontSize:13, userSelect:'none',
borderRight:'1px solid rgba(255,255,255,0.06)'}}>
{symB}
</td>
<td style={{background:bgB, padding:'3px 8px', whiteSpace:'pre-wrap', wordBreak:'break-all',
color:colB}}>
{(isAdd||isChange||isSame) ? (d.b ?? d.text ?? ' ') : ''}
<td style={{padding:'3px 10px', whiteSpace:'pre-wrap', wordBreak:'break-all',
color: d.type==='add'?'#a8f0eb': d.type==='del'?'#ffb3cc':'rgba(255,255,255,0.65)'}}>
{d.text || ' '}
</td>
</tr>
);
});
})()}
))}
</tbody>
</table>
</div>