From 10be9b41b762a6b55a2c35555e522e98da61feee Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Tue, 22 Jul 2025 15:58:55 -0600 Subject: [PATCH] accept line numbers --- .../StreamingMessageCode.tsx | 4 - .../ui/typography/SyntaxHighlight/Line.tsx | 32 -------- .../SyntaxHighlighter.module.css | 16 ++-- .../SyntaxHighlighter.stories.tsx | 5 +- .../SyntaxHighlight/SyntaxHighlighter.tsx | 76 ++++++++++++------- .../SyntaxHighlight/shiki-instance.ts | 41 +--------- 6 files changed, 60 insertions(+), 114 deletions(-) delete mode 100644 apps/web/src/components/ui/typography/SyntaxHighlight/Line.tsx diff --git a/apps/web/src/components/ui/streaming/StreamingMessageCode/StreamingMessageCode.tsx b/apps/web/src/components/ui/streaming/StreamingMessageCode/StreamingMessageCode.tsx index e844f492f..a4e907701 100644 --- a/apps/web/src/components/ui/streaming/StreamingMessageCode/StreamingMessageCode.tsx +++ b/apps/web/src/components/ui/streaming/StreamingMessageCode/StreamingMessageCode.tsx @@ -153,9 +153,6 @@ const HiddenSection: React.FC<{ ); -const lineNumberStyles: React.CSSProperties = { - minWidth: '2.25em' -}; const MemoizedSyntaxHighlighter = React.memo( ({ lineNumber, text }: { lineNumber: number; text: string }) => { return ( @@ -163,7 +160,6 @@ const MemoizedSyntaxHighlighter = React.memo( language={'yaml'} showLineNumbers startingLineNumber={lineNumber} - lineNumberStyle={lineNumberStyles} className={'m-0! w-fit! border-none! p-0!'}> {text} diff --git a/apps/web/src/components/ui/typography/SyntaxHighlight/Line.tsx b/apps/web/src/components/ui/typography/SyntaxHighlight/Line.tsx deleted file mode 100644 index 65abfb804..000000000 --- a/apps/web/src/components/ui/typography/SyntaxHighlight/Line.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import type { ThemedToken } from 'shiki'; - -interface LineProps { - tokens: ThemedToken[]; - lineNumber: number; - showLineNumber: boolean; - animation?: string; - animationDuration?: number; - isNew?: boolean; -} - -export const Line: React.FC = React.memo( - ({ tokens, lineNumber, showLineNumber, animation, animationDuration = 700, isNew = false }) => { - const lineStyle = - isNew && animation && animation !== 'none' - ? { animation: `${animation} ${animationDuration}ms ease-in-out forwards` } - : undefined; - - return ( -
- {tokens.map((token, index) => ( - - {token.content} - - ))} -
- ); - } -); - -Line.displayName = 'Line'; diff --git a/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.module.css b/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.module.css index 3d41585ef..1928ac5b9 100644 --- a/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.module.css +++ b/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.module.css @@ -1,28 +1,26 @@ /* Styles for the Shiki syntax highlighter wrapper */ .shikiWrapper :global(pre) { margin: 0; - padding: 0; + padding: 0em; overflow-x: auto; - background: transparent !important; } .shikiWrapper :global(code) { - background: transparent !important; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; line-height: 1.45; } -/* Single-line code padding for non-line-number view */ -.shikiWrapper:not(.shikiWrapperWithLineNumbers) :global(pre) { - padding: 1rem; -} - /* CSS counter-based line numbers */ .shikiWrapper.withLineNumbers :global(code) { counter-reset: step var(--line-number-start, 0); } -.shikiWrapper.withLineNumbers :global(code .line)::before { +/* Line class for syntax highlighted lines */ +.line { + display: block; +} + +.shikiWrapper.withLineNumbers .line::before { content: counter(step); counter-increment: step; width: 2rem; diff --git a/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.stories.tsx b/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.stories.tsx index a7f3e49d8..75d860055 100644 --- a/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.stories.tsx +++ b/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.stories.tsx @@ -95,7 +95,7 @@ export const SqlExample: Story = { language: 'sql', isDarkMode: false, showLineNumbers: false, - className: 'border rounded-lg p-4 mr-2 max-w-[350px]' + className: 'border rounded-lg pl-4 py-3 mr-2 max-w-[350px]' } }; @@ -222,6 +222,7 @@ const StreamingAnimationStory = ({ animation={animation} isDarkMode={isDarkMode} showLineNumbers={showLineNumbers} + animationDuration={800} className="rounded-lg border p-4"> {currentCode} @@ -231,7 +232,7 @@ const StreamingAnimationStory = ({ export const StreamingFadeIn: Story = { render: () => ( - + ) }; diff --git a/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.tsx b/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.tsx index 04d140686..7e4482f28 100644 --- a/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.tsx +++ b/apps/web/src/components/ui/typography/SyntaxHighlight/SyntaxHighlighter.tsx @@ -1,9 +1,9 @@ -import { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { cn } from '@/lib/classMerge'; import { getCodeTokens } from './shiki-instance'; import styles from './SyntaxHighlighter.module.css'; import { animations, type MarkdownAnimation } from '../animation-common'; -import { Line } from './Line'; +import type { ThemedToken } from 'shiki'; export const SyntaxHighlighter = (props: { children: string; @@ -11,6 +11,7 @@ export const SyntaxHighlighter = (props: { showLineNumbers?: boolean; startingLineNumber?: number; className?: string; + containerClassName?: string; customStyle?: React.CSSProperties; isDarkMode?: boolean; animation?: MarkdownAnimation; @@ -22,6 +23,7 @@ export const SyntaxHighlighter = (props: { showLineNumbers = false, startingLineNumber = 1, className = '', + containerClassName = '', customStyle = {}, isDarkMode = false, animation = 'none', @@ -31,23 +33,12 @@ export const SyntaxHighlighter = (props: { const [tokens, setTokens] = useState(null); const [isLoading, setIsLoading] = useState(true); - // Track which lines have been rendered before - const renderedLinesRef = useRef>(new Set()); - const previousLineCountRef = useRef(0); - useEffect(() => { const loadTokens = async () => { try { - const theme = isDarkMode ? 'buster-dark' : 'buster-light'; + const theme = isDarkMode ? 'github-dark' : 'github-light'; const tokenData = await getCodeTokens(children, language, theme); - // Check if content got shorter (lines were removed) - const currentLineCount = tokenData.tokens.length; - if (currentLineCount < previousLineCountRef.current) { - renderedLinesRef.current.clear(); - } - previousLineCountRef.current = currentLineCount; - setTokens(tokenData); setIsLoading(false); } catch (error) { @@ -61,7 +52,9 @@ export const SyntaxHighlighter = (props: { if (isLoading || !tokens) { return ( -
+
           {children}
         
@@ -70,38 +63,34 @@ export const SyntaxHighlighter = (props: { } return ( -
+
-
+            : undefined)
+        }}>
+        
           
             {tokens.tokens.map((line: any[], index: number) => {
-              const isNewLine = !renderedLinesRef.current.has(index);
-              if (isNewLine) {
-                renderedLinesRef.current.add(index);
-              }
-
               return (
                 
               );
             })}
@@ -111,3 +100,32 @@ export const SyntaxHighlighter = (props: {
     
); }; + +// Line component for rendering individual lines with animation support +interface LineProps { + tokens: ThemedToken[]; + lineNumber: number; + animation?: string; + animationDuration?: number; +} + +const Line: React.FC = React.memo( + ({ tokens, animation, lineNumber, animationDuration = 700 }) => { + const lineStyle = + animation && animation !== 'none' + ? { animation: `${animation} ${animationDuration}ms ease-in-out forwards` } + : undefined; + + return ( +
+ {tokens.map((token, index) => ( + + {token.content} + + ))} +
+ ); + } +); + +Line.displayName = 'Line'; diff --git a/apps/web/src/components/ui/typography/SyntaxHighlight/shiki-instance.ts b/apps/web/src/components/ui/typography/SyntaxHighlight/shiki-instance.ts index 7ae99f4a1..a672d6071 100644 --- a/apps/web/src/components/ui/typography/SyntaxHighlight/shiki-instance.ts +++ b/apps/web/src/components/ui/typography/SyntaxHighlight/shiki-instance.ts @@ -8,14 +8,6 @@ import githubDark from '@shikijs/themes/github-dark'; let highlighterInstance: HighlighterCore | null = null; let initializationPromise: Promise | null = null; -// Cache for highlighted code -const highlightCache = new Map(); - -// Generate cache key -const getCacheKey = (code: string, language: string, theme: string): string => { - return `${language}:${theme}:${code}`; -}; - // Initialize the highlighter with pre-loaded languages and themes export const initializeHighlighter = async (): Promise => { // Return existing instance if available @@ -45,20 +37,13 @@ export const initializeHighlighter = async (): Promise => { } }; -// Highlight code with caching +// Highlight code export const highlightCode = async ( code: string, language: 'sql' | 'yaml', theme: 'github-light' | 'github-dark', transformers?: ShikiTransformer[] ): Promise => { - // Check cache first - const cacheKey = getCacheKey(code, language, theme); - const cached = highlightCache.get(cacheKey); - if (cached && !transformers?.length) { - return cached; - } - // Get or initialize highlighter const highlighter = await initializeHighlighter(); @@ -69,44 +54,24 @@ export const highlightCode = async ( transformers }); - // Cache if no transformers (transformers might add dynamic content like line numbers) - if (!transformers?.length) { - highlightCache.set(cacheKey, html); - } - return html; }; -// Get tokens for code with caching -const tokenCache = new Map(); - +// Get tokens for code export const getCodeTokens = async ( code: string, language: 'sql' | 'yaml', - theme: 'buster-light' | 'buster-dark' + theme: 'github-light' | 'github-dark' ): Promise => { - const cacheKey = getCacheKey(code, language, theme); - const cached = tokenCache.get(cacheKey); - if (cached) { - return cached; - } - const highlighter = await initializeHighlighter(); const tokens = highlighter.codeToTokens(code, { lang: language, theme }); - tokenCache.set(cacheKey, tokens); return tokens; }; -// Clear cache (useful for memory management if needed) -export const clearHighlightCache = (): void => { - highlightCache.clear(); - tokenCache.clear(); -}; - // Pre-initialize highlighter on module load for better performance if (typeof window !== 'undefined') { initializeHighlighter().catch((error) => {