import { useMemo } from 'react' import hljs from 'highlight.js' import { Copy, Download } from 'lucide-react' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import type { GitBlob } from '@/lib/gitApi' interface FileViewerProps { blob: GitBlob loading?: boolean } // Syntax-highlighted file viewer with line numbers, copy, and download buttons. // Uses highlight.js for highlighting; binary files show a placeholder. export function FileViewer({ blob, loading }: FileViewerProps) { const { html, lineCount } = useMemo(() => { if (blob.isBinary || !blob.content) return { html: '', lineCount: 0 } const ext = blob.path.split('.').pop() ?? '' const result = hljs.getLanguage(ext) ? hljs.highlight(blob.content, { language: ext }) : hljs.highlightAuto(blob.content) return { html: result.value, lineCount: blob.content.split('\n').length, } }, [blob]) if (loading) return function copyToClipboard() { navigator.clipboard.writeText(blob.content) } return (
{/* Metadata bar */}
{lineCount.toLocaleString()} lines · {formatBytes(blob.size)}
{blob.isBinary ? (
Binary file — {formatBytes(blob.size)}
) : ( // Line numbers are a fixed column; code scrolls horizontally independently. // Keeping them in separate divs avoids having to split highlighted HTML by line.
{Array.from({ length: lineCount }, (_, i) => (
{i + 1}
))}
            
          
)}
) } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / 1024 / 1024).toFixed(1)} MB` } function FileViewerSkeleton() { return (
{Array.from({ length: 20 }).map((_, i) => ( ))}
{Array.from({ length: 20 }).map((_, i) => ( ))}
) }