editor
This commit is contained in:
90
build-wasm.sh
Normal file
90
build-wasm.sh
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build script for all WASM libraries in the MMX repository
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}🚀 Building all WASM libraries for MMX${NC}"
|
||||||
|
|
||||||
|
# Set PATH to include cargo
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
# Check if wasm-pack is installed
|
||||||
|
if ! command -v wasm-pack &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ wasm-pack not found. Please install it first:${NC}"
|
||||||
|
echo "curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Change to rust directory
|
||||||
|
cd rust
|
||||||
|
|
||||||
|
echo -e "${YELLOW}📋 Found WASM crates to build:${NC}"
|
||||||
|
|
||||||
|
# List all WASM crates (crates with crate-type = ["cdylib"])
|
||||||
|
wasm_crates=()
|
||||||
|
for dir in */; do
|
||||||
|
if [[ -f "$dir/Cargo.toml" ]]; then
|
||||||
|
if grep -q 'crate-type.*=.*\["cdylib"\]' "$dir/Cargo.toml"; then
|
||||||
|
wasm_crates+=("${dir%/}")
|
||||||
|
echo -e " • ${dir%/}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#wasm_crates[@]} -eq 0 ]]; then
|
||||||
|
echo -e "${YELLOW}⚠️ No WASM crates found${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build each WASM crate
|
||||||
|
for crate in "${wasm_crates[@]}"; do
|
||||||
|
echo -e "${BLUE}🔨 Building $crate...${NC}"
|
||||||
|
|
||||||
|
cd "$crate"
|
||||||
|
|
||||||
|
# Determine output directory based on crate name
|
||||||
|
case "$crate" in
|
||||||
|
"rhai-codemirror")
|
||||||
|
output_dir="../../webui/src/wasm/rhai-codemirror"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
output_dir="../../webui/src/wasm/$crate"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Build with wasm-pack
|
||||||
|
if wasm-pack build --target web --out-dir "$output_dir"; then
|
||||||
|
echo -e "${GREEN}✅ Successfully built $crate${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Failed to build $crate${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GREEN}🎉 All WASM libraries built successfully!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📦 Built libraries:${NC}"
|
||||||
|
for crate in "${wasm_crates[@]}"; do
|
||||||
|
case "$crate" in
|
||||||
|
"rhai-codemirror")
|
||||||
|
output_dir="../webui/src/wasm/rhai-codemirror"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
output_dir="../webui/src/wasm/$crate"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo -e " • $crate → $output_dir"
|
||||||
|
done
|
||||||
14
rust/Cargo.lock
generated
14
rust/Cargo.lock
generated
@@ -314,6 +314,20 @@ dependencies = [
|
|||||||
"thin-vec",
|
"thin-vec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai-codemirror"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"wee_alloc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rhai_codegen"
|
name = "rhai_codegen"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["script-runner", "minimax"]
|
members = ["script-runner", "minimax", "rhai-codemirror"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
22
rust/rhai-codemirror/Cargo.toml
Normal file
22
rust/rhai-codemirror/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "rhai-codemirror"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
opt-level = 's'
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rhai = { workspace = true, features = ["internals", "wasm-bindgen"] }
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
|
js-sys = { workspace = true }
|
||||||
|
web-sys = { workspace = true, features = ["console"] }
|
||||||
|
console_error_panic_hook = { workspace = true }
|
||||||
|
wee_alloc = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde-wasm-bindgen = { workspace = true }
|
||||||
64
rust/rhai-codemirror/src/codemirror.rs
Normal file
64
rust/rhai-codemirror/src/codemirror.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use js_sys::{Array, JsString, RegExp};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen(module = "codemirror")]
|
||||||
|
extern "C" {
|
||||||
|
pub type StringStream;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn eol(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn sol(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn peek(this: &StringStream) -> JsString;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn next(this: &StringStream) -> JsString;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eat)]
|
||||||
|
pub fn eat_regexp(this: &StringStream, m: &RegExp) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eatWhile)]
|
||||||
|
pub fn eat_while_regexp(this: &StringStream, m: &RegExp) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eatSpace)]
|
||||||
|
pub fn eat_space(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = skipToEnd)]
|
||||||
|
pub fn skip_to_end(this: &StringStream);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = skipTo)]
|
||||||
|
pub fn skip_to(this: &StringStream, str: &str) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = match)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_str(this: &StringStream, pattern: &str, consume: bool, case_fold: bool) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = match)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_regexp(this: &StringStream, pattern: &RegExp, consume: bool) -> Array;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = backUp)]
|
||||||
|
pub fn back_up(this: &StringStream, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn column(this: &StringStream) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn indentation(this: &StringStream) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn current(this: &StringStream) -> String;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = lookAhead)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn look_ahead(this: &StringStream, n: u32) -> Option<String>;
|
||||||
|
}
|
||||||
20
rust/rhai-codemirror/src/lib.rs
Normal file
20
rust/rhai-codemirror/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod codemirror;
|
||||||
|
mod rhai_mode;
|
||||||
|
|
||||||
|
pub use rhai_mode::*;
|
||||||
|
|
||||||
|
// Use `wee_alloc` as the global allocator for smaller WASM size.
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn main_js() -> Result<(), JsValue> {
|
||||||
|
// This provides better error messages in debug mode.
|
||||||
|
// It's disabled in release mode so it doesn't bloat up the file size.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
353
rust/rhai-codemirror/src/rhai_mode.rs
Normal file
353
rust/rhai-codemirror/src/rhai_mode.rs
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
use crate::codemirror;
|
||||||
|
use js_sys::RegExp;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::console;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct RhaiMode {
|
||||||
|
indent_unit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct State {
|
||||||
|
token_state: rhai::TokenizeState,
|
||||||
|
unclosed_bracket_count: i32,
|
||||||
|
line_indent: u32,
|
||||||
|
is_defining_identifier: bool,
|
||||||
|
/// Buffered character, if any. (For use by `StreamAdapter`.)
|
||||||
|
buf: Option<char>,
|
||||||
|
/// Interpolated string brace counting stack
|
||||||
|
interpolated_str_brace_stack: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ELECTRIC_INPUT: RegExp = RegExp::new("^\\s*[}\\])]$", "");
|
||||||
|
static LINE_COMMENT: JsValue = JsValue::from_str("//");
|
||||||
|
static CODEMIRROR_PASS: RefCell<JsValue> = RefCell::new(JsValue::null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn init_codemirror_pass(codemirror_pass: JsValue) {
|
||||||
|
CODEMIRROR_PASS.with(|v| v.replace(codemirror_pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl RhaiMode {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(indent_unit: u32) -> Self {
|
||||||
|
Self { indent_unit }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = startState)]
|
||||||
|
pub fn start_state(&self) -> State {
|
||||||
|
State {
|
||||||
|
token_state: rhai::TokenizeState {
|
||||||
|
include_comments: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
unclosed_bracket_count: 0,
|
||||||
|
line_indent: 0,
|
||||||
|
is_defining_identifier: false,
|
||||||
|
buf: None,
|
||||||
|
interpolated_str_brace_stack: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = copyState)]
|
||||||
|
pub fn copy_state(&self, state: &State) -> State {
|
||||||
|
state.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(
|
||||||
|
&self,
|
||||||
|
stream: codemirror::StringStream,
|
||||||
|
state: &mut State,
|
||||||
|
) -> Result<Option<String>, JsValue> {
|
||||||
|
token(stream, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indent(&self, state: &mut State, text_after: String) -> JsValue {
|
||||||
|
indent(self, state, text_after)
|
||||||
|
.map(JsValue::from)
|
||||||
|
.unwrap_or_else(|| CODEMIRROR_PASS.with(|v| v.borrow().clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter, js_name = electricInput)]
|
||||||
|
pub fn electric_input(&self) -> RegExp {
|
||||||
|
ELECTRIC_INPUT.with(|v| v.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter, js_name = lineComment)]
|
||||||
|
pub fn line_comment(&self) -> JsValue {
|
||||||
|
LINE_COMMENT.with(|v| v.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StreamAdapter {
|
||||||
|
/// Buffered character, if any.
|
||||||
|
buf: Option<char>,
|
||||||
|
stream: codemirror::StringStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rhai::InputStream for StreamAdapter {
|
||||||
|
fn unget(&mut self, ch: char) {
|
||||||
|
self.buf = Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next(&mut self) -> Option<char> {
|
||||||
|
if let Some(ch) = self.buf.take() {
|
||||||
|
return Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = self.stream.next();
|
||||||
|
if first.is_falsy() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert_eq!(first.length(), 1);
|
||||||
|
let first_code_unit = first.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) = std::char::decode_utf16(std::iter::once(first_code_unit)).next() {
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// The first value is likely an unpared surrogate, so we get one
|
||||||
|
// more UTF-16 unit to attempt to make a proper Unicode scalar.
|
||||||
|
let second = self.stream.next();
|
||||||
|
if second.is_falsy() {
|
||||||
|
return Some(std::char::REPLACEMENT_CHARACTER);
|
||||||
|
}
|
||||||
|
assert_eq!(second.length(), 1);
|
||||||
|
let second_code_unit = second.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) =
|
||||||
|
std::char::decode_utf16([first_code_unit, second_code_unit].iter().copied()).next()
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// Turns out to not be a proper surrogate pair, so back up one
|
||||||
|
// unit for it to be decoded separately.
|
||||||
|
self.stream.back_up(1);
|
||||||
|
Some(std::char::REPLACEMENT_CHARACTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_next(&mut self) -> Option<char> {
|
||||||
|
if let Some(ch) = self.buf {
|
||||||
|
return Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = self.stream.peek();
|
||||||
|
if first.is_falsy() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert_eq!(first.length(), 1);
|
||||||
|
let first_code_unit = first.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) = std::char::decode_utf16(std::iter::once(first_code_unit)).next() {
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// The first value is likely an unpared surrogate, so we get one more
|
||||||
|
// value to attempt to make a proper Unicode scalar value.
|
||||||
|
self.stream.next();
|
||||||
|
let second = self.stream.peek();
|
||||||
|
if second.is_falsy() {
|
||||||
|
return Some(std::char::REPLACEMENT_CHARACTER);
|
||||||
|
}
|
||||||
|
self.stream.back_up(1);
|
||||||
|
assert_eq!(second.length(), 1);
|
||||||
|
let second_code_unit = second.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) =
|
||||||
|
std::char::decode_utf16([first_code_unit, second_code_unit].iter().copied()).next()
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
Some(std::char::REPLACEMENT_CHARACTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token(stream: codemirror::StringStream, state: &mut State) -> Result<Option<String>, JsValue> {
|
||||||
|
if stream.sol() {
|
||||||
|
state.line_indent = stream.indentation();
|
||||||
|
state.unclosed_bracket_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream_adapter = StreamAdapter {
|
||||||
|
stream,
|
||||||
|
buf: state.buf,
|
||||||
|
};
|
||||||
|
let (next_token, _) = rhai::get_next_token(
|
||||||
|
&mut stream_adapter,
|
||||||
|
&mut state.token_state,
|
||||||
|
&mut rhai::Position::default(),
|
||||||
|
);
|
||||||
|
state.buf = stream_adapter.buf;
|
||||||
|
match &next_token {
|
||||||
|
rhai::Token::LeftBrace
|
||||||
|
| rhai::Token::LeftBracket
|
||||||
|
| rhai::Token::LeftParen
|
||||||
|
| rhai::Token::MapStart => {
|
||||||
|
if state.unclosed_bracket_count < 0 {
|
||||||
|
state.unclosed_bracket_count = 0;
|
||||||
|
}
|
||||||
|
state.unclosed_bracket_count += 1;
|
||||||
|
}
|
||||||
|
rhai::Token::RightBrace | rhai::Token::RightBracket | rhai::Token::RightParen => {
|
||||||
|
state.unclosed_bracket_count -= 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
let res = match &next_token {
|
||||||
|
rhai::Token::IntegerConstant(_) => "number",
|
||||||
|
rhai::Token::FloatConstant(_) => "number",
|
||||||
|
rhai::Token::Identifier(_) => {
|
||||||
|
if state.is_defining_identifier {
|
||||||
|
"def"
|
||||||
|
} else {
|
||||||
|
"variable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rhai::Token::CharConstant(_) => "string-2",
|
||||||
|
rhai::Token::StringConstant(_) => "string",
|
||||||
|
rhai::Token::InterpolatedString(_) => {
|
||||||
|
state.interpolated_str_brace_stack.push(0);
|
||||||
|
"string"
|
||||||
|
}
|
||||||
|
rhai::Token::LeftBrace => {
|
||||||
|
if let Some(brace_counting) = state.interpolated_str_brace_stack.last_mut() {
|
||||||
|
*brace_counting += 1;
|
||||||
|
}
|
||||||
|
"bracket"
|
||||||
|
}
|
||||||
|
rhai::Token::RightBrace => {
|
||||||
|
if let Some(brace_counting) = state.interpolated_str_brace_stack.last_mut() {
|
||||||
|
*brace_counting -= 1;
|
||||||
|
if *brace_counting == 0 {
|
||||||
|
state.interpolated_str_brace_stack.pop();
|
||||||
|
state.token_state.is_within_text_terminated_by = Some("`".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"bracket"
|
||||||
|
}
|
||||||
|
rhai::Token::LeftParen => "bracket",
|
||||||
|
rhai::Token::RightParen => "bracket",
|
||||||
|
rhai::Token::LeftBracket => "bracket",
|
||||||
|
rhai::Token::RightBracket => "bracket",
|
||||||
|
rhai::Token::QuestionBracket => "bracket",
|
||||||
|
rhai::Token::Unit => "bracket", // empty fn parens are parsed as this
|
||||||
|
rhai::Token::Plus => "operator",
|
||||||
|
rhai::Token::UnaryPlus => "operator",
|
||||||
|
rhai::Token::Minus => "operator",
|
||||||
|
rhai::Token::UnaryMinus => "operator",
|
||||||
|
rhai::Token::Multiply => "operator",
|
||||||
|
rhai::Token::Divide => "operator",
|
||||||
|
rhai::Token::Modulo => "operator",
|
||||||
|
rhai::Token::PowerOf => "operator",
|
||||||
|
rhai::Token::LeftShift => "operator",
|
||||||
|
rhai::Token::RightShift => "operator",
|
||||||
|
rhai::Token::SemiColon => "operator",
|
||||||
|
rhai::Token::Colon => "operator",
|
||||||
|
rhai::Token::DoubleColon => "operator",
|
||||||
|
rhai::Token::Comma => "operator",
|
||||||
|
rhai::Token::Period => "operator",
|
||||||
|
rhai::Token::ExclusiveRange => "operator",
|
||||||
|
rhai::Token::InclusiveRange => "operator",
|
||||||
|
rhai::Token::MapStart => "bracket",
|
||||||
|
rhai::Token::Equals => "operator",
|
||||||
|
rhai::Token::True => "builtin",
|
||||||
|
rhai::Token::False => "builtin",
|
||||||
|
rhai::Token::Let => "keyword",
|
||||||
|
rhai::Token::Const => "keyword",
|
||||||
|
rhai::Token::If => "keyword",
|
||||||
|
rhai::Token::Else => "keyword",
|
||||||
|
rhai::Token::While => "keyword",
|
||||||
|
rhai::Token::Loop => "keyword",
|
||||||
|
rhai::Token::For => "keyword",
|
||||||
|
rhai::Token::In => "keyword",
|
||||||
|
rhai::Token::NotIn => "keyword",
|
||||||
|
rhai::Token::LessThan => "operator",
|
||||||
|
rhai::Token::GreaterThan => "operator",
|
||||||
|
rhai::Token::LessThanEqualsTo => "operator",
|
||||||
|
rhai::Token::GreaterThanEqualsTo => "operator",
|
||||||
|
rhai::Token::EqualsTo => "operator",
|
||||||
|
rhai::Token::NotEqualsTo => "operator",
|
||||||
|
rhai::Token::Bang => "operator",
|
||||||
|
rhai::Token::Elvis => "operator",
|
||||||
|
rhai::Token::DoubleQuestion => "operator",
|
||||||
|
rhai::Token::Pipe => "operator",
|
||||||
|
rhai::Token::Or => "operator",
|
||||||
|
rhai::Token::XOr => "operator",
|
||||||
|
rhai::Token::Ampersand => "operator",
|
||||||
|
rhai::Token::And => "operator",
|
||||||
|
rhai::Token::Fn => "keyword",
|
||||||
|
rhai::Token::Continue => "keyword",
|
||||||
|
rhai::Token::Break => "keyword",
|
||||||
|
rhai::Token::Return => "keyword",
|
||||||
|
rhai::Token::Throw => "keyword",
|
||||||
|
rhai::Token::PlusAssign => "operator",
|
||||||
|
rhai::Token::MinusAssign => "operator",
|
||||||
|
rhai::Token::MultiplyAssign => "operator",
|
||||||
|
rhai::Token::DivideAssign => "operator",
|
||||||
|
rhai::Token::LeftShiftAssign => "operator",
|
||||||
|
rhai::Token::RightShiftAssign => "operator",
|
||||||
|
rhai::Token::AndAssign => "operator",
|
||||||
|
rhai::Token::OrAssign => "operator",
|
||||||
|
rhai::Token::XOrAssign => "operator",
|
||||||
|
rhai::Token::ModuloAssign => "operator",
|
||||||
|
rhai::Token::PowerOfAssign => "operator",
|
||||||
|
rhai::Token::Private => "keyword",
|
||||||
|
// Import/Export/As tokens not available in this Rhai version
|
||||||
|
rhai::Token::DoubleArrow => "operator",
|
||||||
|
rhai::Token::Underscore => "operator",
|
||||||
|
rhai::Token::Switch => "keyword",
|
||||||
|
rhai::Token::Do => "keyword",
|
||||||
|
rhai::Token::Until => "keyword",
|
||||||
|
rhai::Token::Try => "keyword",
|
||||||
|
rhai::Token::Catch => "keyword",
|
||||||
|
rhai::Token::Comment(_) => "comment",
|
||||||
|
rhai::Token::LexError(e) => {
|
||||||
|
console::log_1(&JsValue::from_str(&format!("LexError: {}", e)));
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
rhai::Token::Reserved(_) => "keyword",
|
||||||
|
// Custom token not available in this Rhai version
|
||||||
|
rhai::Token::EOF => return Ok(None),
|
||||||
|
token @ _ => {
|
||||||
|
console::log_1(&JsValue::from_str(&format!("Unhandled token {:?}", token)));
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &next_token {
|
||||||
|
rhai::Token::Fn | rhai::Token::Let | rhai::Token::Const | rhai::Token::For => {
|
||||||
|
state.is_defining_identifier = true;
|
||||||
|
}
|
||||||
|
rhai::Token::Comment(_) => {}
|
||||||
|
_ => {
|
||||||
|
state.is_defining_identifier = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(res.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indent(mode: &RhaiMode, state: &State, text_after: String) -> Option<u32> {
|
||||||
|
let should_dedent = || {
|
||||||
|
text_after
|
||||||
|
.trim_start()
|
||||||
|
.starts_with(['}', ']', ')'].as_ref())
|
||||||
|
};
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if state.unclosed_bracket_count > 0 {
|
||||||
|
if should_dedent() {
|
||||||
|
Some(state.line_indent)
|
||||||
|
} else {
|
||||||
|
Some(state.line_indent + mode.indent_unit)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if should_dedent() {
|
||||||
|
Some(state.line_indent.saturating_sub(mode.indent_unit))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,19 +2,51 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, forwardRef, useImperativeHandle, useState } from "react";
|
import { useEffect, useRef, forwardRef, useImperativeHandle, useState } from "react";
|
||||||
import styles from "@/styles/Editor.module.css";
|
import styles from "@/styles/Editor.module.css";
|
||||||
|
import { loadRhaiWasm, initRhaiMode } from "@/utils/wasmLoader";
|
||||||
|
|
||||||
// Dynamic import for CodeMirror to avoid SSR issues
|
// Dynamic import for CodeMirror to avoid SSR issues
|
||||||
let CodeMirror: any = null;
|
let CodeMirror: any = null;
|
||||||
|
let isCodeMirrorReady = false;
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
import("codemirror")
|
import("codemirror")
|
||||||
.then(async (cm) => {
|
.then(async (cm) => {
|
||||||
CodeMirror = cm.default;
|
CodeMirror = cm.default;
|
||||||
await import("codemirror/mode/javascript/javascript");
|
|
||||||
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/lib/codemirror.css");
|
await import("codemirror/addon/comment/comment");
|
||||||
await import("codemirror/theme/monokai.css");
|
// @ts-ignore - CodeMirror addon type issues
|
||||||
|
await import("codemirror/addon/fold/brace-fold");
|
||||||
|
// @ts-ignore - CodeMirror addon type issues
|
||||||
|
await import("codemirror/addon/fold/foldgutter");
|
||||||
|
// @ts-ignore - CodeMirror addon type issues
|
||||||
|
await import("codemirror/addon/search/match-highlighter");
|
||||||
|
require("codemirror/lib/codemirror.css");
|
||||||
|
require("codemirror/theme/monokai.css");
|
||||||
|
require("codemirror/addon/fold/foldgutter.css");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadRhaiWasm();
|
||||||
|
initRhaiMode(CodeMirror);
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load CodeMirror:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +58,7 @@ interface EditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = forwardRef<any, EditorProps>(function Editor(
|
export const Editor = forwardRef<any, EditorProps>(function Editor(
|
||||||
{ initialValue = "", onChange, onRequestRun, onReady },
|
{ initialValue = "", onChange, onReady },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -37,19 +69,30 @@ export const Editor = forwardRef<any, EditorProps>(function Editor(
|
|||||||
|
|
||||||
// Initialize editor only once
|
// Initialize editor only once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!CodeMirror || !textareaRef.current || editorRef.current) return;
|
if (!isCodeMirrorReady || !CodeMirror || !textareaRef.current || editorRef.current) return;
|
||||||
|
|
||||||
const editor = CodeMirror.fromTextArea(textareaRef.current, {
|
const editor = CodeMirror.fromTextArea(textareaRef.current, {
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
mode: "javascript", // Placeholder mode, will be 'rhai' when WASM is integrated
|
mode: "rhai",
|
||||||
theme: "monokai",
|
theme: "monokai",
|
||||||
|
indentUnit: 4,
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
autoCloseBrackets: true,
|
foldGutter: {
|
||||||
styleActiveLine: true,
|
rangeFinder: CodeMirror.fold.brace,
|
||||||
extraKeys: {
|
|
||||||
"Ctrl-Enter": () => {
|
|
||||||
onRequestRun?.();
|
|
||||||
},
|
},
|
||||||
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||||
|
styleActiveLine: true,
|
||||||
|
highlightSelectionMatches: {
|
||||||
|
minChars: 3,
|
||||||
|
showToken: true,
|
||||||
|
annotateScrollbar: true,
|
||||||
|
},
|
||||||
|
rulers: [],
|
||||||
|
autoCloseBrackets: {
|
||||||
|
pairs: `()[]{}''""`,
|
||||||
|
closeBefore: `)]}'":;,`,
|
||||||
|
triples: "",
|
||||||
|
explode: "()[]{}",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,25 +113,14 @@ export const Editor = forwardRef<any, EditorProps>(function Editor(
|
|||||||
editorRef.current = null;
|
editorRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []); // Empty dependency array - only initialize once
|
}, []); // Only run once
|
||||||
|
|
||||||
// Update keyboard shortcut when onRequestRun changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorRef.current && onRequestRun) {
|
|
||||||
editorRef.current.setOption("extraKeys", {
|
|
||||||
"Ctrl-Enter": () => {
|
|
||||||
onRequestRun();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [onRequestRun]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.editorContainer}>
|
<div className={styles.editorContainer}>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
defaultValue={initialValue}
|
defaultValue={initialValue}
|
||||||
placeholder="Enter your Rhai code here..."
|
placeholder="Code goes here"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export default function Playground() {
|
|||||||
className={styles.result}
|
className={styles.result}
|
||||||
readOnly
|
readOnly
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Script output will appear here..."
|
placeholder="Use print() to produce output"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const Terminal = forwardRef<TerminalRef, {}>(function Terminal(_props, re
|
|||||||
});
|
});
|
||||||
|
|
||||||
term.open(terminalRef.current);
|
term.open(terminalRef.current);
|
||||||
term.write('Terminal ready...\r\n');
|
term.write('Terminal ready.\r\n');
|
||||||
|
|
||||||
xtermRef.current = term;
|
xtermRef.current = term;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -15,3 +15,24 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Enhanced CodeMirror styles from playground */
|
||||||
|
.editorContainer :global(.CodeMirror) {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
height: 100% !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer :global(.rhai-error) {
|
||||||
|
text-decoration: underline wavy red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer :global(.cm-matchhighlight) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer :global(.CodeMirror-selection-highlight-scrollbar) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|||||||
38
webui/src/utils/wasmLoader.ts
Normal file
38
webui/src/utils/wasmLoader.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// WASM loader for Rhai CodeMirror mode
|
||||||
|
import init, { RhaiMode, init_codemirror_pass } from '@/wasm/rhai-codemirror/rhai_codemirror.js';
|
||||||
|
|
||||||
|
let wasmInitialized = false;
|
||||||
|
let wasmModule: any = null;
|
||||||
|
|
||||||
|
export const loadRhaiWasm = async () => {
|
||||||
|
if (wasmInitialized) {
|
||||||
|
return wasmModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize the WASM module
|
||||||
|
wasmModule = await init();
|
||||||
|
wasmInitialized = true;
|
||||||
|
|
||||||
|
return wasmModule;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load Rhai WASM module:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initRhaiMode = (CodeMirror: any) => {
|
||||||
|
if (!wasmInitialized || !wasmModule) {
|
||||||
|
throw new Error('WASM module not loaded. Call loadRhaiWasm() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize CodeMirror Pass for the WASM module
|
||||||
|
init_codemirror_pass(CodeMirror.Pass);
|
||||||
|
|
||||||
|
// Define the Rhai mode using the WASM-based RhaiMode
|
||||||
|
CodeMirror.defineMode("rhai", (config: any) => {
|
||||||
|
return new RhaiMode(config.indentUnit || 4);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { RhaiMode };
|
||||||
Reference in New Issue
Block a user