import React, { useMemo, useState } from 'react'; import { getBaseName } from '@renderer/utils/pathUtils'; import { createLogger } from '@shared/utils/logger'; import { Check, Copy, FileCode } from 'lucide-react'; const logger = createLogger('Component:CodeBlockViewer'); import { highlightLine } from './syntaxHighlighter'; // ============================================================================= // Types // ============================================================================= interface CodeBlockViewerProps { fileName: string; // e.g., "src/components/Header.tsx" content: string; // The actual file content language?: string; // Inferred from file extension if not provided startLine?: number; // If partial read, starting line endLine?: number; // If partial read, ending line maxHeight?: string; // CSS max-height class (default: "max-h-96") } // ============================================================================= // Language Detection // ============================================================================= const EXTENSION_LANGUAGE_MAP: Record = { // JavaScript/TypeScript '.ts': 'typescript', '.tsx': 'tsx', '.js': 'javascript', '.jsx': 'jsx', '.mjs': 'javascript', '.cjs': 'javascript', // Python '.py': 'python', '.pyw': 'python', '.pyx': 'python', // Web '.html': 'html', '.htm': 'html', '.css': 'css', '.scss': 'scss', '.sass': 'sass', '.less': 'less', // Data formats '.json': 'json', '.jsonl': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.toml': 'toml', '.xml': 'xml', // Shell '.sh': 'bash', '.bash': 'bash', '.zsh': 'zsh', '.fish': 'fish', // Systems '.rs': 'rust', '.go': 'go', '.c': 'c', '.h': 'c', '.cpp': 'cpp', '.cc': 'cpp', '.hpp': 'hpp', '.java': 'java', '.kt': 'kotlin', '.swift': 'swift', // Config '.env': 'env', '.gitignore': 'gitignore', '.dockerignore': 'dockerignore', '.md': 'markdown', '.mdx': 'mdx', // Other '.sql': 'sql', '.graphql': 'graphql', '.gql': 'graphql', '.vue': 'vue', '.svelte': 'svelte', '.rb': 'ruby', '.php': 'php', '.lua': 'lua', '.r': 'r', '.R': 'r', }; /** * Infer language from file name/extension. */ function inferLanguage(fileName: string): string { // Check for dotfiles with specific names const baseName = getBaseName(fileName); if (baseName === 'Dockerfile') return 'dockerfile'; if (baseName === 'Makefile') return 'makefile'; if (baseName.startsWith('.env')) return 'env'; // Extract extension const extMatch = /(\.[^./]+)$/.exec(fileName); if (extMatch) { const ext = extMatch[1].toLowerCase(); return EXTENSION_LANGUAGE_MAP[ext] ?? 'text'; } return 'text'; } // ============================================================================= // Component // ============================================================================= export const CodeBlockViewer: React.FC = ({ fileName, content, language, startLine = 1, endLine, maxHeight = 'max-h-96', }): React.JSX.Element => { const [isCopied, setIsCopied] = useState(false); // Infer language from file extension if not provided const detectedLanguage = language ?? inferLanguage(fileName); // Split content into lines const lines = useMemo(() => content.split('\n'), [content]); const totalLines = lines.length; // Calculate the actual line range for display const actualEndLine = endLine ?? startLine + totalLines - 1; // Handle copy const handleCopy = async (): Promise => { try { await navigator.clipboard.writeText(content); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch { logger.error('Failed to copy to clipboard'); } }; // Extract just the filename for display const displayFileName = getBaseName(fileName) || fileName; return (
{/* Header */}
{displayFileName} {(startLine > 1 || endLine) && ( (lines {startLine}-{actualEndLine}) )} {detectedLanguage}
{/* Copy button */}
{/* Code content */}
          
            {lines.map((line, index) => {
              const lineNumber = startLine + index;
              return (
                
{/* Line number */} {lineNumber} {/* Code line */} {highlightLine(line, detectedLanguage)}
); })}
); };