mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 14:59:27 +08:00
refactor(CodePreview): smoothing code highlighting on streaming (#7842)
This commit is contained in:
parent
fba6c1642d
commit
de75992e7b
@ -9,6 +9,7 @@ import { debounce } from 'lodash'
|
|||||||
import { ChevronsDownUp, ChevronsUpDown, Text as UnWrapIcon, WrapText as WrapIcon } from 'lucide-react'
|
import { ChevronsDownUp, ChevronsUpDown, Text as UnWrapIcon, WrapText as WrapIcon } from 'lucide-react'
|
||||||
import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ThemedToken } from 'shiki/core'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface CodePreviewProps {
|
interface CodePreviewProps {
|
||||||
@ -150,7 +151,8 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
|||||||
{
|
{
|
||||||
'--gutter-width': `${gutterDigits}ch`,
|
'--gutter-width': `${gutterDigits}ch`,
|
||||||
fontSize: `${fontSize - 1}px`,
|
fontSize: `${fontSize - 1}px`,
|
||||||
maxHeight: shouldCollapse ? MAX_COLLAPSE_HEIGHT : undefined
|
maxHeight: shouldCollapse ? MAX_COLLAPSE_HEIGHT : undefined,
|
||||||
|
overflowY: shouldCollapse ? 'auto' : 'hidden'
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}>
|
}>
|
||||||
<div
|
<div
|
||||||
@ -195,9 +197,49 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
|||||||
|
|
||||||
CodePreview.displayName = 'CodePreview'
|
CodePreview.displayName = 'CodePreview'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补全代码行 tokens,把原始内容拼接到高亮内容之后,确保渲染出整行来。
|
||||||
|
*/
|
||||||
|
function completeLineTokens(themedTokens: ThemedToken[], rawLine: string): ThemedToken[] {
|
||||||
|
// 如果出现空行,补一个空格保证行高
|
||||||
|
if (rawLine.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
content: ' ',
|
||||||
|
offset: 0,
|
||||||
|
color: 'inherit',
|
||||||
|
bgColor: 'inherit',
|
||||||
|
htmlStyle: {
|
||||||
|
opacity: '0.35'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const themedContent = themedTokens.map((token) => token.content).join('')
|
||||||
|
const extraContent = rawLine.slice(themedContent.length)
|
||||||
|
|
||||||
|
// 已有内容已经全部高亮,直接返回
|
||||||
|
if (!extraContent) return themedTokens
|
||||||
|
|
||||||
|
// 补全剩余内容
|
||||||
|
return [
|
||||||
|
...themedTokens,
|
||||||
|
{
|
||||||
|
content: extraContent,
|
||||||
|
offset: themedContent.length,
|
||||||
|
color: 'inherit',
|
||||||
|
bgColor: 'inherit',
|
||||||
|
htmlStyle: {
|
||||||
|
opacity: '0.35'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
interface VirtualizedRowData {
|
interface VirtualizedRowData {
|
||||||
rawLine: string
|
rawLine: string
|
||||||
tokenLine?: any[]
|
tokenLine?: ThemedToken[]
|
||||||
showLineNumbers: boolean
|
showLineNumbers: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,17 +252,11 @@ const VirtualizedRow = memo(
|
|||||||
<div className="line">
|
<div className="line">
|
||||||
{showLineNumbers && <span className="line-number">{index + 1}</span>}
|
{showLineNumbers && <span className="line-number">{index + 1}</span>}
|
||||||
<span className="line-content">
|
<span className="line-content">
|
||||||
{tokenLine ? (
|
{completeLineTokens(tokenLine ?? [], rawLine).map((token, tokenIndex) => (
|
||||||
// 渲染高亮后的内容
|
<span key={tokenIndex} style={getReactStyleFromToken(token)}>
|
||||||
tokenLine.map((token, tokenIndex) => (
|
{token.content}
|
||||||
<span key={tokenIndex} style={getReactStyleFromToken(token)}>
|
</span>
|
||||||
{token.content}
|
))}
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
// 渲染原始内容
|
|
||||||
<span className="line-content-raw">{rawLine || ' '}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -234,7 +270,7 @@ const ScrollContainer = styled.div<{
|
|||||||
$lineHeight?: number
|
$lineHeight?: number
|
||||||
}>`
|
}>`
|
||||||
display: block;
|
display: block;
|
||||||
overflow: auto;
|
overflow-x: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
@ -264,10 +300,6 @@ const ScrollContainer = styled.div<{
|
|||||||
overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
|
overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-content-raw {
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user