refactor(CodeEditor): add more options to props for better customization, fix auto theme (#6071)

* refactor(CodeEditor): add more options to props for better customization

- A complete BasicSetupOptions could be passed in to override system-wise options
- EditMcpJsonPopup now use customized options

* fix: accommodate ThemeMode.auto

* fix: typos
This commit is contained in:
one 2025-05-17 01:32:54 +08:00 committed by GitHub
parent ba30bffa49
commit 51071d65fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 37 deletions

View File

@ -1,7 +1,7 @@
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import CodeMirror, { Annotation, EditorView, Extension, keymap } from '@uiw/react-codemirror'
import CodeMirror, { Annotation, BasicSetupOptions, EditorView, Extension, keymap } from '@uiw/react-codemirror'
import diff from 'fast-diff'
import {
ChevronsDownUp,
@ -22,10 +22,15 @@ interface Props {
language: string
onSave?: (newContent: string) => void
onChange?: (newContent: string) => void
// options used to override the default behaviour
maxHeight?: string
/** 用于覆写编辑器的某些设置 */
options?: {
maxHeight?: string
}
collapsible?: boolean
wrappable?: boolean
keymap?: boolean
} & BasicSetupOptions
/** 用于覆写编辑器的样式,会直接传给 CodeMirror 的 style 属性 */
style?: React.CSSProperties
}
/**
@ -33,11 +38,30 @@ interface Props {
*
* CodeToolbar 使
*/
const CodeEditor = ({ children, language, onSave, onChange, options }: Props) => {
const { fontSize, codeShowLineNumbers, codeCollapsible, codeWrappable, codeEditor } = useSettings()
const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, style }: Props) => {
const {
fontSize,
codeShowLineNumbers: _lineNumbers,
codeCollapsible: _collapsible,
codeWrappable: _wrappable,
codeEditor
} = useSettings()
const collapsible = useMemo(() => options?.collapsible ?? _collapsible, [options?.collapsible, _collapsible])
const wrappable = useMemo(() => options?.wrappable ?? _wrappable, [options?.wrappable, _wrappable])
const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap])
// 合并 codeEditor 和 options 的 basicSetupoptions 优先
const customBasicSetup = useMemo(() => {
return {
lineNumbers: _lineNumbers,
...(codeEditor as BasicSetupOptions),
...(options as BasicSetupOptions)
}
}, [codeEditor, _lineNumbers, options])
const { activeCmTheme, languageMap } = useCodeStyle()
const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable)
const [isExpanded, setIsExpanded] = useState(!collapsible)
const [isUnwrapped, setIsUnwrapped] = useState(!wrappable)
const initialContent = useRef(children?.trimEnd() ?? '')
const [langExtension, setLangExtension] = useState<Extension[]>([])
const [editorReady, setEditorReady] = useState(false)
@ -75,13 +99,13 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
tooltip: isExpanded ? t('code_block.collapse') : t('code_block.expand'),
visible: () => {
const scrollHeight = editorViewRef?.current?.scrollDOM?.scrollHeight
return codeCollapsible && (scrollHeight ?? 0) > 350
return collapsible && (scrollHeight ?? 0) > 350
},
onClick: () => setIsExpanded((prev) => !prev)
})
return () => removeTool(TOOL_SPECS.expand.id)
}, [codeCollapsible, isExpanded, registerTool, removeTool, t, editorReady])
}, [collapsible, isExpanded, registerTool, removeTool, t, editorReady])
// 自动换行工具
useEffect(() => {
@ -89,12 +113,12 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
...TOOL_SPECS.wrap,
icon: isUnwrapped ? <WrapIcon className="icon" /> : <UnWrapIcon className="icon" />,
tooltip: isUnwrapped ? t('code_block.wrap.on') : t('code_block.wrap.off'),
visible: () => codeWrappable,
visible: () => wrappable,
onClick: () => setIsUnwrapped((prev) => !prev)
})
return () => removeTool(TOOL_SPECS.wrap.id)
}, [codeWrappable, isUnwrapped, registerTool, removeTool, t])
}, [wrappable, isUnwrapped, registerTool, removeTool, t])
const handleSave = useCallback(() => {
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
@ -132,12 +156,12 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
}, [children])
useEffect(() => {
setIsExpanded(!codeCollapsible)
}, [codeCollapsible])
setIsExpanded(!collapsible)
}, [collapsible])
useEffect(() => {
setIsUnwrapped(!codeWrappable)
}, [codeWrappable])
setIsUnwrapped(!wrappable)
}, [wrappable])
// 保存功能的快捷键
const saveKeymap = useMemo(() => {
@ -154,19 +178,15 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
}, [handleSave])
const enabledExtensions = useMemo(() => {
return [
...langExtension,
...(isUnwrapped ? [] : [EditorView.lineWrapping]),
...(codeEditor.keymap ? [saveKeymap] : [])
]
}, [codeEditor.keymap, langExtension, isUnwrapped, saveKeymap])
return [...langExtension, ...(isUnwrapped ? [] : [EditorView.lineWrapping]), ...(enableKeymap ? [saveKeymap] : [])]
}, [enableKeymap, langExtension, isUnwrapped, saveKeymap])
return (
<CodeMirror
// 维持一个稳定值,避免触发 CodeMirror 重置
value={initialContent.current}
width="100%"
maxHeight={codeCollapsible && !isExpanded ? (options?.maxHeight ?? '350px') : 'none'}
maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'}
editable={true}
// @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx
theme={activeCmTheme}
@ -179,32 +199,30 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
if (onChange && viewUpdate.docChanged) onChange(value)
}}
basicSetup={{
lineNumbers: codeShowLineNumbers,
highlightActiveLineGutter: codeEditor.highlightActiveLine,
foldGutter: codeEditor.foldGutter,
dropCursor: true,
allowMultipleSelections: true,
indentOnInput: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: codeEditor.autocompletion,
rectangularSelection: true,
crosshairCursor: true,
highlightActiveLine: codeEditor.highlightActiveLine,
highlightActiveLineGutter: false,
highlightSelectionMatches: true,
closeBracketsKeymap: codeEditor.keymap,
searchKeymap: codeEditor.keymap,
foldKeymap: codeEditor.keymap,
completionKeymap: codeEditor.keymap,
lintKeymap: codeEditor.keymap
closeBracketsKeymap: enableKeymap,
searchKeymap: enableKeymap,
foldKeymap: enableKeymap,
completionKeymap: enableKeymap,
lintKeymap: enableKeymap,
...customBasicSetup // override basicSetup
}}
style={{
fontSize: `${fontSize - 1}px`,
overflow: codeCollapsible && !isExpanded ? 'auto' : 'visible',
overflow: collapsible && !isExpanded ? 'auto' : 'visible',
position: 'relative',
border: '0.5px solid var(--color-code-background)',
borderRadius: '5px',
marginTop: 0
marginTop: 0,
...style
}}
/>
)

View File

@ -1,3 +1,4 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMermaid } from '@renderer/hooks/useMermaid'
import { useSettings } from '@renderer/hooks/useSettings'
import { HighlightChunkResult, ShikiPreProperties, shikiStreamService } from '@renderer/services/ShikiStreamService'
@ -29,7 +30,8 @@ const defaultCodeStyleContext: CodeStyleContextType = {
const CodeStyleContext = createContext<CodeStyleContextType>(defaultCodeStyleContext)
export const CodeStyleProvider: React.FC<PropsWithChildren> = ({ children }) => {
const { codeEditor, codePreview, theme } = useSettings()
const { codeEditor, codePreview } = useSettings()
const { theme } = useTheme()
const [shikiThemes, setShikiThemes] = useState({})
useMermaid()

View File

@ -126,7 +126,18 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
{jsonConfig && (
<div style={{ marginBottom: '16px' }}>
<CodeToolbarProvider>
<CodeEditor language="json" onChange={handleChange} options={{ maxHeight: '60vh' }}>
<CodeEditor
language="json"
onChange={handleChange}
maxHeight="60vh"
options={{
collapsible: true,
wrappable: true,
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
keymap: true
}}>
{jsonConfig}
</CodeEditor>
</CodeToolbarProvider>