import React, { useMemo, useRef, useCallback, useEffect } from 'react'; import { areFilesEqual, areOptionsEqual, FileDiff as PierreFileDiff, VirtualizedFileDiff, Virtualizer, type FileContents, type FileDiffMetadata, type FileDiffOptions, type DiffLineAnnotation, type SelectedLineRange, type AnnotationSide, type VirtualFileMetrics, } from '@pierre/diffs'; import { buildPierreLineAnnotations, type PierreAnnotationData, PierreDiffCommentOverlays, toPierreAnnotationId, useInlineCommentController, } from '@/components/comments'; import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { useWorkerPool } from '@/contexts/DiffWorkerProvider'; import { ensurePierreThemeRegistered, getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry'; import { getDefaultTheme } from '@/lib/theme/themes'; import { useDeviceInfo } from '@/lib/device'; import { cn } from '@/lib/utils'; // Threshold (bytes) above which syntax highlighting is degraded for performance const LARGE_CONTENT_BYTES = 500_000; interface PierreDiffViewerProps { original: string; modified: string; fileDiff?: FileDiffMetadata; language: string; fileName?: string; renderSideBySide: boolean; wrapLines?: boolean; layout?: 'fill' | 'inline'; enableComments?: boolean; } /** * Base CSS injected into Pierre's Shadow DOM. Pins font-family/size to the * app tokens (so Files view and Diff view render at the same scale on mobile) * and enables touch-friendly line interactions. Re-exported so plain * consumers (e.g. `MobileFilesSurface`) can inject the same. */ const PIERRE_RUNTIME_BASE_CSS = ` :host { font-family: var(--font-mono); font-size: var(--text-code); } pre, [data-code] { font-family: var(--font-mono); font-size: var(--text-code); } /* Mobile touch selection support */ [data-line-number] { touch-action: manipulation; -webkit-tap-highlight-color: transparent; cursor: pointer; } /* Ensure interactive line numbers work on touch */ pre[data-interactive-line-numbers] [data-line-number] { touch-action: manipulation; } `; // CSS injected into Pierre's Shadow DOM for WebKit scroll optimization + // diff-specific separator height. Note: avoid will-change and contain:paint // as they break resize behavior. const WEBKIT_SCROLL_FIX_CSS = ` ${PIERRE_RUNTIME_BASE_CSS} :host { --diffs-bg-separator-override: var(--surface-elevated); } [data-diff-header], [data-diff] { [data-separator] { height: 24px !important; } } [data-separator="line-info-basic"] { height: 24px !important; background: var(--diffs-bg) !important; position: relative; } [data-diff-type="single"] [data-gutter], [data-diff-type="split"] [data-deletions] [data-gutter] { [data-separator-wrapper] { position: absolute; left: 100%; display: flex; align-items: center; gap: unset; width: max-content; background: transparent; color: var(--diffs-fg-number); font-family: var(--diffs-header-font-family, var(--font-sans)); font-size: 0.75rem; line-height: 1; margin-left: calc(-2ch - 2px); } [data-separator-wrapper][data-separator-multi-button] { margin-left: calc(-3ch - 2px); } [data-expand-button], [data-separator-content] { display: block; align-self: unset; min-width: unset; min-height: unset; padding: 0; flex-shrink: 0; grid-column: unset; border: none; width: auto; height: auto; background-color: transparent; color: inherit; font: inherit; } [data-expand-button]:not([data-expand-all-button]) { &[data-expand-down]::before { content: '\\2191'; } &[data-expand-up]::before { content: '\\2193'; } &[data-expand-both]::before { content: '\\2195'; } svg { display: none; } } [data-separator-content] { background: transparent; margin-left: calc(2px + 1ch); } [data-expand-all-button] { position: relative; margin-left: 14px; text-transform: lowercase; } [data-expand-all-button]::before { content: ''; display: block; position: absolute; top: 50%; left: -8px; margin-top: -1px; width: 3px; height: 3px; border-radius: 2px; background-color: var(--diffs-fg-number); pointer-events: none; } [data-separator-content]:hover, [data-expand-button]:hover, [data-expand-all-button]:hover { color: var(--diffs-fg); } [data-expand-all-button]:hover { text-decoration: underline; } } `; // Fast cache key - use length + samples instead of full hash function fnv1a32(input: string): string { // Fast + stable across runtimes; good enough for cache keys. let hash = 0x811c9dc5; for (let i = 0; i < input.length; i += 1) { hash ^= input.charCodeAt(i); // hash *= 16777619 (but keep 32-bit) hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0; } return hash.toString(16); } function makeContentCacheKey(contents: string): string { // Avoid hashing full file; sample head+tail. const sample = contents.length > 400 ? `${contents.slice(0, 200)}${contents.slice(-200)}` : contents; return `${contents.length}:${fnv1a32(sample)}`; } const extractSelectedCode = ( original: string, modified: string, fileDiff: FileDiffMetadata | undefined, range: SelectedLineRange, ): string => { // Default to modified if side is ambiguous, as users mostly comment on new code const isOriginal = range.side === 'deletions'; const content = fileDiff ? (isOriginal ? fileDiff.deletionLines : fileDiff.additionLines).join('') : (isOriginal ? original : modified); const lines = content.split('\n'); // Ensure bounds const from = Math.min(range.start, range.end); const to = Math.max(range.start, range.end); const startLine = Math.max(1, from); const endLine = Math.min(lines.length, to); if (startLine > endLine) return ''; return lines.slice(startLine - 1, endLine).join('\n'); }; const isSameSelection = (left: SelectedLineRange | null, right: SelectedLineRange | null): boolean => { if (left === right) return true; if (!left || !right) return false; return left.start === right.start && left.end === right.end && left.side === right.side; }; const isScrollable = (value: string): boolean => value === 'auto' || value === 'scroll' || value === 'overlay'; const findScrollParent = (node: HTMLElement | null): HTMLElement | null => { let current = node?.parentElement ?? null; while (current) { const style = window.getComputedStyle(current); if (isScrollable(style.overflowY)) return current; current = current.parentElement; } return null; }; const preserveScrollPosition = (wrapper: HTMLElement | null, container: HTMLElement | null): (() => void) => { if (!wrapper || !container || typeof window === 'undefined') return () => {}; const scrollParent = findScrollParent(wrapper); if (!scrollParent) return () => {}; const height = container.getBoundingClientRect().height; if (!height) return () => {}; const top = wrapper.getBoundingClientRect().top - scrollParent.getBoundingClientRect().top; const previousMinHeight = container.style.minHeight; container.style.minHeight = `${Math.ceil(height)}px`; let done = false; return () => { if (done) return; done = true; container.style.minHeight = previousMinHeight; const nextTop = wrapper.getBoundingClientRect().top - scrollParent.getBoundingClientRect().top; const delta = nextTop - top; if (delta) { scrollParent.scrollTop += delta; } }; }; const waitForDiffReady = ( container: HTMLElement, onReady: () => void, ): (() => void) => { if (typeof window === 'undefined') return () => {}; let frameId: number | null = null; let observer: MutationObserver | null = null; let cancelled = false; const finish = () => { if (cancelled) return; observer?.disconnect(); observer = null; frameId = window.requestAnimationFrame(() => { frameId = window.requestAnimationFrame(() => { if (!cancelled) onReady(); }); }); }; const getRoot = (): ShadowRoot | undefined => { const host = container.querySelector('diffs-container'); return host?.shadowRoot ?? undefined; }; const isReady = (root = getRoot()) => { return Boolean(root?.querySelector('[data-line]')); }; if (isReady()) { finish(); } else if (typeof MutationObserver !== 'undefined') { observer = new MutationObserver(() => { const root = getRoot(); if (!root) return; if (isReady(root)) { finish(); return; } observer?.disconnect(); observer = new MutationObserver(() => { if (isReady(root)) { finish(); } }); observer.observe(root, { childList: true, subtree: true }); }); observer.observe(container, { childList: true, subtree: true }); } else { frameId = window.requestAnimationFrame(finish); } return () => { cancelled = true; observer?.disconnect(); if (frameId !== null) { window.cancelAnimationFrame(frameId); } }; }; type SharedVirtualizer = { virtualizer: Virtualizer; root: Document | HTMLElement; release: () => void; }; type VirtualizerTarget = { key: Document | HTMLElement; root: Document | HTMLElement; content: HTMLElement | undefined; }; type VirtualizerEntry = { virtualizer: Virtualizer; refs: number; }; const virtualizerCache = new WeakMap(); const VIRTUAL_METRICS: Partial = { lineHeight: 24, hunkSeparatorHeight: 24, spacing: 0, }; function resolveVirtualizerTarget(container: HTMLElement): VirtualizerTarget { const root = container.closest('[data-diff-virtual-root]'); if (root instanceof HTMLElement) { const content = root.querySelector('[data-diff-virtual-content]'); return { key: root, root, content: content instanceof HTMLElement ? content : undefined, }; } return { key: document, root: document, content: undefined, }; } function acquireSharedVirtualizer(container: HTMLElement): SharedVirtualizer | null { if (typeof document === 'undefined') return null; const target = resolveVirtualizerTarget(container); let entry = virtualizerCache.get(target.key); if (!entry) { const virtualizer = new Virtualizer(); virtualizer.setup(target.root, target.content); entry = { virtualizer, refs: 0 }; virtualizerCache.set(target.key, entry); } entry.refs += 1; let released = false; return { virtualizer: entry.virtualizer, root: target.root, release: () => { if (released) return; released = true; const current = virtualizerCache.get(target.key); if (!current) return; current.refs -= 1; if (current.refs > 0) return; current.virtualizer.cleanUp(); virtualizerCache.delete(target.key); }, }; } const wakeVirtualizer = ( instance: PierreFileDiff, sharedVirtualizer: SharedVirtualizer | null, forceUpdate: () => void, ): (() => void) => { if (typeof window === 'undefined') return () => {}; const frameIds: number[] = []; const run = () => { try { instance.rerender(); } catch { // ignored } const root = sharedVirtualizer?.root; if (root instanceof HTMLElement) { root.dispatchEvent(new Event('scroll', { bubbles: false })); } else { document.dispatchEvent(new Event('scroll', { bubbles: false })); } window.dispatchEvent(new Event('resize')); forceUpdate(); }; frameIds.push(window.requestAnimationFrame(run)); frameIds.push(window.requestAnimationFrame(() => { frameIds.push(window.requestAnimationFrame(run)); })); return () => { for (const frameId of frameIds) { window.cancelAnimationFrame(frameId); } }; }; export const PierreDiffViewer: React.FC = ({ original, modified, fileDiff, language, fileName, renderSideBySide, wrapLines, layout = 'fill', enableComments = true, }) => { const themeContext = useOptionalThemeSystem(); const isDark = themeContext?.currentTheme.metadata.variant === 'dark'; const lightTheme = themeContext?.availableThemes.find(t => t.metadata.id === themeContext.lightThemeId) ?? getDefaultTheme(false); const darkTheme = themeContext?.availableThemes.find(t => t.metadata.id === themeContext.darkThemeId) ?? getDefaultTheme(true); const { isMobile } = useDeviceInfo(); const diffCommentController = useInlineCommentController({ source: 'diff', fileLabel: fileName || 'unknown', language, getCodeForRange: (range) => extractSelectedCode(original, modified, fileDiff, range), toStoreRange: (range) => ({ startLine: range.start, endLine: range.end, side: range.side === 'deletions' ? 'original' : 'modified', }), fromDraftRange: (draft) => ({ start: draft.startLine, end: draft.endLine, side: draft.side === 'original' ? 'deletions' : 'additions', }), }); const { drafts: fileDrafts, selection, setSelection, commentText, setCommentText, editingDraftId, saveComment, cancel, startEdit, deleteDraft, } = diffCommentController; const selectionRef = useRef(null); const editingDraftIdRef = useRef(null); const commentTextRef = useRef(''); // Use a ref to track if we're currently applying a selection programmatically // to avoid loop with onLineSelected callback const isApplyingSelectionRef = useRef(false); const lastAppliedSelectionRef = useRef(null); useEffect(() => { selectionRef.current = selection; }, [selection]); useEffect(() => { editingDraftIdRef.current = editingDraftId; }, [editingDraftId]); useEffect(() => { commentTextRef.current = commentText; }, [commentText]); const handleSelectionChange = useCallback((range: SelectedLineRange | null) => { if (!enableComments) { return; } // Ignore callbacks while we're programmatically applying selection if (isApplyingSelectionRef.current) { return; } const prevSelection = selectionRef.current; if (!range && prevSelection && commentTextRef.current.trim()) { return; } // Mobile tap-to-extend: if selection exists and new tap is on same side, extend range if (isMobile && prevSelection && range && range.side === prevSelection.side) { const start = Math.min(prevSelection.start, range.start); const end = Math.max(prevSelection.end, range.end); setSelection({ ...range, start, end }); } else { setSelection(range); } // Clear editing state when selection changes user-driven if (range) { if (!editingDraftIdRef.current) { setCommentText(''); } } }, [enableComments, isMobile, setCommentText, setSelection]); const handleCancelComment = useCallback(() => { cancel(); }, [cancel]); const renderAnnotation = useCallback((annotation: DiffLineAnnotation) => { const div = document.createElement('div'); div.style.position = 'relative'; const id = toPierreAnnotationId(annotation.metadata); div.dataset.annotationId = id; div.dataset.annotationSide = annotation.side; div.dataset.annotationLine = String(annotation.lineNumber); return div; }, []); const handleSaveComment = useCallback((textToSave: string, rangeOverride?: SelectedLineRange) => { saveComment(textToSave, rangeOverride ?? selection ?? undefined); }, [saveComment, selection]); const applySelection = useCallback((range: SelectedLineRange) => { setSelection(range); const instance = diffInstanceRef.current; if (!instance) return; try { isApplyingSelectionRef.current = true; instance.setSelectedLines(range); lastAppliedSelectionRef.current = range; } catch { // ignore } finally { isApplyingSelectionRef.current = false; } }, [setSelection]); const resolveClickedSide = useCallback((numberCell: HTMLElement): AnnotationSide => { const lineType = numberCell.closest('[data-line-type]')?.getAttribute('data-line-type') ?? numberCell.getAttribute('data-line-type'); if (lineType === 'change-deletion') { return 'deletions'; } if (lineType === 'change-addition') { return 'additions'; } const explicitColumnSide = numberCell.getAttribute('data-column-side') ?? numberCell.getAttribute('data-side') ?? numberCell.closest('[data-column-side]')?.getAttribute('data-column-side'); if (explicitColumnSide === 'deletions' || explicitColumnSide === 'left' || explicitColumnSide === 'original') { return 'deletions'; } if (explicitColumnSide === 'additions' || explicitColumnSide === 'right' || explicitColumnSide === 'modified') { return 'additions'; } const row = numberCell.closest('[data-line-type]'); if (row instanceof HTMLElement) { const rowRect = row.getBoundingClientRect(); const cellRect = numberCell.getBoundingClientRect(); const rowCenter = rowRect.left + rowRect.width / 2; const cellCenter = cellRect.left + cellRect.width / 2; return cellCenter < rowCenter ? 'deletions' : 'additions'; } return 'additions'; }, []); ensurePierreThemeRegistered(lightTheme); ensurePierreThemeRegistered(darkTheme); const diffThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`; const isLargeContent = useMemo(() => { if (fileDiff) { const deletionLength = fileDiff.deletionLines.reduce((total, line) => total + line.length, 0); const additionLength = fileDiff.additionLines.reduce((total, line) => total + line.length, 0); return Math.max(deletionLength, additionLength) > LARGE_CONTENT_BYTES; } return Math.max(original.length, modified.length) > LARGE_CONTENT_BYTES; }, [fileDiff, modified.length, original.length]); const diffRootRef = useRef(null); const diffContainerRef = useRef(null); const diffInstanceRef = useRef | null>(null); const sharedVirtualizerRef = useRef(null); const instanceVirtualizerRef = useRef(null); const instanceWorkerPoolRef = useRef(null); const instanceVirtualHunkSeparatorsRef = useRef['hunkSeparators'] | undefined>(undefined); const instanceFileDiffRef = useRef(undefined); const instanceOldFileRef = useRef(undefined); const instanceNewFileRef = useRef(undefined); const [, forceUpdate] = React.useReducer((x) => x + 1, 0); const workerPool = useWorkerPool(isLargeContent ? 'unified' : (renderSideBySide ? 'split' : 'unified')); const lightResolvedTheme = useMemo(() => getResolvedShikiTheme(lightTheme), [lightTheme]); const darkResolvedTheme = useMemo(() => getResolvedShikiTheme(darkTheme), [darkTheme]); // Fast-path: update base diff theme vars immediately. // Without this, already-mounted diffs can keep old bg/bars until async highlight completes. React.useLayoutEffect(() => { const root = diffRootRef.current; if (!root) return; const container = root.querySelector('diffs-container') as HTMLElement | null; if (!container) return; const currentResolved = isDark ? darkResolvedTheme : lightResolvedTheme; const getColor = ( resolved: typeof currentResolved, key: string, ): string | undefined => { const colors = resolved.colors as Record | undefined; return colors?.[key]; }; const lightAdd = getColor(lightResolvedTheme, 'terminal.ansiGreen'); const lightDel = getColor(lightResolvedTheme, 'terminal.ansiRed'); const lightMod = getColor(lightResolvedTheme, 'terminal.ansiBlue'); const darkAdd = getColor(darkResolvedTheme, 'terminal.ansiGreen'); const darkDel = getColor(darkResolvedTheme, 'terminal.ansiRed'); const darkMod = getColor(darkResolvedTheme, 'terminal.ansiBlue'); // Apply on host; vars inherit into shadow root. container.style.setProperty('--shiki-light', lightResolvedTheme.fg); container.style.setProperty('--shiki-light-bg', lightResolvedTheme.bg); if (lightAdd) container.style.setProperty('--shiki-light-addition-color', lightAdd); if (lightDel) container.style.setProperty('--shiki-light-deletion-color', lightDel); if (lightMod) container.style.setProperty('--shiki-light-modified-color', lightMod); container.style.setProperty('--shiki-dark', darkResolvedTheme.fg); container.style.setProperty('--shiki-dark-bg', darkResolvedTheme.bg); if (darkAdd) container.style.setProperty('--shiki-dark-addition-color', darkAdd); if (darkDel) container.style.setProperty('--shiki-dark-deletion-color', darkDel); if (darkMod) container.style.setProperty('--shiki-dark-modified-color', darkMod); container.style.setProperty('--diffs-bg', currentResolved.bg); container.style.setProperty('--diffs-fg', currentResolved.fg); const currentAdd = isDark ? darkAdd : lightAdd; const currentDel = isDark ? darkDel : lightDel; const currentMod = isDark ? darkMod : lightMod; if (currentAdd) container.style.setProperty('--diffs-addition-color-override', currentAdd); if (currentDel) container.style.setProperty('--diffs-deletion-color-override', currentDel); if (currentMod) container.style.setProperty('--diffs-modified-color-override', currentMod); // Pierre also inlines theme styles on
 inside shadow root.
    // Patch it too so already-expanded diffs switch instantly.
    const pre = container.shadowRoot?.querySelector('pre') as HTMLPreElement | null;
    if (pre) {
      pre.style.setProperty('--shiki-light', lightResolvedTheme.fg);
      pre.style.setProperty('--shiki-light-bg', lightResolvedTheme.bg);
      if (lightAdd) pre.style.setProperty('--shiki-light-addition-color', lightAdd);
      if (lightDel) pre.style.setProperty('--shiki-light-deletion-color', lightDel);
      if (lightMod) pre.style.setProperty('--shiki-light-modified-color', lightMod);

      pre.style.setProperty('--shiki-dark', darkResolvedTheme.fg);
      pre.style.setProperty('--shiki-dark-bg', darkResolvedTheme.bg);
      if (darkAdd) pre.style.setProperty('--shiki-dark-addition-color', darkAdd);
      if (darkDel) pre.style.setProperty('--shiki-dark-deletion-color', darkDel);
      if (darkMod) pre.style.setProperty('--shiki-dark-modified-color', darkMod);

      pre.style.setProperty('--diffs-bg', currentResolved.bg);
      pre.style.setProperty('--diffs-fg', currentResolved.fg);
      if (currentAdd) pre.style.setProperty('--diffs-addition-color-override', currentAdd);
      if (currentDel) pre.style.setProperty('--diffs-deletion-color-override', currentDel);
      if (currentMod) pre.style.setProperty('--diffs-modified-color-override', currentMod);
    }
  }, [darkResolvedTheme, diffThemeKey, isDark, lightResolvedTheme]);


  const options = useMemo(() => ({
    theme: {
      dark: darkTheme.metadata.id,
      light: lightTheme.metadata.id,
    },
    themeType: isDark ? ('dark' as const) : ('light' as const),
    diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
    diffIndicators: 'none' as const,
    hunkSeparators: 'line-info-basic' as const,
    // Perf: disable intra-line diff (word-level) globally.
    lineDiffType: 'none' as const,
    // Perf: degrade tokenization/highlighting for large files (>500KB)
    maxLineDiffLength: isLargeContent ? 0 : 1000,
    maxLineLengthForHighlighting: isLargeContent ? 1 : 1000,
    tokenizeMaxLineLength: isLargeContent ? 1 : 1000,
    collapsedContextThreshold: 0,
    expansionLineCount: 20,
    overflow: wrapLines ? ('wrap' as const) : ('scroll' as const),
    disableFileHeader: true,
    enableLineSelection: enableComments,
    enableHoverUtility: false,
    onLineSelected: enableComments ? handleSelectionChange : undefined,
    unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
    renderAnnotation: enableComments ? renderAnnotation : undefined,
  }), [darkTheme.metadata.id, enableComments, isDark, isLargeContent, lightTheme.metadata.id, renderSideBySide, wrapLines, handleSelectionChange, renderAnnotation]);


  const lineAnnotations = useMemo(() => {
    if (!enableComments) {
      return [];
    }

    return buildPierreLineAnnotations({
      drafts: fileDrafts,
      editingDraftId,
      selection,
    });
  }, [editingDraftId, enableComments, fileDrafts, selection]);

  const lineAnnotationsRef = useRef(lineAnnotations);

  useEffect(() => {
    lineAnnotationsRef.current = lineAnnotations;
  }, [lineAnnotations]);

  useEffect(() => {
    const container = diffContainerRef.current;
    return () => {
      diffInstanceRef.current?.cleanUp();
      diffInstanceRef.current = null;
      sharedVirtualizerRef.current?.release();
      sharedVirtualizerRef.current = null;
      instanceVirtualizerRef.current = null;
      instanceWorkerPoolRef.current = null;
      instanceVirtualHunkSeparatorsRef.current = undefined;
      instanceFileDiffRef.current = undefined;
      instanceOldFileRef.current = undefined;
      instanceNewFileRef.current = undefined;
      if (container) {
        container.innerHTML = '';
      }
    };
  }, []);

  useEffect(() => {
    if (typeof window === 'undefined') return;

    const container = diffContainerRef.current;
    const wrapper = diffRootRef.current;
    if (!container) return;
    if (!workerPool) return;

    const preserveDone = preserveScrollPosition(wrapper, container);
    let sharedVirtualizer = sharedVirtualizerRef.current;
    if (!sharedVirtualizer) {
      sharedVirtualizer = acquireSharedVirtualizer(container);
      sharedVirtualizerRef.current = sharedVirtualizer;
    }
    sharedVirtualizerRef.current = sharedVirtualizer;
    const virtualizer = sharedVirtualizer?.virtualizer ?? null;

    const oldFile: FileContents | undefined = fileDiff ? undefined : {
      name: fileName || '',
      contents: original,
      lang: language as FileContents['lang'],
      cacheKey: `old:${diffThemeKey}:${fileName}:${makeContentCacheKey(original)}`,
    };
    const newFile: FileContents | undefined = fileDiff ? undefined : {
      name: fileName || '',
      contents: modified,
      lang: language as FileContents['lang'],
      cacheKey: `new:${diffThemeKey}:${fileName}:${makeContentCacheKey(modified)}`,
    };

    const targetChanged = fileDiff
      ? instanceFileDiffRef.current !== fileDiff
      : instanceFileDiffRef.current !== undefined
        || !oldFile
        || !newFile
        || !instanceOldFileRef.current
        || !instanceNewFileRef.current
        || !areFilesEqual(instanceOldFileRef.current, oldFile)
        || !areFilesEqual(instanceNewFileRef.current, newFile);

    const currentInstance = diffInstanceRef.current;
    const shouldReset = Boolean(
      currentInstance
      && (
        instanceVirtualizerRef.current !== virtualizer
        || instanceWorkerPoolRef.current !== workerPool
        || (virtualizer && (instanceVirtualHunkSeparatorsRef.current !== options.hunkSeparators || targetChanged))
      )
    );

    if (shouldReset) {
      currentInstance?.cleanUp();
      diffInstanceRef.current = null;
      container.innerHTML = '';
    }

    let instance = diffInstanceRef.current;
    const forceRender = !shouldReset && currentInstance
      ? !areOptionsEqual(currentInstance.options, options)
      : false;
    if (!instance) {
      instance = sharedVirtualizer
        ? new VirtualizedFileDiff(
            options as FileDiffOptions,
            sharedVirtualizer.virtualizer,
            VIRTUAL_METRICS,
            workerPool,
          )
        : new PierreFileDiff(options as FileDiffOptions, workerPool);
      diffInstanceRef.current = instance;
      lastAppliedSelectionRef.current = null;
    } else {
      instance.setOptions(options as FileDiffOptions);
    }

    instanceVirtualizerRef.current = virtualizer;
    instanceWorkerPoolRef.current = workerPool;
    instanceVirtualHunkSeparatorsRef.current = virtualizer ? options.hunkSeparators : undefined;
    instanceFileDiffRef.current = fileDiff;
    instanceOldFileRef.current = oldFile;
    instanceNewFileRef.current = newFile;

    if (fileDiff) {
      instance.render({
        fileDiff,
        forceRender,
        lineAnnotations: lineAnnotationsRef.current,
        containerWrapper: container,
      });
    } else {
      if (!oldFile || !newFile) return;

      instance.render({
        oldFile,
        newFile,
        forceRender,
        lineAnnotations: lineAnnotationsRef.current,
        containerWrapper: container,
      });
    }

    const cancelReady = waitForDiffReady(container, () => {
      preserveDone();
      wakeVirtualizer(instance, sharedVirtualizer, forceUpdate);
    });

    return () => {
      cancelReady();
      preserveDone();
    };
  }, [diffThemeKey, fileDiff, fileName, language, modified, options, original, workerPool]);

  useEffect(() => {
    const instance = diffInstanceRef.current;
    if (!instance) return;

    try {
      instance.setLineAnnotations(lineAnnotations);
    } catch (error) {
      console.error('Failed to apply diff line annotations', error);
      try {
        instance.setLineAnnotations([]);
      } catch {
        // ignored
      }
    }

    requestAnimationFrame(() => {
      if (diffInstanceRef.current !== instance) return;
      try {
        instance.rerender();
      } catch (err) {
        void err;
      }
      forceUpdate();
    });
  }, [lineAnnotations]);

  useEffect(() => {
    const instance = diffInstanceRef.current;
    if (!instance) return;

    // Only push selection to the diff when clearing.
    // User-driven selections already originate from the diff itself.
    if (selection !== null) {
      return;
    }

    // Guard against feedback loops and redundant updates
    const lastApplied = lastAppliedSelectionRef.current;
    if (isSameSelection(selection, lastApplied)) {
      return;
    }

    try {
      isApplyingSelectionRef.current = true;
      instance.setSelectedLines(selection);
      lastAppliedSelectionRef.current = selection;
    } catch {
      // ignore
    } finally {
      isApplyingSelectionRef.current = false;
    }
  }, [selection]);

  useEffect(() => {
    if (!enableComments) return;

    const container = diffContainerRef.current;
    if (!container) return;

    let rafId: number | null = null;
    let cleanup = () => {};

    const setup = () => {
      const host = container.querySelector('diffs-container');
      const shadowRoot = host?.shadowRoot;
      if (!shadowRoot) {
        rafId = requestAnimationFrame(setup);
        return;
      }

      const onClickCapture = (event: Event) => {
        if (!(event instanceof MouseEvent) || event.button !== 0) return;
        if (!(event.target instanceof Element)) return;

        const numberCell = event.target.closest('[data-column-number]');
        if (!(numberCell instanceof HTMLElement)) return;

        const lineRaw = numberCell.getAttribute('data-column-number');
        const lineNumber = lineRaw ? parseInt(lineRaw, 10) : NaN;
        if (Number.isNaN(lineNumber)) return;

        const side = resolveClickedSide(numberCell);

        handleSelectionChange({
          start: lineNumber,
          end: lineNumber,
          side,
        });

        event.preventDefault();
        event.stopPropagation();
      };

      shadowRoot.addEventListener('click', onClickCapture, true);
      cleanup = () => {
        shadowRoot.removeEventListener('click', onClickCapture, true);
      };
    };

    setup();

    return () => {
      if (rafId !== null) {
        cancelAnimationFrame(rafId);
      }
      cleanup();
    };
  }, [diffThemeKey, enableComments, fileName, handleSelectionChange, resolveClickedSide]);

  // MutationObserver to trigger re-renders when annotation DOM nodes are added/removed
  useEffect(() => {
    const container = diffContainerRef.current;
    if (!container) return;

    let observer: MutationObserver | null = null;
    let rafId: number | null = null;

    const setupObserver = () => {
      const diffsContainer = container.querySelector('diffs-container');
      if (!(diffsContainer instanceof HTMLElement)) return;

      const shadowRoot = diffsContainer.shadowRoot;

      observer = new MutationObserver(() => {
        if (rafId) cancelAnimationFrame(rafId);
        rafId = requestAnimationFrame(() => {
          forceUpdate();
          rafId = null;
        });
      });

      observer.observe(diffsContainer, { childList: true, subtree: true });
      if (shadowRoot) {
        observer.observe(shadowRoot, { childList: true, subtree: true });
      }
    };

    const timeoutId = setTimeout(setupObserver, 100);

    return () => {
      clearTimeout(timeoutId);
      if (rafId) cancelAnimationFrame(rafId);
      observer?.disconnect();
    };
  }, [diffThemeKey, fileName]);

  if (typeof window === 'undefined') {
    return null;
  }

  const commentOverlays = enableComments ? (
     {
        applySelection({
          start: draft.startLine,
          end: draft.endLine,
          side: draft.side === 'original' ? 'deletions' : 'additions',
        });
        startEdit(draft);
      }}
      onDelete={deleteDraft}
    />
  ) : null;

  if (layout === 'fill') {
    return (
      
{commentOverlays}
); } // Fallback for 'inline' layout return (
{commentOverlays}
); };