|
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'; |
|
import { themeStore } from '~/lib/stores/theme'; |
|
|
|
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; |
|
charChanges?: Array<{ |
|
value: string; |
|
type: 'added' | 'removed' | 'unchanged'; |
|
}>; |
|
} |
|
|
|
interface FullscreenButtonProps { |
|
onClick: () => void; |
|
isFullscreen: boolean; |
|
} |
|
|
|
const FullscreenButton = memo(({ onClick, isFullscreen }: FullscreenButtonProps) => ( |
|
<button |
|
onClick={onClick} |
|
className="ml-4 p-1 rounded hover:bg-bolt-elements-background-depth-3 text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-colors" |
|
title={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} |
|
> |
|
<div className={isFullscreen ? 'i-ph:corners-in' : 'i-ph:corners-out'} /> |
|
</button> |
|
)); |
|
|
|
const FullscreenOverlay = memo(({ isFullscreen, children }: { isFullscreen: boolean; children: React.ReactNode }) => { |
|
if (!isFullscreen) { |
|
return <>{children}</>; |
|
} |
|
|
|
return ( |
|
<div className="fixed inset-0 z-[9999] bg-black/50 flex items-center justify-center p-6"> |
|
<div className="w-full h-full max-w-[90vw] max-h-[90vh] bg-bolt-elements-background-depth-2 rounded-lg border border-bolt-elements-borderColor shadow-xl overflow-hidden"> |
|
{children} |
|
</div> |
|
</div> |
|
); |
|
}); |
|
|
|
const MAX_FILE_SIZE = 1024 * 1024; |
|
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, |
|
}; |
|
} |
|
|
|
|
|
const normalizeContent = (content: string): string[] => { |
|
return content |
|
.replace(/\r\n/g, '\n') |
|
.split('\n') |
|
.map((line) => line.trimEnd()); |
|
}; |
|
|
|
const beforeLines = normalizeContent(beforeCode); |
|
const afterLines = normalizeContent(afterCode); |
|
|
|
|
|
if (beforeLines.join('\n') === afterLines.join('\n')) { |
|
return { |
|
beforeLines, |
|
afterLines, |
|
hasChanges: false, |
|
lineChanges: { before: new Set(), after: new Set() }, |
|
unifiedBlocks: [], |
|
isBinary: false, |
|
}; |
|
} |
|
|
|
const lineChanges = { |
|
before: new Set<number>(), |
|
after: new Set<number>(), |
|
}; |
|
|
|
const unifiedBlocks: DiffBlock[] = []; |
|
|
|
|
|
let i = 0, |
|
j = 0; |
|
|
|
while (i < beforeLines.length || j < afterLines.length) { |
|
if (i < beforeLines.length && j < afterLines.length && beforeLines[i] === afterLines[j]) { |
|
|
|
unifiedBlocks.push({ |
|
lineNumber: j, |
|
content: afterLines[j], |
|
type: 'unchanged', |
|
correspondingLine: i, |
|
}); |
|
i++; |
|
j++; |
|
} else { |
|
|
|
let matchFound = false; |
|
const lookAhead = 3; |
|
|
|
|
|
for (let k = 1; k <= lookAhead && i + k < beforeLines.length && j + k < afterLines.length; k++) { |
|
if (beforeLines[i + k] === afterLines[j]) { |
|
|
|
for (let l = 0; l < k; l++) { |
|
lineChanges.before.add(i + l); |
|
unifiedBlocks.push({ |
|
lineNumber: i + l, |
|
content: beforeLines[i + l], |
|
type: 'removed', |
|
correspondingLine: j, |
|
charChanges: [{ value: beforeLines[i + l], type: 'removed' }], |
|
}); |
|
} |
|
i += k; |
|
matchFound = true; |
|
break; |
|
} else if (beforeLines[i] === afterLines[j + k]) { |
|
|
|
for (let l = 0; l < k; l++) { |
|
lineChanges.after.add(j + l); |
|
unifiedBlocks.push({ |
|
lineNumber: j + l, |
|
content: afterLines[j + l], |
|
type: 'added', |
|
correspondingLine: i, |
|
charChanges: [{ value: afterLines[j + l], type: 'added' }], |
|
}); |
|
} |
|
j += k; |
|
matchFound = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!matchFound) { |
|
|
|
if (i < beforeLines.length && j < afterLines.length) { |
|
const beforeLine = beforeLines[i]; |
|
const afterLine = afterLines[j]; |
|
|
|
|
|
let prefixLength = 0; |
|
|
|
while ( |
|
prefixLength < beforeLine.length && |
|
prefixLength < afterLine.length && |
|
beforeLine[prefixLength] === afterLine[prefixLength] |
|
) { |
|
prefixLength++; |
|
} |
|
|
|
let suffixLength = 0; |
|
|
|
while ( |
|
suffixLength < beforeLine.length - prefixLength && |
|
suffixLength < afterLine.length - prefixLength && |
|
beforeLine[beforeLine.length - 1 - suffixLength] === afterLine[afterLine.length - 1 - suffixLength] |
|
) { |
|
suffixLength++; |
|
} |
|
|
|
const prefix = beforeLine.slice(0, prefixLength); |
|
const beforeMiddle = beforeLine.slice(prefixLength, beforeLine.length - suffixLength); |
|
const afterMiddle = afterLine.slice(prefixLength, afterLine.length - suffixLength); |
|
const suffix = beforeLine.slice(beforeLine.length - suffixLength); |
|
|
|
if (beforeMiddle || afterMiddle) { |
|
|
|
if (beforeMiddle) { |
|
lineChanges.before.add(i); |
|
unifiedBlocks.push({ |
|
lineNumber: i, |
|
content: beforeLine, |
|
type: 'removed', |
|
correspondingLine: j, |
|
charChanges: [ |
|
{ value: prefix, type: 'unchanged' }, |
|
{ value: beforeMiddle, type: 'removed' }, |
|
{ value: suffix, type: 'unchanged' }, |
|
], |
|
}); |
|
i++; |
|
} |
|
|
|
if (afterMiddle) { |
|
lineChanges.after.add(j); |
|
unifiedBlocks.push({ |
|
lineNumber: j, |
|
content: afterLine, |
|
type: 'added', |
|
correspondingLine: i - 1, |
|
charChanges: [ |
|
{ value: prefix, type: 'unchanged' }, |
|
{ value: afterMiddle, type: 'added' }, |
|
{ value: suffix, type: 'unchanged' }, |
|
], |
|
}); |
|
j++; |
|
} |
|
} else { |
|
|
|
if (i < beforeLines.length) { |
|
lineChanges.before.add(i); |
|
unifiedBlocks.push({ |
|
lineNumber: i, |
|
content: beforeLines[i], |
|
type: 'removed', |
|
correspondingLine: j, |
|
charChanges: [{ value: beforeLines[i], type: 'removed' }], |
|
}); |
|
i++; |
|
} |
|
|
|
if (j < afterLines.length) { |
|
lineChanges.after.add(j); |
|
unifiedBlocks.push({ |
|
lineNumber: j, |
|
content: afterLines[j], |
|
type: 'added', |
|
correspondingLine: i - 1, |
|
charChanges: [{ value: afterLines[j], type: 'added' }], |
|
}); |
|
j++; |
|
} |
|
} |
|
} else { |
|
|
|
if (i < beforeLines.length) { |
|
lineChanges.before.add(i); |
|
unifiedBlocks.push({ |
|
lineNumber: i, |
|
content: beforeLines[i], |
|
type: 'removed', |
|
correspondingLine: j, |
|
charChanges: [{ value: beforeLines[i], type: 'removed' }], |
|
}); |
|
i++; |
|
} |
|
|
|
if (j < afterLines.length) { |
|
lineChanges.after.add(j); |
|
unifiedBlocks.push({ |
|
lineNumber: j, |
|
content: afterLines[j], |
|
type: 'added', |
|
correspondingLine: i - 1, |
|
charChanges: [{ value: afterLines[j], type: 'added' }], |
|
}); |
|
j++; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
const processedBlocks = unifiedBlocks.sort((a, b) => a.lineNumber - b.lineNumber); |
|
|
|
return { |
|
beforeLines, |
|
afterLines, |
|
hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0, |
|
lineChanges, |
|
unifiedBlocks: processedBlocks, |
|
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-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1'; |
|
const lineContentStyles = |
|
'px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary'; |
|
const diffPanelStyles = 'h-full overflow-auto diff-panel-content'; |
|
|
|
|
|
const diffLineStyles = { |
|
added: 'bg-green-500/10 dark:bg-green-500/20 border-l-4 border-green-500', |
|
removed: 'bg-red-500/10 dark:bg-red-500/20 border-l-4 border-red-500', |
|
unchanged: '', |
|
}; |
|
|
|
const changeColorStyles = { |
|
added: 'text-green-700 dark:text-green-500 bg-green-500/10 dark:bg-green-500/20', |
|
removed: 'text-red-700 dark:text-red-500 bg-red-500/10 dark:bg-red-500/20', |
|
unchanged: 'text-bolt-elements-textPrimary', |
|
}; |
|
|
|
const renderContentWarning = (type: 'binary' | 'error') => ( |
|
<div className="h-full flex items-center justify-center p-4"> |
|
<div className="text-center text-bolt-elements-textTertiary"> |
|
<div className={`i-ph:${type === 'binary' ? 'file-x' : 'warning-circle'} text-4xl text-red-400 mb-2 mx-auto`} /> |
|
<p className="font-medium text-bolt-elements-textPrimary"> |
|
{type === 'binary' ? 'Binary file detected' : 'Error processing file'} |
|
</p> |
|
<p className="text-sm mt-1"> |
|
{type === 'binary' ? 'Diff view is not available for binary files' : 'Could not generate diff preview'} |
|
</p> |
|
</div> |
|
</div> |
|
); |
|
|
|
const NoChangesView = memo( |
|
({ |
|
beforeCode, |
|
language, |
|
highlighter, |
|
theme, |
|
}: { |
|
beforeCode: string; |
|
language: string; |
|
highlighter: any; |
|
theme: string; |
|
}) => ( |
|
<div className="h-full flex flex-col items-center justify-center p-4"> |
|
<div className="text-center text-bolt-elements-textTertiary"> |
|
<div className="i-ph:files text-4xl text-green-400 mb-2 mx-auto" /> |
|
<p className="font-medium text-bolt-elements-textPrimary">Files are identical</p> |
|
<p className="text-sm mt-1">Both versions match exactly</p> |
|
</div> |
|
<div className="mt-4 w-full max-w-2xl bg-bolt-elements-background-depth-1 rounded-lg border border-bolt-elements-borderColor overflow-hidden"> |
|
<div className="p-2 text-xs font-bold text-bolt-elements-textTertiary border-b border-bolt-elements-borderColor"> |
|
Current Content |
|
</div> |
|
<div className="overflow-auto max-h-96"> |
|
{beforeCode.split('\n').map((line, index) => ( |
|
<div key={index} className="flex group min-w-fit"> |
|
<div className={lineNumberStyles}>{index + 1}</div> |
|
<div className={lineContentStyles}> |
|
<span className="mr-2"> </span> |
|
<span |
|
dangerouslySetInnerHTML={{ |
|
__html: highlighter |
|
? highlighter |
|
.codeToHtml(line, { |
|
lang: language, |
|
theme: theme === 'dark' ? 'github-dark' : 'github-light', |
|
}) |
|
.replace(/<\/?pre[^>]*>/g, '') |
|
.replace(/<\/?code[^>]*>/g, '') |
|
: line, |
|
}} |
|
/> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
</div> |
|
), |
|
); |
|
|
|
// Otimização do processamento de diferenças com memoização |
|
const useProcessChanges = (beforeCode: string, afterCode: string) => { |
|
return useMemo(() => processChanges(beforeCode, afterCode), [beforeCode, afterCode]); |
|
}; |
|
|
|
// Componente otimizado para renderização de linhas de código |
|
const CodeLine = memo( |
|
({ |
|
lineNumber, |
|
content, |
|
type, |
|
highlighter, |
|
language, |
|
block, |
|
theme, |
|
}: { |
|
lineNumber: number; |
|
content: string; |
|
type: 'added' | 'removed' | 'unchanged'; |
|
highlighter: any; |
|
language: string; |
|
block: DiffBlock; |
|
theme: string; |
|
}) => { |
|
const bgColor = diffLineStyles[type]; |
|
|
|
const renderContent = () => { |
|
if (type === 'unchanged' || !block.charChanges) { |
|
const highlightedCode = highlighter |
|
? highlighter |
|
.codeToHtml(content, { lang: language, theme: theme === 'dark' ? 'github-dark' : 'github-light' }) |
|
.replace(/<\/?pre[^>]*>/g, '') |
|
.replace(/<\/?code[^>]*>/g, '') |
|
: content; |
|
return <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />; |
|
} |
|
|
|
return ( |
|
<> |
|
{block.charChanges.map((change, index) => { |
|
const changeClass = changeColorStyles[change.type]; |
|
|
|
const highlightedCode = highlighter |
|
? highlighter |
|
.codeToHtml(change.value, { |
|
lang: language, |
|
theme: theme === 'dark' ? 'github-dark' : 'github-light', |
|
}) |
|
.replace(/<\/?pre[^>]*>/g, '') |
|
.replace(/<\/?code[^>]*>/g, '') |
|
: change.value; |
|
|
|
return <span key={index} className={changeClass} dangerouslySetInnerHTML={{ __html: highlightedCode }} />; |
|
})} |
|
</> |
|
); |
|
}; |
|
|
|
return ( |
|
<div className="flex group min-w-fit"> |
|
<div className={lineNumberStyles}>{lineNumber + 1}</div> |
|
<div className={`${lineContentStyles} ${bgColor}`}> |
|
<span className="mr-2 text-bolt-elements-textTertiary"> |
|
{type === 'added' && <span className="text-green-700 dark:text-green-500">+</span>} |
|
{type === 'removed' && <span className="text-red-700 dark:text-red-500">-</span>} |
|
{type === 'unchanged' && ' '} |
|
</span> |
|
{renderContent()} |
|
</div> |
|
</div> |
|
); |
|
}, |
|
); |
|
|
|
// Componente para exibir informações sobre o arquivo |
|
const FileInfo = memo( |
|
({ |
|
filename, |
|
hasChanges, |
|
onToggleFullscreen, |
|
isFullscreen, |
|
beforeCode, |
|
afterCode, |
|
}: { |
|
filename: string; |
|
hasChanges: boolean; |
|
onToggleFullscreen: () => void; |
|
isFullscreen: boolean; |
|
beforeCode: string; |
|
afterCode: string; |
|
}) => { |
|
// Calculate additions and deletions from the current document |
|
const { additions, deletions } = useMemo(() => { |
|
if (!hasChanges) { |
|
return { additions: 0, deletions: 0 }; |
|
} |
|
|
|
const changes = diffLines(beforeCode, afterCode, { |
|
newlineIsToken: false, |
|
ignoreWhitespace: true, |
|
ignoreCase: false, |
|
}); |
|
|
|
return changes.reduce( |
|
(acc: { additions: number; deletions: number }, change: Change) => { |
|
if (change.added) { |
|
acc.additions += change.value.split('\n').length; |
|
} |
|
|
|
if (change.removed) { |
|
acc.deletions += change.value.split('\n').length; |
|
} |
|
|
|
return acc; |
|
}, |
|
{ additions: 0, deletions: 0 }, |
|
); |
|
}, [hasChanges, beforeCode, afterCode]); |
|
|
|
const showStats = additions > 0 || deletions > 0; |
|
|
|
return ( |
|
<div className="flex items-center bg-bolt-elements-background-depth-1 p-2 text-sm text-bolt-elements-textPrimary shrink-0"> |
|
<div className="i-ph:file mr-2 h-4 w-4 shrink-0" /> |
|
<span className="truncate">{filename}</span> |
|
<span className="ml-auto shrink-0 flex items-center gap-2"> |
|
{hasChanges ? ( |
|
<> |
|
{showStats && ( |
|
<div className="flex items-center gap-1 text-xs"> |
|
{additions > 0 && <span className="text-green-700 dark:text-green-500">+{additions}</span>} |
|
{deletions > 0 && <span className="text-red-700 dark:text-red-500">-{deletions}</span>} |
|
</div> |
|
)} |
|
<span className="text-yellow-600 dark:text-yellow-400">Modified</span> |
|
<span className="text-bolt-elements-textTertiary text-xs">{new Date().toLocaleTimeString()}</span> |
|
</> |
|
) : ( |
|
<span className="text-green-700 dark:text-green-400">No Changes</span> |
|
)} |
|
<FullscreenButton onClick={onToggleFullscreen} isFullscreen={isFullscreen} /> |
|
</span> |
|
</div> |
|
); |
|
}, |
|
); |
|
|
|
const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language }: CodeComparisonProps) => { |
|
const [isFullscreen, setIsFullscreen] = useState(false); |
|
const [highlighter, setHighlighter] = useState<any>(null); |
|
const theme = useStore(themeStore); |
|
|
|
const toggleFullscreen = useCallback(() => { |
|
setIsFullscreen((prev) => !prev); |
|
}, []); |
|
|
|
const { unifiedBlocks, hasChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode); |
|
|
|
useEffect(() => { |
|
getHighlighter({ |
|
themes: ['github-dark', 'github-light'], |
|
langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'], |
|
}).then(setHighlighter); |
|
}, []); |
|
|
|
if (isBinary || error) { |
|
return renderContentWarning(isBinary ? 'binary' : 'error'); |
|
} |
|
|
|
return ( |
|
<FullscreenOverlay isFullscreen={isFullscreen}> |
|
<div className="w-full h-full flex flex-col"> |
|
<FileInfo |
|
filename={filename} |
|
hasChanges={hasChanges} |
|
onToggleFullscreen={toggleFullscreen} |
|
isFullscreen={isFullscreen} |
|
beforeCode={beforeCode} |
|
afterCode={afterCode} |
|
/> |
|
<div className={diffPanelStyles}> |
|
{hasChanges ? ( |
|
<div className="overflow-x-auto min-w-full"> |
|
{unifiedBlocks.map((block, index) => ( |
|
<CodeLine |
|
key={`${block.lineNumber}-${index}`} |
|
lineNumber={block.lineNumber} |
|
content={block.content} |
|
type={block.type} |
|
highlighter={highlighter} |
|
language={language} |
|
block={block} |
|
theme={theme} |
|
/> |
|
))} |
|
</div> |
|
) : ( |
|
<NoChangesView beforeCode={beforeCode} language={language} highlighter={highlighter} theme={theme} /> |
|
)} |
|
</div> |
|
</div> |
|
</FullscreenOverlay> |
|
); |
|
}); |
|
|
|
interface DiffViewProps { |
|
fileHistory: Record<string, FileHistory>; |
|
setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>; |
|
actionRunner: ActionRunner; |
|
} |
|
|
|
export const DiffView = memo(({ fileHistory, setFileHistory }: 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; |
|
|
|
// Normalizar o conteúdo para comparação |
|
const normalizedCurrentContent = currentContent.replace(/\r\n/g, '\n').trim(); |
|
const normalizedOriginalContent = (existingHistory?.originalContent || file.content) |
|
.replace(/\r\n/g, '\n') |
|
.trim(); |
|
|
|
// Se não há histórico existente, criar um novo apenas se houver diferenças |
|
if (!existingHistory) { |
|
if (normalizedCurrentContent !== normalizedOriginalContent) { |
|
const newChanges = diffLines(file.content, currentContent); |
|
setFileHistory((prev) => ({ |
|
...prev, |
|
[selectedFile]: { |
|
originalContent: file.content, |
|
lastModified: Date.now(), |
|
changes: newChanges, |
|
versions: [ |
|
{ |
|
timestamp: Date.now(), |
|
content: currentContent, |
|
}, |
|
], |
|
changeSource: 'auto-save', |
|
}, |
|
})); |
|
} |
|
|
|
return; |
|
} |
|
|
|
// Se já existe histórico, verificar se há mudanças reais desde a última versão |
|
const lastVersion = existingHistory.versions[existingHistory.versions.length - 1]; |
|
const normalizedLastContent = lastVersion?.content.replace(/\r\n/g, '\n').trim(); |
|
|
|
if (normalizedCurrentContent === normalizedLastContent) { |
|
return; // Não criar novo histórico se o conteúdo é o mesmo |
|
} |
|
|
|
// Verificar se há mudanças significativas usando diffFiles |
|
const relativePath = extractRelativePath(selectedFile); |
|
const unifiedDiff = diffFiles(relativePath, existingHistory.originalContent, currentContent); |
|
|
|
if (unifiedDiff) { |
|
const newChanges = diffLines(existingHistory.originalContent, currentContent); |
|
|
|
// Verificar se as mudanças são significativas |
|
const hasSignificantChanges = newChanges.some( |
|
(change) => (change.added || change.removed) && change.value.trim().length > 0, |
|
); |
|
|
|
if (hasSignificantChanges) { |
|
const newHistory: FileHistory = { |
|
originalContent: existingHistory.originalContent, |
|
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 ( |
|
<div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary"> |
|
Select a file to view differences |
|
</div> |
|
); |
|
} |
|
|
|
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 ( |
|
<div className="h-full overflow-hidden"> |
|
<InlineDiffComparison |
|
beforeCode={effectiveOriginalContent} |
|
afterCode={currentContent} |
|
language={language} |
|
filename={selectedFile} |
|
lightTheme="github-light" |
|
darkTheme="github-dark" |
|
/> |
|
</div> |
|
); |
|
} catch (error) { |
|
console.error('DiffView render error:', error); |
|
return ( |
|
<div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-red-400"> |
|
<div className="text-center"> |
|
<div className="i-ph:warning-circle text-4xl mb-2" /> |
|
<p>Failed to render diff view</p> |
|
</div> |
|
</div> |
|
); |
|
} |
|
}); |
|
|