mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-01 10:18:37 +00:00
* refactor(ui): replace in-repo Command primitive with cmdk wrapper * feat(command-palette): add global Cmd+K palette with v1 actions * feat(command-palette): add session, file, and commit search sources * refactor: add provider names to model constants * feat(command-palette): add settings, navigation, message search, and ⌘K hints * feat(command-palette): add git fetch/pull/push and branch switch actions * refactor(command-palette): consolidate fetch source hooks behind useApiSource * refactor(command-palette): extract useCommandKey and SETTINGS_MAIN_TABS metadata * refactor(command-palette): extract groups into declarative registry * refactor(command-palette): wire openFile through PaletteOpsContext * refactor: migrate openSettings and refreshProjects from window.* to PaletteOpsContext * refactor(command-palette): inline groups and delete registry indirection * refactor(command-palette): return items array directly from source hooks * refactor(palette-ops): flatten Handle wrapper into ref-based registry * refactor: inline useCommandKey as MOD_KEY constant in two call sites * feat: introduce pages and fix bug on branch switching * fix: small labels * fix: coderabbit issues * fix: coderabbit comments * Update src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
254 lines
7.8 KiB
TypeScript
254 lines
7.8 KiB
TypeScript
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 (
|
|
<CodeEditorLoadingState
|
|
isDarkMode={isDarkMode}
|
|
isSidebar={isSidebar}
|
|
loadingText={t('loading', { fileName: file.name })}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Binary file display
|
|
if (isBinary) {
|
|
return (
|
|
<CodeEditorBinaryFile
|
|
file={file}
|
|
isSidebar={isSidebar}
|
|
isFullscreen={isFullscreen}
|
|
onClose={onClose}
|
|
onToggleFullscreen={() => 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 (
|
|
<>
|
|
<style>{getEditorStyles(isDarkMode)}</style>
|
|
<div className={outerContainerClassName}>
|
|
<div className={innerContainerClassName}>
|
|
<CodeEditorHeader
|
|
file={file}
|
|
isSidebar={isSidebar}
|
|
isFullscreen={isFullscreen}
|
|
isMarkdownFile={isMarkdownFile}
|
|
markdownPreview={markdownPreview}
|
|
saving={saving}
|
|
saveSuccess={saveSuccess}
|
|
onToggleMarkdownPreview={() => 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 && (
|
|
<div className="border-b border-red-200 bg-red-50 px-3 py-1.5 text-xs text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-300">
|
|
{saveError}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 overflow-hidden">
|
|
<CodeEditorSurface
|
|
content={content}
|
|
onChange={setContent}
|
|
markdownPreview={markdownPreview}
|
|
isMarkdownFile={isMarkdownFile}
|
|
isDarkMode={isDarkMode}
|
|
fontSize={fontSize}
|
|
showLineNumbers={showLineNumbers}
|
|
extensions={extensions}
|
|
/>
|
|
</div>
|
|
|
|
<CodeEditorFooter
|
|
content={content}
|
|
linesLabel={t('footer.lines')}
|
|
charactersLabel={t('footer.characters')}
|
|
shortcutsLabel={t('footer.shortcuts')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|