Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 21 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;"]
CMD ["nginx", "-g", "daemon off;"]

10 changes: 10 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
<link data-trunk rel="copy-dir" href="src/assets/animations" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">

<!-- CodeMirror for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
<link data-trunk rel="css" href="src/assets/style/simplicity-theme.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/addon/mode/simple.min.js"></script>
<link data-trunk rel="copy-file" href="src/assets/js/codemirror/simplicityhl.js" />
<link data-trunk rel="copy-file" href="src/assets/js/codemirror-bridge.js" />
<script src="/simplicityhl.js"></script>
<script src="/codemirror-bridge.js"></script>

<noscript>This page requires JavaScript</noscript>
</head>
<body></body>
Expand Down
124 changes: 124 additions & 0 deletions src/assets/js/codemirror-bridge.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
})();

139 changes: 139 additions & 0 deletions src/assets/js/codemirror/simplicityhl.js
Original file line number Diff line number Diff line change
@@ -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");
});

10 changes: 10 additions & 0 deletions src/assets/style/components/program_window/program_input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading