This commit is contained in:
2025-10-29 21:14:44 -07:00
parent 667624d0ca
commit 965253386a
13 changed files with 146 additions and 58 deletions

View File

@@ -205,7 +205,7 @@ fn token(stream: codemirror::StringStream, state: &mut State) -> Result<Option<S
if state.is_defining_identifier { if state.is_defining_identifier {
"def" "def"
} else { } else {
"variable" "identifier"
} }
} }
rhai::Token::CharConstant(_) => "string-2", rhai::Token::CharConstant(_) => "string-2",

View File

@@ -7,6 +7,7 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"codemirror": "^5.65.1", "codemirror": "^5.65.1",
"lucide-react": "^0.548.0",
"next": "14.0.0", "next": "14.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@@ -477,6 +478,8 @@
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lucide-react": ["lucide-react@0.548.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],

View File

@@ -13,6 +13,7 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"codemirror": "^5.65.1", "codemirror": "^5.65.1",
"lucide-react": "^0.548.0",
"next": "14.0.0", "next": "14.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0" "react-dom": "18.2.0"

View File

@@ -2,7 +2,7 @@ import "@/styles/globals.css";
import { Metadata } from "next"; import { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Rhai Playground", title: "Minimax",
description: "An interactive Rhai scripting language playground", description: "An interactive Rhai scripting language playground",
}; };
@@ -18,12 +18,6 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<head>
<link
rel="stylesheet"
href="https://cdn.materialdesignicons.com/5.3.45/css/materialdesignicons.min.css"
/>
</head>
<body>{children}</body> <body>{children}</body>
</html> </html>
); );

View File

