/** * Shared code editor with Nunjucks support by default. * Uses react-simple-code-editor + Prism + prism-react-renderer; highlights base language + {{ }}, {% %}, {# #}. * Wherever this editor is used, it provides the same features (syntax highlighting + Nunjucks). * The parent (node) provides the expected base language for highlighting. */ import React, { useCallback, useLayoutEffect, useRef } from 'react' import Editor from 'react-simple-code-editor' import { highlight, type HighlightLanguage } from '@/lib/syntaxHighlight' export type CodeEditorProps = { value: string onValueChange: (value: string) => void /** Base language for syntax highlighting (Nunjucks is always applied on top). */ language: HighlightLanguage /** Stable id for the underlying textarea (for insert-at-cursor). */ textareaId?: string readOnly?: boolean placeholder?: string padding?: number tabSize?: number insertSpaces?: boolean ignoreTabKey?: boolean style?: React.CSSProperties className?: string textareaClassName?: string preClassName?: string /** Minimum height so the container doesn't collapse before resize. */ minHeight?: number } const defaultStyle: React.CSSProperties = { fontFamily: 'ui-monospace, monospace', fontSize: 12, lineHeight: 1.5, overflow: 'auto', } export function CodeEditor({ value, onValueChange, language, textareaId, readOnly = false, padding = 8, tabSize = 2, insertSpaces = true, ignoreTabKey = false, style, className, textareaClassName, preClassName, minHeight = 120, }: CodeEditorProps) { const containerRef = useRef(null) const selectionRestoreRef = useRef<{ start: number; end: number } | null>(null) const highlightCode = useCallback( (code: string) => highlight(code, language), [language] ) const handleValueChange = useCallback( (newValue: string) => { if (readOnly) { onValueChange(newValue) return } const ta = (textareaId ? document.getElementById(textareaId) : containerRef.current?.querySelector('textarea')) as HTMLTextAreaElement | null const start = ta?.selectionStart ?? newValue.length const end = ta?.selectionEnd ?? newValue.length selectionRestoreRef.current = { start, end } onValueChange(newValue) }, [onValueChange, readOnly, textareaId] ) useLayoutEffect(() => { const pending = selectionRestoreRef.current if (pending == null) return selectionRestoreRef.current = null const el = (textareaId ? document.getElementById(textareaId) : containerRef.current?.querySelector('textarea')) as HTMLTextAreaElement | null if (el) { const { start, end } = pending const safeEnd = Math.min(end, el.value.length) const safeStart = Math.min(start, safeEnd) el.setSelectionRange(safeStart, safeEnd) } }, [value, textareaId]) return (
) }