diff --git a/src/renderer/src/components/CodeBlockView/index.tsx b/src/renderer/src/components/CodeBlockView/index.tsx index 86a5f0d043..3b9f34ceba 100644 --- a/src/renderer/src/components/CodeBlockView/index.tsx +++ b/src/renderer/src/components/CodeBlockView/index.tsx @@ -199,12 +199,11 @@ const CodeBlockView: React.FC = ({ children, language, onSave }) => { // 源代码视图组件 const sourceView = useMemo(() => { - const SourceView = codeEditor.enabled ? CodeEditor : CodePreview - return ( - - {children} - - ) + if (codeEditor.enabled) { + return + } else { + return {children} + } }, [children, codeEditor.enabled, language, onSave]) // 特殊视图组件映射 diff --git a/src/renderer/src/components/CodeEditor/hook.ts b/src/renderer/src/components/CodeEditor/hook.ts new file mode 100644 index 0000000000..c5bbab2d0d --- /dev/null +++ b/src/renderer/src/components/CodeEditor/hook.ts @@ -0,0 +1,65 @@ +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' +import { Extension } from '@uiw/react-codemirror' +import { useEffect, useState } from 'react' + +let linterPromise: Promise | null = null +function importLintPackage() { + if (!linterPromise) { + linterPromise = import('@codemirror/lint').then((mod) => mod.linter) + } + return linterPromise +} + +// 语言对应的 linter 加载器 +const linterLoaders: Record Promise> = { + json: async () => { + const [linter, jsonParseLinter] = await Promise.all([ + importLintPackage(), + import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter) + ]) + return linter(jsonParseLinter()) + } +} + +export const useLanguageExtensions = (language: string, lint?: boolean) => { + const { languageMap } = useCodeStyle() + const [extensions, setExtensions] = useState([]) + + // 加载语言 + useEffect(() => { + let normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase() + + // 如果语言名包含 `-`,转换为驼峰命名法 + if (normalizedLang.includes('-')) { + normalizedLang = normalizedLang.replace(/-([a-z])/g, (_, char) => char.toUpperCase()) + } + + import('@uiw/codemirror-extensions-langs') + .then(({ loadLanguage }) => { + const extension = loadLanguage(normalizedLang as any) + if (extension) { + setExtensions((prev) => [...prev, extension]) + } + }) + .catch((error) => { + console.debug(`Failed to load language: ${normalizedLang}`, error) + }) + }, [language, languageMap]) + + useEffect(() => { + if (!lint) return + + const loader = linterLoaders[language] + if (loader) { + loader() + .then((extension) => { + setExtensions((prev) => [...prev, extension]) + }) + .catch((error) => { + console.error(`Failed to load linter for ${language}`, error) + }) + } + }, [language, lint]) + + return extensions +} diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx index 6d1f059006..867d32e17e 100644 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ b/src/renderer/src/components/CodeEditor/index.tsx @@ -14,17 +14,23 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { memo } from 'react' import { useTranslation } from 'react-i18next' +import { useLanguageExtensions } from './hook' + // 标记非用户编辑的变更 const External = Annotation.define() interface Props { - children: string + value: string + placeholder?: string | HTMLElement language: string onSave?: (newContent: string) => void onChange?: (newContent: string) => void + minHeight?: string maxHeight?: string /** 用于覆写编辑器的某些设置 */ options?: { + stream?: boolean // 用于流式响应场景,默认 false + lint?: boolean collapsible?: boolean wrappable?: boolean keymap?: boolean @@ -36,11 +42,22 @@ interface Props { } /** - * 源代码编辑器,基于 CodeMirror + * 源代码编辑器,基于 CodeMirror,封装了 ReactCodeMirror。 * * 目前必须和 CodeToolbar 配合使用。 */ -const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, extensions, style }: Props) => { +const CodeEditor = ({ + value, + placeholder, + language, + onSave, + onChange, + minHeight, + maxHeight, + options, + extensions, + style +}: Props) => { const { fontSize, codeShowLineNumbers: _lineNumbers, @@ -61,38 +78,18 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, } }, [codeEditor, _lineNumbers, options]) - const { activeCmTheme, languageMap } = useCodeStyle() + const { activeCmTheme } = useCodeStyle() const [isExpanded, setIsExpanded] = useState(!collapsible) const [isUnwrapped, setIsUnwrapped] = useState(!wrappable) - const initialContent = useRef(children?.trimEnd() ?? '') - const [langExtension, setLangExtension] = useState([]) + const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? '')) const [editorReady, setEditorReady] = useState(false) const editorViewRef = useRef(null) const { t } = useTranslation() + const langExtensions = useLanguageExtensions(language, options?.lint) + const { registerTool, removeTool } = useCodeToolbar() - // 加载语言 - useEffect(() => { - let normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase() - - // 如果语言名包含 `-`,转换为驼峰命名法 - if (normalizedLang.includes('-')) { - normalizedLang = normalizedLang.replace(/-([a-z])/g, (_, char) => char.toUpperCase()) - } - - import('@uiw/codemirror-extensions-langs') - .then(({ loadLanguage }) => { - const extension = loadLanguage(normalizedLang as any) - if (extension) { - setLangExtension([extension]) - } - }) - .catch((error) => { - console.debug(`Failed to load language: ${normalizedLang}`, error) - }) - }, [language, languageMap]) - // 展开/折叠工具 useEffect(() => { registerTool({ @@ -144,7 +141,7 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, useEffect(() => { if (!editorViewRef.current) return - const newContent = children?.trimEnd() ?? '' + const newContent = options?.stream ? (value ?? '').trimEnd() : (value ?? '') const currentDoc = editorViewRef.current.state.doc.toString() const changes = prepareCodeChanges(currentDoc, newContent) @@ -155,7 +152,7 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, annotations: [External.of(true)] }) } - }, [children]) + }, [options?.stream, value]) useEffect(() => { setIsExpanded(!collapsible) @@ -182,17 +179,19 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, const customExtensions = useMemo(() => { return [ ...(extensions ?? []), - ...langExtension, + ...langExtensions, ...(isUnwrapped ? [] : [EditorView.lineWrapping]), ...(enableKeymap ? [saveKeymap] : []) ] - }, [extensions, langExtension, isUnwrapped, enableKeymap, saveKeymap]) + }, [extensions, langExtensions, isUnwrapped, enableKeymap, saveKeymap]) return ( = ({ visible, onClose, onSuc const [form] = Form.useForm() const [loading, setLoading] = useState(false) const dispatch = useAppDispatch() - const [editorExtensions, setEditorExtensions] = useState([]) // 新增 editorExtensions 狀態 - - // 載入 CodeMirror JSON Linter 擴充功能 - useEffect(() => { - let isMounted = true - Promise.all([ - import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter), - import('@codemirror/lint').then((mod) => mod.linter) - ]).then(([jsonParseLinter, linter]) => { - if (isMounted) { - setEditorExtensions([linter(jsonParseLinter())]) - } - }) - return () => { - isMounted = false - } - }, []) const handleOk = async () => { try { @@ -177,10 +159,14 @@ const AddMcpServerModal: FC = ({ visible, onClose, onSuc rules={[{ required: true, message: t('settings.mcp.addServer.importFrom.placeholder') }]}> = ({ visible, onClose, onSuc highlightActiveLine: true, keymap: true }} - extensions={editorExtensions} - // 如果表單值為空,顯示範例 JSON;否則顯示表單值 - > - {serverConfigValue ?? initialJsonExample} - + /> diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx index 32228dc96e..497b74639e 100644 --- a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx @@ -4,17 +4,16 @@ import { TopView } from '@renderer/components/TopView' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setMCPServers } from '@renderer/store/mcp' import { MCPServer } from '@renderer/types' -import { Extension } from '@uiw/react-codemirror' import { Modal, Typography } from 'antd' -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' + interface Props { resolve: (data: any) => void } const PopupContainer: React.FC = ({ resolve }) => { const [open, setOpen] = useState(true) - const [editorExtensions, setEditorExtensions] = useState([]) const [jsonConfig, setJsonConfig] = useState('') const [jsonSaving, setJsonSaving] = useState(false) const [jsonError, setJsonError] = useState('') @@ -23,21 +22,6 @@ const PopupContainer: React.FC = ({ resolve }) => { const dispatch = useAppDispatch() const { t } = useTranslation() - useEffect(() => { - let isMounted = true - Promise.all([ - import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter), - import('@codemirror/lint').then((mod) => mod.linter) - ]).then(([jsonParseLinter, linter]) => { - if (isMounted) { - setEditorExtensions([linter(jsonParseLinter())]) - } - }) - return () => { - isMounted = false - } - }, []) - useEffect(() => { try { const mcpServersObj: Record = {} @@ -116,10 +100,6 @@ const PopupContainer: React.FC = ({ resolve }) => { resolve({}) } - const handleChange = useCallback((newContent: string) => { - setJsonConfig(newContent) - }, []) - EditMcpJsonPopup.hide = onCancel return ( @@ -143,10 +123,12 @@ const PopupContainer: React.FC = ({ resolve }) => {
setJsonConfig(value)} maxHeight="60vh" options={{ + lint: true, collapsible: true, wrappable: true, lineNumbers: true, @@ -154,9 +136,7 @@ const PopupContainer: React.FC = ({ resolve }) => { highlightActiveLine: true, keymap: true }} - extensions={editorExtensions}> - {jsonConfig} - + />
)}