From 16772c1d37f0aba2794aad66c065e07d4887d83b Mon Sep 17 00:00:00 2001 From: one Date: Fri, 27 Jun 2025 02:41:28 +0800 Subject: [PATCH] refactor(CodePreview): line numbers as elements --- .../components/CodeBlockView/CodePreview.tsx | 125 +++++++++--------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/src/renderer/src/components/CodeBlockView/CodePreview.tsx b/src/renderer/src/components/CodeBlockView/CodePreview.tsx index b550cd2467..7cfd0ba31d 100644 --- a/src/renderer/src/components/CodeBlockView/CodePreview.tsx +++ b/src/renderer/src/components/CodeBlockView/CodePreview.tsx @@ -4,7 +4,7 @@ import { useSettings } from '@renderer/hooks/useSettings' import { uuid } from '@renderer/utils' import { getReactStyleFromToken } from '@renderer/utils/shiki' 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 { ThemedToken } from 'shiki/core' import styled from 'styled-components' @@ -154,12 +154,18 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { setTimeout(highlightCode, 0) }, [isInViewport, highlightCode]) - useEffect(() => { + const lastDigitsRef = useRef(1) + + useLayoutEffect(() => { const container = codeContainerRef.current if (!container || !codeShowLineNumbers) return 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]) const hasHighlightedCode = tokenLines.length > 0 @@ -167,7 +173,6 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { return ( { maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none' }}> {hasHighlightedCode ? ( - + ) : ( {children} )} @@ -188,43 +193,47 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { * * 独立出来,方便将来做 virtual list */ -const ShikiTokensRenderer: React.FC<{ language: string; tokenLines: ThemedToken[][] }> = memo( - ({ language, tokenLines }) => { - const { getShikiPreProperties } = useCodeStyle() - const rendererRef = useRef(null) +const ShikiTokensRenderer: React.FC<{ + language: string + tokenLines: ThemedToken[][] + showLineNumbers: boolean +}> = memo(({ language, tokenLines, showLineNumbers }) => { + const { getShikiPreProperties } = useCodeStyle() + const rendererRef = useRef(null) - // 设置 pre 标签属性 - useEffect(() => { - getShikiPreProperties(language).then((properties) => { - const pre = rendererRef.current - if (pre) { - pre.className = properties.class - pre.style.cssText = properties.style - pre.tabIndex = properties.tabindex - } - }) - }, [language, getShikiPreProperties]) + // 设置 pre 标签属性 + useEffect(() => { + getShikiPreProperties(language).then((properties) => { + const pre = rendererRef.current + if (pre) { + pre.className = properties.class + pre.style.cssText = properties.style + pre.tabIndex = properties.tabindex + } + }) + }, [language, getShikiPreProperties]) - return ( -
-        
-          {tokenLines.map((lineTokens, lineIndex) => (
-            
+  return (
+    
+      
+        {tokenLines.map((lineTokens, lineIndex) => (
+          
+            {showLineNumbers && {lineIndex + 1}}
+            
               {lineTokens.map((token, tokenIndex) => (
                 
                   {token.content}
                 
               ))}
             
-          ))}
-        
-      
- ) - } -) +
+ ))} +
+
+ ) +}) const ContentContainer = styled.div<{ - $lineNumbers: boolean $wrap: boolean $fadeIn: boolean }>` @@ -233,9 +242,8 @@ const ContentContainer = styled.div<{ border-radius: inherit; margin-top: 0; - /* 动态宽度计算 */ - --line-digits: 0; - --gutter-width: max(calc(var(--line-digits) * 0.7rem), 2.1rem); + /* gutter 宽度默认值 */ + --gutter-width: 0.6rem; .shiki { padding: 1em; @@ -246,38 +254,35 @@ const ContentContainer = styled.div<{ flex-direction: column; .line { - display: block; + display: flex; + align-items: flex-start; min-height: 1.3rem; - padding-left: ${(props) => (props.$lineNumbers ? 'var(--gutter-width)' : '0')}; - * { - overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')}; - white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')}; + .line-number { + width: var(--gutter-width); + 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 { from { opacity: 0;