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:
one 2025-09-02 21:52:14 +08:00 committed by GitHub
parent f153f77a7e
commit 089477eb1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 33 deletions

View File

@ -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]
) )

View File

@ -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 的 basicSetupoptions 优先 // 合并 codeEditor 和 options 的 basicSetupoptions 优先
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

View File

@ -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);

View File

@ -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