Spaces:
Build error
Build error
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 ( | |
<div className="flex items-center justify-center"> | |
<div | |
className={cn( | |
"animate-spin rounded-full border-4 border-gray-200 border-t-blue-500", | |
className, | |
)} | |
role="status" | |
aria-label="Loading" | |
/> | |
</div> | |
); | |
} | |
const STATUS_MAP: Record<GitChangeStatus, string | IconType> = { | |
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<editor_t.IStandaloneDiffEditor>(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 = <span>{status}</span>; | |
} else { | |
const StatusIcon = status; // now it's recognized as a component | |
statusIcon = <StatusIcon className="w-5 h-5" />; | |
} | |
const isFetchingData = isLoading || isRefetching; | |
return ( | |
<div data-testid="file-diff-viewer-outer" className="w-full flex flex-col"> | |
<div | |
className={cn( | |
"flex justify-between items-center px-2.5 py-3.5 border border-neutral-600 rounded-xl hover:cursor-pointer", | |
!isCollapsed && !isLoading && "border-b-0 rounded-b-none", | |
)} | |
onClick={() => setIsCollapsed((prev) => !prev)} | |
> | |
<span className="text-sm w-full text-content flex items-center gap-2"> | |
{isFetchingData && <LoadingSpinner className="w-5 h-5" />} | |
{!isFetchingData && statusIcon} | |
<strong className="w-full truncate">{filePath}</strong> | |
<button data-testid="collapse" type="button"> | |
<ChevronUp | |
className={cn( | |
"w-4 h-4 transition-transform", | |
isCollapsed && "transform rotate-180", | |
)} | |
/> | |
</button> | |
</span> | |
</div> | |
{isSuccess && !isCollapsed && ( | |
<div | |
className="w-full border border-neutral-600 overflow-hidden" | |
style={{ height: `${editorHeight}px` }} | |
> | |
<DiffEditor | |
data-testid="file-diff-viewer" | |
className="w-full h-full" | |
language={getLanguageFromPath(filePath)} | |
original={isAdded ? "" : diff.original} | |
modified={isDeleted ? "" : diff.modified} | |
theme="custom-diff-theme" | |
onMount={handleEditorDidMount} | |
beforeMount={beforeMount} | |
options={{ | |
renderValidationDecorations: "off", | |
readOnly: true, | |
renderSideBySide: !isAdded && !isDeleted, | |
scrollBeyondLastLine: false, | |
minimap: { | |
enabled: false, | |
}, | |
hideUnchangedRegions: { | |
enabled: true, | |
}, | |
automaticLayout: true, | |
scrollbar: { | |
// Make scrollbar less intrusive | |
alwaysConsumeMouseWheel: false, | |
}, | |
}} | |
/> | |
</div> | |
)} | |
</div> | |
); | |
} | |