From 0a6807831c1e6d2ed1c5ff703625eacfdd033214 Mon Sep 17 00:00:00 2001 From: Steve Dunham Date: Tue, 5 Nov 2024 20:57:30 -0800 Subject: [PATCH] switch to preact --- playground/package-lock.json | 40 +++++++- playground/package.json | 4 +- playground/src/main.ts | 190 +++++++++++++++++++++++++---------- playground/style.css | 24 ++++- 4 files changed, 202 insertions(+), 56 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index c3b0e1e..ffab5d7 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -8,7 +8,9 @@ "name": "playground", "version": "0.0.0", "dependencies": { - "monaco-editor": "^0.52.0" + "@preact/signals": "^1.3.0", + "monaco-editor": "^0.52.0", + "preact": "^10.24.3" }, "devDependencies": { "typescript": "~5.6.2", @@ -406,6 +408,32 @@ "node": ">=12" } }, + "node_modules/@preact/signals": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.0.tgz", + "integrity": "sha512-EOMeg42SlLS72dhoq6Vjq08havnLseWmPQ8A0YsgIAqMgWgx7V1a39+Pxo6i7SY5NwJtH4849JogFq3M67AzWg==", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact": "10.x" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.8.0.tgz", + "integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", @@ -780,6 +808,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/rollup": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", diff --git a/playground/package.json b/playground/package.json index ec2630e..065bbb9 100644 --- a/playground/package.json +++ b/playground/package.json @@ -13,6 +13,8 @@ "vite": "^5.4.10" }, "dependencies": { - "monaco-editor": "^0.52.0" + "@preact/signals": "^1.3.0", + "monaco-editor": "^0.52.0", + "preact": "^10.24.3" } } diff --git a/playground/src/main.ts b/playground/src/main.ts index 3270c60..87d63a2 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,79 +1,174 @@ +import { effect, signal } from "@preact/signals"; import { newtConfig, newtTokens } from "./monarch.ts"; import * as monaco from "monaco-editor"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { h, render } from "preact"; monaco.languages.register({ id: "newt" }); monaco.languages.setMonarchTokensProvider("newt", newtTokens); monaco.languages.setLanguageConfiguration("newt", newtConfig); -const newtWorker = new Worker("worker.js")//new URL("worker.js", import.meta.url)) +const newtWorker = new Worker("worker.js"); //new URL("worker.js", import.meta.url)) + +function run(src: string) { + newtWorker.postMessage({ src }); +} + +newtWorker.onmessage = (ev) => { + console.log("worker sent", ev.data); + state.output.value = ev.data.output; + state.javascript.value = ev.data.javascript; +}; self.MonacoEnvironment = { - getWorkerUrl(moduleId, label) { + getWorkerUrl(moduleId, _label) { console.log("Get worker", moduleId); return moduleId; }, }; +const state = { + output: signal(""), + javascript: signal(""), + editor: signal(null), +}; + // I keep pressing this. -document.addEventListener('keydown', (ev) => { - if (ev.metaKey && ev.code == 'KeyS') ev.preventDefault(); -}) +document.addEventListener("keydown", (ev) => { + if (ev.metaKey && ev.code == "KeyS") ev.preventDefault(); +}); let value = localStorage.code || "module Main\n"; -let container = document.getElementById("editor")!; -let result = document.getElementById("result")!; -const editor = monaco.editor.create(container, { - value, - language: "newt", - theme: "vs", - automaticLayout: true, - minimap: { enabled: false }, +// let result = document.getElementById("result")!; + +// the editor might handle this itself with the right prodding. +effect(() => { + let text = state.output.value; + let editor = state.editor.value; + if (editor) processOutput(editor, text); }); +interface EditorProps { + initialValue: string; +} + +function Editor({ initialValue }: EditorProps) { + const ref = useRef(null); + useEffect(() => { + const container = ref.current!; + const editor = monaco.editor.create(container, { + value, + language: "newt", + theme: "vs", + automaticLayout: true, + minimap: { enabled: false }, + }); + state.editor.value = editor; + + editor.onDidChangeModelContent((ev) => { + console.log("mc", ev); + let value = editor.getValue(); + clearTimeout(timeout); + timeout = setTimeout(() => run(value), 1000); + localStorage.code = value; + }); + run(initialValue); + }, []); + + return h("div", { id: "editor", ref }); +} + +// for extra credit, we could have a read-only monaco +function JavaScript() { + const text = state.javascript.value; + return h("div", {id:'javascript'}, text); +} + +function Result() { + const text = state.output.value; + return h("div", { id: "result" }, text); +} + +const RESULTS = "Output" +const JAVASCRIPT = "JS" + +function Tabs() { + const [selected, setSelected] = useState(RESULTS); + + const Tab = (label: string) => { + let onClick = () => setSelected(label); + let className = "tab"; + if (label == selected) className += " selected"; + return h("div", { className, onClick }, label); + }; + + let body; + switch (selected) { + case RESULTS: + body = h(Result, {}); + break; + case JAVASCRIPT: + body = h(JavaScript, {}); + break; + default: + body = h("div", {}); + } + + return h( + "div", + { className: "tabPanel" }, + h("div", { className: "tabBar" }, Tab(RESULTS), Tab(JAVASCRIPT)), + h("div", { className: "tabBody" }, body) + ); +} + +function App() { + return h( + "div", + { className: "wrapper" }, + h(Editor, { initialValue: value }), + h(Tabs, {}) + ); +} + +render(h(App, {}), document.getElementById("app")!); + let timeout: number | undefined; -function run(src: string) { - newtWorker.postMessage({src}) -} - -newtWorker.onmessage = (ev) => { - console.log('worker sent', ev.data) - let {output, javascript} = ev.data - processOutput(output); -} - // Adapted from the vscode extension, but types are slightly different // and positions are 1-based. -const processOutput = (output: string) => { - result.innerText = output - - let model = editor.getModel()! - let markers: monaco.editor.IMarkerData[] = [] - let lines = output.split('\n') +const processOutput = ( + editor: monaco.editor.IStandaloneCodeEditor, + output: string +) => { + let model = editor.getModel()!; + let markers: monaco.editor.IMarkerData[] = []; + let lines = output.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const match = line.match(/(INFO|ERROR) at \((\d+), (\d+)\):\s*(.*)/); if (match) { let [_full, kind, line, col, message] = match; - let lineNumber = +line+1 - let column = +col+1; - let start = {column, lineNumber}; + let lineNumber = +line + 1; + let column = +col + 1; + let start = { column, lineNumber }; // we don't have the full range, so grab the surrounding word - let endColumn = column + 1 - let word = model.getWordAtPosition(start) - if (word) endColumn = word.endColumn + let endColumn = column + 1; + let word = model.getWordAtPosition(start); + if (word) endColumn = word.endColumn; // heuristics to grab the entire message: // anything indented // Context:, or Goal: are part of PRINTME // unexpected / expecting appear in parse errors - while ( - lines[i + 1]?.match(/^( )/) - ) { + while (lines[i + 1]?.match(/^( )/)) { message += "\n" + lines[++i]; } - const severity = kind === 'ERROR' ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Info + const severity = + kind === "ERROR" + ? monaco.MarkerSeverity.Error + : monaco.MarkerSeverity.Info; markers.push({ severity, @@ -82,19 +177,8 @@ const processOutput = (output: string) => { endLineNumber: lineNumber, startColumn: column, endColumn, - }) + }); } } - console.log('setMarkers', markers) - monaco.editor.setModelMarkers(model, 'newt', markers) -} - -editor.onDidChangeModelContent((ev) => { - console.log("mc", ev); - let value = editor.getValue(); - clearTimeout(timeout); - timeout = setTimeout(() => run(value), 1000); - localStorage.code = value; -}); - -run(value); + monaco.editor.setModelMarkers(model, "newt", markers); +}; diff --git a/playground/style.css b/playground/style.css index 0d12998..9435ea9 100644 --- a/playground/style.css +++ b/playground/style.css @@ -12,10 +12,32 @@ } .wrapper > div { flex: 1 1; + width: 100px; } -#result { +.tabPanel { + display: flex; + flex-direction: column; +} +.tabBar { + display:flex; + flex-direction: row; + gap: 10px; + margin: 10px 0; + border-bottom: solid 1px black; +} +.tab { + padding: 4px; + border: solid 1px transparent; +} +.tab.selected { + border: solid 1px black; + border-bottom: 0px +} + +#result, #javascript { overflow: auto; font-family: 'Comic Code', monospace; font-size: 12px; white-space: pre; + padding: 5px; }