2025-12-14 02:29:46 +02:00
|
|
|
import React, { useMemo, useRef } from 'react';
|
|
|
|
|
import { FileDiff } from '@pierre/diffs/react';
|
|
|
|
|
import { parseDiffFromFile, type FileContents, type FileDiffMetadata } from '@pierre/diffs';
|
|
|
|
|
|
|
|
|
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
|
|
|
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
2025-12-15 00:20:26 +02:00
|
|
|
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
|
|
|
|
|
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
|
2025-12-14 02:29:46 +02:00
|
|
|
|
|
|
|
|
interface PierreDiffViewerProps {
|
|
|
|
|
original: string;
|
|
|
|
|
modified: string;
|
|
|
|
|
language: string;
|
|
|
|
|
fileName?: string;
|
|
|
|
|
renderSideBySide: boolean;
|
|
|
|
|
wrapLines?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CSS injected into Pierre's Shadow DOM for WebKit scroll optimization
|
|
|
|
|
// Note: avoid will-change and contain:paint as they break resize behavior
|
|
|
|
|
const WEBKIT_SCROLL_FIX_CSS = `
|
2025-12-16 13:26:03 +02:00
|
|
|
:host {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--text-code);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 02:29:46 +02:00
|
|
|
:host, pre, [data-diffs], [data-code] {
|
|
|
|
|
transform: translateZ(0);
|
|
|
|
|
-webkit-transform: translateZ(0);
|
|
|
|
|
-webkit-backface-visibility: hidden;
|
|
|
|
|
backface-visibility: hidden;
|
|
|
|
|
}
|
2025-12-16 13:26:03 +02:00
|
|
|
|
|
|
|
|
pre, [data-code] {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--text-code);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 02:29:46 +02:00
|
|
|
[data-code] {
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
}
|
2025-12-15 13:56:01 +02:00
|
|
|
/* Reduce hunk separator height */
|
|
|
|
|
[data-separator-content] {
|
|
|
|
|
height: 24px !important;
|
|
|
|
|
}
|
|
|
|
|
[data-expand-button] {
|
|
|
|
|
height: 24px !important;
|
|
|
|
|
width: 24px !important;
|
|
|
|
|
}
|
2025-12-17 14:10:23 +02:00
|
|
|
[data-separator-multi-button] {
|
|
|
|
|
row-gap: 0 !important;
|
|
|
|
|
}
|
|
|
|
|
[data-expand-up] {
|
|
|
|
|
height: 12px !important;
|
|
|
|
|
min-height: 12px !important;
|
|
|
|
|
max-height: 12px !important;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
margin-top: 3px !important;
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
border-radius: 4px 4px 0 0 !important;
|
|
|
|
|
}
|
|
|
|
|
[data-expand-down] {
|
|
|
|
|
height: 12px !important;
|
|
|
|
|
min-height: 12px !important;
|
|
|
|
|
max-height: 12px !important;
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
margin-top: -3px !important;
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
border-radius: 0 0 4px 4px !important;
|
|
|
|
|
}
|
2025-12-14 02:29:46 +02:00
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Fast cache key - use length + samples instead of full hash
|
|
|
|
|
function getCacheKey(fileName: string, original: string, modified: string): string {
|
|
|
|
|
// Sample a few characters instead of hashing entire content
|
|
|
|
|
const sampleOriginal = original.length > 100
|
|
|
|
|
? `${original.slice(0, 50)}${original.slice(-50)}`
|
|
|
|
|
: original;
|
|
|
|
|
const sampleModified = modified.length > 100
|
|
|
|
|
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
|
|
|
|
: modified;
|
|
|
|
|
return `${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|
|
|
|
original,
|
|
|
|
|
modified,
|
|
|
|
|
language,
|
|
|
|
|
fileName = 'file',
|
|
|
|
|
renderSideBySide,
|
|
|
|
|
wrapLines = false,
|
|
|
|
|
}) => {
|
|
|
|
|
const themeSystem = useOptionalThemeSystem();
|
|
|
|
|
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
|
|
|
|
|
|
2025-12-15 00:20:26 +02:00
|
|
|
ensureFlexokiThemesRegistered();
|
|
|
|
|
|
2025-12-14 02:29:46 +02:00
|
|
|
// Cache the last computed diff to avoid recomputing on every render
|
|
|
|
|
const diffCacheRef = useRef<{
|
|
|
|
|
key: string;
|
|
|
|
|
fileDiff: FileDiffMetadata;
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
|
|
|
|
// Pre-parse the diff with cacheKey for worker pool caching
|
|
|
|
|
const fileDiff = useMemo(() => {
|
|
|
|
|
const cacheKey = getCacheKey(fileName, original, modified);
|
|
|
|
|
|
|
|
|
|
// Return cached diff if inputs haven't changed
|
|
|
|
|
if (diffCacheRef.current?.key === cacheKey) {
|
|
|
|
|
return diffCacheRef.current.fileDiff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const oldFile: FileContents = {
|
|
|
|
|
name: fileName,
|
|
|
|
|
contents: original,
|
|
|
|
|
lang: language as FileContents['lang'],
|
|
|
|
|
cacheKey: `old-${cacheKey}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const newFile: FileContents = {
|
|
|
|
|
name: fileName,
|
|
|
|
|
contents: modified,
|
|
|
|
|
lang: language as FileContents['lang'],
|
|
|
|
|
cacheKey: `new-${cacheKey}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const diff = parseDiffFromFile(oldFile, newFile);
|
|
|
|
|
|
|
|
|
|
// Cache the result
|
|
|
|
|
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
|
|
|
|
|
|
|
|
|
return diff;
|
|
|
|
|
}, [fileName, original, modified, language]);
|
|
|
|
|
|
|
|
|
|
const options = useMemo(() => ({
|
|
|
|
|
theme: {
|
2025-12-15 00:20:26 +02:00
|
|
|
dark: flexokiThemeNames.dark,
|
|
|
|
|
light: flexokiThemeNames.light,
|
2025-12-14 02:29:46 +02:00
|
|
|
},
|
|
|
|
|
themeType: isDark ? ('dark' as const) : ('light' as const),
|
|
|
|
|
diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
|
|
|
|
|
diffIndicators: 'none' as const,
|
|
|
|
|
hunkSeparators: 'line-info' as const,
|
|
|
|
|
lineDiffType: 'word-alt' as const,
|
|
|
|
|
overflow: wrapLines ? ('wrap' as const) : ('scroll' as const),
|
|
|
|
|
disableFileHeader: true,
|
|
|
|
|
enableLineSelection: false,
|
|
|
|
|
enableHoverUtility: false,
|
|
|
|
|
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
|
|
|
|
}), [isDark, renderSideBySide, wrapLines]);
|
|
|
|
|
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ScrollableOverlay
|
|
|
|
|
outerClassName="pierre-diff-wrapper size-full"
|
|
|
|
|
disableHorizontal={false}
|
|
|
|
|
>
|
|
|
|
|
<FileDiff
|
|
|
|
|
fileDiff={fileDiff}
|
|
|
|
|
options={options}
|
|
|
|
|
/>
|
|
|
|
|
</ScrollableOverlay>
|
|
|
|
|
);
|
|
|
|
|
};
|