mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +08:00
refactor(CodePreview): line numbers as elements
This commit is contained in:
parent
766897e733
commit
16772c1d37
@ -4,7 +4,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
|||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { getReactStyleFromToken } from '@renderer/utils/shiki'
|
import { getReactStyleFromToken } from '@renderer/utils/shiki'
|
||||||
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, useRef, useState } from 'react'
|
import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ThemedToken } from 'shiki/core'
|
import { ThemedToken } from 'shiki/core'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -154,12 +154,18 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
|||||||
setTimeout(highlightCode, 0)
|
setTimeout(highlightCode, 0)
|
||||||
}, [isInViewport, highlightCode])
|
}, [isInViewport, highlightCode])
|
||||||
|
|
||||||
useEffect(() => {
|
const lastDigitsRef = useRef(1)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
const container = codeContainerRef.current
|
const container = codeContainerRef.current
|
||||||
if (!container || !codeShowLineNumbers) return
|
if (!container || !codeShowLineNumbers) return
|
||||||
|
|
||||||
const digits = Math.max(tokenLines.length.toString().length, 1)
|
const digits = Math.max(tokenLines.length.toString().length, 1)
|
||||||
container.style.setProperty('--line-digits', digits.toString())
|
if (digits === lastDigitsRef.current) return
|
||||||
|
|
||||||
|
const gutterWidth = digits * 0.6
|
||||||
|
container.style.setProperty('--gutter-width', `${gutterWidth}rem`)
|
||||||
|
lastDigitsRef.current = digits
|
||||||
}, [codeShowLineNumbers, tokenLines.length])
|
}, [codeShowLineNumbers, tokenLines.length])
|
||||||
|
|
||||||
const hasHighlightedCode = tokenLines.length > 0
|
const hasHighlightedCode = tokenLines.length > 0
|
||||||
@ -167,7 +173,6 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
|||||||
return (
|
return (
|
||||||
<ContentContainer
|
<ContentContainer
|
||||||
ref={codeContainerRef}
|
ref={codeContainerRef}
|
||||||
$lineNumbers={codeShowLineNumbers}
|
|
||||||
$wrap={codeWrappable && !isUnwrapped}
|
$wrap={codeWrappable && !isUnwrapped}
|
||||||
$fadeIn={hasHighlightedCode}
|
$fadeIn={hasHighlightedCode}
|
||||||
style={{
|
style={{
|
||||||
@ -175,7 +180,7 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
|||||||
maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none'
|
maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none'
|
||||||
}}>
|
}}>
|
||||||
{hasHighlightedCode ? (
|
{hasHighlightedCode ? (
|
||||||
<ShikiTokensRenderer language={language} tokenLines={tokenLines} />
|
<ShikiTokensRenderer language={language} tokenLines={tokenLines} showLineNumbers={codeShowLineNumbers} />
|
||||||
) : (
|
) : (
|
||||||
<CodePlaceholder>{children}</CodePlaceholder>
|
<CodePlaceholder>{children}</CodePlaceholder>
|
||||||
)}
|
)}
|
||||||
@ -188,43 +193,47 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
|||||||
*
|
*
|
||||||
* 独立出来,方便将来做 virtual list
|
* 独立出来,方便将来做 virtual list
|
||||||
*/
|
*/
|
||||||
const ShikiTokensRenderer: React.FC<{ language: string; tokenLines: ThemedToken[][] }> = memo(
|
const ShikiTokensRenderer: React.FC<{
|
||||||
({ language, tokenLines }) => {
|
language: string
|
||||||
const { getShikiPreProperties } = useCodeStyle()
|
tokenLines: ThemedToken[][]
|
||||||
const rendererRef = useRef<HTMLPreElement>(null)
|
showLineNumbers: boolean
|
||||||
|
}> = memo(({ language, tokenLines, showLineNumbers }) => {
|
||||||
|
const { getShikiPreProperties } = useCodeStyle()
|
||||||
|
const rendererRef = useRef<HTMLPreElement>(null)
|
||||||
|
|
||||||
// 设置 pre 标签属性
|
// 设置 pre 标签属性
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getShikiPreProperties(language).then((properties) => {
|
getShikiPreProperties(language).then((properties) => {
|
||||||
const pre = rendererRef.current
|
const pre = rendererRef.current
|
||||||
if (pre) {
|
if (pre) {
|
||||||
pre.className = properties.class
|
pre.className = properties.class
|
||||||
pre.style.cssText = properties.style
|
pre.style.cssText = properties.style
|
||||||
pre.tabIndex = properties.tabindex
|
pre.tabIndex = properties.tabindex
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [language, getShikiPreProperties])
|
}, [language, getShikiPreProperties])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pre className="shiki" ref={rendererRef}>
|
<pre className="shiki" ref={rendererRef}>
|
||||||
<code>
|
<code>
|
||||||
{tokenLines.map((lineTokens, lineIndex) => (
|
{tokenLines.map((lineTokens, lineIndex) => (
|
||||||
<span key={`line-${lineIndex}`} className="line">
|
<span key={`line-${lineIndex}`} className="line">
|
||||||
|
{showLineNumbers && <span className="line-number">{lineIndex + 1}</span>}
|
||||||
|
<span className="line-content">
|
||||||
{lineTokens.map((token, tokenIndex) => (
|
{lineTokens.map((token, tokenIndex) => (
|
||||||
<span key={`token-${tokenIndex}`} style={getReactStyleFromToken(token)}>
|
<span key={`token-${tokenIndex}`} style={getReactStyleFromToken(token)}>
|
||||||
{token.content}
|
{token.content}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
))}
|
</span>
|
||||||
</code>
|
))}
|
||||||
</pre>
|
</code>
|
||||||
)
|
</pre>
|
||||||
}
|
)
|
||||||
)
|
})
|
||||||
|
|
||||||
const ContentContainer = styled.div<{
|
const ContentContainer = styled.div<{
|
||||||
$lineNumbers: boolean
|
|
||||||
$wrap: boolean
|
$wrap: boolean
|
||||||
$fadeIn: boolean
|
$fadeIn: boolean
|
||||||
}>`
|
}>`
|
||||||
@ -233,9 +242,8 @@ const ContentContainer = styled.div<{
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
/* 动态宽度计算 */
|
/* gutter 宽度默认值 */
|
||||||
--line-digits: 0;
|
--gutter-width: 0.6rem;
|
||||||
--gutter-width: max(calc(var(--line-digits) * 0.7rem), 2.1rem);
|
|
||||||
|
|
||||||
.shiki {
|
.shiki {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
@ -246,38 +254,35 @@ const ContentContainer = styled.div<{
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
min-height: 1.3rem;
|
min-height: 1.3rem;
|
||||||
padding-left: ${(props) => (props.$lineNumbers ? 'var(--gutter-width)' : '0')};
|
|
||||||
|
|
||||||
* {
|
.line-number {
|
||||||
overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
|
width: var(--gutter-width);
|
||||||
white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
|
text-align: right;
|
||||||
|
opacity: 0.35;
|
||||||
|
margin-right: 1rem;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
* {
|
||||||
|
overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
|
||||||
|
white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.$lineNumbers &&
|
|
||||||
`
|
|
||||||
code {
|
|
||||||
counter-reset: step;
|
|
||||||
counter-increment: step 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
code .line::before {
|
|
||||||
content: counter(step);
|
|
||||||
counter-increment: step;
|
|
||||||
width: 1rem;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
text-align: right;
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
@keyframes contentFadeIn {
|
@keyframes contentFadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user