import { memo, useMemo, useState, useEffect, useCallback } from 'react'; import { useStore } from '@nanostores/react'; import { workbenchStore } from '~/lib/stores/workbench'; import type { FileMap } from '~/lib/stores/files'; import type { EditorDocument } from '~/components/editor/codemirror/CodeMirrorEditor'; import { diffLines, type Change } from 'diff'; import { getHighlighter } from 'shiki'; import '~/styles/diff-view.css'; import { diffFiles, extractRelativePath } from '~/utils/diff'; import { ActionRunner } from '~/lib/runtime/action-runner'; import type { FileHistory } from '~/types/actions'; import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension'; interface CodeComparisonProps { beforeCode: string; afterCode: string; language: string; filename: string; lightTheme: string; darkTheme: string; } interface DiffBlock { lineNumber: number; content: string; type: 'added' | 'removed' | 'unchanged'; correspondingLine?: number; } interface FullscreenButtonProps { onClick: () => void; isFullscreen: boolean; } const FullscreenButton = memo(({ onClick, isFullscreen }: FullscreenButtonProps) => ( )); const FullscreenOverlay = memo(({ isFullscreen, children }: { isFullscreen: boolean; children: React.ReactNode }) => { if (!isFullscreen) return <>{children}; return (
{children}
); }); const MAX_FILE_SIZE = 1024 * 1024; // 1MB const BINARY_REGEX = /[\x00-\x08\x0E-\x1F]/; const isBinaryFile = (content: string) => { return content.length > MAX_FILE_SIZE || BINARY_REGEX.test(content); }; const processChanges = (beforeCode: string, afterCode: string) => { try { if (isBinaryFile(beforeCode) || isBinaryFile(afterCode)) { return { beforeLines: [], afterLines: [], hasChanges: false, lineChanges: { before: new Set(), after: new Set() }, unifiedBlocks: [], isBinary: true }; } // Normalizar quebras de linha para evitar falsos positivos const normalizedBefore = beforeCode.replace(/\r\n/g, '\n').trim(); const normalizedAfter = afterCode.replace(/\r\n/g, '\n').trim(); // Se os conteúdos são idênticos após normalização, não há mudanças if (normalizedBefore === normalizedAfter) { return { beforeLines: normalizedBefore.split('\n'), afterLines: normalizedAfter.split('\n'), hasChanges: false, lineChanges: { before: new Set(), after: new Set() }, unifiedBlocks: [] }; } // Processar as diferenças com configurações mais precisas const changes = diffLines(normalizedBefore, normalizedAfter, { newlineIsToken: true, ignoreWhitespace: false, ignoreCase: false }); // Mapear as mudanças com mais precisão const beforeLines = normalizedBefore.split('\n'); const afterLines = normalizedAfter.split('\n'); const lineChanges = { before: new Set(), after: new Set() }; let beforeLineNumber = 0; let afterLineNumber = 0; const unifiedBlocks = changes.map(change => { const lines = change.value.split('\n').filter(line => line.length > 0); if (change.added) { lines.forEach((_, i) => lineChanges.after.add(afterLineNumber + i)); const block = lines.map((line, i) => ({ lineNumber: afterLineNumber + i, content: line, type: 'added' as const })); afterLineNumber += lines.length; return block; } if (change.removed) { lines.forEach((_, i) => lineChanges.before.add(beforeLineNumber + i)); const block = lines.map((line, i) => ({ lineNumber: beforeLineNumber + i, content: line, type: 'removed' as const })); beforeLineNumber += lines.length; return block; } const block = lines.map((line, i) => ({ lineNumber: afterLineNumber + i, content: line, type: 'unchanged' as const, correspondingLine: beforeLineNumber + i })); beforeLineNumber += lines.length; afterLineNumber += lines.length; return block; }).flat(); return { beforeLines, afterLines, hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0, lineChanges, unifiedBlocks, isBinary: false }; } catch (error) { console.error('Error processing changes:', error); return { beforeLines: [], afterLines: [], hasChanges: false, lineChanges: { before: new Set(), after: new Set() }, unifiedBlocks: [], error: true, isBinary: false }; } }; const lineNumberStyles = "w-12 shrink-0 pl-2 py-0.5 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1"; const lineContentStyles = "px-4 py-0.5 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary"; const renderContentWarning = (type: 'binary' | 'error') => (

{type === 'binary' ? 'Binary file detected' : 'Error processing file'}

