"use client" import { useCallback, useMemo, useRef, useState } from "react" import dynamic from "next/dynamic" import { ChevronLeft, ChevronRight } from "lucide-react" import type { DiffOnMount } from "@monaco-editor/react" import type { editor as MonacoEditorNs } from "monaco-editor" import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes" import { cn } from "@/lib/utils" import "@/lib/monaco-local" const MonacoDiffEditor = dynamic( async () => { const mod = await import("@monaco-editor/react") return { default: mod.DiffEditor } }, { ssr: false } ) export interface DiffViewerProps { original: string modified: string originalLabel?: string modifiedLabel?: string language?: string className?: string } export function DiffViewer({ original, modified, originalLabel = "Original", modifiedLabel = "Modified", language = "plaintext", className, }: DiffViewerProps) { const editorTheme = useMonacoThemeSync() const diffEditorRef = useRef( null ) const [diffChanges, setDiffChanges] = useState( [] ) const [currentChangeIndex, setCurrentChangeIndex] = useState(-1) const handleEditorMount: DiffOnMount = useCallback((editor) => { diffEditorRef.current = editor let scrolledToFirst = false const updateDiffs = () => { const changes = editor.getLineChanges() setDiffChanges(changes ?? []) if (changes && changes.length > 0) { setCurrentChangeIndex(0) // Auto-scroll to the first change only once if (!scrolledToFirst) { scrolledToFirst = true const first = changes[0] const lineNumber = first.modifiedStartLineNumber || first.originalStartLineNumber || 1 const modifiedEditor = editor.getModifiedEditor() modifiedEditor.revealLineInCenter(lineNumber) modifiedEditor.setPosition({ lineNumber, column: 1 }) } } } editor.onDidUpdateDiff(updateDiffs) setTimeout(updateDiffs, 300) }, []) const navigateToChange = useCallback( (index: number) => { const editor = diffEditorRef.current if (!editor || diffChanges.length === 0) return const clampedIndex = Math.max(0, Math.min(index, diffChanges.length - 1)) setCurrentChangeIndex(clampedIndex) const change = diffChanges[clampedIndex] const lineNumber = change.modifiedStartLineNumber || change.originalStartLineNumber || 1 const modifiedEditor = editor.getModifiedEditor() modifiedEditor.revealLineInCenter(lineNumber) modifiedEditor.setPosition({ lineNumber, column: 1 }) }, [diffChanges] ) const handlePrevChange = useCallback(() => { if (currentChangeIndex > 0) { navigateToChange(currentChangeIndex - 1) } }, [currentChangeIndex, navigateToChange]) const handleNextChange = useCallback(() => { if (currentChangeIndex < diffChanges.length - 1) { navigateToChange(currentChangeIndex + 1) } }, [currentChangeIndex, diffChanges.length, navigateToChange]) const { additions, deletions } = useMemo(() => { let add = 0 let del = 0 for (const change of diffChanges) { // Monaco ILineChange: endLineNumber === 0 means no lines on that side // Pure insertion: originalEndLineNumber === 0 // Pure deletion: modifiedEndLineNumber === 0 const isInsertion = change.originalEndLineNumber === 0 const isDeletion = change.modifiedEndLineNumber === 0 if (isInsertion) { add += change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1 } else if (isDeletion) { del += change.originalEndLineNumber - change.originalStartLineNumber + 1 } else { del += change.originalEndLineNumber - change.originalStartLineNumber + 1 add += change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1 } } return { additions: add, deletions: del } }, [diffChanges]) return (
{originalLabel} {modifiedLabel} {diffChanges.length > 0 && ( <> +{additions} -{deletions} {diffChanges.length}{" "} {diffChanges.length === 1 ? "change" : "changes"}
{currentChangeIndex + 1} / {diffChanges.length}
)}
Loading diff viewer...
} options={{ readOnly: true, renderSideBySide: true, renderSideBySideInlineBreakpoint: 0, automaticLayout: true, fontSize: 13, minimap: { enabled: false }, scrollBeyondLastLine: false, renderOverviewRuler: false, ignoreTrimWhitespace: true, renderIndicators: true, originalEditable: false, }} />
) }