import type { DiffFile, DiffHunk } from '$lib/types/index'; export function parseDiff(raw: string): DiffFile[] { if (!raw.trim()) return []; const files: DiffFile[] = []; const fileChunks = raw.split(/^diff --git /m).filter(Boolean); for (const chunk of fileChunks) { const lines = chunk.split('\n'); let path = ''; let oldPath: string | undefined; // Parse file header const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/); if (headerMatch) { oldPath = headerMatch[1]; path = headerMatch[2]; if (oldPath === path) oldPath = undefined; } const language = getLanguage(path); const hunks: DiffHunk[] = []; let currentHunk: DiffHunk | null = null; let oldLineNum = 0; let newLineNum = 0; for (const line of lines) { const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)/); if (hunkMatch) { const oldStart = parseInt(hunkMatch[1], 10); const oldLines = hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1; const newStart = parseInt(hunkMatch[3], 10); const newLines = hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1; currentHunk = { header: line, oldStart, oldLines, newStart, newLines, lines: [] }; hunks.push(currentHunk); oldLineNum = oldStart; newLineNum = newStart; continue; } if (!currentHunk) continue; if (line.startsWith('+')) { currentHunk.lines.push({ type: 'add', content: line.substring(1), newNumber: newLineNum++ }); } else if (line.startsWith('-')) { currentHunk.lines.push({ type: 'del', content: line.substring(1), oldNumber: oldLineNum++ }); } else if (line.startsWith(' ') || line === '') { // Only treat as context if we're inside a hunk and it looks like a context line if (line.startsWith(' ')) { currentHunk.lines.push({ type: 'context', content: line.substring(1), oldNumber: oldLineNum++, newNumber: newLineNum++ }); } } else if (line.startsWith('\\')) { // "No newline at end of file" marker — skip } } if (path) { files.push({ path, oldPath, hunks, language }); } } return files; } export function getLanguage(filePath: string): string { const ext = filePath.split('.').pop()?.toLowerCase() || ''; const map: Record = { ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx', py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java', kt: 'kotlin', kts: 'kotlin', swift: 'swift', cs: 'csharp', fs: 'fsharp', php: 'php', r: 'r', lua: 'lua', dart: 'dart', scala: 'scala', groovy: 'groovy', pl: 'perl', pm: 'perl', ex: 'elixir', exs: 'elixir', erl: 'erlang', hs: 'haskell', clj: 'clojure', css: 'css', scss: 'scss', less: 'less', html: 'html', htm: 'html', xml: 'xml', svg: 'xml', svelte: 'svelte', vue: 'vue', json: 'json', jsonc: 'jsonc', yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini', md: 'markdown', mdx: 'mdx', sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'fish', ps1: 'powershell', sql: 'sql', graphql: 'graphql', gql: 'graphql', proto: 'proto', c: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', h: 'c', hpp: 'cpp', m: 'objective-c', mm: 'objective-cpp', zig: 'zig', nim: 'nim', tf: 'hcl', dockerfile: 'dockerfile', makefile: 'makefile', cmake: 'cmake' }; // Also check filename (no extension) const filename = filePath.split('/').pop()?.toLowerCase() || ''; const filenameMap: Record = { dockerfile: 'dockerfile', makefile: 'makefile', gemfile: 'ruby', rakefile: 'ruby', podfile: 'ruby' }; return map[ext] || filenameMap[filename] || 'text'; }