/** * diff.ts — Muse semantic diff page. * * Unlike Git, Muse knows *what semantically changed* (structured_delta) and * *which files truly changed* (snapshot_diff). We use both to render: * * 1. A stats bar — symbol counts, file counts (things Git cannot show) * 2. Per-file cards — semantic symbol changes above the line diff * 3. Real line diff — comparing parent vs current content, not just dumping * the whole file green */ import hljs from 'highlight.js/lib/core'; import { extToLang } from '../lang-detect.ts'; import python from 'highlight.js/lib/languages/python'; import typescript from 'highlight.js/lib/languages/typescript'; import javascript from 'highlight.js/lib/languages/javascript'; import rust from 'highlight.js/lib/languages/rust'; import go from 'highlight.js/lib/languages/go'; import swift from 'highlight.js/lib/languages/swift'; import kotlin from 'highlight.js/lib/languages/kotlin'; import java from 'highlight.js/lib/languages/java'; import ruby from 'highlight.js/lib/languages/ruby'; import cpp from 'highlight.js/lib/languages/cpp'; import json from 'highlight.js/lib/languages/json'; import yaml from 'highlight.js/lib/languages/yaml'; import toml from 'highlight.js/lib/languages/ini'; import bash from 'highlight.js/lib/languages/bash'; import xml from 'highlight.js/lib/languages/xml'; import css from 'highlight.js/lib/languages/css'; import sql from 'highlight.js/lib/languages/sql'; import markdown from 'highlight.js/lib/languages/markdown'; import plaintext from 'highlight.js/lib/languages/plaintext'; hljs.registerLanguage('python', python); hljs.registerLanguage('typescript', typescript); hljs.registerLanguage('javascript', javascript); hljs.registerLanguage('rust', rust); hljs.registerLanguage('go', go); hljs.registerLanguage('swift', swift); hljs.registerLanguage('kotlin', kotlin); hljs.registerLanguage('java', java); hljs.registerLanguage('ruby', ruby); hljs.registerLanguage('cpp', cpp); hljs.registerLanguage('json', json); hljs.registerLanguage('yaml', yaml); hljs.registerLanguage('toml', toml); hljs.registerLanguage('bash', bash); hljs.registerLanguage('xml', xml); hljs.registerLanguage('css', css); hljs.registerLanguage('sql', sql); hljs.registerLanguage('markdown', markdown); hljs.registerLanguage('plaintext', plaintext); // ── Types ───────────────────────────────────────────────────────────────────── interface ChildOp { address: string; op: string; content_summary: string; } interface FileOp { address: string; op: string; child_summary: string; child_ops: ChildOp[]; } interface StructuredDelta { domain: string; ops: FileOp[]; } interface SnapshotDiff { added: string[]; modified: string[]; removed: string[]; total_files: number; } interface CommitData { commitId: string; message: string; author: string; timestamp: string; branch: string; parentIds: string[]; } interface PageData { page: string; repoId: string; commitId: string; shortId: string; owner: string; repoSlug: string; baseUrl: string; parentId: string | null; structuredDelta: StructuredDelta | null; snapshotDiff: SnapshotDiff; commit: CommitData | null; viewerType: string; domainName: string; } // ── Helpers ─────────────────────────────────────────────────────────────────── function esc(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function highlight(code: string, lang: string): string { try { return hljs.highlight(code, { language: lang, ignoreIllegals: true }).value; } catch { return esc(code); } } /** Determine op class from operation string. */ function opClass(op: string): string { if (op === 'insert') return 'df3-op-add'; if (op === 'delete') return 'df3-op-del'; return 'df3-op-mod'; } function opSign(op: string): string { if (op === 'insert') return '+'; if (op === 'delete') return '−'; return '~'; } /** Detect kind from content_summary string. */ function kindChip(summary: string): string { const s = summary.toLowerCase(); if (s.includes('class')) return 'class'; if (s.includes('method')) return 'method'; if (s.includes('function') || s.includes('func')) return 'func'; if (s.includes('import')) return 'import'; if (s.includes('variable') || s.includes('constant')) return 'var'; return ''; } // ── Myers line diff ─────────────────────────────────────────────────────────── // Simple O(ND) diff — sufficient for typical commit sizes. type LineKind = 'add' | 'del' | 'ctx'; interface DiffLine { kind: LineKind; text: string; oldN: number; newN: number; } function diffLines(oldLines: string[], newLines: string[]): DiffLine[] { const m = oldLines.length, n = newLines.length; const max = m + n; const v: number[] = new Array(2 * max + 1).fill(0); const trace: number[][] = []; outer: for (let d = 0; d <= max; d++) { trace.push([...v]); for (let k = -d; k <= d; k += 2) { const ki = k + max; let x: number; if (k === -d || (k !== d && v[ki - 1] < v[ki + 1])) { x = v[ki + 1]; } else { x = v[ki - 1] + 1; } let y = x - k; while (x < m && y < n && oldLines[x] === newLines[y]) { x++; y++; } v[ki] = x; if (x >= m && y >= n) break outer; } } // Backtrack const result: DiffLine[] = []; let x = m, y = n; for (let d = trace.length - 1; d >= 0; d--) { const vd = trace[d]; const k = x - y; const ki = k + max; const prevK = (k === -d || (k !== d && vd[ki - 1] < vd[ki + 1])) ? k + 1 : k - 1; const prevX = vd[prevK + max]; const prevY = prevX - prevK; while (x > prevX && y > prevY) { result.unshift({ kind: 'ctx', text: oldLines[x - 1], oldN: x, newN: y }); x--; y--; } if (d > 0) { if (x === prevX) { result.unshift({ kind: 'add', text: newLines[y - 1], oldN: 0, newN: y }); y--; } else { result.unshift({ kind: 'del', text: oldLines[x - 1], oldN: x, newN: 0 }); x--; } } } return result; } /** Render a diff as an HTML table with syntax-highlighted tokens. */ function renderDiffTable(lines: DiffLine[], lang: string): string { // Highlight the full old and new texts then split, to preserve multi-line spans. const oldText = lines.filter(l => l.kind !== 'add').map(l => l.text).join('\n'); const newText = lines.filter(l => l.kind !== 'del').map(l => l.text).join('\n'); const oldHl = highlight(oldText, lang).split('\n'); const newHl = highlight(newText, lang).split('\n'); let oldI = 0, newI = 0; const rows = lines.map(l => { let hlCode: string; let rowCls: string; let sign: string; if (l.kind === 'del') { hlCode = oldHl[oldI++] ?? esc(l.text); rowCls = 'df3-dl-del'; sign = '−'; } else if (l.kind === 'add') { hlCode = newHl[newI++] ?? esc(l.text); rowCls = 'df3-dl-add'; sign = '+'; } else { hlCode = oldHl[oldI++] ?? esc(l.text); newI++; rowCls = 'df3-dl-ctx'; sign = ' '; } const ln = l.kind === 'del' ? l.oldN : l.kind === 'add' ? l.newN : l.oldN; return `
Could not load file content.
'}No file changes in this commit.
`; return; } // Build lookup: file path → FileOp from structured delta const deltaByFile = new Map