11import { ActionIcon , Button , CopyButton , Paper , ScrollArea , Text , useMantineTheme } from '@mantine/core' ;
2- import { IconCheck , IconClipboardCopy , IconChevronDown , IconChevronUp } from '@tabler/icons-react' ;
3- import { useEffect , useState } from 'react' ;
2+ import { IconCheck , IconChevronDown , IconChevronUp , IconClipboardCopy } from '@tabler/icons-react' ;
3+ import type { HLJSApi } from 'highlight.js' ;
4+ import { useEffect , useMemo , useState } from 'react' ;
5+ import { FixedSizeList as List } from 'react-window' ;
46
57import './HighlightCode.theme.scss' ;
6- import { type HLJSApi } from 'highlight.js' ;
78
89export default function HighlightCode ( { language, code } : { language : string ; code : string } ) {
910 const theme = useMantineTheme ( ) ;
@@ -14,15 +15,56 @@ export default function HighlightCode({ language, code }: { language: string; co
1415 import ( 'highlight.js' ) . then ( ( mod ) => setHljs ( mod . default || mod ) ) ;
1516 } , [ ] ) ;
1617
17- const lines = code . split ( '\n' ) ;
18- const lineNumbers = lines . map ( ( _ , i ) => i + 1 ) ;
19- const displayLines = expanded ? lines : lines . slice ( 0 , 50 ) ;
20- const displayLineNumbers = expanded ? lineNumbers : lineNumbers . slice ( 0 , 50 ) ;
18+ const lines = useMemo ( ( ) => code . split ( '\n' ) , [ code ] ) ;
19+ const visible = expanded ? lines . length : Math . min ( lines . length , 50 ) ;
20+ const expandable = lines . length > 50 ;
2121
22- let lang = language ;
23- if ( ! hljs || ! hljs . getLanguage ( lang ) ) {
24- lang = 'text' ;
25- }
22+ const lang = useMemo ( ( ) => {
23+ if ( ! hljs ) return 'plaintext' ;
24+ if ( hljs . getLanguage ( language ) ) return language ;
25+
26+ return 'plaintext' ;
27+ } , [ hljs , language ] ) ;
28+
29+ const hlLines = useMemo ( ( ) => {
30+ if ( ! hljs ) return lines ;
31+
32+ return lines . map (
33+ ( line ) =>
34+ hljs . highlight ( line , {
35+ language : lang ,
36+ } ) . value ,
37+ ) ;
38+ } , [ lines , hljs , lang ] ) ;
39+
40+ const Row = ( { index, style } : { index : number ; style : React . CSSProperties } ) => (
41+ < div
42+ style = { {
43+ ...style ,
44+ display : 'flex' ,
45+ alignItems : 'flex-start' ,
46+ whiteSpace : 'pre' ,
47+ fontFamily : 'monospace' ,
48+ fontSize : '0.8rem' ,
49+ } }
50+ >
51+ < Text
52+ component = 'span'
53+ c = 'dimmed'
54+ mr = 'md'
55+ style = { {
56+ userSelect : 'none' ,
57+ width : 40 ,
58+ textAlign : 'right' ,
59+ flexShrink : 0 ,
60+ } }
61+ >
62+ { index + 1 }
63+ </ Text >
64+
65+ < code className = 'theme hljs' style = { { flex : 1 } } dangerouslySetInnerHTML = { { __html : hlLines [ index ] } } />
66+ </ div >
67+ ) ;
2668
2769 return (
2870 < Paper withBorder p = 'xs' my = 'md' pos = 'relative' >
@@ -44,37 +86,17 @@ export default function HighlightCode({ language, code }: { language: string; co
4486 ) }
4587 </ CopyButton >
4688
47- < ScrollArea type = 'auto' dir = 'ltr' offsetScrollbars = { false } >
48- < pre style = { { margin : 0 , whiteSpace : 'pre' , overflowX : 'auto' } } className = 'theme' >
49- < code className = 'theme' >
50- { displayLines . map ( ( line , i ) => (
51- < div key = { i } >
52- < Text
53- component = 'span'
54- size = 'sm'
55- c = 'dimmed'
56- mr = 'md'
57- style = { { userSelect : 'none' , fontFamily : 'monospace' } }
58- >
59- { displayLineNumbers [ i ] }
60- </ Text >
61- < span
62- className = 'line'
63- dangerouslySetInnerHTML = { {
64- __html : lang === 'none' || ! hljs ? line : hljs . highlight ( line , { language : lang } ) . value ,
65- } }
66- />
67- </ div >
68- ) ) }
69- </ code >
70- </ pre >
89+ < ScrollArea type = 'auto' offsetScrollbars = { false } style = { { maxHeight : 400 } } >
90+ < List height = { 400 } width = '100%' itemCount = { visible } itemSize = { 20 } overscanCount = { 10 } >
91+ { Row }
92+ </ List >
7193 </ ScrollArea >
7294
73- { lines . length > 50 && (
95+ { expandable && (
7496 < Button
75- variant = 'outline '
97+ variant = 'light '
7698 size = 'compact-sm'
77- onClick = { ( ) => setExpanded ( ! expanded ) }
99+ onClick = { ( ) => setExpanded ( ( e ) => ! e ) }
78100 leftSection = { expanded ? < IconChevronUp size = '1rem' /> : < IconChevronDown size = '1rem' /> }
79101 style = { { position : 'absolute' , bottom : '0.5rem' , right : '0.5rem' } }
80102 >
0 commit comments