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 (