({ value, onChange, placeholder, className, "data-testid": testId }: CodeEditorProps)
| 9 | } |
| 10 | |
| 11 | export function CodeEditor({ value, onChange, placeholder, className, "data-testid": testId }: CodeEditorProps) { |
| 12 | const [lineNumbers, setLineNumbers] = useState<number[]>([1]); |
| 13 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 14 | const highlightRef = useRef<HTMLDivElement>(null); |
| 15 | |
| 16 | useEffect(() => { |
| 17 | const lines = value.split('\n').length; |
| 18 | setLineNumbers(Array.from({ length: lines }, (_, i) => i + 1)); |
| 19 | }, [value]); |
| 20 | |
| 21 | const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => { |
| 22 | if (highlightRef.current) { |
| 23 | highlightRef.current.scrollTop = e.currentTarget.scrollTop; |
| 24 | highlightRef.current.scrollLeft = e.currentTarget.scrollLeft; |
| 25 | } |
| 26 | }; |
| 27 | |
| 28 | const escapeHtml = (text: string) => { |
| 29 | const div = document.createElement('div'); |
| 30 | div.textContent = text; |
| 31 | return div.innerHTML; |
| 32 | }; |
| 33 | |
| 34 | const highlightSyntax = (code: string) => { |
| 35 | const escaped = escapeHtml(code); |
| 36 | return escaped |
| 37 | .replace(/\b(query|mutation|subscription|fragment|type|input|interface|enum|scalar|union|schema|extend|implements)\b/g, '<span class="text-primary">$1</span>') |
| 38 | .replace(/\b(String|Int|Float|Boolean|ID|true|false|null)\b/g, '<span class="text-secondary">$1</span>') |
| 39 | .replace(/"([^"]*)"/g, '<span class="text-chart-1">"$1"</span>') |
| 40 | .replace(/#.*/g, '<span class="text-muted-foreground">$&</span>') |
| 41 | .replace(/\{|\}/g, '<span class="text-accent-foreground">$&</span>') |
| 42 | .replace(/\(|\)/g, '<span class="text-chart-2">$&</span>'); |
| 43 | }; |
| 44 | |
| 45 | return ( |
| 46 | <div className={`relative ${className}`}> |
| 47 | <div className="flex h-full border border-border rounded-md overflow-hidden bg-card/50"> |
| 48 | <div className="flex-none bg-muted/30 border-r border-border px-3 py-3 select-none"> |
| 49 | <div className="font-mono text-xs text-muted-foreground text-right space-y-[1px]"> |
| 50 | {lineNumbers.map((num) => ( |
| 51 | <div key={num} className="h-5 leading-5"> |
| 52 | {num} |
| 53 | </div> |
| 54 | ))} |
| 55 | </div> |
| 56 | </div> |
| 57 | |
| 58 | <div className="flex-1 relative"> |
| 59 | <div |
| 60 | ref={highlightRef} |
| 61 | className="absolute inset-0 p-3 font-mono text-sm pointer-events-none overflow-hidden whitespace-pre-wrap break-words" |
| 62 | dangerouslySetInnerHTML={{ |
| 63 | __html: highlightSyntax(value || placeholder || ''), |
| 64 | }} |
| 65 | /> |
| 66 | |
| 67 | <textarea |
| 68 | ref={textareaRef} |
nothing calls this directly
no test coverage detected