mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 10:00:08 +08:00
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:
parent
ba30bffa49
commit
51071d65fb
@ -1,7 +1,7 @@
|
|||||||
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar'
|
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar'
|
||||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
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 diff from 'fast-diff'
|
||||||
import {
|
import {
|
||||||
ChevronsDownUp,
|
ChevronsDownUp,
|
||||||
@ -22,10 +22,15 @@ interface Props {
|
|||||||
language: string
|
language: string
|
||||||
onSave?: (newContent: string) => void
|
onSave?: (newContent: string) => void
|
||||||
onChange?: (newContent: string) => void
|
onChange?: (newContent: string) => void
|
||||||
// options used to override the default behaviour
|
|
||||||
options?: {
|
|
||||||
maxHeight?: string
|
maxHeight?: string
|
||||||
}
|
/** 用于覆写编辑器的某些设置 */
|
||||||
|
options?: {
|
||||||
|
collapsible?: boolean
|
||||||
|
wrappable?: boolean
|
||||||
|
keymap?: boolean
|
||||||
|
} & BasicSetupOptions
|
||||||
|
/** 用于覆写编辑器的样式,会直接传给 CodeMirror 的 style 属性 */
|
||||||
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,11 +38,30 @@ interface Props {
|
|||||||
*
|
*
|
||||||
* 目前必须和 CodeToolbar 配合使用。
|
* 目前必须和 CodeToolbar 配合使用。
|
||||||
*/
|
*/
|
||||||
const CodeEditor = ({ children, language, onSave, onChange, options }: Props) => {
|
const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, style }: Props) => {
|
||||||
const { fontSize, codeShowLineNumbers, codeCollapsible, codeWrappable, codeEditor } = useSettings()
|
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 的 basicSetup,options 优先
|
||||||
|
const customBasicSetup = useMemo(() => {
|
||||||
|
return {
|
||||||
|
lineNumbers: _lineNumbers,
|
||||||
|
...(codeEditor as BasicSetupOptions),
|
||||||
|
...(options as BasicSetupOptions)
|
||||||
|
}
|
||||||
|
}, [codeEditor, _lineNumbers, options])
|
||||||
|
|
||||||
const { activeCmTheme, languageMap } = useCodeStyle()
|
const { activeCmTheme, languageMap } = useCodeStyle()
|
||||||
const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
|
const [isExpanded, setIsExpanded] = useState(!collapsible)
|
||||||
const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable)
|
const [isUnwrapped, setIsUnwrapped] = useState(!wrappable)
|
||||||
const initialContent = useRef(children?.trimEnd() ?? '')
|
const initialContent = useRef(children?.trimEnd() ?? '')
|
||||||
const [langExtension, setLangExtension] = useState<Extension[]>([])
|
const [langExtension, setLangExtension] = useState<Extension[]>([])
|
||||||
const [editorReady, setEditorReady] = useState(false)
|
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'),
|
tooltip: isExpanded ? t('code_block.collapse') : t('code_block.expand'),
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const scrollHeight = editorViewRef?.current?.scrollDOM?.scrollHeight
|
const scrollHeight = editorViewRef?.current?.scrollDOM?.scrollHeight
|
||||||
return codeCollapsible && (scrollHeight ?? 0) > 350
|
return collapsible && (scrollHeight ?? 0) > 350
|
||||||
},
|
},
|
||||||
onClick: () => setIsExpanded((prev) => !prev)
|
onClick: () => setIsExpanded((prev) => !prev)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => removeTool(TOOL_SPECS.expand.id)
|
return () => removeTool(TOOL_SPECS.expand.id)
|
||||||
}, [codeCollapsible, isExpanded, registerTool, removeTool, t, editorReady])
|
}, [collapsible, isExpanded, registerTool, removeTool, t, editorReady])
|
||||||
|
|
||||||
// 自动换行工具
|
// 自动换行工具
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -89,12 +113,12 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
|
|||||||
...TOOL_SPECS.wrap,
|
...TOOL_SPECS.wrap,
|
||||||
icon: isUnwrapped ? <WrapIcon className="icon" /> : <UnWrapIcon className="icon" />,
|
icon: isUnwrapped ? <WrapIcon className="icon" /> : <UnWrapIcon className="icon" />,
|
||||||
tooltip: isUnwrapped ? t('code_block.wrap.on') : t('code_block.wrap.off'),
|
tooltip: isUnwrapped ? t('code_block.wrap.on') : t('code_block.wrap.off'),
|
||||||
visible: () => codeWrappable,
|
visible: () => wrappable,
|
||||||
onClick: () => setIsUnwrapped((prev) => !prev)
|
onClick: () => setIsUnwrapped((prev) => !prev)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => removeTool(TOOL_SPECS.wrap.id)
|
return () => removeTool(TOOL_SPECS.wrap.id)
|
||||||
}, [codeWrappable, isUnwrapped, registerTool, removeTool, t])
|
}, [wrappable, isUnwrapped, registerTool, removeTool, t])
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
|
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
|
||||||
@ -132,12 +156,12 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
|
|||||||
}, [children])
|
}, [children])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsExpanded(!codeCollapsible)
|
setIsExpanded(!collapsible)
|
||||||
}, [codeCollapsible])
|
}, [collapsible])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsUnwrapped(!codeWrappable)
|
setIsUnwrapped(!wrappable)
|
||||||
}, [codeWrappable])
|
}, [wrappable])
|
||||||
|
|
||||||
// 保存功能的快捷键
|
// 保存功能的快捷键
|
||||||
const saveKeymap = useMemo(() => {
|
const saveKeymap = useMemo(() => {
|
||||||
@ -154,19 +178,15 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
|
|||||||
}, [handleSave])
|
}, [handleSave])
|
||||||
|
|
||||||
const enabledExtensions = useMemo(() => {
|
const enabledExtensions = useMemo(() => {
|
||||||
return [
|
return [...langExtension, ...(isUnwrapped ? [] : [EditorView.lineWrapping]), ...(enableKeymap ? [saveKeymap] : [])]
|
||||||
...langExtension,
|
}, [enableKeymap, langExtension, isUnwrapped, saveKeymap])
|
||||||
...(isUnwrapped ? [] : [EditorView.lineWrapping]),
|
|
||||||
...(codeEditor.keymap ? [saveKeymap] : [])
|
|
||||||
]
|
|
||||||
}, [codeEditor.keymap, langExtension, isUnwrapped, saveKeymap])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
// 维持一个稳定值,避免触发 CodeMirror 重置
|
// 维持一个稳定值,避免触发 CodeMirror 重置
|
||||||
value={initialContent.current}
|
value={initialContent.current}
|
||||||
width="100%"
|
width="100%"
|
||||||
maxHeight={codeCollapsible && !isExpanded ? (options?.maxHeight ?? '350px') : 'none'}
|
maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'}
|
||||||
editable={true}
|
editable={true}
|
||||||
// @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx
|
// @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx
|
||||||
theme={activeCmTheme}
|
theme={activeCmTheme}
|
||||||
@ -179,32 +199,30 @@ const CodeEditor = ({ children, language, onSave, onChange, options }: Props) =>
|
|||||||
if (onChange && viewUpdate.docChanged) onChange(value)
|
if (onChange && viewUpdate.docChanged) onChange(value)
|
||||||
}}
|
}}
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: codeShowLineNumbers,
|
|
||||||
highlightActiveLineGutter: codeEditor.highlightActiveLine,
|
|
||||||
foldGutter: codeEditor.foldGutter,
|
|
||||||
dropCursor: true,
|
dropCursor: true,
|
||||||
allowMultipleSelections: true,
|
allowMultipleSelections: true,
|
||||||
indentOnInput: true,
|
indentOnInput: true,
|
||||||
bracketMatching: true,
|
bracketMatching: true,
|
||||||
closeBrackets: true,
|
closeBrackets: true,
|
||||||
autocompletion: codeEditor.autocompletion,
|
|
||||||
rectangularSelection: true,
|
rectangularSelection: true,
|
||||||
crosshairCursor: true,
|
crosshairCursor: true,
|
||||||
highlightActiveLine: codeEditor.highlightActiveLine,
|
highlightActiveLineGutter: false,
|
||||||
highlightSelectionMatches: true,
|
highlightSelectionMatches: true,
|
||||||
closeBracketsKeymap: codeEditor.keymap,
|
closeBracketsKeymap: enableKeymap,
|
||||||
searchKeymap: codeEditor.keymap,
|
searchKeymap: enableKeymap,
|
||||||
foldKeymap: codeEditor.keymap,
|
foldKeymap: enableKeymap,
|
||||||
completionKeymap: codeEditor.keymap,
|
completionKeymap: enableKeymap,
|
||||||
lintKeymap: codeEditor.keymap
|
lintKeymap: enableKeymap,
|
||||||
|
...customBasicSetup // override basicSetup
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${fontSize - 1}px`,
|
fontSize: `${fontSize - 1}px`,
|
||||||
overflow: codeCollapsible && !isExpanded ? 'auto' : 'visible',
|
overflow: collapsible && !isExpanded ? 'auto' : 'visible',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
border: '0.5px solid var(--color-code-background)',
|
border: '0.5px solid var(--color-code-background)',
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
marginTop: 0
|
marginTop: 0,
|
||||||
|
...style
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { HighlightChunkResult, ShikiPreProperties, shikiStreamService } from '@renderer/services/ShikiStreamService'
|
import { HighlightChunkResult, ShikiPreProperties, shikiStreamService } from '@renderer/services/ShikiStreamService'
|
||||||
@ -29,7 +30,8 @@ const defaultCodeStyleContext: CodeStyleContextType = {
|
|||||||
const CodeStyleContext = createContext<CodeStyleContextType>(defaultCodeStyleContext)
|
const CodeStyleContext = createContext<CodeStyleContextType>(defaultCodeStyleContext)
|
||||||
|
|
||||||
export const CodeStyleProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
export const CodeStyleProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const { codeEditor, codePreview, theme } = useSettings()
|
const { codeEditor, codePreview } = useSettings()
|
||||||
|
const { theme } = useTheme()
|
||||||
const [shikiThemes, setShikiThemes] = useState({})
|
const [shikiThemes, setShikiThemes] = useState({})
|
||||||
useMermaid()
|
useMermaid()
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,18 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
{jsonConfig && (
|
{jsonConfig && (
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<CodeToolbarProvider>
|
<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}
|
{jsonConfig}
|
||||||
</CodeEditor>
|
</CodeEditor>
|
||||||
</CodeToolbarProvider>
|
</CodeToolbarProvider>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user