{type === 'binary' ? 'Diff view is not available for binary files' : 'Could not generate diff preview'}

); const NoChangesView = memo(({ beforeCode, language, highlighter }: { beforeCode: string; language: string; highlighter: any; }) => (

Files are identical

Both versions match exactly

Current Content
{beforeCode.split('\n').map((line, index) => (
{index + 1}
]*>/g, '') .replace(/<\/?code[^>]*>/g, '') : line }} />
))}
)); const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language, lightTheme, darkTheme }: CodeComparisonProps) => { const [isFullscreen, setIsFullscreen] = useState(false); const [highlighter, setHighlighter] = useState(null); const toggleFullscreen = useCallback(() => { setIsFullscreen(prev => !prev); }, []); const { unifiedBlocks, hasChanges, isBinary, error } = useMemo(() => processChanges(beforeCode, afterCode), [beforeCode, afterCode]); useEffect(() => { getHighlighter({ themes: ['github-dark'], langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'] }).then(setHighlighter); }, []); if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error'); const renderDiffBlock = (block: DiffBlock, index?: number) => { const key = index !== undefined ? `${block.lineNumber}-${index}` : block.lineNumber; const bgColor = { added: 'bg-green-500/20 border-l-4 border-green-500', removed: 'bg-red-500/20 border-l-4 border-red-500', unchanged: '' }[block.type]; const highlightedCode = highlighter ? highlighter.codeToHtml(block.content, { lang: language, theme: 'github-dark' }) : block.content; return (
{block.lineNumber + 1}
{block.type === 'added' && '+'} {block.type === 'removed' && '-'} {block.type === 'unchanged' && ' '} ]*>/g, '').replace(/<\/?code[^>]*>/g, '') }} />
); }; return (
{filename} {hasChanges ? ( Modified ) : ( No Changes )}
{hasChanges ? (
{unifiedBlocks.map((block, index) => renderDiffBlock(block, index))}
) : ( )}
); }); const SideBySideComparison = memo(({ beforeCode, afterCode, language, filename, lightTheme, darkTheme, }: CodeComparisonProps) => { const [isFullscreen, setIsFullscreen] = useState(false); const [highlighter, setHighlighter] = useState(null); const toggleFullscreen = useCallback(() => { setIsFullscreen(prev => !prev); }, []); const { beforeLines, afterLines, hasChanges, lineChanges, isBinary, error } = useMemo(() => processChanges(beforeCode, afterCode), [beforeCode, afterCode]); useEffect(() => { getHighlighter({ themes: ['github-dark'], langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'] }).then(setHighlighter); }, []); if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error'); const renderCode = (code: string) => { if (!highlighter) return code; const highlightedCode = highlighter.codeToHtml(code, { lang: language, theme: 'github-dark' }); return highlightedCode.replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, ''); }; return (
{filename} {hasChanges ? ( Modified ) : ( No Changes )}
{hasChanges ? (
{beforeLines.map((line, index) => (
{index + 1}
{lineChanges.before.has(index) ? '-' : ' '}
))}
{afterLines.map((line, index) => (
{index + 1}
{lineChanges.after.has(index) ? '+' : ' '}
))}
) : ( )}
); }); interface DiffViewProps { fileHistory: Record; setFileHistory: React.Dispatch>>; diffViewMode: 'inline' | 'side'; actionRunner: ActionRunner; } export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actionRunner }: DiffViewProps) => { const files = useStore(workbenchStore.files) as FileMap; const selectedFile = useStore(workbenchStore.selectedFile); const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument; const unsavedFiles = useStore(workbenchStore.unsavedFiles); useEffect(() => { if (selectedFile && currentDocument) { const file = files[selectedFile]; if (!file || !('content' in file)) return; const existingHistory = fileHistory[selectedFile]; const currentContent = currentDocument.value; const relativePath = extractRelativePath(selectedFile); const unifiedDiff = diffFiles( relativePath, existingHistory?.originalContent || file.content, currentContent ); if (unifiedDiff) { const newChanges = diffLines( existingHistory?.originalContent || file.content, currentContent ); const newHistory: FileHistory = { originalContent: existingHistory?.originalContent || file.content, lastModified: Date.now(), changes: [ ...(existingHistory?.changes || []), ...newChanges ].slice(-100), // Limitar histórico de mudanças versions: [ ...(existingHistory?.versions || []), { timestamp: Date.now(), content: currentContent } ].slice(-10), // Manter apenas as 10 últimas versões changeSource: 'auto-save' }; setFileHistory(prev => ({ ...prev, [selectedFile]: newHistory })); } } }, [selectedFile, currentDocument?.value, files, setFileHistory, unsavedFiles]); if (!selectedFile || !currentDocument) { return (
Select a file to view differences
); } const file = files[selectedFile]; const originalContent = file && 'content' in file ? file.content : ''; const currentContent = currentDocument.value; const history = fileHistory[selectedFile]; const effectiveOriginalContent = history?.originalContent || originalContent; const language = getLanguageFromExtension(selectedFile.split('.').pop() || ''); try { return (
{diffViewMode === 'inline' ? ( ) : ( )}
); } catch (error) { console.error('DiffView render error:', error); return (

Failed to render diff view

); } });