import { EditorView } from '@codemirror/view'; import { unifiedMergeView } from '@codemirror/merge'; import type { Extension } from '@codemirror/state'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { usePaletteOps } from '../../../contexts/PaletteOpsContext'; import { useCodeEditorDocument } from '../hooks/useCodeEditorDocument'; import { useCodeEditorSettings } from '../hooks/useCodeEditorSettings'; import { useEditorKeyboardShortcuts } from '../hooks/useEditorKeyboardShortcuts'; import type { CodeEditorFile } from '../types/types'; import { createMinimapExtension, createScrollToFirstChunkExtension, getLanguageExtensions } from '../utils/editorExtensions'; import { getEditorStyles } from '../utils/editorStyles'; import { createEditorToolbarPanelExtension } from '../utils/editorToolbarPanel'; import CodeEditorFooter from './subcomponents/CodeEditorFooter'; import CodeEditorHeader from './subcomponents/CodeEditorHeader'; import CodeEditorLoadingState from './subcomponents/CodeEditorLoadingState'; import CodeEditorSurface from './subcomponents/CodeEditorSurface'; import CodeEditorBinaryFile from './subcomponents/CodeEditorBinaryFile'; type CodeEditorProps = { file: CodeEditorFile; onClose: () => void; projectPath?: string; isSidebar?: boolean; isExpanded?: boolean; onToggleExpand?: (() => void) | null; onPopOut?: (() => void) | null; }; export default function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null, onPopOut = null, }: CodeEditorProps) { const { t } = useTranslation('codeEditor'); const paletteOps = usePaletteOps(); const [isFullscreen, setIsFullscreen] = useState(false); const [showDiff, setShowDiff] = useState(Boolean(file.diffInfo)); const [markdownPreview, setMarkdownPreview] = useState(false); const { isDarkMode, wordWrap, minimapEnabled, showLineNumbers, fontSize, } = useCodeEditorSettings(); const { content, setContent, loading, saving, saveSuccess, saveError, isBinary, handleSave, handleDownload, } = useCodeEditorDocument({ file, projectPath, }); const isMarkdownFile = useMemo(() => { const extension = file.name.split('.').pop()?.toLowerCase(); return extension === 'md' || extension === 'markdown'; }, [file.name]); const minimapExtension = useMemo( () => ( createMinimapExtension({ file, showDiff, minimapEnabled, isDarkMode, }) ), [file, isDarkMode, minimapEnabled, showDiff], ); const scrollToFirstChunkExtension = useMemo( () => createScrollToFirstChunkExtension({ file, showDiff }), [file, showDiff], ); const toolbarPanelExtension = useMemo( () => ( createEditorToolbarPanelExtension({ file, showDiff, isSidebar, isExpanded, onToggleDiff: () => setShowDiff((previous) => !previous), onPopOut, onToggleExpand, labels: { changes: t('toolbar.changes'), previousChange: t('toolbar.previousChange'), nextChange: t('toolbar.nextChange'), hideDiff: t('toolbar.hideDiff'), showDiff: t('toolbar.showDiff'), collapse: t('toolbar.collapse'), expand: t('toolbar.expand'), }, }) ), [file, isExpanded, isSidebar, onPopOut, onToggleExpand, showDiff, t], ); const extensions = useMemo(() => { const allExtensions: Extension[] = [ ...getLanguageExtensions(file.name), ...toolbarPanelExtension, ]; if (file.diffInfo && showDiff && file.diffInfo.old_string !== undefined) { allExtensions.push( unifiedMergeView({ original: file.diffInfo.old_string, mergeControls: false, highlightChanges: true, syntaxHighlightDeletions: false, gutter: true, }), ); allExtensions.push(...minimapExtension); allExtensions.push(...scrollToFirstChunkExtension); } if (wordWrap) { allExtensions.push(EditorView.lineWrapping); } return allExtensions; }, [ file.diffInfo, file.name, minimapExtension, scrollToFirstChunkExtension, showDiff, toolbarPanelExtension, wordWrap, ]); useEditorKeyboardShortcuts({ onSave: handleSave, onClose, dependency: content, }); if (loading) { return ( ); } // Binary file display if (isBinary) { return ( setIsFullscreen((previous) => !previous)} title={t('binaryFile.title', 'Binary File')} message={t('binaryFile.message', 'The file "{{fileName}}" cannot be displayed in the text editor because it is a binary file.', { fileName: file.name })} /> ); } const outerContainerClassName = isSidebar ? 'w-full h-full flex flex-col' : `fixed inset-0 z-[9999] md:bg-black/50 md:flex md:items-center md:justify-center md:p-4 ${isFullscreen ? 'md:p-0' : ''}`; const innerContainerClassName = isSidebar ? 'bg-background flex flex-col w-full h-full' : `bg-background shadow-2xl flex flex-col w-full h-full md:rounded-lg md:shadow-2xl${ isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]' }`; return ( <>
setMarkdownPreview((previous) => !previous)} onOpenSettings={() => paletteOps.openSettings('appearance')} onDownload={handleDownload} onSave={handleSave} onToggleFullscreen={() => setIsFullscreen((previous) => !previous)} onClose={onClose} labels={{ showingChanges: t('header.showingChanges'), editMarkdown: t('actions.editMarkdown'), previewMarkdown: t('actions.previewMarkdown'), settings: t('toolbar.settings'), download: t('actions.download'), save: t('actions.save'), saving: t('actions.saving'), saved: t('actions.saved'), fullscreen: t('actions.fullscreen'), exitFullscreen: t('actions.exitFullscreen'), close: t('actions.close'), }} /> {saveError && (
{saveError}
)}
); }