2025-12-17 16:16:28 +02:00
|
|
|
import React, { useMemo, useEffect, useRef } from 'react';
|
2025-12-14 02:29:46 +02:00
|
|
|
import { WorkerPoolContextProvider, useWorkerPool } from '@pierre/diffs/react';
|
2025-12-17 16:16:28 +02:00
|
|
|
import { parseDiffFromFile, type FileContents } from '@pierre/diffs';
|
2025-12-14 02:29:46 +02:00
|
|
|
import type { SupportedLanguages } from '@pierre/diffs';
|
|
|
|
|
|
|
|
|
|
import { useOptionalThemeSystem } from './useThemeSystem';
|
|
|
|
|
import { workerFactory } from '@/lib/diff/workerFactory';
|
2025-12-15 00:20:26 +02:00
|
|
|
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
|
|
|
|
|
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
|
2025-12-17 16:16:28 +02:00
|
|
|
import { useGitStore } from '@/stores/useGitStore';
|
|
|
|
|
import { getLanguageFromExtension } from '@/lib/toolHelpers';
|
2025-12-14 02:29:46 +02:00
|
|
|
|
2025-12-17 16:16:28 +02:00
|
|
|
// Preload common languages for faster initial diff rendering
|
2025-12-14 02:29:46 +02:00
|
|
|
const PRELOAD_LANGS: SupportedLanguages[] = [
|
|
|
|
|
'typescript',
|
|
|
|
|
'javascript',
|
|
|
|
|
'tsx',
|
2025-12-17 16:16:28 +02:00
|
|
|
'jsx',
|
2025-12-14 02:29:46 +02:00
|
|
|
'json',
|
2025-12-17 16:16:28 +02:00
|
|
|
'html',
|
|
|
|
|
'css',
|
|
|
|
|
'markdown',
|
|
|
|
|
'yaml',
|
|
|
|
|
'python',
|
|
|
|
|
'rust',
|
|
|
|
|
'go',
|
|
|
|
|
'bash',
|
2025-12-14 02:29:46 +02:00
|
|
|
];
|
|
|
|
|
|
2026-01-28 12:29:01 +02:00
|
|
|
// Limit warmup to prevent memory bloat with many modified files
|
|
|
|
|
const WARMUP_MAX_FILES = 10;
|
|
|
|
|
|
2025-12-17 16:16:28 +02:00
|
|
|
// Matches cache key logic in `packages/ui/src/components/views/PierreDiffViewer.tsx`
|
|
|
|
|
function getPierreCacheKey(fileName: string, original: string, modified: string): string {
|
|
|
|
|
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}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 02:29:46 +02:00
|
|
|
interface DiffWorkerProviderProps {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 16:16:28 +02:00
|
|
|
type IdleDeadlineLike = { timeRemaining(): number; didTimeout: boolean };
|
|
|
|
|
|
|
|
|
|
function scheduleWarmupWork(cb: (deadline?: IdleDeadlineLike) => void): () => void {
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
return () => {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const id = window.setTimeout(() => cb(undefined), 0);
|
|
|
|
|
return () => window.clearTimeout(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Component that warms up the worker pool and precomputes diff ASTs
|
|
|
|
|
const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
|
|
|
const workerPool = useWorkerPool();
|
|
|
|
|
const activeDirectory = useGitStore((state) => state.activeDirectory);
|
|
|
|
|
const lastStatusChange = useGitStore((state) => {
|
|
|
|
|
if (!activeDirectory) return 0;
|
|
|
|
|
return state.directories.get(activeDirectory)?.lastStatusChange ?? 0;
|
|
|
|
|
});
|
|
|
|
|
const diffCacheSize = useGitStore((state) => {
|
|
|
|
|
if (!activeDirectory) return 0;
|
|
|
|
|
return state.directories.get(activeDirectory)?.diffCache.size ?? 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const didDummyWarmupRef = useRef(false);
|
|
|
|
|
const warmedStatusRef = useRef(new Map<string, number>());
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!workerPool || didDummyWarmupRef.current) return;
|
|
|
|
|
|
|
|
|
|
didDummyWarmupRef.current = true;
|
|
|
|
|
|
|
|
|
|
const dummyFile: FileContents = {
|
|
|
|
|
name: 'warmup.ts',
|
|
|
|
|
contents: 'const x = 1;',
|
|
|
|
|
lang: 'typescript',
|
|
|
|
|
cacheKey: 'warmup-file',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const dummyDiff = parseDiffFromFile(dummyFile, { ...dummyFile, contents: 'const x = 2;', cacheKey: 'warmup-file-2' });
|
|
|
|
|
|
|
|
|
|
const dummyInstance = {
|
|
|
|
|
onHighlightSuccess: () => {},
|
|
|
|
|
onHighlightError: () => {},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
workerPool.highlightDiffAST(dummyInstance, dummyDiff);
|
|
|
|
|
}, [workerPool]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!workerPool || !activeDirectory) return;
|
|
|
|
|
if (!lastStatusChange || diffCacheSize === 0) return;
|
|
|
|
|
|
|
|
|
|
const dirState = useGitStore.getState().directories.get(activeDirectory);
|
|
|
|
|
if (!dirState || dirState.diffCache.size === 0) return;
|
|
|
|
|
|
|
|
|
|
const alreadyWarmedAt = warmedStatusRef.current.get(activeDirectory) ?? 0;
|
|
|
|
|
if (alreadyWarmedAt >= dirState.lastStatusChange) return;
|
|
|
|
|
|
|
|
|
|
// Only warm once per status-change tick
|
|
|
|
|
warmedStatusRef.current.set(activeDirectory, dirState.lastStatusChange);
|
|
|
|
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
let cancelScheduled: (() => void) | null = null;
|
|
|
|
|
|
2026-01-28 12:29:01 +02:00
|
|
|
// Limit entries to prevent memory bloat with many modified files
|
|
|
|
|
const allEntries = Array.from(dirState.diffCache.entries());
|
|
|
|
|
const entries = allEntries.slice(0, WARMUP_MAX_FILES);
|
2025-12-17 16:16:28 +02:00
|
|
|
|
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
|
|
const processChunk = (deadline?: IdleDeadlineLike) => {
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
|
|
|
|
|
const chunkStart = Date.now();
|
|
|
|
|
while (index < entries.length) {
|
|
|
|
|
if (deadline && deadline.timeRemaining() < 8) break;
|
|
|
|
|
if (!deadline && Date.now() - chunkStart > 8) break;
|
|
|
|
|
|
|
|
|
|
const [filePath, diff] = entries[index];
|
|
|
|
|
index += 1;
|
|
|
|
|
|
|
|
|
|
const language = getLanguageFromExtension(filePath) || 'text';
|
|
|
|
|
const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified);
|
|
|
|
|
|
|
|
|
|
const oldFile: FileContents = {
|
|
|
|
|
name: filePath,
|
|
|
|
|
contents: diff.original,
|
|
|
|
|
lang: language as FileContents['lang'],
|
|
|
|
|
cacheKey: `old-${cacheKey}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const newFile: FileContents = {
|
|
|
|
|
name: filePath,
|
|
|
|
|
contents: diff.modified,
|
|
|
|
|
lang: language as FileContents['lang'],
|
|
|
|
|
cacheKey: `new-${cacheKey}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fileDiff = parseDiffFromFile(oldFile, newFile);
|
|
|
|
|
|
|
|
|
|
// Use a unique instance per request so Pierre doesn't ignore earlier results.
|
|
|
|
|
const instance = {
|
|
|
|
|
onHighlightSuccess: () => {},
|
|
|
|
|
onHighlightError: () => {},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
workerPool.highlightDiffAST(instance, fileDiff);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (index < entries.length) {
|
|
|
|
|
cancelScheduled = scheduleWarmupWork(processChunk);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Kick off immediately, then continue in chunks.
|
|
|
|
|
processChunk(undefined);
|
|
|
|
|
|
|
|
|
|
if (index < entries.length) {
|
|
|
|
|
cancelScheduled = scheduleWarmupWork(processChunk);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
cancelScheduled?.();
|
|
|
|
|
};
|
|
|
|
|
}, [workerPool, activeDirectory, lastStatusChange, diffCacheSize]);
|
|
|
|
|
|
|
|
|
|
return <>{children}</>;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-14 02:29:46 +02:00
|
|
|
export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children }) => {
|
|
|
|
|
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
|
|
|
const highlighterOptions = 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),
|
|
|
|
|
langs: PRELOAD_LANGS,
|
|
|
|
|
}), [isDark]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<WorkerPoolContextProvider
|
|
|
|
|
poolOptions={{
|
|
|
|
|
workerFactory,
|
2026-01-28 12:29:01 +02:00
|
|
|
poolSize: 2,
|
|
|
|
|
totalASTLRUCacheSize: 50,
|
2025-12-14 02:29:46 +02:00
|
|
|
}}
|
|
|
|
|
highlighterOptions={highlighterOptions}
|
|
|
|
|
>
|
2025-12-17 16:16:28 +02:00
|
|
|
<WorkerPoolWarmup>
|
|
|
|
|
{children}
|
|
|
|
|
</WorkerPoolWarmup>
|
2025-12-14 02:29:46 +02:00
|
|
|
</WorkerPoolContextProvider>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-14 02:34:15 +02:00
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
2025-12-14 02:29:46 +02:00
|
|
|
export { useWorkerPool };
|