refactor(CodeTool): use hook for codeblock tools rather than context (#6273)

* refactor(CodeTool): use hook for codeblock tools rather than context

* fix: codeblock overflow behaviour

* fix: CodePreview scrollbar

* refactor: move margin to CodeHeader

* refactor: add min-width to codeblock
This commit is contained in:
one 2025-05-25 18:01:27 +08:00 committed by GitHub
parent a7520169e6
commit 2f312d68a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 159 additions and 227 deletions

View File

@ -321,6 +321,7 @@ mjx-container {
.cm-lineWrapping * { .cm-lineWrapping * {
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap;
} }
} }
} }

View File

@ -18,7 +18,8 @@ body[theme-mode='light'] {
height: 6px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
background: transparent; background: transparent;
} }
@ -30,7 +31,7 @@ body[theme-mode='light'] {
} }
} }
pre::-webkit-scrollbar-thumb { pre:not(.shiki)::-webkit-scrollbar-thumb {
border-radius: 0; border-radius: 0;
background: rgba(0, 0, 0, 0.08); background: rgba(0, 0, 0, 0.08);
&:hover { &:hover {

View File

@ -1,4 +1,4 @@
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar' import { CodeTool, TOOL_SPECS, useCodeTool } 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 { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
@ -12,6 +12,7 @@ import styled from 'styled-components'
interface CodePreviewProps { interface CodePreviewProps {
children: string children: string
language: string language: string
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
} }
/** /**
@ -20,7 +21,7 @@ interface CodePreviewProps {
* - shiki tokenizer * - shiki tokenizer
* - tokenizer * - tokenizer
*/ */
const CodePreview = ({ children, language }: CodePreviewProps) => { const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings() const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings()
const { activeShikiTheme, highlightCodeChunk, cleanupTokenizers } = useCodeStyle() const { activeShikiTheme, highlightCodeChunk, cleanupTokenizers } = useCodeStyle()
const [isExpanded, setIsExpanded] = useState(!codeCollapsible) const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
@ -35,7 +36,7 @@ const CodePreview = ({ children, language }: CodePreviewProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { registerTool, removeTool } = useCodeToolbar() const { registerTool, removeTool } = useCodeTool(setTools)
// 展开/折叠工具 // 展开/折叠工具
useEffect(() => { useEffect(() => {
@ -171,14 +172,13 @@ const CodePreview = ({ children, language }: CodePreviewProps) => {
ref={codeContentRef} ref={codeContentRef}
$lineNumbers={codeShowLineNumbers} $lineNumbers={codeShowLineNumbers}
$wrap={codeWrappable && !isUnwrapped} $wrap={codeWrappable && !isUnwrapped}
$fadeIn={hasHighlightedCode}
style={{ style={{
fontSize: fontSize - 1, fontSize: fontSize - 1,
maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none' maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none'
}}> }}>
{hasHighlightedCode ? ( {hasHighlightedCode ? (
<div className="fade-in-effect"> <ShikiTokensRenderer language={language} tokenLines={tokenLines} />
<ShikiTokensRenderer language={language} tokenLines={tokenLines} />
</div>
) : ( ) : (
<CodePlaceholder>{children}</CodePlaceholder> <CodePlaceholder>{children}</CodePlaceholder>
)} )}
@ -229,26 +229,22 @@ const ShikiTokensRenderer: React.FC<{ language: string; tokenLines: ThemedToken[
const ContentContainer = styled.div<{ const ContentContainer = styled.div<{
$lineNumbers: boolean $lineNumbers: boolean
$wrap: boolean $wrap: boolean
$fadeIn: boolean
}>` }>`
display: block;
position: relative; position: relative;
overflow: auto; overflow: auto;
display: flex;
flex-direction: column;
border: 0.5px solid transparent; border: 0.5px solid transparent;
border-radius: 5px; border-radius: 5px;
margin-top: 0; margin-top: 0;
::-webkit-scrollbar-thumb {
border-radius: 10px;
}
.shiki { .shiki {
display: inline-block;
min-width: 100%;
padding: 1em; padding: 1em;
code { code {
display: flex; display: block;
flex-direction: column;
width: 100%;
.line { .line {
display: block; display: block;
@ -256,7 +252,7 @@ const ContentContainer = styled.div<{
padding-left: ${(props) => (props.$lineNumbers ? '2rem' : '0')}; padding-left: ${(props) => (props.$lineNumbers ? '2rem' : '0')};
* { * {
word-wrap: ${(props) => (props.$wrap ? 'break-word' : undefined)}; overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')}; white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
} }
} }
@ -292,18 +288,15 @@ const ContentContainer = styled.div<{
} }
} }
.fade-in-effect { animation: ${(props) => (props.$fadeIn ? 'contentFadeIn 0.3s ease-in-out forwards' : 'none')};
animation: contentFadeIn 0.3s ease-in-out forwards;
}
` `
const CodePlaceholder = styled.div` const CodePlaceholder = styled.div`
display: block;
opacity: 0.1; opacity: 0.1;
flex-direction: column;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
overflow-x: hidden; overflow-x: hidden;
display: block;
min-height: 1.3rem; min-height: 1.3rem;
` `

View File

@ -1,5 +1,5 @@
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar' import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
import { useMermaid } from '@renderer/hooks/useMermaid' import { useMermaid } from '@renderer/hooks/useMermaid'
import { Flex } from 'antd' import { Flex } from 'antd'
import React, { memo, startTransition, useCallback, useEffect, useRef, useState } from 'react' import React, { memo, startTransition, useCallback, useEffect, useRef, useState } from 'react'
@ -7,9 +7,10 @@ import styled from 'styled-components'
interface Props { interface Props {
children: string children: string
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
} }
const MermaidPreview: React.FC<Props> = ({ children }) => { const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
const { mermaid, isLoading, error: mermaidError } = useMermaid() const { mermaid, isLoading, error: mermaidError } = useMermaid()
const mermaidRef = useRef<HTMLDivElement>(null) const mermaidRef = useRef<HTMLDivElement>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -25,6 +26,7 @@ const MermaidPreview: React.FC<Props> = ({ children }) => {
// 使用工具栏 // 使用工具栏
usePreviewTools({ usePreviewTools({
setTools,
handleZoom, handleZoom,
handleCopyImage, handleCopyImage,
handleDownload handleDownload

View File

@ -1,5 +1,5 @@
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar' import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
import { Spin } from 'antd' import { Spin } from 'antd'
import pako from 'pako' import pako from 'pako'
import React, { memo, useCallback, useRef, useState } from 'react' import React, { memo, useCallback, useRef, useState } from 'react'
@ -134,9 +134,10 @@ const PlantUMLServerImage: React.FC<PlantUMLServerImageProps> = ({ format, diagr
interface PlantUMLProps { interface PlantUMLProps {
children: string children: string
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
} }
const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children }) => { const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children, setTools }) => {
const { t } = useTranslation() const { t } = useTranslation()
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
@ -165,6 +166,7 @@ const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children }) => {
// 使用工具栏 // 使用工具栏
usePreviewTools({ usePreviewTools({
setTools,
handleZoom, handleZoom,
handleCopyImage, handleCopyImage,
handleDownload: customDownload handleDownload: customDownload

View File

@ -1,12 +1,13 @@
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar' import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
import { memo, useRef } from 'react' import { memo, useRef } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
children: string children: string
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
} }
const SvgPreview: React.FC<Props> = ({ children }) => { const SvgPreview: React.FC<Props> = ({ children, setTools }) => {
const svgContainerRef = useRef<HTMLDivElement>(null) const svgContainerRef = useRef<HTMLDivElement>(null)
// 使用通用图像工具 // 使用通用图像工具
@ -17,6 +18,7 @@ const SvgPreview: React.FC<Props> = ({ children }) => {
// 使用工具栏 // 使用工具栏
usePreviewTools({ usePreviewTools({
setTools,
handleCopyImage, handleCopyImage,
handleDownload handleDownload
}) })

View File

@ -1,6 +1,6 @@
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import CodeEditor from '@renderer/components/CodeEditor' import CodeEditor from '@renderer/components/CodeEditor'
import { CodeToolbar, CodeToolContext, TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar' import { CodeTool, CodeToolbar, TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { pyodideService } from '@renderer/services/PyodideService' import { pyodideService } from '@renderer/services/PyodideService'
import { extractTitle } from '@renderer/utils/formats' import { extractTitle } from '@renderer/utils/formats'
@ -49,6 +49,9 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
const [isRunning, setIsRunning] = useState(false) const [isRunning, setIsRunning] = useState(false)
const [output, setOutput] = useState('') const [output, setOutput] = useState('')
const [tools, setTools] = useState<CodeTool[]>([])
const { registerTool, removeTool } = useCodeTool(setTools)
const isExecutable = useMemo(() => { const isExecutable = useMemo(() => {
return codeExecution.enabled && language === 'python' return codeExecution.enabled && language === 'python'
}, [codeExecution.enabled, language]) }, [codeExecution.enabled, language])
@ -59,33 +62,17 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
return hasSpecialView && viewMode === 'special' return hasSpecialView && viewMode === 'special'
}, [hasSpecialView, viewMode]) }, [hasSpecialView, viewMode])
const { updateContext, registerTool, removeTool } = useCodeToolbar() const handleCopySource = useCallback(() => {
navigator.clipboard.writeText(children)
window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' })
}, [children, t])
useEffect(() => { const handleDownloadSource = useCallback(() => {
updateContext({
code: children,
language
})
}, [children, language, updateContext])
const handleCopySource = useCallback(
(ctx?: CodeToolContext) => {
if (!ctx) return
navigator.clipboard.writeText(ctx.code)
window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' })
},
[t]
)
const handleDownloadSource = useCallback((ctx?: CodeToolContext) => {
if (!ctx) return
const { code, language } = ctx
let fileName = '' let fileName = ''
// 尝试提取标题 // 尝试提取标题
if (language === 'html' && code.includes('</html>')) { if (language === 'html' && children.includes('</html>')) {
const title = extractTitle(code) const title = extractTitle(children)
if (title) { if (title) {
fileName = `${title}.html` fileName = `${title}.html`
} }
@ -96,31 +83,26 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}` fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}`
} }
window.api.file.save(fileName, code) window.api.file.save(fileName, children)
}, []) }, [children, language])
const handleRunScript = useCallback( const handleRunScript = useCallback(() => {
(ctx?: CodeToolContext) => { setIsRunning(true)
if (!ctx) return setOutput('')
setIsRunning(true) pyodideService
setOutput('') .runScript(children, {}, codeExecution.timeoutMinutes * 60000)
.then((formattedOutput) => {
pyodideService setOutput(formattedOutput)
.runScript(ctx.code, {}, codeExecution.timeoutMinutes * 60000) })
.then((formattedOutput) => { .catch((error) => {
setOutput(formattedOutput) console.error('Unexpected error:', error)
}) setOutput(`Unexpected error: ${error.message || 'Unknown error'}`)
.catch((error) => { })
console.error('Unexpected error:', error) .finally(() => {
setOutput(`Unexpected error: ${error.message || 'Unknown error'}`) setIsRunning(false)
}) })
.finally(() => { }, [children, codeExecution.timeoutMinutes])
setIsRunning(false)
})
},
[codeExecution.timeoutMinutes]
)
useEffect(() => { useEffect(() => {
// 复制按钮 // 复制按钮
@ -191,7 +173,7 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
...TOOL_SPECS.run, ...TOOL_SPECS.run,
icon: isRunning ? <LoadingOutlined /> : <CirclePlay className="icon" />, icon: isRunning ? <LoadingOutlined /> : <CirclePlay className="icon" />,
tooltip: t('code_block.run'), tooltip: t('code_block.run'),
onClick: (ctx) => !isRunning && handleRunScript(ctx) onClick: () => !isRunning && handleRunScript()
}) })
return () => isExecutable && removeTool(TOOL_SPECS.run.id) return () => isExecutable && removeTool(TOOL_SPECS.run.id)
@ -200,20 +182,32 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
// 源代码视图组件 // 源代码视图组件
const sourceView = useMemo(() => { const sourceView = useMemo(() => {
if (codeEditor.enabled) { if (codeEditor.enabled) {
return <CodeEditor value={children} language={language} onSave={onSave} options={{ stream: true }} /> return (
<CodeEditor
value={children}
language={language}
onSave={onSave}
options={{ stream: true }}
setTools={setTools}
/>
)
} else { } else {
return <CodePreview language={language}>{children}</CodePreview> return (
<CodePreview language={language} setTools={setTools}>
{children}
</CodePreview>
)
} }
}, [children, codeEditor.enabled, language, onSave]) }, [children, codeEditor.enabled, language, onSave, setTools])
// 特殊视图组件映射 // 特殊视图组件映射
const specialView = useMemo(() => { const specialView = useMemo(() => {
if (language === 'mermaid') { if (language === 'mermaid') {
return <MermaidPreview>{children}</MermaidPreview> return <MermaidPreview setTools={setTools}>{children}</MermaidPreview>
} else if (language === 'plantuml' && isValidPlantUML(children)) { } else if (language === 'plantuml' && isValidPlantUML(children)) {
return <PlantUmlPreview>{children}</PlantUmlPreview> return <PlantUmlPreview setTools={setTools}>{children}</PlantUmlPreview>
} else if (language === 'svg') { } else if (language === 'svg') {
return <SvgPreview>{children}</SvgPreview> return <SvgPreview setTools={setTools}>{children}</SvgPreview>
} }
return null return null
}, [children, language]) }, [children, language])
@ -246,7 +240,7 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
return ( return (
<CodeBlockWrapper className="code-block" $isInSpecialView={isInSpecialView}> <CodeBlockWrapper className="code-block" $isInSpecialView={isInSpecialView}>
{renderHeader} {renderHeader}
<CodeToolbar /> <CodeToolbar tools={tools} />
{renderContent} {renderContent}
{renderArtifacts} {renderArtifacts}
{isExecutable && output && <StatusBar>{output}</StatusBar>} {isExecutable && output && <StatusBar>{output}</StatusBar>}
@ -255,10 +249,11 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
} }
const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>` const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
/* FIXME: 在 bubble style 中撑开一些宽度*/
min-width: min(calc(60vw - var(--sidebar-width)), 700px);
position: relative; position: relative;
.code-toolbar { .code-toolbar {
margin-top: ${(props) => (props.$isInSpecialView ? '20px' : '0')};
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')}; background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
border-radius: ${(props) => (props.$isInSpecialView ? '0' : '4px')}; border-radius: ${(props) => (props.$isInSpecialView ? '0' : '4px')};
opacity: 0; opacity: 0;
@ -279,13 +274,13 @@ const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
const CodeHeader = styled.div<{ $isInSpecialView: boolean }>` const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
color: var(--color-text); color: var(--color-text);
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
padding: 0 10px; padding: 0 10px;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-top-right-radius: 8px; border-top-right-radius: 8px;
margin-top: ${(props) => (props.$isInSpecialView ? '6px' : '0')};
height: ${(props) => (props.$isInSpecialView ? '16px' : '34px')}; height: ${(props) => (props.$isInSpecialView ? '16px' : '34px')};
` `

View File

@ -1,4 +1,4 @@
import { TOOL_SPECS, useCodeToolbar } from '@renderer/components/CodeToolbar' import { CodeTool, TOOL_SPECS, useCodeTool } 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, BasicSetupOptions, EditorView, Extension, keymap } from '@uiw/react-codemirror' import CodeMirror, { Annotation, BasicSetupOptions, EditorView, Extension, keymap } from '@uiw/react-codemirror'
@ -25,6 +25,7 @@ interface Props {
language: string language: string
onSave?: (newContent: string) => void onSave?: (newContent: string) => void
onChange?: (newContent: string) => void onChange?: (newContent: string) => void
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
minHeight?: string minHeight?: string
maxHeight?: string maxHeight?: string
/** 用于覆写编辑器的某些设置 */ /** 用于覆写编辑器的某些设置 */
@ -52,6 +53,7 @@ const CodeEditor = ({
language, language,
onSave, onSave,
onChange, onChange,
setTools,
minHeight, minHeight,
maxHeight, maxHeight,
options, options,
@ -88,7 +90,7 @@ const CodeEditor = ({
const langExtensions = useLanguageExtensions(language, options?.lint) const langExtensions = useLanguageExtensions(language, options?.lint)
const { registerTool, removeTool } = useCodeToolbar() const { registerTool, removeTool } = useCodeTool(setTools)
// 展开/折叠工具 // 展开/折叠工具
useEffect(() => { useEffect(() => {

View File

@ -1,71 +0,0 @@
import React, { createContext, use, useCallback, useMemo, useState } from 'react'
import { CodeTool, CodeToolContext } from './types'
// 定义上下文默认值
const defaultContext: CodeToolContext = {
code: '',
language: ''
}
export interface CodeToolbarContextType {
tools: CodeTool[]
context: CodeToolContext
registerTool: (tool: CodeTool) => void
removeTool: (id: string) => void
updateContext: (newContext: Partial<CodeToolContext>) => void
}
const defaultCodeToolbarContext: CodeToolbarContextType = {
tools: [],
context: defaultContext,
registerTool: () => {},
removeTool: () => {},
updateContext: () => {}
}
const CodeToolbarContext = createContext<CodeToolbarContextType>(defaultCodeToolbarContext)
export const CodeToolbarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [tools, setTools] = useState<CodeTool[]>([])
const [context, setContext] = useState<CodeToolContext>(defaultContext)
// 注册工具如果已存在同ID工具则替换
const registerTool = useCallback((tool: CodeTool) => {
setTools((prev) => {
const filtered = prev.filter((t) => t.id !== tool.id)
return [...filtered, tool].sort((a, b) => b.order - a.order)
})
}, [])
// 移除工具
const removeTool = useCallback((id: string) => {
setTools((prev) => prev.filter((tool) => tool.id !== id))
}, [])
// 更新上下文
const updateContext = useCallback((newContext: Partial<CodeToolContext>) => {
setContext((prev) => ({ ...prev, ...newContext }))
}, [])
const value: CodeToolbarContextType = useMemo(
() => ({
tools,
context,
registerTool,
removeTool,
updateContext
}),
[tools, context, registerTool, removeTool, updateContext]
)
return <CodeToolbarContext value={value}>{children}</CodeToolbarContext>
}
export const useCodeToolbar = () => {
const context = use(CodeToolbarContext)
if (!context) {
throw new Error('useCodeToolbar must be used within a CodeToolbarProvider')
}
return context
}

View File

@ -0,0 +1,26 @@
import { useCallback } from 'react'
import { CodeTool } from './types'
export const useCodeTool = (setTools?: (value: React.SetStateAction<CodeTool[]>) => void) => {
// 注册工具如果已存在同ID工具则替换
const registerTool = useCallback(
(tool: CodeTool) => {
setTools?.((prev) => {
const filtered = prev.filter((t) => t.id !== tool.id)
return [...filtered, tool].sort((a, b) => b.order - a.order)
})
},
[setTools]
)
// 移除工具
const removeTool = useCallback(
(id: string) => {
setTools?.((prev) => prev.filter((tool) => tool.id !== id))
},
[setTools]
)
return { registerTool, removeTool }
}

View File

@ -1,5 +1,5 @@
export * from './constants' export * from './constants'
export * from './context' export * from './hook'
export * from './toolbar' export * from './toolbar'
export * from './types' export * from './types'
export * from './usePreviewTools' export * from './usePreviewTools'

View File

@ -5,7 +5,6 @@ import React, { memo, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { useCodeToolbar } from './context'
import { CodeTool } from './types' import { CodeTool } from './types'
interface CodeToolButtonProps { interface CodeToolButtonProps {
@ -13,22 +12,19 @@ interface CodeToolButtonProps {
} }
const CodeToolButton: React.FC<CodeToolButtonProps> = memo(({ tool }) => { const CodeToolButton: React.FC<CodeToolButtonProps> = memo(({ tool }) => {
const { context } = useCodeToolbar()
return ( return (
<Tooltip key={`${tool.id}-${tool.tooltip}`} title={tool.tooltip} mouseEnterDelay={0.5}> <Tooltip key={tool.id} title={tool.tooltip} mouseEnterDelay={0.5}>
<ToolWrapper onClick={() => tool.onClick(context)}>{tool.icon}</ToolWrapper> <ToolWrapper onClick={() => tool.onClick()}>{tool.icon}</ToolWrapper>
</Tooltip> </Tooltip>
) )
}) })
export const CodeToolbar: React.FC = memo(() => { export const CodeToolbar: React.FC<{ tools: CodeTool[] }> = memo(({ tools }) => {
const { tools, context } = useCodeToolbar()
const [showQuickTools, setShowQuickTools] = useState(false) const [showQuickTools, setShowQuickTools] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
// 根据条件显示工具 // 根据条件显示工具
const visibleTools = tools.filter((tool) => !tool.visible || tool.visible(context)) const visibleTools = tools.filter((tool) => !tool.visible || tool.visible())
// 按类型分组 // 按类型分组
const coreTools = visibleTools.filter((tool) => tool.type === 'core') const coreTools = visibleTools.filter((tool) => tool.type === 'core')

View File

@ -20,16 +20,6 @@ export interface CodeToolSpec {
export interface CodeTool extends CodeToolSpec { export interface CodeTool extends CodeToolSpec {
icon: React.ReactNode icon: React.ReactNode
tooltip: string tooltip: string
visible?: (ctx?: CodeToolContext) => boolean visible?: () => boolean
onClick: (ctx?: CodeToolContext) => void onClick: () => void
}
/**
*
* @param code
* @param language
*/
export interface CodeToolContext {
code: string
language: string
} }

View File

@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next'
import { DownloadPngIcon, DownloadSvgIcon } from '../Icons/DownloadIcons' import { DownloadPngIcon, DownloadSvgIcon } from '../Icons/DownloadIcons'
import { TOOL_SPECS } from './constants' import { TOOL_SPECS } from './constants'
import { useCodeToolbar } from './context' import { useCodeTool } from './hook'
import { CodeTool } from './types'
// 预编译正则表达式用于查询位置 // 预编译正则表达式用于查询位置
const TRANSFORM_REGEX = /translate\((-?\d+\.?\d*)px,\s*(-?\d+\.?\d*)px\)/ const TRANSFORM_REGEX = /translate\((-?\d+\.?\d*)px,\s*(-?\d+\.?\d*)px\)/
@ -272,6 +273,7 @@ export const usePreviewToolHandlers = (
} }
export interface PreviewToolsOptions { export interface PreviewToolsOptions {
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
handleZoom?: (delta: number) => void handleZoom?: (delta: number) => void
handleCopyImage?: () => Promise<void> handleCopyImage?: () => Promise<void>
handleDownload?: (format: 'svg' | 'png') => void handleDownload?: (format: 'svg' | 'png') => void
@ -280,9 +282,9 @@ export interface PreviewToolsOptions {
/** /**
* Hook * Hook
*/ */
export const usePreviewTools = ({ handleZoom, handleCopyImage, handleDownload }: PreviewToolsOptions) => { export const usePreviewTools = ({ setTools, handleZoom, handleCopyImage, handleDownload }: PreviewToolsOptions) => {
const { t } = useTranslation() const { t } = useTranslation()
const { registerTool, removeTool } = useCodeToolbar() const { registerTool, removeTool } = useCodeTool(setTools)
useEffect(() => { useEffect(() => {
// 根据提供的功能有选择性地注册工具 // 根据提供的功能有选择性地注册工具

View File

@ -1,5 +1,4 @@
import CodeBlockView from '@renderer/components/CodeBlockView' import CodeBlockView from '@renderer/components/CodeBlockView'
import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
import React, { memo, useCallback } from 'react' import React, { memo, useCallback } from 'react'
interface Props { interface Props {
@ -24,11 +23,9 @@ const CodeBlock: React.FC<Props> = ({ children, className, id, onSave }) => {
) )
return match ? ( return match ? (
<CodeToolbarProvider> <CodeBlockView language={language} onSave={handleSave}>
<CodeBlockView language={language} onSave={handleSave}> {children}
{children} </CodeBlockView>
</CodeBlockView>
</CodeToolbarProvider>
) : ( ) : (
<code className={className} style={{ textWrap: 'wrap' }}> <code className={className} style={{ textWrap: 'wrap' }}>
{children} {children}

View File

@ -1,6 +1,5 @@
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import CodeEditor from '@renderer/components/CodeEditor' import CodeEditor from '@renderer/components/CodeEditor'
import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setMCPServerActive } from '@renderer/store/mcp' import { setMCPServerActive } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
@ -157,25 +156,23 @@ const AddMcpServerModal: FC<AddMcpServerModalProps> = ({ visible, onClose, onSuc
name="serverConfig" name="serverConfig"
label={t('settings.mcp.addServer.importFrom.tooltip')} label={t('settings.mcp.addServer.importFrom.tooltip')}
rules={[{ required: true, message: t('settings.mcp.addServer.importFrom.placeholder') }]}> rules={[{ required: true, message: t('settings.mcp.addServer.importFrom.placeholder') }]}>
<CodeToolbarProvider> <CodeEditor
<CodeEditor // 如果表單值為空,顯示範例 JSON否則顯示表單值
// 如果表單值為空,顯示範例 JSON否則顯示表單值 value={serverConfigValue}
value={serverConfigValue} placeholder={initialJsonExample}
placeholder={initialJsonExample} language="json"
language="json" onChange={handleEditorChange}
onChange={handleEditorChange} maxHeight="300px"
maxHeight="300px" options={{
options={{ lint: true,
lint: true, collapsible: true,
collapsible: true, wrappable: true,
wrappable: true, lineNumbers: true,
lineNumbers: true, foldGutter: true,
foldGutter: true, highlightActiveLine: true,
highlightActiveLine: true, keymap: true
keymap: true }}
}} />
/>
</CodeToolbarProvider>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

View File

@ -1,5 +1,4 @@
import CodeEditor from '@renderer/components/CodeEditor' import CodeEditor from '@renderer/components/CodeEditor'
import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMCPServers } from '@renderer/store/mcp' import { setMCPServers } from '@renderer/store/mcp'
@ -121,23 +120,21 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
</div> </div>
{jsonConfig && ( {jsonConfig && (
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<CodeToolbarProvider> <CodeEditor
<CodeEditor value={jsonConfig}
value={jsonConfig} language="json"
language="json" onChange={(value) => setJsonConfig(value)}
onChange={(value) => setJsonConfig(value)} maxHeight="60vh"
maxHeight="60vh" options={{
options={{ lint: true,
lint: true, collapsible: true,
collapsible: true, wrappable: true,
wrappable: true, lineNumbers: true,
lineNumbers: true, foldGutter: true,
foldGutter: true, highlightActiveLine: true,
highlightActiveLine: true, keymap: true
keymap: true }}
}} />
/>
</CodeToolbarProvider>
</div> </div>
)} )}
<Typography.Text type="secondary">{t('settings.mcp.jsonModeHint')}</Typography.Text> <Typography.Text type="secondary">{t('settings.mcp.jsonModeHint')}</Typography.Text>