@making-sense/antlr-editor
Version:
ANTLR Typescript editor
407 lines • 18.3 kB
JavaScript
;
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