UNPKG

@making-sense/antlr-editor

Version:
407 lines 18.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const ParserFacade_1 = require("./utils/ParserFacade"); const providers_1 = require("./utils/providers"); const variables_1 = require("./utils/variables"); const EditorFooter_1 = __importDefault(require("./EditorFooter")); // Check if we're in a test environment const isTestEnvironment = typeof process !== "undefined" && process.env.NODE_ENV === "test"; // Import Monaco Editor components directly const react_2 = __importDefault(require("@monaco-editor/react")); const react_3 = require("@monaco-editor/react"); const monaco = __importStar(require("monaco-editor")); react_3.loader.config({ monaco }); // Mock objects for test environment const mockMonacoEditor = () => null; // Use conditional components const MonacoEditor = isTestEnvironment ? mockMonacoEditor : react_2.default; const Editor = ({ script, setScript, onListErrors, customFetcher, variables, variablesInputURLs, tools, height = "50vh", width = "100%", theme = "vs-dark", options, shortcuts, FooterComponent, displayFooter = true }) => { const editorRef = (0, react_1.useRef)(null); const monacoRef = (0, react_1.useRef)(null); const [ready, setReady] = (0, react_1.useState)(false); const [vars, setVars] = (0, react_1.useState)((0, variables_1.buildVariables)(variables)); const [isEditorReady, setIsEditorReady] = (0, react_1.useState)(false); const [cursor, setCursor] = (0, react_1.useState)({ line: 1, column: 1, selectionLength: 0 }); // Cleanup function to properly dispose of Monaco resources const cleanupMonaco = (0, react_1.useCallback)(() => { if (editorRef.current) { try { // Get the model before disposing const model = editorRef.current.getModel(); // Detach the model first to prevent further rendering editorRef.current.setModel(null); // Dispose the model if (model) { model.dispose(); } // Dispose the editor instance editorRef.current.dispose(); } catch (error) { // Silently catch dispose errors - they're expected during cleanup console.debug("Monaco editor disposal (expected):", error); } editorRef.current = null; } // Clear Monaco reference monacoRef.current = null; setIsEditorReady(false); // Cleanup providers (0, providers_1.cleanupProviders)(); }, []); // Handle Monaco disposal errors gracefully - suppress instead of remounting (0, react_1.useEffect)(() => { const handleMonacoError = (event) => { const message = event.error?.message || ""; if (message.includes("InstantiationService has been disposed") || message.includes("domNode") || message.includes("renderText") || message.includes("AnimationFrameQueueItem")) { // Suppress Monaco cleanup errors - they're harmless during layout changes console.debug("Monaco cleanup error suppressed:", message); event.preventDefault(); event.stopPropagation(); return false; } return true; }; const handleUnhandledRejection = (event) => { const message = event.reason?.message || ""; if (message.includes("InstantiationService has been disposed") || message.includes("domNode") || message.includes("renderText")) { // Suppress Monaco cleanup errors in promises console.debug("Monaco cleanup promise error suppressed:", message); event.preventDefault(); return false; } return true; }; window.addEventListener("error", handleMonacoError, true); window.addEventListener("unhandledrejection", handleUnhandledRejection); return () => { window.removeEventListener("error", handleMonacoError, true); window.removeEventListener("unhandledrejection", handleUnhandledRejection); }; }, []); // Cleanup on unmount (0, react_1.useEffect)(() => { return () => { cleanupMonaco(); }; }, [cleanupMonaco]); // Track if component is mounted to prevent layout operations after unmount const isMountedRef = (0, react_1.useRef)(true); (0, react_1.useEffect)(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); const onMount = (0, react_1.useCallback)((editor, mon, t) => { editorRef.current = editor; monacoRef.current = mon; setIsEditorReady(true); // Wrap setModel to prevent multiple View creations during layout changes const originalSetModel = editor.setModel.bind(editor); editor.setModel = function (model) { const currentModel = editor.getModel(); // Only set model if it's actually different if (currentModel !== model) { try { originalSetModel(model); } catch (error) { if (!error.message?.includes("InstantiationService has been disposed")) { throw error; } console.debug("Suppressed setModel error during layout change"); } } }; // Safe layout wrapper - only layout if mounted const originalLayout = editor.layout.bind(editor); editor.layout = function (...args) { if (!isMountedRef.current) { console.debug("Skipped layout call on unmounted editor"); return; } try { originalLayout(...args); } catch (error) { if (!error.message?.includes("InstantiationService has been disposed") && !error.message?.includes("domNode")) { throw error; } console.debug("Suppressed layout error during cleanup"); } }; // Patch the editor's internal rendering to prevent domNode errors // This is a deep patch to prevent errors from bubbling up try { const editorInternal = editor._view; if (editorInternal && editorInternal._renderingCoordinator) { const coordinator = editorInternal._renderingCoordinator; const originalOnRenderScheduled = coordinator._onRenderScheduled; if (originalOnRenderScheduled) { coordinator._onRenderScheduled = function () { if (!isMountedRef.current || !editorRef.current) { console.debug("Skipped render on unmounted editor"); return; } try { originalOnRenderScheduled.call(this); } catch (error) { if (error.message?.includes("domNode") || error.message?.includes("renderText")) { console.debug("Suppressed Monaco rendering error:", error.message); return; } throw error; } }; } } } catch { console.debug("Could not patch Monaco rendering coordinator (non-critical)"); } // Monaco Editor markers will automatically show error tooltips on hover // No need for custom hover provider as it causes duplicates // Ensure theme is applied for proper error highlighting if (!isTestEnvironment && mon?.editor) { // Force theme application mon.editor.setTheme(theme || "vs-dark"); } let parseContentTO; let contentChangeTO; parseContent(t, script); editor.onDidChangeModelContent(() => { if (parseContentTO) clearTimeout(parseContentTO); parseContentTO = setTimeout(() => { parseContent(t, script); }, 0); if (!contentChangeTO) { if (setScript) { contentChangeTO = setTimeout(() => { setScript(editor.getValue()); contentChangeTO = undefined; }, 200); } } }); editor.onDidChangeCursorPosition((e) => { setCursor(prev => ({ ...prev, line: e.position.lineNumber, column: e.position.column })); }); editor.onDidChangeCursorSelection((e) => { const selection = e.selection; const length = editor?.getModel()?.getValueInRange(selection).length; setCursor(prev => ({ ...prev, selectionLength: length || 0 })); }); if (shortcuts) { Object.entries(shortcuts).forEach(([comboString, action]) => { comboString.split(",").forEach(combo => { const keys = combo.trim().toLowerCase().split("+"); let keyCode = null; let keyMod = 0; keys.forEach(k => { if (k === "ctrl") keyMod |= mon?.KeyMod?.CtrlCmd || 1; else if (k === "meta") keyMod |= mon?.KeyMod?.CtrlCmd || 1; else if (k === "shift") keyMod |= mon?.KeyMod?.Shift || 2; else if (k === "alt") keyMod |= mon?.KeyMod?.Alt || 4; else { const upper = k.length === 1 ? k.toUpperCase() : k; if (mon?.KeyCode && `Key${upper}` in mon.KeyCode) { keyCode = mon.KeyCode[`Key${upper}`]; } else if (mon?.KeyCode && upper in mon.KeyCode) { keyCode = mon.KeyCode[upper]; } else { keyCode = null; } } }); if (keyCode !== null) { editor.addCommand(keyMod | keyCode, (e) => { e?.preventDefault?.(); action(); }); } }); }); } editor.onKeyDown((e) => { const isMac = /Mac/.test(navigator.userAgent); const metaPressed = e.metaKey; const ctrlPressed = e.ctrlKey; if ((isMac && metaPressed && e.code === "Enter") || (!isMac && ctrlPressed && e.code === "Enter")) { e.preventDefault(); e.stopPropagation(); shortcuts["ctrl+enter, meta+enter"]?.(); } }); }, [script, shortcuts]); const parseContent = (0, react_1.useCallback)((t, str) => { const editor = editorRef.current; if (!editor) return; // Check if model exists before parsing const model = editor?.getModel(); if (!model) { console.debug("parseContent: model not ready yet"); return; } // Use provided string or get value from editor const content = str !== undefined ? str : editor.getValue(); const monacoErrors = (0, ParserFacade_1.validate)(t)(content).map(error => { return { startLineNumber: error.startLine, startColumn: error.startCol, endLineNumber: error.endLine, endColumn: error.endCol, message: error.message, severity: isTestEnvironment ? 1 : monacoRef.current?.editor?.MarkerSeverity?.Error || 8 }; }); if (!isTestEnvironment && monacoRef.current?.editor) { // Clear existing markers first monacoRef.current.editor.setModelMarkers(model, "owner", []); // Set new markers monacoRef.current.editor.setModelMarkers(model, "owner", monacoErrors); } if (onListErrors) { onListErrors(monacoErrors.map(error => { return { line: error.startLineNumber, column: error.startColumn, message: error.message }; })); } }, [onListErrors]); (0, react_1.useEffect)(() => { if (!Array.isArray(variablesInputURLs) || variablesInputURLs.length === 0) setReady(true); const f = customFetcher || fetch; if (variablesInputURLs && variablesInputURLs.length > 0 && !ready) { Promise.all(variablesInputURLs.map(v => f(v))) .then(res => Promise.all(res.map(r => r.json())).then(res => { const uniqueVars = (0, variables_1.buildUniqueVariables)(res); setVars(v => [...v, ...uniqueVars]); setReady(true); })) .catch(() => { setReady(true); }); } }, [variablesInputURLs]); (0, react_1.useEffect)(() => { if (isEditorReady) { parseContent(tools); } }, [tools.initialRule, isEditorReady, parseContent, tools]); const isDark = theme.includes("dark"); if (!ready) return null; const bannerHeight = displayFooter ? 22 : 0; return ((0, jsx_runtime_1.jsxs)("div", { style: { position: "relative", height, width }, children: [(0, jsx_runtime_1.jsx)("div", { style: { height: `calc(100% - ${bannerHeight}px)` }, children: isTestEnvironment ? ( // Test environment - render a simple textarea (0, jsx_runtime_1.jsx)("textarea", { "data-testid": "monaco-editor-mock", value: script || "", onChange: e => { setScript?.(e.target.value); // Simulate cursor position const textarea = e.target; const cursorPos = textarea.selectionStart; const lines = textarea.value.substring(0, cursorPos).split("\n"); setCursor({ line: lines.length, column: lines[lines.length - 1].length + 1, selectionLength: 0 }); }, style: { width: "100%", height: "100%", border: "1px solid #ccc", fontFamily: "monospace", fontSize: "14px", padding: "10px", resize: "none" }, placeholder: "Editor content (test mode)" })) : ( // Production environment - use Monaco Editor (0, jsx_runtime_1.jsx)(MonacoEditor, { value: script, height: "100%", width: "100%", onMount: (e, m) => { parseContent(tools, script); onMount(e, m, tools); (0, providers_1.getEditorWillMount)(tools)({ variables: vars, editor: e })(m); }, onChange: () => { if (isEditorReady) { parseContent(tools); } }, theme: theme, language: tools.id, options: options })) }), displayFooter && ((0, jsx_runtime_1.jsx)("div", { style: { position: "absolute", height: bannerHeight, width: "100%", bottom: 0, left: 0, gap: "12px", padding: "4px 8px", background: isDark ? "#1e1e1e" : "#f3f3f3", color: isDark ? "#ccc" : "#333", borderTop: `1px solid ${isDark ? "#333" : "#ccc"}`, zIndex: 10, boxSizing: "border-box" }, children: (0, jsx_runtime_1.jsx)(EditorFooter_1.default, { cursor: cursor, FooterComponent: FooterComponent }) }))] })); }; exports.default = Editor; //# sourceMappingURL=Editor.js.map