@@ -1,6 +1,6 @@ 'eq','ai'=>$i,'bi'=>$i]; @@ -131,9 +132,9 @@ } if ($ud === '') { $ops = []; - for ($i=0; $i<$N && $i<$M; $i++) $ops[]=['op'=>'eq','ai'=>$i,'bi'=>$i]; - for ($i=$M; $i<$N; $i++) $ops[]=['op'=>'del','ai'=>$i]; - for ($j=$N; $j<$M; $j++) $ops[]=['op'=>'add','bi'=>$j]; + for ($i=0; $i'eq','ai'=>$i,'bi'=>$i]; + for ($i=$M; $i<$N; $i++) $ops[] = ['op'=>'del','ai'=>$i]; + for ($j=$N; $j<$M; $j++) $ops[] = ['op'=>'add','bi'=>$j]; return $ops; } @@ -144,25 +145,34 @@ foreach ($lines as $ln) { if ($ln === '' && $ln !== '0') continue; - if (preg_match('/^@@\s+-([0-9]+)(?:,([0-9]+))?\s+\+([0-9]+)(?:,([0-9]+))?\s+@@/', $ln, $m)) { - $startA = max(0, (int)$m[1] - 1); // 0-based + // Hunk header: @@ -aStart,aLen +bStart,bLen @@ + if (($ln[0] ?? '') === '@' && + preg_match('/^@@\s+-([0-9]+)(?:,([0-9]+))?\s+\+([0-9]+)(?:,([0-9]+))?\s+@@/', $ln, $m)) { + + $startA = max(0, (int)$m[1] - 1); // convert to 0-based $startB = max(0, (int)$m[3] - 1); - // Emit equal block before this hunk (between current cursors and hunk start) - $eq = min(max(0, $startA - $ai), max(0, $startB - $bi)); - for ($k=0; $k<$eq; $k++) { $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++; } + + // Between hunks: move forward by equal lines only + $gap = min(max(0, $startA - $ai), max(0, $startB - $bi)); + for ($k=0; $k<$gap; $k++) { $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++; } continue; } $tag = $ln[0] ?? ''; - if ($tag === ' ') { $ops[] = ['op'=>'eq', 'ai'=>$ai, 'bi'=>$bi]; $ai++; $bi++; } - elseif ($tag === '-') { $ops[] = ['op'=>'del', 'ai'=>$ai]; $ai++; } - elseif ($tag === '+') { $ops[] = ['op'=>'add', 'bi'=>$bi]; $bi++; } - else { - // headers '---', '+++', empty, etc > ignore + if ($tag === ' ') { // context (equal) + $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++; + } elseif ($tag === '-') { // deletion + $ops[] = ['op'=>'del','ai'=>$ai]; $ai++; + } elseif ($tag === '+') { // addition + $ops[] = ['op'=>'add','bi'=>$bi]; $bi++; + } elseif ($tag === '\\') { + // "\ No newline at end of file" > ignore + } else { + // headers '---' / '+++' or noise > ignore } } - // Trailing equals after last hunk + // Trailing equals after the last hunk $tailEq = min($N - $ai, $M - $bi); for ($k=0; $k<$tailEq; $k++) { $ops[] = ['op'=>'eq','ai'=>$ai,'bi'=>$bi]; $ai++; $bi++; } for (; $ai<$N; $ai++) $ops[] = ['op'=>'del','ai'=>$ai]; @@ -545,6 +555,66 @@ $ops = diff_lines_idx($leftLines, $rightLines, $normalizer); +/* ---------- change counts & exposure ---------- + * Treat an adjacent del-run followed by an add-run (or vice-versa) as "mods". + * Each paired line counts as 1 change in the total. + * Returns: [$adds, $dels, $mods, $total, $no_changes] + */ +function compute_change_counts(array $ops): array { + $adds = 0; $dels = 0; $mods = 0; + $n = count($ops); + for ($i = 0; $i < $n; ) { + $op = $ops[$i]['op'] ?? 'eq'; + if ($op !== 'add' && $op !== 'del') { $i++; continue; } + + // First run (all adds or all dels) + $t1 = $op; + $c1 = 0; + $j = $i; + while ($j < $n && ($ops[$j]['op'] ?? 'eq') === $t1) { $c1++; $j++; } + + // Optional immediately-adjacent opposite run + $t2 = ($t1 === 'add') ? 'del' : 'add'; + $c2 = 0; + $k = $j; + while ($k < $n && ($ops[$k]['op'] ?? 'eq') === $t2) { $c2++; $k++; } + + // Pair min(c1,c2) as modifications + $pair = min($c1, $c2); + $mods += $pair; + + if ($t1 === 'add') { + $adds += $c1 - $pair; + $dels += $c2 - $pair; + } else { + $dels += $c1 - $pair; + $adds += $c2 - $pair; + } + + // Advance past both runs + $i = ($c2 > 0) ? $k : $j; + } + + $total = $adds + $dels + $mods; // modified lines count as 1 + $no_changes = ($total === 0); + return [$adds, $dels, $mods, $total, $no_changes]; +} + +/* ---------- change counts (mods collapse -/+ into 1) ---------- */ +[$adds, $dels, $mods, $changed_total, $no_changes] = compute_change_counts($ops); + +header('X-Diff-No-Changes: ' . ($no_changes ? '1' : '0')); +header('X-Diff-Change-Add: ' . $adds); +header('X-Diff-Change-Del: ' . $dels); +header('X-Diff-Change-Mod: ' . $mods); +header('X-Diff-Change-Total: ' . $changed_total); + +$GLOBALS['diff_no_changes'] = $no_changes; +$GLOBALS['diff_changes_add'] = $adds; +$GLOBALS['diff_changes_del'] = $dels; +$GLOBALS['diff_changes_mod'] = $mods; // available if you want a separate badge +$GLOBALS['diff_changes_total'] = $changed_total; // theme uses this for ±T + /* ---------- Build tables server-side ---------- */ [$sideRows, $uniRows] = build_tables_idx($ops, $leftLines, $rightLines); @@ -579,7 +649,7 @@ // expose split pct to the view if needed by JS $GLOBALS['split_pct'] = $split_pct; -// Also expose new goodies for the toolbar (the theme may choose to use them) +// badges/toggle url for the theme $GLOBALS['diff_engine_badge'] = $diff_engine_badge; $GLOBALS['ignore_ws_on'] = $ignore_ws_on; $GLOBALS['ignore_ws_toggle'] = $ignore_ws_toggle_url;