import { DiffEditor, Monaco } from "@monaco-editor/react"; import React from "react"; import { editor as editor_t } from "monaco-editor"; import { LuFileDiff, LuFileMinus, LuFilePlus } from "react-icons/lu"; import { IconType } from "react-icons/lib"; import { GitChangeStatus } from "#/api/open-hands.types"; import { getLanguageFromPath } from "#/utils/get-language-from-path"; import { cn } from "#/utils/utils"; import ChevronUp from "#/icons/chveron-up.svg?react"; import { useGitDiff } from "#/hooks/query/use-get-diff"; interface LoadingSpinnerProps { className?: string; } // TODO: Move out of this file and replace the current spinner with this one function LoadingSpinner({ className }: LoadingSpinnerProps) { return (
); } const STATUS_MAP: Record = { A: LuFilePlus, D: LuFileMinus, M: LuFileDiff, R: "Renamed", U: "Untracked", }; export interface FileDiffViewerProps { path: string; type: GitChangeStatus; } export function FileDiffViewer({ path, type }: FileDiffViewerProps) { const [isCollapsed, setIsCollapsed] = React.useState(true); const [editorHeight, setEditorHeight] = React.useState(400); const diffEditorRef = React.useRef(null); const isAdded = type === "A" || type === "U"; const isDeleted = type === "D"; const filePath = React.useMemo(() => { if (type === "R") { const parts = path.split(/\s+/).slice(1); return parts[parts.length - 1]; } return path; }, [path, type]); const { data: diff, isLoading, isSuccess, isRefetching, } = useGitDiff({ filePath, type, enabled: !isCollapsed, }); // Function to update editor height based on content const updateEditorHeight = React.useCallback(() => { if (diffEditorRef.current) { const originalEditor = diffEditorRef.current.getOriginalEditor(); const modifiedEditor = diffEditorRef.current.getModifiedEditor(); if (originalEditor && modifiedEditor) { // Get the content height from both editors and use the larger one const originalHeight = originalEditor.getContentHeight(); const modifiedHeight = modifiedEditor.getContentHeight(); const contentHeight = Math.max(originalHeight, modifiedHeight); // Add a small buffer to avoid scrollbar setEditorHeight(contentHeight + 20); } } }, []); const beforeMount = (monaco: Monaco) => { monaco.editor.defineTheme("custom-diff-theme", { base: "vs-dark", inherit: true, rules: [ { token: "comment", foreground: "6a9955" }, { token: "keyword", foreground: "569cd6" }, { token: "string", foreground: "ce9178" }, { token: "number", foreground: "b5cea8" }, ], colors: { "diffEditor.insertedTextBackground": "#014b01AA", // Stronger green background "diffEditor.removedTextBackground": "#750000AA", // Stronger red background "diffEditor.insertedLineBackground": "#003f00AA", // Dark green for added lines "diffEditor.removedLineBackground": "#5a0000AA", // Dark red for removed lines "diffEditor.border": "#444444", // Border between diff editors "editorUnnecessaryCode.border": "#00000000", // No border for unnecessary code "editorUnnecessaryCode.opacity": "#00000077", // Slightly faded }, }); }; const handleEditorDidMount = (editor: editor_t.IStandaloneDiffEditor) => { diffEditorRef.current = editor; updateEditorHeight(); const originalEditor = editor.getOriginalEditor(); const modifiedEditor = editor.getModifiedEditor(); originalEditor.onDidContentSizeChange(updateEditorHeight); modifiedEditor.onDidContentSizeChange(updateEditorHeight); }; const status = (type === "U" ? STATUS_MAP.A : STATUS_MAP[type]) || "?"; let statusIcon: React.ReactNode; if (typeof status === "string") { statusIcon = {status}; } else { const StatusIcon = status; // now it's recognized as a component statusIcon = ; } const isFetchingData = isLoading || isRefetching; return (
setIsCollapsed((prev) => !prev)} > {isFetchingData && } {!isFetchingData && statusIcon} {filePath}
{isSuccess && !isCollapsed && (
)}
); }