import React, { useCallback, useEffect, useRef, useState } from "react" import { cn } from "@/lib/utils" import type { CodeCompletionItem, ShaderError, ShaderType } from "../../types/shader-system" interface GLSLCodeEditorProps { value: string onChange: (value: string) => void onCompile?: () => void shaderType: ShaderType errors?: ShaderError[] readOnly?: boolean className?: string } // GLSL keywords for syntax highlighting const GLSL_KEYWORDS = [ // Storage qualifiers "const", "attribute", "uniform", "varying", "in", "out", "inout", // Precision qualifiers "highp", "mediump", "lowp", "precision", // Control flow "if", "else", "for", "while", "do", "break", "continue", "return", "discard", // Types "void", "bool", "int", "uint", "float", "double", "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "uvec2", "uvec3", "uvec4", "bvec2", "bvec3", "bvec4", "dvec2", "dvec3", "dvec4", "mat2", "mat3", "mat4", "dmat2", "dmat3", "dmat4", "sampler2D", "sampler3D", "samplerCube", "sampler2DShadow", // Layout qualifiers "layout", "location", "binding", "offset", // Other "struct", "true", "false", ] const GLSL_BUILTIN_FUNCTIONS = [ // Math functions "abs", "sign", "floor", "ceil", "fract", "mod", "min", "max", "clamp", "mix", "step", "smoothstep", "length", "distance", "dot", "cross", "normalize", "reflect", "refract", "pow", "exp", "log", "exp2", "log2", "sqrt", "inversesqrt", "sin", "cos", "tan", "asin", "acos", "atan", // Matrix functions "matrixCompMult", "transpose", "inverse", // Vector functions "lessThan", "lessThanEqual", "greaterThan", "greaterThanEqual", "equal", "notEqual", "any", "all", "not", // Texture functions "texture", "texture2D", "textureCube", "textureProj", // Derivative functions "dFdx", "dFdy", "fwidth", // Noise functions "noise", "noise1", "noise2", "noise3", "noise4", ] const GLSL_BUILTIN_VARIABLES = [ // Vertex shader "gl_Position", "gl_PointSize", "gl_ClipDistance", // Fragment shader "gl_FragCoord", "gl_FrontFacing", "gl_PointCoord", "gl_FragColor", "gl_FragData", // Both "gl_VertexID", "gl_InstanceID", "gl_PrimitiveID", ] export function GLSLCodeEditor({ value, onChange, onCompile, shaderType, errors = [], readOnly = false, className, }: GLSLCodeEditorProps) { const editorRef = useRef(null) const lineNumbersRef = useRef(null) const [cursorPosition, setCursorPosition] = useState({ line: 1, column: 1 }) const [selectedText, setSelectedText] = useState("") const [showAutoComplete, setShowAutoComplete] = useState(false) const [autoCompleteItems, setAutoCompleteItems] = useState([]) // Update line numbers useEffect(() => { if (lineNumbersRef.current) { const lines = value.split("\n").length const lineNumbers = Array.from({ length: lines }, (_, i) => i + 1).join("\n") lineNumbersRef.current.textContent = lineNumbers } }, [value]) // Handle cursor position const updateCursorPosition = useCallback(() => { if (!editorRef.current) return const { selectionStart } = editorRef.current const lines = value.substring(0, selectionStart).split("\n") const line = lines.length const column = lines[lines.length - 1].length + 1 setCursorPosition({ line, column }) // Update selected text const { selectionEnd } = editorRef.current if (selectionStart !== selectionEnd) { setSelectedText(value.substring(selectionStart, selectionEnd)) } else { setSelectedText("") } }, [value]) // Handle text change const handleChange = useCallback( (e: React.ChangeEvent) => { onChange(e.target.value) updateCursorPosition() }, [onChange, updateCursorPosition], ) // Handle key events const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { // Tab handling if (e.key === "Tab" && !e.shiftKey) { e.preventDefault() const { selectionStart, selectionEnd } = e.currentTarget const newValue = `${value.substring(0, selectionStart)} ${value.substring(selectionEnd)}` onChange(newValue) // Restore cursor position setTimeout(() => { if (editorRef.current) { editorRef.current.selectionStart = selectionStart + 2 editorRef.current.selectionEnd = selectionStart + 2 } }, 0) } // Compile shortcut if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault() onCompile?.() } // Auto-complete trigger if (e.key === "." || (e.ctrlKey && e.key === " ")) { // Trigger autocomplete const currentWord = getCurrentWord() if (currentWord) { generateAutoCompleteItems(currentWord) setShowAutoComplete(true) } } // Close autocomplete if (e.key === "Escape") { setShowAutoComplete(false) } }, [value, onChange, onCompile], ) // Get current word at cursor const getCurrentWord = useCallback(() => { if (!editorRef.current) return "" const { selectionStart } = editorRef.current const beforeCursor = value.substring(0, selectionStart) const afterCursor = value.substring(selectionStart) const beforeMatch = /[\w.]+$/.exec(beforeCursor) const afterMatch = /^[\w.]+/.exec(afterCursor) const before = beforeMatch ? beforeMatch[0] : "" const after = afterMatch ? afterMatch[0] : "" return before + after }, [value]) // Generate autocomplete items const generateAutoCompleteItems = useCallback((prefix: string) => { const items: CodeCompletionItem[] = [] // Add matching keywords GLSL_KEYWORDS.filter((k) => k.startsWith(prefix)).forEach((keyword) => { items.push({ label: keyword, kind: "keyword", insertText: keyword, sortText: `1${keyword}`, }) }) // Add matching functions GLSL_BUILTIN_FUNCTIONS.filter((f) => f.startsWith(prefix)).forEach((func) => { items.push({ label: func, kind: "function", insertText: `${func}(`, detail: "Built-in function", sortText: `2${func}`, }) }) // Add matching variables GLSL_BUILTIN_VARIABLES.filter((v) => v.startsWith(prefix)).forEach((variable) => { items.push({ label: variable, kind: "variable", insertText: variable, detail: "Built-in variable", sortText: `3${variable}`, }) }) setAutoCompleteItems(items) }, []) // Syntax highlighting (simplified) const getHighlightedCode = useCallback(() => { let highlighted = value // Highlight keywords GLSL_KEYWORDS.forEach((keyword) => { const regex = new RegExp(`\\b${keyword}\\b`, "g") highlighted = highlighted.replace(regex, `${keyword}`) }) // Highlight functions GLSL_BUILTIN_FUNCTIONS.forEach((func) => { const regex = new RegExp(`\\b${func}\\b`, "g") highlighted = highlighted.replace(regex, `${func}`) }) // Highlight numbers highlighted = highlighted.replace(/\b(\d+\.?\d*)\b/g, '$1') // Highlight strings highlighted = highlighted.replace(/"([^"]*)"/g, '"$1"') // Highlight comments highlighted = highlighted.replace(/(\/\/.*$)/gm, '$1') highlighted = highlighted.replace(/(\/\*[\s\S]*?\*\/)/g, '$1') return highlighted }, [value]) // Get error decorations const getErrorDecorations = useCallback(() => { return errors.map((error) => ({ line: error.line, className: "error-line", message: error.message, })) }, [errors]) return (
{/* Line numbers */}
1
{/* Editor */}