include engine; /** * LspClient manages a WebSocket connection to an Aussom Language Server * Protocol (LSP) server. The endpoint is configured at construction time * and the connection can be opened or closed on demand. */ class LspClient { /** * The WSS endpoint URL for the LSP server. */ private endpoint = ""; /** * True when the WebSocket connection is currently open. */ private connected = false; /** * Constructs a new LspClient with the given endpoint URL. * @p Endpoint is a string with the WSS URL of the LSP server. */ public LspClient(string Endpoint) { this.endpoint = Endpoint; } /** * Opens the WebSocket connection to the LSP server if an endpoint * has been configured. Sets up window._lspClient with a full * request/response correlator, document-sync helpers, and the * LSP initialize handshake. Has no effect when endpoint is empty. * @r This LspClient instance for method chaining. */ public connect() { if (this.endpoint != "") { Win.eval("(function(ep){var c={_ws:null,_nextId:2,_pending:{},_ready:false,_docVersion:0,request:function(m,p){var self=this,id=this._nextId++;return new Promise(function(res,rej){self._pending[id]={resolve:res,reject:rej};self._send({jsonrpc:'2.0',id:id,method:m,params:p});});},notify:function(m,p){this._send({jsonrpc:'2.0',method:m,params:p});},didOpen:function(t){this._docVersion=1;this.notify('textDocument/didOpen',{textDocument:{uri:'file:///playground.aus',languageId:'aussom',version:1,text:t}});},didChange:function(t){this._docVersion++;this.notify('textDocument/didChange',{textDocument:{uri:'file:///playground.aus',version:this._docVersion},contentChanges:[{text:t}]});},_send:function(msg){if(this._ws&&this._ws.readyState===WebSocket.OPEN){this._ws.send(JSON.stringify(msg));}}};var ws=new WebSocket(ep);c._ws=ws;ws.onopen=function(){console.log('[LSP] Connected');c._send({jsonrpc:'2.0',id:1,method:'initialize',params:{processId:null,clientInfo:{name:'aussom-playground',version:'1.0'},rootUri:null,capabilities:{textDocument:{synchronization:{didSave:false},completion:{completionItem:{snippetSupport:false}},hover:{},definition:{},references:{}}}}});};ws.onmessage=function(e){try{var msg=JSON.parse(e.data);if(msg.id!==undefined&&!msg.method){if(msg.id===1&&msg.result){c._send({jsonrpc:'2.0',method:'initialized',params:{}});}else{var p=c._pending[msg.id];if(p){delete c._pending[msg.id];if(msg.error)p.reject(msg.error);else p.resolve(msg.result);}}}if(msg.method&&msg.id!==undefined){c._send({jsonrpc:'2.0',id:msg.id,result:null});if(msg.method==='client/registerCapability'){c._ready=true;console.log('[LSP] Ready.');}}if(msg.method&&msg.id===undefined){if(msg.method==='textDocument/publishDiagnostics'&&window.monaco&&window.monacoEditor){var model=window.monacoEditor.getModel();if(model){var smap={1:8,2:4,3:2,4:1};var markers=(msg.params.diagnostics||[]).map(function(d){return{severity:smap[d.severity]||8,startLineNumber:d.range.start.line+1,startColumn:d.range.start.character+1,endLineNumber:d.range.end.line+1,endColumn:d.range.end.character+1,message:d.message,source:d.source||'aussom'};});monaco.editor.setModelMarkers(model,'aussom-lsp',markers);}}}}catch(ex){console.error('[LSP] Parse error:',ex);}};ws.onclose=function(){c._ready=false;console.log('[LSP] Disconnected.');};ws.onerror=function(e){console.error('[LSP] Error:',e);};window._lspClient=c;})('" + this.endpoint + "')"); } return this; } /** * Closes the WebSocket connection and releases the client reference. * Safe to call even when not connected. * @r This LspClient instance for method chaining. */ public disconnect() { Win.eval("if(window._lspClient&&window._lspClient._ws){window._lspClient._ws.close();window._lspClient._ws=null;window._lspClient._ready=false;}"); return this; } /** * Returns true if the WebSocket is currently in the OPEN state. * @r A bool that is true when connected and false otherwise. */ public isConnected() { result = Win.eval("(window._lspClient&&window._lspClient._ws&&window._lspClient._ws.readyState===WebSocket.OPEN)?'true':'false'"); return result == "true"; } /** * Returns the configured LSP endpoint URL. * @r A string with the WSS endpoint URL. */ public getEndpoint() { return this.endpoint; } } /** * MonacoEditor provides an Aussom-friendly interface to the Monaco editor * instance initialised by the page's inline JavaScript bridge. * All operations delegate to window.monacoEditor via Win.eval. */ class MonacoEditor { /** * Retrieves the current source code from the Monaco editor. * Returns an empty string if the editor has not yet been initialised. * @r A string with the full editor content. */ public getCode() { return Win.eval("window.monacoEditor ? window.monacoEditor.getValue() : ''"); } /** * Replaces the entire editor content with the provided code string. * The value is staged through the hidden initial-code textarea to * safely handle any special characters. * @p code is a string with the Aussom source code to load into the editor. * @r This MonacoEditor instance for method chaining. */ public setCode(string code) { Win.setValueByName("initial-code", code); Win.eval("if(window.monacoEditor){ window.monacoEditor.setValue(document.getElementsByName('initial-code')[0].value); }"); return this; } /** * Changes the Monaco editor color theme. Delegates to the page bridge * so that the Bootstrap page theme is also updated to match. * @p theme is a string with the Monaco theme identifier. * @r This MonacoEditor instance for method chaining. */ public setTheme(string theme) { Win.eval("window.playgroundBridge && window.playgroundBridge.setTheme('" + theme + "')"); return this; } /** * Triggers a layout recalculation on the Monaco editor. Call this * after the editor container is shown or resized. * @r This MonacoEditor instance for method chaining. */ public layout() { Win.eval("if(window.monacoEditor){ window.monacoEditor.layout(); }"); return this; } } /** * OutputDisplay manages the output panel. It appends styled line elements * to the output container and provides convenience methods for different * message categories. */ class OutputDisplay { /** * The DOM element that serves as the scrollable output container. */ private container = null; /** * Constructs an OutputDisplay wrapping the given DOM element. * @p El is the Element representing the output container node. */ public OutputDisplay(El) { this.container = El; } /** * Appends a new output line with the given HTML content and CSS class. * @p message is a string with the HTML content to display. * @p cssClass is a string with the CSS class to apply for styling. * @r This OutputDisplay instance for method chaining. */ public append(string message, string cssClass) { line = new Div(); line.setAttr("class", "output-line " + cssClass); line.setHtml(message); this.container.add(line); return this; } /** * Appends a plain text output line. * @p message is a string with the text to display. * @r This OutputDisplay instance for method chaining. */ public appendText(string message) { return this.append(message, "output-normal"); } /** * Appends an error output line rendered in the danger color. * @p message is a string with the error text to display. * @r This OutputDisplay instance for method chaining. */ public appendError(string message) { return this.append(message, "output-error"); } /** * Appends an informational output line rendered in the info color. * @p message is a string with the info text to display. * @r This OutputDisplay instance for method chaining. */ public appendInfo(string message) { return this.append(message, "output-info"); } /** * Appends a success output line rendered in the success color. * @p message is a string with the success text to display. * @r This OutputDisplay instance for method chaining. */ public appendSuccess(string message) { return this.append(message, "output-success"); } /** * Removes all child elements from the output container. * @r This OutputDisplay instance for method chaining. */ public clear() { this.container.clear(); return this; } } /** * PlaygroundApp is the top-level controller for the Aussom Playground. * It builds the full page UI using Aussom HNodes, wires up event listeners, * and drives Monaco editor initialisation. */ class PlaygroundApp { private outputContent = null; private editor = null; private lsp = null; private output = null; private themeSelect = null; private runBtn = null; private clearBtn = null; private tabEditorBtn = null; private tabArgsBtn = null; private editorTabPane = null; private argsTabPane = null; private argsInput = null; private consoleTabBtn = null; private demoTabBtn = null; private consolePane = null; private demoPane = null; private demoContent = null; private currentOutputTab = "console"; private lspEndpoint = "wss://lsp.aussom-lang.com/lsp"; private currentTab = "editor"; /** * Aussom-script entry point. Called automatically when the script loads. * @p args is a list with any startup arguments. */ public main(args) { this.init(); } /** * Builds the UI, wires listeners, and kicks off Monaco initialisation. */ private init() { this.buildUI(); // Set initial args from what was injected by the server. argsEl = Doc.getElementById("args-input"); argsEl.setHtml(Doc.getElementById("initialArgs").getHtml()); this.editor = new MonacoEditor(); this.output = new OutputDisplay(this.outputContent); this.lsp = new LspClient(this.lspEndpoint); if (this.lspEndpoint != "") { this.lsp.connect(); } // Kick off Monaco. initEditor() reads the server-injected #initialCode // script tag and the #theme-select dropdown. Win.eval("window.playgroundBridge.initEditor()"); this.output.appendInfo("// Aussom Playground ready. Press Run to execute your code."); } /** * Constructs the full playground UI using HNodes and appends it to * the #app-wrapper div that the server renders into the page skeleton. */ private buildUI() { // The app wrapper. appWrapper = new Div(); appWrapper.setId('app-wrapper'); // Navbar navbar = new Nav(); navbar.setAttr("class", "navbar px-3 py-1").setId("main-navbar"); // Left: logo + brand text brandContainer = new Div(); brandContainer.setAttr("class", "d-flex align-items-center gap-3 flex-grow-1"); logoImg = new BsImage('aussom-logo.png', 'Aussom logo'); logoImg.setStyle('height', '28px'); logoImg.setStyle('width', 'auto'); logoImg.setAttr('class', 'me-2'); brandSpan = new Span(); brandSpan.setAttr("class", "navbar-brand mb-0 fw-bold fs-5"); ausSpan = new Span("Aussom"); ausSpan.setAttr("class", "text-primary"); brandSpan.add(ausSpan).add(new Text(" Playground")); brandContainer.add(logoImg).add(brandSpan); // Right: theme selector + run button rightContainer = new Div(); rightContainer.setAttr("class", "d-flex align-items-center gap-2"); themeLabel = new Label("Editor theme"); themeLabel.setAttr("class", "visually-hidden").setFor("theme-select"); this.themeSelect = new Select("theme-select"); this.themeSelect.setId("theme-select"); this.themeSelect.setAttr("class", "form-select form-select-sm"); this.themeSelect.setStyle("width", "auto"); this.themeSelect.addOption("Dark", "vs-dark"); this.themeSelect.addOption("Light", "vs"); this.themeSelect.addOption("High Contrast Dark", "hc-black"); this.themeSelect.addOption("High Contrast Light", "hc-light"); this.themeSelect.addListener("change", ::onThemeChange); this.runBtn = new Button(); this.runBtn.setId("run-btn").setAttr("type", "button"); this.runBtn.setAttr("class", "btn btn-success btn-sm d-flex align-items-center gap-1"); runIcon = new I(); runIcon.setAttr("class", "bi bi-play-fill"); this.runBtn.add(runIcon).add(new Span("Run")); this.runBtn.addListener("click", ::onRunClick); rightContainer.add(themeLabel).add(this.themeSelect).add(this.runBtn); navbar.add(brandContainer).add(rightContainer); // Main Layout playgroundMain = new Div(); playgroundMain.setId("playground-main"); // Editor Panel editorPanel = new Div(); editorPanel.setId("editor-panel"); // Tab navigation tabNav = new Div(); tabNav.setId("editor-tab-nav").setAttr("class", "d-flex"); this.tabEditorBtn = new Button("Editor"); this.tabEditorBtn.setId("tab-editor").setAttr("type", "button"); this.tabEditorBtn.setAttr("class", "nav-link active px-3 py-2"); this.tabEditorBtn.addListener("click", ::onEditorTabClick); this.tabArgsBtn = new Button("Input Data"); this.tabArgsBtn.setId("tab-args").setAttr("type", "button"); this.tabArgsBtn.setAttr("class", "nav-link px-3 py-2"); this.tabArgsBtn.addListener("click", ::onArgsTabClick); tabNav.add(this.tabEditorBtn).add(this.tabArgsBtn); // Editor tab pane (contains the Monaco container) this.editorTabPane = new Div(); this.editorTabPane.setId("editor-tab-pane").setAttr("class", "tab-pane-area"); this.editorTabPane.setStyle("display", "flex"); monacoContainer = new Div(); monacoContainer.setId("monaco-container"); this.editorTabPane.add(monacoContainer); // Args tab pane this.argsTabPane = new Div(); this.argsTabPane.setId("args-tab-pane").setAttr("class", "tab-pane-area"); this.argsTabPane.setStyle("display", "none"); this.argsInput = new Textarea("args-input"); this.argsInput.setId("args-input"); this.argsInput.setAttr("placeholder", "Enter program input data or arguments here..."); this.argsInput.setStyle("font-family", "'Consolas', 'Monaco', monospace"); this.argsInput.setStyle("resize", "none"); this.argsTabPane.add(this.argsInput); editorPanel.add(tabNav).add(this.editorTabPane).add(this.argsTabPane); // Output panel outputPanel = new Div(); outputPanel.setId("output-panel"); // Output header with tab nav + clear button outputHeader = new Div(); outputHeader.setId("output-header"); outputHeader.setAttr("class", "d-flex align-items-center justify-content-between px-3 py-1 border-bottom"); outputTabNav = new Div(); outputTabNav.setId("output-tab-nav").setAttr("class", "d-flex"); this.consoleTabBtn = new Button("Console"); this.consoleTabBtn.setId("tab-console").setAttr("type", "button"); this.consoleTabBtn.setAttr("class", "nav-link active px-3 py-2"); this.consoleTabBtn.addListener("click", ::onConsoleTabClick); this.demoTabBtn = new Button("Demo Panel"); this.demoTabBtn.setId("tab-demo").setAttr("type", "button"); this.demoTabBtn.setAttr("class", "nav-link px-3 py-2"); this.demoTabBtn.addListener("click", ::onDemoTabClick); outputTabNav.add(this.consoleTabBtn).add(this.demoTabBtn); this.clearBtn = new Button(); this.clearBtn.setId("clear-btn").setAttr("type", "button"); this.clearBtn.setAttr("class", "btn btn-sm btn-outline-secondary py-0"); clearIcon = new I(); clearIcon.setAttr("class", "bi bi-x-lg"); this.clearBtn.add(clearIcon).add(new Span("Clear")); this.clearBtn.addListener("click", ::onClearClick); outputHeader.add(outputTabNav).add(this.clearBtn); // Console tab pane (active by default) this.consolePane = new Div(); this.consolePane.setId("console-pane").setAttr("class", "tab-pane-area"); this.consolePane.setStyle("display", "flex"); this.outputContent = new Div(); this.outputContent.setId("output-content"); this.consolePane.add(this.outputContent); // Demo Panel tab pane (hidden by default) this.demoPane = new Div(); this.demoPane.setId("demo-pane").setAttr("class", "tab-pane-area"); this.demoPane.setStyle("display", "none"); this.demoContent = new Div(); this.demoContent.setId("demo-panel"); this.demoPane.add(this.demoContent); outputPanel.add(outputHeader).add(this.consolePane).add(this.demoPane); // Assemble and attach to the page playgroundMain.add(editorPanel).add(outputPanel); appWrapper.add(navbar).add(playgroundMain); Doc.body().add(appWrapper.obj); } /** * Handles a click on the Run button. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onRunClick(string eventName, object eventObj) { this.output.appendInfo("Running:"); // Clear the Demo Panel before each run this.demoContent.clear(); eng = new Engine(); lgr = new Logger(); //lgr.setLevel("TRC"); lgr.registerOnTrc(::onTrc); lgr.registerOnDbg(::onDbg); lgr.registerOnLog(::onInfo); lgr.registerOnInfo(::onInfo); lgr.registerOnWarn(::onWarn); lgr.registerOnErr(::onErr); lgr.registerOnPrintln(::onPrint); lgr.registerOnPrint(::onPrint); // Register the new logger and store the old one for later. oldLogger = eng.registerLogger(lgr); // Get the editor code code = this.editor.getCode(); startTime = (new Date()).getTime(); try { argStr = this.getArgsValue(); if (!argStr || argStr.trim() == "") { argStr = "[]"; } args = json.parse(argStr); if (!(args instanceof 'list')) { throw "Input Data not of type JSON list. Found '" + lang.type(args) + "' instead."; } eng.runString("browserScript.aus", code, args, false); } catch (e) { this.output.appendError("Exception: " + e); } endTime = (new Date()).getTime(); this.output.appendInfo("Done in " + ((endTime - startTime)/1000.0) + "s."); // Swap the old logger back eng.registerLogger(oldLogger); // Auto-switch to Demo Panel if the script added content if (this.demoContent.getChildren().size() > 0) { this.showOutputTab("demo"); } } public onTrc(string message) { this.output.appendInfo("[t] " + message + "\n"); } public onDbg(string message) { this.output.appendInfo("[d] " + message + "\n"); } public onInfo(string message) { this.output.appendSuccess("[i] " + message + "\n"); } public onWarn(string message) { this.output.appendError("[w] " + message + "\n"); } public onErr(string message) { this.output.appendError("[e] " + message + "\n"); } public onPrint(string message) { this.output.appendText(message); } public onPrintln(string message) { this.output.appendText(message + "\n"); } /** * Handles a click on the Clear button. Clears the active output tab. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onClearClick(string eventName, object eventObj) { if (this.currentOutputTab == "console") { this.output.clear(); } else { this.demoContent.clear(); } } /** * Handles a change in the theme dropdown. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onThemeChange(string eventName, object eventObj) { theme = Win.getSelectValueByName("theme-select"); this.applyTheme(theme); } /** * Applies the given Monaco theme and updates the Bootstrap page theme. * @p theme is a string with the Monaco theme identifier. */ private applyTheme(string theme) { this.editor.setTheme(theme); bsTheme = "light"; if (theme == "vs-dark" || theme == "hc-black") { bsTheme = "dark"; } Doc.getElementById("html-root").setAttr("data-bs-theme", bsTheme); } /** * Handles a click on the Editor tab button. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onEditorTabClick(string eventName, object eventObj) { this.showTab("editor"); } /** * Handles a click on the Input Data tab button. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onArgsTabClick(string eventName, object eventObj) { this.showTab("args"); } /** * Shows the requested tab pane and updates tab button active states. * Triggers a Monaco layout recalculation when switching back to the * editor pane so the editor fills its container correctly. * @p tabName is a string with either "editor" or "args". */ private showTab(string tabName) { if (tabName == "editor") { this.editorTabPane.setStyle("display", "flex"); this.argsTabPane.setStyle("display", "none"); this.tabEditorBtn.setAttr("class", "nav-link active px-3 py-2"); this.tabArgsBtn.setAttr("class", "nav-link px-3 py-2"); this.currentTab = "editor"; this.editor.layout(); } else { this.editorTabPane.setStyle("display", "none"); this.argsTabPane.setStyle("display", "flex"); this.tabEditorBtn.setAttr("class", "nav-link px-3 py-2"); this.tabArgsBtn.setAttr("class", "nav-link active px-3 py-2"); this.currentTab = "args"; } } /** * Handles a click on the Console output tab. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onConsoleTabClick(string eventName, object eventObj) { this.showOutputTab("console"); } /** * Handles a click on the Demo Panel output tab. * @p eventName is a string with the event type name. * @p eventObj is the native event object. */ private onDemoTabClick(string eventName, object eventObj) { this.showOutputTab("demo"); } /** * Shows the requested output tab pane and updates tab button states. * @p tabName is a string with either "console" or "demo". */ private showOutputTab(string tabName) { if (tabName == "console") { this.consolePane.setStyle("display", "flex"); this.demoPane.setStyle("display", "none"); this.consoleTabBtn.setAttr("class", "nav-link active px-3 py-2"); this.demoTabBtn.setAttr("class", "nav-link px-3 py-2"); } else { this.consolePane.setStyle("display", "none"); this.demoPane.setStyle("display", "flex"); this.consoleTabBtn.setAttr("class", "nav-link px-3 py-2"); this.demoTabBtn.setAttr("class", "nav-link active px-3 py-2"); } this.currentOutputTab = tabName; } /** * Returns the current value of the args/input-data textarea. * @r A string with the contents of the Input Data pane. */ public getArgsValue() { return Win.getValueByName("args-input"); } }