import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued' import { Highlight, type PrismTheme } from 'prism-react-renderer' import { CopyButton } from '../shared/CopyButton' import { useUIStore } from '../../stores/uiStore' type Props = { filePath: string oldString: string newString: string } function inferLanguage(filePath: string): string { const ext = filePath.split('.').pop()?.toLowerCase() const langMap: Record = { ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx', py: 'python', rs: 'rust', go: 'go', rb: 'ruby', json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml', md: 'markdown', css: 'css', html: 'markup', xml: 'markup', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash', } return langMap[ext ?? ''] || 'text' } /** Shared warm syntax theme — must stay in sync with CodeViewer */ const warmSyntaxTheme: PrismTheme = { plain: { color: 'var(--color-code-fg)', backgroundColor: 'transparent', }, styles: [ { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--color-code-comment)', fontStyle: 'italic' as const } }, { types: ['string', 'attr-value', 'template-string'], style: { color: 'var(--color-code-string)' } }, { types: ['keyword', 'selector', 'important', 'atrule'], style: { color: 'var(--color-code-keyword)' } }, { types: ['function'], style: { color: 'var(--color-code-function)' } }, { types: ['tag'], style: { color: 'var(--color-code-keyword)' } }, { types: ['number', 'boolean'], style: { color: 'var(--color-code-number)' } }, { types: ['operator'], style: { color: 'var(--color-code-fg)' } }, { types: ['punctuation'], style: { color: 'var(--color-code-punctuation)' } }, { types: ['variable', 'parameter'], style: { color: 'var(--color-code-fg)' } }, { types: ['property', 'attr-name'], style: { color: 'var(--color-code-property)' } }, { types: ['builtin', 'class-name', 'constant', 'symbol'], style: { color: 'var(--color-code-type)' } }, { types: ['regex'], style: { color: 'var(--color-primary-container)' } }, { types: ['inserted'], style: { color: 'var(--color-code-inserted)' } }, { types: ['deleted'], style: { color: 'var(--color-code-deleted)' } }, ], } function highlightSyntax(str: string, language: string) { return ( {({ tokens, getTokenProps }) => ( <> {tokens.map((line, i) => ( {line.map((token, key) => ( ))} ))} )} ) } const diffStyles = { variables: { light: { diffViewerBackground: 'var(--color-code-bg)', diffViewerColor: 'var(--color-code-fg)', addedBackground: 'var(--color-diff-added-bg)', addedColor: 'var(--color-code-fg)', removedBackground: 'var(--color-diff-removed-bg)', removedColor: 'var(--color-code-fg)', wordAddedBackground: 'var(--color-diff-added-word)', wordRemovedBackground: 'var(--color-diff-removed-word)', addedGutterBackground: 'var(--color-diff-added-gutter)', removedGutterBackground: 'var(--color-diff-removed-gutter)', gutterBackground: 'var(--color-surface-container-low)', gutterBackgroundDark: 'var(--color-surface-container)', highlightBackground: 'var(--color-diff-highlight-bg)', highlightGutterBackground: 'var(--color-diff-highlight-gutter)', codeFoldGutterBackground: 'var(--color-surface-container-high)', codeFoldBackground: 'var(--color-surface-container-highest)', emptyLineBackground: 'var(--color-surface-container-low)', gutterColor: 'var(--color-text-tertiary)', addedGutterColor: 'var(--color-diff-added-text)', removedGutterColor: 'var(--color-diff-removed-text)', codeFoldContentColor: 'var(--color-text-tertiary)', diffViewerTitleBackground: 'var(--color-diff-title-bg)', diffViewerTitleColor: 'var(--color-diff-title-color)', diffViewerTitleBorderColor: 'var(--color-diff-title-border)', }, }, diffContainer: { borderRadius: '0', fontSize: '12px', lineHeight: '1.45', fontFamily: 'var(--font-mono)', }, line: { padding: '1px 0', }, gutter: { padding: '1px 8px', minWidth: '40px', fontSize: '11px', }, wordDiff: { padding: '1px 2px', borderRadius: '2px', }, } export function DiffViewer({ filePath, oldString, newString }: Props) { const theme = useUIStore((state) => state.theme) const language = inferLanguage(filePath) const oldLines = oldString.split('\n') const newLines = newString.split('\n') const additions = newLines.filter((l, i) => l !== (oldLines[i] ?? null)).length const deletions = oldLines.filter((l, i) => l !== (newLines[i] ?? null)).length return (
{/* Header */}
{filePath}
+{additions} -{deletions}
{/* Diff area */}
highlightSyntax(str, language)} hideLineNumbers={false} styles={diffStyles} useDarkTheme={theme === 'dark'} />
) }