mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-23 18:10:26 +08:00
refactor(CodeViewer): improve props, aligned to CodeEditor (#9786)
* refactor(CodeViewer): improve props, aligned to CodeEditor * refactor: simplify internal variables * refactor: remove default lineNumbers * fix: shiki theme container style * revert: use ReactMarkdown for prompt editing
This commit is contained in:
parent
f153f77a7e
commit
089477eb1e
@ -257,12 +257,13 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
|||||||
) : (
|
) : (
|
||||||
<CodeViewer
|
<CodeViewer
|
||||||
className="source-view"
|
className="source-view"
|
||||||
|
value={children}
|
||||||
language={language}
|
language={language}
|
||||||
|
onHeightChange={handleHeightChange}
|
||||||
expanded={shouldExpand}
|
expanded={shouldExpand}
|
||||||
wrapped={shouldWrap}
|
wrapped={shouldWrap}
|
||||||
onHeightChange={handleHeightChange}>
|
maxHeight={`${MAX_COLLAPSED_CODE_HEIGHT}px`}
|
||||||
{children}
|
/>
|
||||||
</CodeViewer>
|
|
||||||
),
|
),
|
||||||
[children, codeEditor.enabled, handleHeightChange, language, onSave, shouldExpand, shouldWrap]
|
[children, codeEditor.enabled, handleHeightChange, language, onSave, shouldExpand, shouldWrap]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -48,8 +48,6 @@ export interface CodeEditorProps {
|
|||||||
maxHeight?: string
|
maxHeight?: string
|
||||||
/** Minimum editor height. */
|
/** Minimum editor height. */
|
||||||
minHeight?: string
|
minHeight?: string
|
||||||
/** Font size that overrides the app setting. */
|
|
||||||
fontSize?: string
|
|
||||||
/** Editor options that extend BasicSetupOptions. */
|
/** Editor options that extend BasicSetupOptions. */
|
||||||
options?: {
|
options?: {
|
||||||
/**
|
/**
|
||||||
@ -70,6 +68,8 @@ export interface CodeEditorProps {
|
|||||||
} & BasicSetupOptions
|
} & BasicSetupOptions
|
||||||
/** Additional extensions for CodeMirror. */
|
/** Additional extensions for CodeMirror. */
|
||||||
extensions?: Extension[]
|
extensions?: Extension[]
|
||||||
|
/** Font size that overrides the app setting. */
|
||||||
|
fontSize?: number
|
||||||
/** Style overrides for the editor, passed directly to CodeMirror's style property. */
|
/** Style overrides for the editor, passed directly to CodeMirror's style property. */
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
/** CSS class name appended to the default `code-editor` class. */
|
/** CSS class name appended to the default `code-editor` class. */
|
||||||
@ -108,9 +108,9 @@ const CodeEditor = ({
|
|||||||
height,
|
height,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
minHeight,
|
minHeight,
|
||||||
fontSize,
|
|
||||||
options,
|
options,
|
||||||
extensions,
|
extensions,
|
||||||
|
fontSize: customFontSize,
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
editable = true,
|
editable = true,
|
||||||
@ -121,7 +121,7 @@ const CodeEditor = ({
|
|||||||
const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap])
|
const enableKeymap = useMemo(() => options?.keymap ?? codeEditor.keymap, [options?.keymap, codeEditor.keymap])
|
||||||
|
|
||||||
// 合并 codeEditor 和 options 的 basicSetup,options 优先
|
// 合并 codeEditor 和 options 的 basicSetup,options 优先
|
||||||
const customBasicSetup = useMemo(() => {
|
const basicSetup = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
lineNumbers: _lineNumbers,
|
lineNumbers: _lineNumbers,
|
||||||
...(codeEditor as BasicSetupOptions),
|
...(codeEditor as BasicSetupOptions),
|
||||||
@ -129,7 +129,7 @@ const CodeEditor = ({
|
|||||||
}
|
}
|
||||||
}, [codeEditor, _lineNumbers, options])
|
}, [codeEditor, _lineNumbers, options])
|
||||||
|
|
||||||
const customFontSize = useMemo(() => fontSize ?? `${_fontSize - 1}px`, [fontSize, _fontSize])
|
const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize])
|
||||||
|
|
||||||
const { activeCmTheme } = useCodeStyle()
|
const { activeCmTheme } = useCodeStyle()
|
||||||
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
|
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
|
||||||
@ -214,10 +214,10 @@ const CodeEditor = ({
|
|||||||
foldKeymap: enableKeymap,
|
foldKeymap: enableKeymap,
|
||||||
completionKeymap: enableKeymap,
|
completionKeymap: enableKeymap,
|
||||||
lintKeymap: enableKeymap,
|
lintKeymap: enableKeymap,
|
||||||
...customBasicSetup // override basicSetup
|
...basicSetup // override basicSetup
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
fontSize: customFontSize,
|
fontSize,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
borderRadius: 'inherit',
|
borderRadius: 'inherit',
|
||||||
...style
|
...style
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { MAX_COLLAPSED_CODE_HEIGHT } from '@renderer/config/constant'
|
|
||||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight'
|
import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -11,13 +10,49 @@ import { ThemedToken } from 'shiki/core'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface CodeViewerProps {
|
interface CodeViewerProps {
|
||||||
|
/** Code string value. */
|
||||||
|
value: string
|
||||||
|
/**
|
||||||
|
* Code language string.
|
||||||
|
* - Case-insensitive.
|
||||||
|
* - Supports common names: javascript, json, python, etc.
|
||||||
|
* - Supports shiki aliases: c#/csharp, objective-c++/obj-c++/objc++, etc.
|
||||||
|
*/
|
||||||
language: string
|
language: string
|
||||||
children: string
|
/** Fired when the editor height changes. */
|
||||||
expanded?: boolean
|
|
||||||
wrapped?: boolean
|
|
||||||
onHeightChange?: (scrollHeight: number) => void
|
onHeightChange?: (scrollHeight: number) => void
|
||||||
className?: string
|
/**
|
||||||
|
* Height of the scroll container.
|
||||||
|
* Only works when expanded is false.
|
||||||
|
*/
|
||||||
height?: string | number
|
height?: string | number
|
||||||
|
/**
|
||||||
|
* Maximum height of the scroll container.
|
||||||
|
* Only works when expanded is false.
|
||||||
|
*/
|
||||||
|
maxHeight?: string | number
|
||||||
|
/** Viewer options. */
|
||||||
|
options?: {
|
||||||
|
/**
|
||||||
|
* Whether to show line numbers.
|
||||||
|
*/
|
||||||
|
lineNumbers?: boolean
|
||||||
|
}
|
||||||
|
/** Font size that overrides the app setting. */
|
||||||
|
fontSize?: number
|
||||||
|
/** CSS class name appended to the default `code-viewer` class. */
|
||||||
|
className?: string
|
||||||
|
/**
|
||||||
|
* Whether the editor is expanded.
|
||||||
|
* If true, the height and maxHeight props are ignored.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
expanded?: boolean
|
||||||
|
/**
|
||||||
|
* Whether the code lines are wrapped.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
wrapped?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,19 +61,33 @@ interface CodeViewerProps {
|
|||||||
* - 使用虚拟滚动和按需高亮,改善页面内有大量长代码块时的响应
|
* - 使用虚拟滚动和按需高亮,改善页面内有大量长代码块时的响应
|
||||||
* - 并发安全
|
* - 并发安全
|
||||||
*/
|
*/
|
||||||
const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, className, height }: CodeViewerProps) => {
|
const CodeViewer = ({
|
||||||
const { codeShowLineNumbers, fontSize } = useSettings()
|
value,
|
||||||
|
language,
|
||||||
|
height,
|
||||||
|
maxHeight,
|
||||||
|
onHeightChange,
|
||||||
|
options,
|
||||||
|
fontSize: customFontSize,
|
||||||
|
className,
|
||||||
|
expanded = true,
|
||||||
|
wrapped = true
|
||||||
|
}: CodeViewerProps) => {
|
||||||
|
const { codeShowLineNumbers: _lineNumbers, fontSize: _fontSize } = useSettings()
|
||||||
const { getShikiPreProperties, isShikiThemeDark } = useCodeStyle()
|
const { getShikiPreProperties, isShikiThemeDark } = useCodeStyle()
|
||||||
const shikiThemeRef = useRef<HTMLDivElement>(null)
|
const shikiThemeRef = useRef<HTMLDivElement>(null)
|
||||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||||
const callerId = useRef(`${Date.now()}-${uuid()}`).current
|
const callerId = useRef(`${Date.now()}-${uuid()}`).current
|
||||||
|
|
||||||
const rawLines = useMemo(() => (typeof children === 'string' ? children.trimEnd().split('\n') : []), [children])
|
const fontSize = useMemo(() => customFontSize ?? _fontSize - 1, [customFontSize, _fontSize])
|
||||||
|
const lineNumbers = useMemo(() => options?.lineNumbers ?? _lineNumbers, [options?.lineNumbers, _lineNumbers])
|
||||||
|
|
||||||
|
const rawLines = useMemo(() => (typeof value === 'string' ? value.trimEnd().split('\n') : []), [value])
|
||||||
|
|
||||||
// 计算行号数字位数
|
// 计算行号数字位数
|
||||||
const gutterDigits = useMemo(
|
const gutterDigits = useMemo(
|
||||||
() => (codeShowLineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0),
|
() => (lineNumbers ? Math.max(rawLines.length.toString().length, 1) : 0),
|
||||||
[codeShowLineNumbers, rawLines.length]
|
[lineNumbers, rawLines.length]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 设置 pre 标签属性
|
// 设置 pre 标签属性
|
||||||
@ -68,7 +117,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
|
|||||||
const getScrollElement = useCallback(() => scrollerRef.current, [])
|
const getScrollElement = useCallback(() => scrollerRef.current, [])
|
||||||
const getItemKey = useCallback((index: number) => `${callerId}-${index}`, [callerId])
|
const getItemKey = useCallback((index: number) => `${callerId}-${index}`, [callerId])
|
||||||
// `line-height: 1.6` 为全局样式,但是为了避免测量误差在这里取整
|
// `line-height: 1.6` 为全局样式,但是为了避免测量误差在这里取整
|
||||||
const estimateSize = useCallback(() => Math.round((fontSize - 1) * 1.6), [fontSize])
|
const estimateSize = useCallback(() => Math.round(fontSize * 1.6), [fontSize])
|
||||||
|
|
||||||
// 创建 virtualizer 实例
|
// 创建 virtualizer 实例
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
@ -105,20 +154,19 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
|
|||||||
}, [rawLines.length, onHeightChange])
|
}, [rawLines.length, onHeightChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={shikiThemeRef} style={height ? { height } : undefined}>
|
<div ref={shikiThemeRef} style={expanded ? undefined : { height }}>
|
||||||
<ScrollContainer
|
<ScrollContainer
|
||||||
ref={scrollerRef}
|
ref={scrollerRef}
|
||||||
className="shiki-scroller"
|
className="shiki-scroller"
|
||||||
$wrap={wrapped}
|
$wrap={wrapped}
|
||||||
$expanded={expanded}
|
$expand={expanded}
|
||||||
$lineHeight={estimateSize()}
|
$lineHeight={estimateSize()}
|
||||||
$height={height}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--gutter-width': `${gutterDigits}ch`,
|
'--gutter-width': `${gutterDigits}ch`,
|
||||||
fontSize: `${fontSize - 1}px`,
|
fontSize,
|
||||||
maxHeight: expanded ? undefined : height ? undefined : MAX_COLLAPSED_CODE_HEIGHT,
|
height: expanded ? undefined : height,
|
||||||
height: height,
|
maxHeight: expanded ? undefined : maxHeight,
|
||||||
overflowY: expanded ? 'hidden' : 'auto'
|
overflowY: expanded ? 'hidden' : 'auto'
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}>
|
}>
|
||||||
@ -142,7 +190,7 @@ const CodeViewer = ({ children, language, expanded, wrapped, onHeightChange, cla
|
|||||||
<VirtualizedRow
|
<VirtualizedRow
|
||||||
rawLine={rawLines[virtualItem.index]}
|
rawLine={rawLines[virtualItem.index]}
|
||||||
tokenLine={tokenLines[virtualItem.index]}
|
tokenLine={tokenLines[virtualItem.index]}
|
||||||
showLineNumbers={codeShowLineNumbers}
|
showLineNumbers={lineNumbers}
|
||||||
index={virtualItem.index}
|
index={virtualItem.index}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -226,9 +274,8 @@ VirtualizedRow.displayName = 'VirtualizedRow'
|
|||||||
|
|
||||||
const ScrollContainer = styled.div<{
|
const ScrollContainer = styled.div<{
|
||||||
$wrap?: boolean
|
$wrap?: boolean
|
||||||
$expanded?: boolean
|
$expand?: boolean
|
||||||
$lineHeight?: number
|
$lineHeight?: number
|
||||||
$height?: string | number
|
|
||||||
}>`
|
}>`
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -244,7 +291,7 @@ const ScrollContainer = styled.div<{
|
|||||||
line-height: ${(props) => props.$lineHeight}px;
|
line-height: ${(props) => props.$lineHeight}px;
|
||||||
/* contain 优化 wrap 时滚动性能,will-change 优化 unwrap 时滚动性能 */
|
/* contain 优化 wrap 时滚动性能,will-change 优化 unwrap 时滚动性能 */
|
||||||
contain: ${(props) => (props.$wrap ? 'content' : 'none')};
|
contain: ${(props) => (props.$wrap ? 'content' : 'none')};
|
||||||
will-change: ${(props) => (!props.$wrap && !props.$expanded ? 'transform' : 'auto')};
|
will-change: ${(props) => (!props.$wrap && !props.$expand ? 'transform' : 'auto')};
|
||||||
|
|
||||||
.line-number {
|
.line-number {
|
||||||
width: var(--gutter-width, 1.2ch);
|
width: var(--gutter-width, 1.2ch);
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import 'emoji-picker-element'
|
|||||||
|
|
||||||
import { CloseCircleFilled } from '@ant-design/icons'
|
import { CloseCircleFilled } from '@ant-design/icons'
|
||||||
import CodeEditor from '@renderer/components/CodeEditor'
|
import CodeEditor from '@renderer/components/CodeEditor'
|
||||||
import CodeViewer from '@renderer/components/CodeViewer'
|
|
||||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||||
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
|
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
|
||||||
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
||||||
@ -14,6 +13,7 @@ import { Button, Input, Popover } from 'antd'
|
|||||||
import { Edit, HelpCircle, Save } from 'lucide-react'
|
import { Edit, HelpCircle, Save } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingDivider } from '..'
|
import { SettingDivider } from '..'
|
||||||
@ -122,7 +122,9 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
|
|||||||
<TextAreaContainer>
|
<TextAreaContainer>
|
||||||
<RichEditorContainer>
|
<RichEditorContainer>
|
||||||
{showPreview ? (
|
{showPreview ? (
|
||||||
<CodeViewer children={processedPrompt} language="markdown" expanded={true} height="100%" />
|
<MarkdownContainer>
|
||||||
|
<ReactMarkdown>{processedPrompt || prompt}</ReactMarkdown>
|
||||||
|
</MarkdownContainer>
|
||||||
) : (
|
) : (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={prompt}
|
value={prompt}
|
||||||
@ -214,4 +216,10 @@ const RichEditorContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const MarkdownContainer = styled.div.attrs({ className: 'markdown' })`
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow: auto;
|
||||||
|
`
|
||||||
|
|
||||||
export default AssistantPromptSettings
|
export default AssistantPromptSettings
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user