diff --git a/Cargo.toml b/Cargo.toml index 05963ec..fe488b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ leptos_router = { version = "0.6.15", features = ["csr"] } console_error_panic_hook = "0.1.7" hex-conservative = "0.2.1" js-sys = "0.3.70" +wasm-bindgen = "0.2.93" web-sys = { version = "0.3.70", features = [ "Navigator", "Clipboard", diff --git a/Dockerfile b/Dockerfile index a71c86e..34d305b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,9 @@ +# Build: +# docker build -t simplicity-webide . +# +# Run: +# docker run -d -p 8080:80 --name simplicity-webide simplicity-webide + # Stage 1: Builder # This stage installs all dependencies and builds the application. FROM debian:bookworm-slim AS builder @@ -37,17 +43,28 @@ ENV CFLAGS_wasm32_unknown_unknown="-I/usr/lib/clang/16/include" WORKDIR /app COPY . . -# Build the application -RUN trunk build --release && \ - sh fix-links.sh +# Build the application WITHOUT the fix-links.sh script (for local deployment) +RUN trunk build --release # Stage 2: Final Image # This stage creates a minimal image to serve the built static files. FROM nginx:1.27-alpine-slim +# Copy custom nginx configuration for proper routing +RUN echo 'server { \ + listen 80; \ + server_name localhost; \ + root /usr/share/nginx/html; \ + index index.html; \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + # Copy the built assets from the builder stage COPY --from=builder /app/dist /usr/share/nginx/html # Expose port 80 and start Nginx EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] + diff --git a/index.html b/index.html index f3225a8..6211228 100644 --- a/index.html +++ b/index.html @@ -21,6 +21,16 @@ + + + + + + + + + +
diff --git a/src/assets/js/codemirror-bridge.js b/src/assets/js/codemirror-bridge.js new file mode 100644 index 0000000..66e0bf1 --- /dev/null +++ b/src/assets/js/codemirror-bridge.js @@ -0,0 +1,124 @@ +/** + * CodeMirror Bridge for Simplicity Web IDE + * This file bridges CodeMirror with the Leptos/WASM application + */ + +window.SimplicityEditor = (function() { + let editorInstance = null; + + return { + /** + * Initialize CodeMirror editor on a textarea element + * @param {string} textareaId - The ID of the textarea element + * @param {string} initialValue - Initial code content + * @returns {boolean} Success status + */ + init: function(textareaId, initialValue) { + try { + const textarea = document.getElementById(textareaId); + if (!textarea) { + console.error('Textarea not found:', textareaId); + return false; + } + + // Create CodeMirror instance + editorInstance = CodeMirror.fromTextArea(textarea, { + mode: 'simplicityhl', + theme: 'simplicity', + lineNumbers: true, + matchBrackets: true, + autoCloseBrackets: true, + indentUnit: 4, + tabSize: 4, + indentWithTabs: false, + lineWrapping: false, + extraKeys: { + "Tab": function(cm) { + cm.replaceSelection(" ", "end"); + }, + "Shift-Tab": function(cm) { + // Unindent + const cursor = cm.getCursor(); + const line = cm.getLine(cursor.line); + if (line.startsWith(" ")) { + cm.replaceRange("", + { line: cursor.line, ch: 0 }, + { line: cursor.line, ch: 4 } + ); + } + }, + "Ctrl-Enter": function(cm) { + // Trigger run - dispatch custom event + window.dispatchEvent(new CustomEvent('codemirror-ctrl-enter')); + } + } + }); + + // Set initial value + if (initialValue) { + editorInstance.setValue(initialValue); + } + + // Listen for changes and update the hidden textarea + editorInstance.on('change', function(cm) { + textarea.value = cm.getValue(); + // Dispatch input event so Leptos can detect the change + textarea.dispatchEvent(new Event('input', { bubbles: true })); + }); + + console.log('CodeMirror initialized successfully'); + return true; + } catch (error) { + console.error('Failed to initialize CodeMirror:', error); + return false; + } + }, + + /** + * Get the current editor content + * @returns {string|null} Current content or null if editor not initialized + */ + getValue: function() { + return editorInstance ? editorInstance.getValue() : null; + }, + + /** + * Set the editor content + * @param {string} value - New content + */ + setValue: function(value) { + if (editorInstance) { + editorInstance.setValue(value || ''); + } + }, + + /** + * Refresh the editor (useful after visibility changes) + */ + refresh: function() { + if (editorInstance) { + setTimeout(function() { + editorInstance.refresh(); + }, 1); + } + }, + + /** + * Focus the editor + */ + focus: function() { + if (editorInstance) { + editorInstance.focus(); + } + }, + + /** + * Get the editor instance + * @returns {object|null} CodeMirror instance or null + */ + getInstance: function() { + return editorInstance; + } + }; +})(); + diff --git a/src/assets/js/codemirror/simplicityhl.js b/src/assets/js/codemirror/simplicityhl.js new file mode 100644 index 0000000..c1215a3 --- /dev/null +++ b/src/assets/js/codemirror/simplicityhl.js @@ -0,0 +1,139 @@ +/** + * SimplicityHL (Simfony) Syntax Highlighting Mode for CodeMirror + * Based on the official VSCode extension: https://marketplace.visualstudio.com/items?itemName=Blockstream.simplicityhl + * + * In VSCode: + * - jet/witness/param = entity.name.namespace (namespace color) + * - function_name after :: = entity.name.function (function color) + * + * Token types used: + * - comment: gray + * - keyword: pink (fn, let, match, if, else, etc.) + * - def: green (function names in definitions) + * - variable: white (regular identifiers) + * - variable-2: orange (function calls, parameters) + * - variable-3: purple (constants, UPPERCASE) + * - builtin: cyan (types, built-in functions) + * - atom: purple (true, false, None, Left, Right, Some) + * - string: yellow + * - number: purple + * - operator: pink + * - meta: pink (macros like assert!) + * - namespace: special handling for jet::, witness::, param:: + */ + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../../addon/mode/simple")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../../addon/mode/simple"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + // Custom token function for namespace::identifier patterns + function tokenNamespace(stream, state) { + // Check if we're at jet::, witness::, or param:: + if (stream.match(/\b(jet|witness|param)::/)) { + state.nextToken = 'namespacedId'; + return 'builtin'; // namespace part gets cyan + } + return null; + } + + CodeMirror.defineSimpleMode("simplicityhl", { + start: [ + // === COMMENTS (first priority) === + {regex: /\/\/.*/, token: "comment"}, + {regex: /\/\*/, token: "comment", next: "comment"}, + + // === MACROS (assert!, panic!, etc.) === + {regex: /\b[a-z_][a-zA-Z0-9_]*!/, token: "meta"}, + + // === KEYWORDS === + {regex: /\b(fn|let|match|if|else|while|for|return|type|mod|const)\b/, token: "keyword"}, + + // === BOOLEAN & SPECIAL CONSTANTS === + {regex: /\b(true|false|None)\b/, token: "atom"}, + + // === EITHER/OPTION VARIANTS === + {regex: /\b(Left|Right|Some)\b/, token: "atom"}, + + // === TYPES - Cyan === + {regex: /\b(u1|u2|u4|u8|u16|u32|u64|u128|u256|i8|i16|i32|i64|bool)\b/, token: "builtin"}, + {regex: /\b(Either|Option|List)\b/, token: "builtin"}, + {regex: /\b(Ctx8|Pubkey|Message64|Message|Signature|Scalar|Fe|Gej|Ge|Point)\b/, token: "builtin"}, + {regex: /\b(Height|Time|Distance|Duration|Lock|Outpoint)\b/, token: "builtin"}, + {regex: /\b(Confidential1|ExplicitAsset|Asset1|ExplicitAmount|Amount1|ExplicitNonce|Nonce|TokenAmount1)\b/, token: "builtin"}, + {regex: /\b[A-Z][a-zA-Z0-9_]*\b/, token: "builtin"}, + + // === BUILT-IN FUNCTIONS === + {regex: /\b(unwrap|unwrap_left|unwrap_right|for_while|is_none|array_fold|into|fold|dbg)\b/, token: "builtin"}, + + // === NAMESPACE::IDENTIFIER PATTERNS === + // jet:: (cyan) + function_name (green) + {regex: /\b(jet)(::)([a-z_][a-zA-Z0-9_]*)/, token: ["builtin", "operator", "def"]}, + + // witness:: (cyan) + CONSTANT (purple) + {regex: /\b(witness)(::)([A-Z_][A-Z0-9_]*)/, token: ["builtin", "operator", "variable-3"]}, + + // param:: (cyan) + name (green) + {regex: /\b(param)(::)([a-z_][a-zA-Z0-9_]*)/, token: ["builtin", "operator", "def"]}, + + // === NUMBERS === + {regex: /\b0x[0-9a-fA-F_]+\b/, token: "number"}, + {regex: /\b0b[01_]+\b/, token: "number"}, + {regex: /\b[0-9][0-9_]*\b/, token: "number"}, + + // === STRINGS === + {regex: /"(?:[^\\"]|\\.)*?"/, token: "string"}, + + // === FUNCTION DEFINITIONS === + {regex: /\b(fn)\s+/, token: "keyword", next: "functionName"}, + + // === FUNCTION CALLS (before general variables) === + {regex: /\b[a-z_][a-zA-Z0-9_]*(?=\s*\()/, token: "variable-2"}, + + // === OPERATORS === + {regex: /->|=>|::|==|!=|<=|>=|&&|\|\|/, token: "operator"}, + {regex: /[+\-*\/%&|^!~<>=:]/, token: "operator"}, + + // === BRACKETS & PUNCTUATION === + {regex: /[\{\[\(]/, indent: true}, + {regex: /[\}\]\)]/, dedent: true}, + {regex: /[;,.]/, token: null}, + + // === VARIABLES === + {regex: /\b[a-z_][a-zA-Z0-9_]*\b/, token: "variable"} + ], + + // State for capturing function names after "fn " + functionName: [ + {regex: /[a-zA-Z_][a-zA-Z0-9_]*/, token: "def", next: "start"}, + {regex: /\s+/, token: null} + ], + + // Multi-line comment state + comment: [ + {regex: /.*?\*\//, token: "comment", next: "start"}, + {regex: /.*/, token: "comment"} + ], + + // Language metadata + meta: { + lineComment: "//", + blockCommentStart: "/*", + blockCommentEnd: "*/", + fold: "brace", + electricChars: "{}[]", + closeBrackets: "()[]{}''\"\"``" + } + }); + + // Register MIME types + CodeMirror.defineMIME("text/x-simplicityhl", "simplicityhl"); + CodeMirror.defineMIME("text/x-simfony", "simplicityhl"); + CodeMirror.defineMIME("text/x-simf", "simplicityhl"); +}); + diff --git a/src/assets/style/components/program_window/program_input.scss b/src/assets/style/components/program_window/program_input.scss index 1bcf400..235692b 100644 --- a/src/assets/style/components/program_window/program_input.scss +++ b/src/assets/style/components/program_window/program_input.scss @@ -26,6 +26,16 @@ } } +// CodeMirror wrapper styles +.CodeMirror { + height: auto; + min-height: 400px; + font-family: 'Roboto Mono', monospace; + font-size: 12px; + border-radius: 7.5px; + border: 1px solid rgba(255, 255, 255, 0.10); +} + .copy-program { position: absolute; top: 40px; diff --git a/src/assets/style/simplicity-theme.css b/src/assets/style/simplicity-theme.css new file mode 100644 index 0000000..63b6345 --- /dev/null +++ b/src/assets/style/simplicity-theme.css @@ -0,0 +1,177 @@ +/* + * Simplicity Theme for CodeMirror + * Exact match for VS Code Dark+ theme with SimplicityHL token mappings + * + * Token mapping from VS Code extension (simfony.tmLanguage.json): + * - storage.type.function → #569cd6 (blue) for "fn", "type", "let" + * - keyword.control → #c586c0 (purple) for "match", "if", "else", "while", "for", "return" + * - keyword.other → #c586c0 (purple) for "mod", "const" + * - keyword.operator → #d4d4d4 (white) for "->", "=>", "=", ":", etc. + * - entity.name.function → #dcdcaa (yellow) for function names and calls + * - entity.name.type → #4ec9b0 (cyan) for types + * - entity.name.namespace → #4ec9b0 (cyan) for "jet", "witness", "param" + * - constant.numeric → #b5cea8 (light green) for numbers + * - constant.language → #569cd6 (blue) for "true", "false", "None" + * - string.quoted → #ce9178 (orange) for strings + * - comment → #6a9955 (green) for comments + * - variable.other → #9cdcfe (light blue) for variables + * - support.function → #dcdcaa (yellow) for "Left", "Right", "Some" + */ + +.cm-s-simplicity.CodeMirror { + background: #1e1e1e; + color: #d4d4d4; +} + +.cm-s-simplicity div.CodeMirror-selected { + background: #264f78; +} + +.cm-s-simplicity .CodeMirror-line::selection, +.cm-s-simplicity .CodeMirror-line > span::selection, +.cm-s-simplicity .CodeMirror-line > span > span::selection { + background: rgba(38, 79, 120, 0.99); +} + +.cm-s-simplicity .CodeMirror-line::-moz-selection, +.cm-s-simplicity .CodeMirror-line > span::-moz-selection, +.cm-s-simplicity .CodeMirror-line > span > span::-moz-selection { + background: rgba(38, 79, 120, 0.99); +} + +.cm-s-simplicity .CodeMirror-gutters { + background: #1e1e1e; + border-right: 1px solid #3e3e42; +} + +.cm-s-simplicity .CodeMirror-guttermarker { + color: #d4d4d4; +} + +.cm-s-simplicity .CodeMirror-guttermarker-subtle { + color: #858585; +} + +.cm-s-simplicity .CodeMirror-linenumber { + color: #858585; +} + +.cm-s-simplicity .CodeMirror-cursor { + border-left: 1px solid #aeafad; +} + +/* Comments - comment.line.double-slash.simfony, comment.block.simfony */ +.cm-s-simplicity span.cm-comment { + color: #6a9955; + font-style: italic; +} + +/* Boolean & None - constant.language.boolean.simfony, constant.language.simfony */ +.cm-s-simplicity span.cm-atom { + color: #569cd6; +} + +/* Numbers - constant.numeric.* */ +.cm-s-simplicity span.cm-number { + color: #b5cea8; +} + +/* Keywords - storage.type.function (fn, let, type), keyword.control (match, if, else, while, for, return), keyword.other (mod, const) */ +.cm-s-simplicity span.cm-keyword { + color: #c586c0; +} + +/* Function definitions - entity.name.function.simfony */ +.cm-s-simplicity span.cm-def { + color: #dcdcaa; +} + +/* Variables - variable.other.simfony */ +.cm-s-simplicity span.cm-variable { + color: #9cdcfe; +} + +/* Function calls - entity.name.function.call.simfony */ +.cm-s-simplicity span.cm-variable-2 { + color: #dcdcaa; +} + +/* Constants (UPPERCASE witness) */ +.cm-s-simplicity span.cm-variable-3 { + color: #4fc1ff; +} + +/* Built-in types, jets, witness, param namespace - entity.name.type.simfony, entity.name.namespace.simfony */ +.cm-s-simplicity span.cm-builtin { + color: #4ec9b0; +} + +/* Type names - entity.name.type.simfony */ +.cm-s-simplicity span.cm-type { + color: #4ec9b0; +} + +/* Strings - string.quoted.double.simfony */ +.cm-s-simplicity span.cm-string { + color: #ce9178; +} + +/* String special chars - constant.character.escape.simfony */ +.cm-s-simplicity span.cm-string-2 { + color: #d16969; +} + +/* Operators - keyword.operator.simfony */ +.cm-s-simplicity span.cm-operator { + color: #d4d4d4; +} + +/* Macros/Preprocessor - keyword.other.preprocessor.directive.simfony */ +.cm-s-simplicity span.cm-meta { + color: #c586c0; +} + +/* Errors */ +.cm-s-simplicity span.cm-error { + background: #f48771; + color: #ffffff; +} + +/* Attributes */ +.cm-s-simplicity span.cm-attribute { + color: #9cdcfe; +} + +/* Tags */ +.cm-s-simplicity span.cm-tag { + color: #569cd6; +} + +/* Links */ +.cm-s-simplicity span.cm-link { + color: #569cd6; + text-decoration: underline; +} + +/* Active line */ +.cm-s-simplicity .CodeMirror-activeline-background { + background: #282828; +} + +/* Matching bracket */ +.cm-s-simplicity .CodeMirror-matchingbracket { + background: #3e3e42; + color: #4ec9b0 !important; + outline: 1px solid #888; +} + +/* Non-matching bracket */ +.cm-s-simplicity .CodeMirror-nonmatchingbracket { + color: #f48771; +} + +/* Cursor line */ +.cm-s-simplicity .CodeMirror-activeline .CodeMirror-gutter-elt { + background: #282828; +} + diff --git a/src/components/program_window/program_tab.rs b/src/components/program_window/program_tab.rs index d73bdbe..bdee911 100644 --- a/src/components/program_window/program_tab.rs +++ b/src/components/program_window/program_tab.rs @@ -2,14 +2,15 @@ use std::sync::Arc; use itertools::Itertools; use leptos::{ - component, create_node_ref, create_rw_signal, ev, event_target_value, html, spawn_local, - use_context, view, IntoView, RwSignal, Signal, SignalGetUntracked, SignalSet, SignalUpdate, + component, create_effect, create_node_ref, create_rw_signal, ev, event_target_value, html, spawn_local, + use_context, view, IntoView, RwSignal, Signal, SignalGet, SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SignalWithUntracked, }; use simplicityhl::parse::ParseFromStr; use simplicityhl::simplicity::jet::elements::ElementsEnv; use simplicityhl::{elements, simplicity}; use simplicityhl::{CompiledProgram, SatisfiedProgram, WitnessValues}; +use wasm_bindgen::prelude::*; use crate::components::copy_to_clipboard::CopyToClipboard; use crate::function::Runner; @@ -166,47 +167,46 @@ impl Runtime { const TAB_KEY: u32 = 9; const ENTER_KEY: u32 = 13; +// JavaScript bindings for CodeMirror +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = ["window", "SimplicityEditor"])] + fn init(textarea_id: &str, initial_value: &str) -> bool; + + #[wasm_bindgen(js_namespace = ["window", "SimplicityEditor"])] + fn refresh(); +} + #[component] pub fn ProgramTab() -> impl IntoView { let program = use_context::