// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT import { EditorState } from "@codemirror/state"; import { highlightSelectionMatches } from "@codemirror/search"; import { indentWithTab, history, defaultKeymap, historyKeymap, } from "@codemirror/commands"; import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle, } from "@codemirror/language"; import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap, } from "@codemirror/autocomplete"; import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, EditorView, showPanel, } from "@codemirror/view"; // Theme import { dracula } from "@uiw/codemirror-theme-dracula"; // Language import { javascript } from "@codemirror/lang-javascript"; import { python } from "@codemirror/lang-python"; import { rust } from "@codemirror/lang-rust"; import { cpp } from "@codemirror/lang-cpp"; import { languageNameFacet } from "./language-facets"; const editor_url = "https://snapshots.slint.dev/master/editor/"; const wasm_url = "https://snapshots.slint.dev/master/wasm-interpreter/slint_wasm_interpreter.js"; let slint_wasm_module = null; // keep them alive var all_instances = new Array(); // Function to create the Copy button and add it to the panel function createCopyButton(view) { const button = document.createElement("button"); button.innerHTML = ` Copy `; button.style.marginRight = "10px"; button.onclick = () => { const content = view.state.doc.toString(); navigator.clipboard.writeText(content).then( () => { alert("Content copied to clipboard!"); }, (err) => { console.error("Could not copy text: ", err); }, ); }; return button; } // Function to create the Run/Preview button and add it to the panel function createRunButton(view) { const button = document.createElement("button"); button.innerHTML = ` Open in SlintPad `; button.onclick = () => { const content = view.state.doc.toString(); window.open( `${editor_url}?snippet=${encodeURIComponent(content)}`, "_blank", ); }; return button; } // Define the status panel with copy and run buttons function statusPanel(view) { const dom = document.createElement("div"); dom.className = "cm-status-panel"; // Add the buttons to the panel const copyButton = createCopyButton(view); dom.appendChild(copyButton); const language = view.state.facet(languageNameFacet); if (language === "slint") { const runButton = createRunButton(view); dom.appendChild(runButton); } return { dom, update(update) { // You can update the panel content based on editor state changes if needed }, }; } // Debounce function to limit how often updates are made function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } async function updateWasmPreview(previewContainer, content) { const { component, error_string } = await slint_wasm_module.compile_from_string(content, ""); var error_div = previewContainer.parentNode.querySelector(".error-status"); if (error_string !== "") { var text = document.createTextNode(error_string); var p = document.createElement("pre"); p.appendChild(text); error_div.innerHTML = "
" +
            p.innerHTML +
            "
"; } else { error_div.innerHTML = ""; } if (component !== undefined) { const canvas_id = previewContainer.getAttribute("data-canvas-id"); const instance = await component.create(canvas_id); await instance.show(); all_instances.push(instance); } } // Wrap updateWasmPreview in a debounce function (500ms delay) const debouncedUpdateWasmPreview = debounce(updateWasmPreview, 500); async function initializePreviewContainers(previewContainer, content) { const canvas_id = "canvas_" + Math.random().toString(36).substr(2, 9); const canvas = document.createElement("canvas"); canvas.id = canvas_id; previewContainer.appendChild(canvas); previewContainer.setAttribute("data-canvas-id", `${canvas_id}`); const error_div = document.createElement("div"); error_div.classList.add("error-status"); previewContainer.parentNode.appendChild(error_div); } async function loadSlintWasmInterpreter(editor) { try { if (slint_wasm_module) { return; } // Dynamically import the Slint WASM module slint_wasm_module = await import(wasm_url); await slint_wasm_module.default(); // Wait for WASM to initialize try { slint_wasm_module.run_event_loop(); // Run the event loop, which will trigger an exception } catch (e) { // Swallow the expected JavaScript exception that breaks out of Rust's event loop } return; } catch (error) { console.error( "Error during Slint WASM interpreter initialization:", error, ); throw error; // Re-throw error to handle it in the calling context } } // Initialize CodeMirror based on the language passed as a data attribute window.initCodeMirror = function (editorDiv, language, content) { const editorDiv_id = editorDiv.getAttribute("id"); const extensions = [ lineNumbers(), highlightActiveLineGutter(), highlightSpecialChars(), history(), foldGutter(), drawSelection(), indentUnit.of(" "), EditorState.allowMultipleSelections.of(true), indentOnInput(), bracketMatching(), closeBrackets(), autocompletion(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), keymap.of([ indentWithTab, ...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ]), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), languageNameFacet.of(language), dracula, showPanel.of(statusPanel), ]; // Get the appropriate language extension let isReadOnly = true; let previewContainer; switch (language.toLowerCase()) { case "javascript": extensions.push(javascript()); break; case "python": extensions.push(python()); break; case "cpp": extensions.push(cpp()); break; case "rust": extensions.push(rust()); break; case "slint": isReadOnly = false; extensions.push(javascript()); if ( editorDiv.getAttribute("data-readonly") === "true" || editorDiv.getAttribute("data-ignore") === "true" ) { break; } previewContainer = document.createElement("div"); previewContainer.classList.add("preview-container"); editorDiv.classList.add("show-preview"); extensions.push( EditorView.updateListener.of(async (editor) => { if (editor.docChanged) { const newContent = editor.state.doc.toString(); debouncedUpdateWasmPreview( previewContainer, newContent, ); } }), ); break; default: } extensions.push(EditorView.editable.of(!isReadOnly)); const editor = new EditorView({ state: EditorState.create({ doc: content, extensions: extensions, }), parent: editorDiv, }); if (previewContainer) { editorDiv.append(previewContainer); loadSlintWasmInterpreter(editor) .then(async () => { initializePreviewContainers(previewContainer, content); updateWasmPreview(previewContainer, content); }) .catch((error) => { console.error("Error loading Slint WASM interpreter:", error); }); } }; document.addEventListener("DOMContentLoaded", async function () { // Find all the divs that need a CodeMirror editor document .querySelectorAll(".codemirror-editor") .forEach(function (editorDiv) { const editorContent = editorDiv.querySelector( ".codemirror-content", ); const language = editorDiv.getAttribute("data-lang"); const content = editorContent.textContent.trim(); window.initCodeMirror(editorDiv, language, content); }); });