@@ -2,6 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { loadAllWasm } from "@/utils/wasmLoader";
const Playground = dynamic(() => import("@/components/Playground"), { const Playground = dynamic(() => import("@/components/Playground"), {
ssr: false, ssr: false,
@@ -20,14 +21,7 @@ const Playground = dynamic(() => import("@/components/Playground"), {
}} }}
> >
<div> <div>
Loading{" "} Loading WASM...
<a
href="https://github.com/rhaiscript/playground"
target="_blank"
>
Rhai Playground
</a>
...
</div> </div>
</div> </div>
), ),
@@ -35,12 +29,24 @@ const Playground = dynamic(() => import("@/components/Playground"), {
export default function Home() { export default function Home() {
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const [isWasmLoaded, setIsWasmLoaded] = useState(false);
useEffect(() => { useEffect(() => {
setIsClient(true); setIsClient(true);
// Load all WASM modules
loadAllWasm()
.then(() => {
setIsWasmLoaded(true);
})
.catch((error) => {
console.error('Failed to load WASM modules:', error);
// Still allow the app to load, but WASM features may not work
setIsWasmLoaded(true);
});
}, []); }, []);
if (!isClient) { if (!isClient || !isWasmLoaded) {
return ( return (
<div <div
id="loading" id="loading"

View File

@@ -14,7 +14,7 @@ if (typeof window !== "undefined") {
CodeMirror = cm.default; CodeMirror = cm.default;
await import("codemirror/addon/edit/matchbrackets"); await import("codemirror/addon/edit/matchbrackets");
await import("codemirror/addon/edit/closebrackets"); await import("codemirror/addon/edit/closebrackets");
await import("codemirror/addon/selection/active-line"); //await import("codemirror/addon/selection/active-line");
await import("codemirror/addon/comment/comment"); await import("codemirror/addon/comment/comment");
// @ts-ignore - CodeMirror addon type issues // @ts-ignore - CodeMirror addon type issues
await import("codemirror/addon/fold/brace-fold"); await import("codemirror/addon/fold/brace-fold");
@@ -23,25 +23,12 @@ if (typeof window !== "undefined") {
// @ts-ignore - CodeMirror addon type issues // @ts-ignore - CodeMirror addon type issues
await import("codemirror/addon/search/match-highlighter"); await import("codemirror/addon/search/match-highlighter");
require("codemirror/lib/codemirror.css"); require("codemirror/lib/codemirror.css");
require("codemirror/theme/monokai.css"); require("codemirror/theme/material-darker.css");
require("codemirror/addon/fold/foldgutter.css"); require("codemirror/addon/fold/foldgutter.css");
try {
await loadRhaiWasm(); await loadRhaiWasm();
initRhaiMode(CodeMirror); initRhaiMode(CodeMirror);
console.log('✅ WASM-based Rhai mode initialized successfully'); console.log('✅ WASM-based Rhai mode initialized successfully');
} catch (error) {
console.warn('⚠️ Failed to load WASM Rhai mode, falling back to JavaScript mode:', error);
// Fallback to JavaScript mode if WASM fails
CodeMirror.defineMode("rhai", (config: any) => {
const jsMode = CodeMirror.getMode(config, "javascript");
return {
...jsMode,
name: "rhai",
helperType: "rhai"
};
});
}
isCodeMirrorReady = true; isCodeMirrorReady = true;
}) })
@@ -74,7 +61,7 @@ export const Editor = forwardRef<any, EditorProps>(function Editor(
const editor = CodeMirror.fromTextArea(textareaRef.current, { const editor = CodeMirror.fromTextArea(textareaRef.current, {
lineNumbers: true, lineNumbers: true,
mode: "rhai", mode: "rhai",
theme: "monokai", theme: "material-darker",
indentUnit: 4, indentUnit: 4,
matchBrackets: true, matchBrackets: true,
foldGutter: { foldGutter: {
@@ -113,7 +100,7 @@ export const Editor = forwardRef<any, EditorProps>(function Editor(
editorRef.current = null; editorRef.current = null;
} }
}; };
}, []); // Only run once }, []); // DO NOT FILL ARRAY
return ( return (
<div className={styles.editorContainer}> <div className={styles.editorContainer}>

View File

@@ -2,8 +2,19 @@
import { ButtonHTMLAttributes, ReactNode } from "react"; import { ButtonHTMLAttributes, ReactNode } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { Play, Square, Loader2 } from "lucide-react";
import styles from "@/styles/Button.module.css"; import styles from "@/styles/Button.module.css";
const iconMap = {
play: Play,
stop: Square,
loading: Loader2,
};
function getIcon(iconName: string) {
return iconMap[iconName as keyof typeof iconMap];
}
interface ButtonProps interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type"> { extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
variant?: variant?:
@@ -47,10 +58,16 @@ export function Button({
title={tooltip} title={tooltip}
{...props} {...props}
> >
{iconLeft && !loading && <i className={`mdi mdi-${iconLeft}`} />} {iconLeft && !loading && (() => {
{loading && <i className="mdi mdi-loading mdi-spin" />} const IconComponent = getIcon(iconLeft);
return IconComponent ? <IconComponent size={16} /> : null;
})()}
{loading && <Loader2 size={16} className={styles.spin} />}
<span>{children}</span> <span>{children}</span>
{iconRight && !loading && <i className={`mdi mdi-${iconRight}`} />} {iconRight && !loading && (() => {
const IconComponent = getIcon(iconRight);
return IconComponent ? <IconComponent size={16} /> : null;
})()}
</button> </button>
); );
} }

View File

@@ -2,8 +2,19 @@
import { useState, useRef, useEffect, ReactNode } from "react"; import { useState, useRef, useEffect, ReactNode } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { HelpCircle, MoreHorizontal, ChevronDown } from "lucide-react";
import styles from "@/styles/Dropdown.module.css"; import styles from "@/styles/Dropdown.module.css";
const iconMap = {
"help-circle": HelpCircle,
"dots-horizontal": MoreHorizontal,
"menu-down": ChevronDown,
};
function getIcon(iconName: string) {
return iconMap[iconName as keyof typeof iconMap];
}
interface DropdownItem { interface DropdownItem {
text: string; text: string;
onClick: () => void; onClick: () => void;
@@ -59,12 +70,13 @@ export function Dropdown({
onClick={() => !disabled && setIsOpen(!isOpen)} onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled} disabled={disabled}
> >
{triggerIcon && <i className={`mdi mdi-${triggerIcon}`} />} {triggerIcon && (() => {
const IconComponent = getIcon(triggerIcon);
return IconComponent ? <IconComponent size={16} /> : null;
})()}
{trigger && <span>{trigger}</span>} {trigger && <span>{trigger}</span>}
{!triggerIcon && !trigger && ( {!triggerIcon && !trigger && <MoreHorizontal size={16} />}
<i className="mdi mdi-dots-horizontal" /> <ChevronDown size={16} />
)}
<i className="mdi mdi-menu-down" />
</button> </button>
{isOpen && ( {isOpen && (

View File

@@ -22,7 +22,14 @@ async function initWasm(): Promise<void> {
self.onmessage = async (event) => { self.onmessage = async (event) => {
const { type, script } = event.data; const { type, script } = event.data;
if (type === "run") { if (type === "init") {
try {
await initWasm();
self.postMessage({ type: "ready" });
} catch (error) {
self.postMessage({ type: "error", error: String(error) });
}
} else if (type === "run") {
try { try {
await initWasm(); await initWasm();

View File

@@ -39,6 +39,19 @@
cursor: wait; cursor: wait;
} }
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Button types */ /* Button types */
.is-primary { .is-primary {
background: #007bff; background: #007bff;

View File

@@ -6,7 +6,7 @@
.editorContainer textarea { .editorContainer textarea {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 1px solid #ccc; border: none;
font-family: "Consolas", "Monaco", "Courier New", monospace; font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
@@ -18,7 +18,7 @@
/* Enhanced CodeMirror styles from playground */ /* Enhanced CodeMirror styles from playground */
.editorContainer :global(.CodeMirror) { .editorContainer :global(.CodeMirror) {
border: 1px solid #ccc; border: none;
height: 100% !important; height: 100% !important;
box-sizing: border-box; box-sizing: border-box;
font-size: 0.95em; font-size: 0.95em;

View File

@@ -71,6 +71,8 @@
.terminalContainer { .terminalContainer {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
padding: 8px;
background: #1D1F21;
} }
.result { .result {

View File

@@ -3,12 +3,17 @@ import init, { RhaiMode, init_codemirror_pass } from '@/wasm/rhai-codemirror/rha
let wasmInitialized = false; let wasmInitialized = false;
let wasmModule: any = null; let wasmModule: any = null;
let wasmLoadPromise: Promise<any> | null = null;
export const loadRhaiWasm = async () => { export const loadRhaiWasm = async () => {
if (wasmInitialized) { if (wasmInitialized) {
return wasmModule; return wasmModule;
} }
if (wasmLoadPromise) {
return wasmLoadPromise;
}
wasmLoadPromise = (async () => {
try { try {
// Initialize the WASM module // Initialize the WASM module
wasmModule = await init(); wasmModule = await init();
@@ -17,8 +22,12 @@ export const loadRhaiWasm = async () => {
return wasmModule; return wasmModule;
} catch (error) { } catch (error) {
console.error('Failed to load Rhai WASM module:', error); console.error('Failed to load Rhai WASM module:', error);
wasmLoadPromise = null; // Reset on error
throw error; throw error;
} }
})();
return wasmLoadPromise;
}; };
export const initRhaiMode = (CodeMirror: any) => { export const initRhaiMode = (CodeMirror: any) => {
@@ -35,4 +44,41 @@ export const initRhaiMode = (CodeMirror: any) => {
}); });
}; };
// Function to preload all WASM modules used by the application
export const loadAllWasm = async (): Promise<void> => {
try {
// Load Rhai CodeMirror WASM
await loadRhaiWasm();
// Load Script Runner WASM by creating and immediately terminating a worker
const worker = new Worker(new URL('../lib/script-runner.worker.ts', import.meta.url));
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
worker.terminate();
reject(new Error('Script runner WASM load timeout'));
}, 10000);
worker.postMessage({ type: 'init' });
worker.onmessage = (event) => {
if (event.data.type === 'ready') {
clearTimeout(timeout);
worker.terminate();
resolve();
}
};
worker.onerror = (error) => {
clearTimeout(timeout);
worker.terminate();
reject(error);
};
});
console.log('✅ All WASM modules loaded successfully');
} catch (error) {
console.error('❌ Failed to load WASM modules:', error);
throw error;
}
};
export { RhaiMode }; export { RhaiMode };