From e167d7e6291f2a3efed341589b6bfd7480e21d31 Mon Sep 17 00:00:00 2001 From: Steve Dunham Date: Tue, 17 Jun 2025 11:48:01 -0700 Subject: [PATCH] switch to codemirror --- playground/.eslintrc | 20 ++ playground/index.html | 2 +- playground/package-lock.json | 412 +++++++++++++++++++++++++++-------- playground/package.json | 4 + playground/src/cmeditor.ts | 196 +++++++++++++++++ playground/src/main.ts | 265 ++++++++++------------ playground/src/monaco.ts | 210 ++++++++++++++++++ playground/src/types.ts | 46 ++++ playground/src/worker.ts | 4 +- playground/style.css | 8 +- playground/tsconfig.json | 2 +- 11 files changed, 925 insertions(+), 244 deletions(-) create mode 100644 playground/.eslintrc create mode 100644 playground/src/cmeditor.ts create mode 100644 playground/src/monaco.ts diff --git a/playground/.eslintrc b/playground/.eslintrc new file mode 100644 index 0000000..db7f5ad --- /dev/null +++ b/playground/.eslintrc @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true, + "classes": true, + "defaultParams": true + } + }, + "rules": { + "curly": ["error", "multi"] + } +} diff --git a/playground/index.html b/playground/index.html index d0bd34e..7ee3ffc 100644 --- a/playground/index.html +++ b/playground/index.html @@ -3,7 +3,7 @@ - + Newt Playground diff --git a/playground/package-lock.json b/playground/package-lock.json index 8c07732..2b4ef72 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -9,15 +9,113 @@ "version": "0.0.0", "dependencies": { "@preact/signals": "^1.3.0", + "codemirror": "^6.0.1", "monaco-editor": "^0.52.0", "preact": "^10.24.3" }, "devDependencies": { + "@codemirror/theme-one-dark": "^6.1.2", + "@lezer/generator": "^1.7.3", + "@lezer/lr": "^1.4.2", "esbuild": "^0.25.0", "typescript": "~5.6.2", "vite": "^6.1.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz", + "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.37.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz", + "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -443,6 +541,50 @@ "node": ">=18" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/generator": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.3.tgz", + "integrity": "sha512-vAI2O1tPF8QMMgp+bdUeeJCneJNkOZvqsrtyb4ohnFVFdboSqPwBEacnt0HH4E+5h+qsIwTHUSAhffU4hzKl1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.1.0", + "@lezer/lr": "^1.3.0" + }, + "bin": { + "lezer-generator": "src/lezer-generator.cjs" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@preact/signals": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.2.tgz", @@ -470,9 +612,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz", - "integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", "cpu": [ "arm" ], @@ -484,9 +626,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz", - "integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", "cpu": [ "arm64" ], @@ -498,9 +640,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz", - "integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", "cpu": [ "arm64" ], @@ -512,9 +654,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz", - "integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", "cpu": [ "x64" ], @@ -526,9 +668,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz", - "integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", "cpu": [ "arm64" ], @@ -540,9 +682,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz", - "integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", "cpu": [ "x64" ], @@ -554,9 +696,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz", - "integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", "cpu": [ "arm" ], @@ -568,9 +710,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz", - "integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", "cpu": [ "arm" ], @@ -582,9 +724,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz", - "integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", "cpu": [ "arm64" ], @@ -596,9 +738,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz", - "integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", "cpu": [ "arm64" ], @@ -610,9 +752,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz", - "integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", "cpu": [ "loong64" ], @@ -624,9 +766,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz", - "integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", "cpu": [ "ppc64" ], @@ -638,9 +780,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz", - "integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", "cpu": [ "riscv64" ], @@ -652,9 +808,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz", - "integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", "cpu": [ "s390x" ], @@ -666,9 +822,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz", - "integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", "cpu": [ "x64" ], @@ -680,9 +836,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz", - "integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", "cpu": [ "x64" ], @@ -694,9 +850,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz", - "integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", "cpu": [ "arm64" ], @@ -708,9 +864,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz", - "integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", "cpu": [ "ia32" ], @@ -722,9 +878,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz", - "integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", "cpu": [ "x64" ], @@ -736,12 +892,33 @@ ] }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true, "license": "MIT" }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -783,6 +960,21 @@ "@esbuild/win32-x64": "0.25.0" } }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -830,6 +1022,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -870,13 +1075,13 @@ } }, "node_modules/rollup": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz", - "integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -886,25 +1091,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.7", - "@rollup/rollup-android-arm64": "4.34.7", - "@rollup/rollup-darwin-arm64": "4.34.7", - "@rollup/rollup-darwin-x64": "4.34.7", - "@rollup/rollup-freebsd-arm64": "4.34.7", - "@rollup/rollup-freebsd-x64": "4.34.7", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.7", - "@rollup/rollup-linux-arm-musleabihf": "4.34.7", - "@rollup/rollup-linux-arm64-gnu": "4.34.7", - "@rollup/rollup-linux-arm64-musl": "4.34.7", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.7", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.7", - "@rollup/rollup-linux-riscv64-gnu": "4.34.7", - "@rollup/rollup-linux-s390x-gnu": "4.34.7", - "@rollup/rollup-linux-x64-gnu": "4.34.7", - "@rollup/rollup-linux-x64-musl": "4.34.7", - "@rollup/rollup-win32-arm64-msvc": "4.34.7", - "@rollup/rollup-win32-ia32-msvc": "4.34.7", - "@rollup/rollup-win32-x64-msvc": "4.34.7", + "@rollup/rollup-android-arm-eabi": "4.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" } }, @@ -918,6 +1124,29 @@ "node": ">=0.10.0" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -933,15 +1162,18 @@ } }, "node_modules/vite": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", - "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -1003,6 +1235,12 @@ "optional": true } } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" } } } diff --git a/playground/package.json b/playground/package.json index acb22a3..c0a6ae4 100644 --- a/playground/package.json +++ b/playground/package.json @@ -9,12 +9,16 @@ "preview": "vite preview" }, "devDependencies": { + "@codemirror/theme-one-dark": "^6.1.2", + "@lezer/generator": "^1.7.3", + "@lezer/lr": "^1.4.2", "esbuild": "^0.25.0", "typescript": "~5.6.2", "vite": "^6.1.0" }, "dependencies": { "@preact/signals": "^1.3.0", + "codemirror": "^6.0.1", "monaco-editor": "^0.52.0", "preact": "^10.24.3" } diff --git a/playground/src/cmeditor.ts b/playground/src/cmeditor.ts new file mode 100644 index 0000000..bc51ce1 --- /dev/null +++ b/playground/src/cmeditor.ts @@ -0,0 +1,196 @@ +import { AbstractEditor, EditorDelegate, Marker } from "./types"; +import { basicSetup } from "codemirror"; +import { EditorView, hoverTooltip, Tooltip } from "@codemirror/view"; +import { Compartment } from "@codemirror/state"; +import { parser } from "./parser.js"; +import { oneDark, oneDark as themeOneDark } from "@codemirror/theme-one-dark"; +import { styleTags, tags as t } from "@lezer/highlight"; +import { linter, Diagnostic } from "@codemirror/lint"; +import { + LanguageSupport, + LRLanguage, + StreamLanguage, + StringStream, +} from "@codemirror/language"; + +let parserWithMetadata = parser.configure({ + props: [ + styleTags({ + Identifier: t.variableName, + LineComment: t.lineComment, + "if then else data where": t.keyword, + }), + // indentNodeProp, foldNodeProp + ], +}); + +const newtLanguage = LRLanguage.define({ + parser: parserWithMetadata, + languageData: { + commentTokens: { + line: "--", + }, + }, +}); +// prettier did this... +const keywords = [ + "let", + "in", + "where", + "case", + "of", + "data", + "U", + "do", + "ptype", + "pfunc", + "module", + "infixl", + "infixr", + "infix", + "∀", + "forall", + "import", + "uses", + "class", + "instance", + "record", + "constructor", + "if", + "then", + "else", + "$", + "λ", + "?", + "@", + ".", + "->", + "→", + ":", + "=>", + ":=", + "$=", + "=", + "<-", + "\\", + "_", + "|", +]; + +interface State { + tokenizer(stream: StringStream, state: State): string | null; +} + +function tokenizer(stream: StringStream, state: State): string | null { + stream.eatSpace(); + if (stream.match("--")) { + stream.skipToEnd(); + return "comment"; + } + if (stream.match(/^[/]-/)) { + state.tokenizer = commentTokenizer; + return state.tokenizer(stream, state); + } + if (stream.match(/^[\w_][\w\d_']*/)) { + let word = stream.current(); + if (keywords.includes(word)) return "keyword"; + if (word[0] >= "A" && word[0] <= "Z") return "typename"; + return "identifier"; + } + // unhandled + stream.next(); + return null; +} + +function commentTokenizer(stream: StringStream, state: State): string | null { + console.log("ctok"); + let dash = false; + let ch; + while ((ch = stream.next())) { + if (dash && ch === "/") { + state.tokenizer = tokenizer; + return "comment"; + } + dash = ch === "-"; + } + console.log("XX", stream.current()); + return "comment"; +} + +const newtLanguage2 = StreamLanguage.define({ + startState: () => ({ tokenizer }), + token(stream, st) { + return st.tokenizer(stream, st); + }, +}); + +function newt() { + return new LanguageSupport(newtLanguage2); +} + +export class CMEditor implements AbstractEditor { + view: EditorView; + delegate: EditorDelegate; + theme: Compartment; + constructor(container: HTMLElement, doc: string, delegate: EditorDelegate) { + this.delegate = delegate; + this.theme = new Compartment(); + + this.view = new EditorView({ + doc, + parent: container, + extensions: [ + basicSetup, + linter((view) => this.delegate.lint(view)), + this.theme.of(EditorView.baseTheme({})), + hoverTooltip((view, pos) => { + let cursor = this.view.state.doc.lineAt(pos); + let line = cursor.number; + let range = this.view.state.wordAt(pos); + console.log(range); + if (range) { + let col = range.from - cursor.from; + let word = this.view.state.doc.sliceString(range.from, range.to); + let entry = this.delegate.getEntry(word, line, col); + console.log("entry", entry); + if (entry) { + let rval: Tooltip = { + pos: range.head, + above: true, + create: () => { + let dom = document.createElement("div"); + dom.className = "tooltip"; + dom.textContent = entry.type; + return { dom }; + }, + }; + return rval; + } + } + // we'll iterate the syntax tree for word. + // let entry = delegate.getEntry(word, line, col) + return null; + }), + newt(), + ], + }); + } + setDark(isDark: boolean) { + this.view.dispatch({ + effects: this.theme.reconfigure( + isDark ? oneDark : EditorView.baseTheme({}) + ), + }); + } + setValue(_doc: string) { + // Is this all we can do? + this.view.dispatch({ + changes: { from: 0, to: this.view.state.doc.length, insert: _doc }, + }); + } + getValue() { + // maybe? + return this.view.state.doc.toString(); + } + setMarkers(_: Marker[]) {} +} diff --git a/playground/src/main.ts b/playground/src/main.ts index a827627..57a88dd 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,91 +1,25 @@ import { effect, signal } from "@preact/signals"; -import { newtConfig, newtTokens } from "./monarch.ts"; -import * as monaco from "monaco-editor"; +import { Diagnostic } from "@codemirror/lint"; import { useEffect, useRef, useState } from "preact/hooks"; import { h, render } from "preact"; import { ChangeEvent } from "preact/compat"; import { archive, preload } from "./preload.ts"; -import { CompileReq, CompileRes, Message } from "./types.ts"; -import { ABBREV } from "./abbrev.ts"; - -// editor.(createModel / setModel / getModels) to switch files - -// TODO - remember files and allow switching? - -// static zip filesystem with user changes overlaid via localStorage -// download individual files (we could use the cheap compression from the pdflib or no compression to make zip download) -// would need way to reset/delete - -interface FC { - file: string; - line: number; - col: number; -} - -interface TopEntry { - fc: FC; - name: String; - type: String; -} -interface TopData { - context: TopEntry[]; -} +import { + AbstractEditor, + EditorDelegate, + CompileReq, + CompileRes, + Message, + TopData, + Marker, +} from "./types.ts"; +import { CMEditor } from "./cmeditor.ts"; let topData: undefined | TopData; -// we need to fix the definition of word -monaco.languages.register({ id: "newt" }); -monaco.languages.setMonarchTokensProvider("newt", newtTokens); -monaco.languages.setLanguageConfiguration("newt", newtConfig); -monaco.languages.registerDefinitionProvider("newt", { - provideDefinition(model, position, token) { - if (!topData) return; - // HACK - we don't know our filename which was generated from `module` decl, so - // assume the last context entry is in our file. - let last = topData.context[topData.context.length - 1]; - let file = last.fc.file; - - const info = model.getWordAtPosition(position); - if (!info) return; - let entry = topData.context.find((entry) => entry.name === info.word); - // we can't switch files at the moment - if (!entry || entry.fc.file !== file) return; - let lineNumber = entry.fc.line + 1; - let column = entry.fc.col + 1; - let word = model.getWordAtPosition({ lineNumber, column }); - let range = new monaco.Range(lineNumber, column, lineNumber, column); - if (word) { - range = new monaco.Range( - lineNumber, - word.startColumn, - lineNumber, - word.endColumn - ); - } - return { uri: model.uri, range }; - }, -}); -monaco.languages.registerHoverProvider("newt", { - provideHover(model, position, token, context) { - if (!topData) return; - const info = model.getWordAtPosition(position); - if (!info) return; - let entry = topData.context.find((entry) => entry.name === info.word); - if (!entry) return; - return { - range: new monaco.Range( - position.lineNumber, - info.startColumn, - position.lineNumber, - info.endColumn - ), - contents: [{ value: `${entry.name} : ${entry.type}` }], - }; - }, -}); const newtWorker = new Worker("worker.js"); +// :FIXME use because Safari let postMessage = (msg: CompileReq) => newtWorker.postMessage(msg); - // Safari/MobileSafari have small stacks in webworkers. if (navigator.vendor.includes("Apple")) { const workerFrame = document.createElement("iframe"); @@ -104,7 +38,7 @@ document.body.appendChild(iframe); function run(src: string) { console.log("SEND TO", iframe.contentWindow); const fileName = state.currentFile.value; - postMessage({ type: "compileRequest", fileName, src }); + // postMessage({ type: "compileRequest", fileName, src }); } function runOutput() { @@ -134,6 +68,9 @@ function setOutput(output: string) { state.output.value = output; } +let lastID = 0; +const nextID = () => "" + lastID++; + window.onmessage = (ev: MessageEvent) => { console.log("window got", ev.data); if ("messages" in ev.data) state.messages.value = ev.data.messages; @@ -142,28 +79,47 @@ window.onmessage = (ev: MessageEvent) => { } // safari callback if ("output" in ev.data) { + newtWorker.onmessage?.(ev) setOutput(ev.data.output); state.javascript.value = ev.data.javascript; } }; +// TODO wrap up IPC + +type Suspense = { + resolve: (value: T | PromiseLike) => void; + reject: (reason?: any) => void; +}; + +const callbacks: Record> = {}; + newtWorker.onmessage = (ev: MessageEvent) => { + let suspense = callbacks[ev.data.id]; + if (suspense) { + suspense.resolve(ev.data.output); + delete callbacks[ev.data.id]; + } + console.log('result', ev.data, 'suspense', suspense) + // FIXME - we want to have the callback take a response for its command setOutput(ev.data.output); state.javascript.value = ev.data.javascript; }; -self.MonacoEnvironment = { - getWorkerUrl(moduleId, _label) { - console.log("Get worker", moduleId); - return moduleId; - }, -}; +function runCommand(req: CompileReq) { + return new Promise( + (resolve, reject) => { + callbacks[req.id] = { resolve, reject } + postMessage(req); + } + ); +} const state = { output: signal(""), javascript: signal(""), messages: signal([]), - editor: signal(null), + editor: signal(null), dark: signal(false), files: signal(["Tour.newt"]), currentFile: signal(localStorage.currentFile ?? "Tour.newt"), @@ -174,13 +130,15 @@ if (window.matchMedia) { function checkDark(ev: { matches: boolean }) { console.log("CHANGE", ev); if (ev.matches) { - monaco.editor.setTheme("vs-dark"); + // monaco.editor.setTheme("vs-dark"); document.body.className = "dark"; state.dark.value = true; + state.editor.value?.setDark(true); } else { - monaco.editor.setTheme("vs"); + // monaco.editor.setTheme("vs"); document.body.className = "light"; state.dark.value = false; + state.editor.value?.setDark(false); } } let query = window.matchMedia("(prefers-color-scheme: dark)"); @@ -215,69 +173,76 @@ let initialVertical = localStorage.vertical == "true"; effect(() => { let text = state.output.value; let editor = state.editor.value; - if (editor) processOutput(editor, text); + // TODO - abstract this for both editors + // if (editor) processOutput(editor, text); }); interface EditorProps { initialValue: string; } +const language: EditorDelegate = { + getEntry(word, _row, _col) { + return topData?.context.find((entry) => entry.name === word); + }, + onChange(value) { + // clearTimeout(timeout); + // timeout = setTimeout(() => { + // run(value); + // localStorage.code = value; + // }, 1000); + }, + getFileName() { + if (!topData) return ""; + let last = topData.context[topData.context.length - 1]; + return last.fc.file; + }, + async lint(view) { + console.log("LINT"); + let src = view.state.doc.toString(); + const fileName = state.currentFile.value; + console.log("FN", fileName); + // console.log("SRC", src); + try { + let out = await runCommand({ + id: nextID(), + type: "compileRequest", + fileName, + src, + }); + console.log("OUT", out); + let markers = processOutput(out); + let diags: Diagnostic[] = [] + for (let marker of markers) { + let col = marker.startColumn + + let line = view.state.doc.line(marker.startLineNumber) + const pos = line.from + col - 1; + let word = view.state.wordAt(pos) + diags.push({ + from: word?.from ?? pos, + to: word?.to ?? pos+1, + severity: marker.severity, + message: marker.message, + }) + } + return diags + } catch (e) { + console.log("ERR", e); + } + + return []; + }, +}; function Editor({ initialValue }: EditorProps) { const ref = useRef(null); + useEffect(() => { const container = ref.current!; - const editor = monaco.editor.create(container, { - value, - language: "newt", - fontFamily: "Comic Code, Menlo, Monaco, Courier New, sans", - automaticLayout: true, - acceptSuggestionOnEnter: "off", - unicodeHighlight: { ambiguousCharacters: false }, - minimap: { enabled: false }, - }); + const editor = new CMEditor(container, value, language); + // const editor = new MonacoEditor(container, value, language) state.editor.value = editor; - - editor.onDidChangeModelContent((ev) => { - clearTimeout(timeout); - timeout = setTimeout(() => { - let value = editor.getValue(); - run(value); - localStorage.code = value; - }, 1000); - let last = ev.changes[ev.changes.length - 1]; - const model = editor.getModel(); - // figure out heuristics here, what chars do we want to trigger? - // the lean one will only be active if it sees you type \ - // and bail if it decides you've gone elsewhere - // it maintains an underline annotation, too. - if (model && last.text && " ')\\".includes(last.text)) { - console.log('LAST', last) - let { startLineNumber, startColumn } = last.range; - const text = model.getValueInRange( - new monaco.Range( - startLineNumber, - Math.max(1, startColumn - 10), - startLineNumber, - startColumn - ) - ); - const m = text.match(/(\\[^ ]+)$/); - if (m) { - let cand = m[0]; - console.log("GOT", cand); - let text = ABBREV[cand]; - if (text) { - const range = new monaco.Range( - startLineNumber, - startColumn - cand.length, - startLineNumber, - startColumn - ); - editor.executeEdits("replaceSequence", [{ range, text: text }]); - } - } - } - }); + editor.setDark(state.dark.value); if (initialValue === LOADING) loadFile("Tour.newt"); else run(initialValue); }, []); @@ -431,11 +396,12 @@ let timeout: number | undefined; // Adapted from the vscode extension, but types are slightly different // and positions are 1-based. const processOutput = ( - editor: monaco.editor.IStandaloneCodeEditor, + // editor: AbstractEditor, output: string ) => { - let model = editor.getModel()!; - let markers: monaco.editor.IMarkerData[] = []; + // let model = editor.getModel()!; + console.log('process output', output) + let markers: Marker[] = []; let lines = output.split("\n"); let m = lines[0].match(/.*Process (.*)/); let fn = ""; @@ -451,11 +417,8 @@ const processOutput = ( if (fn && fn !== file) { lineNumber = column = 0; } - 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; // heuristics to grab the entire message: // anything indented @@ -464,13 +427,9 @@ const processOutput = ( while (lines[i + 1]?.match(/^( )/)) { message += "\n" + lines[++i]; } - const severity = - kind === "ERROR" - ? monaco.MarkerSeverity.Error - : monaco.MarkerSeverity.Info; if (kind === "ERROR" || lineNumber) markers.push({ - severity, + severity: kind === 'ERROR' ? 'error' : 'info', message, startLineNumber: lineNumber, endLineNumber: lineNumber, @@ -479,5 +438,7 @@ const processOutput = ( }); } } - monaco.editor.setModelMarkers(model, "newt", markers); + console.log('markers', markers) + // editor.setMarkers(markers) + return markers; }; diff --git a/playground/src/monaco.ts b/playground/src/monaco.ts new file mode 100644 index 0000000..d103023 --- /dev/null +++ b/playground/src/monaco.ts @@ -0,0 +1,210 @@ +import { ABBREV } from "./abbrev.ts"; +import { newtConfig, newtTokens } from "./monarch.ts"; +import * as monaco from "monaco-editor"; +import { AbstractEditor, EditorDelegate, Marker } from "./types.ts"; + +// we need to fix the definition of word +monaco.languages.register({ id: "newt" }); +monaco.languages.setMonarchTokensProvider("newt", newtTokens); +monaco.languages.setLanguageConfiguration("newt", newtConfig); + +self.MonacoEnvironment = { + getWorkerUrl(moduleId, _label) { + console.log("Get worker", moduleId); + return moduleId; + }, +}; + +export class MonacoEditor implements AbstractEditor { + editor: monaco.editor.IStandaloneCodeEditor; + delegate: EditorDelegate; + + constructor(container: HTMLElement, value: string, language: EditorDelegate) { + this.delegate = language; + let editor = (this.editor = monaco.editor.create(container, { + value, + language: "newt", + fontFamily: "Comic Code, Menlo, Monaco, Courier New, sans", + automaticLayout: true, + acceptSuggestionOnEnter: "off", + unicodeHighlight: { ambiguousCharacters: false }, + minimap: { enabled: false }, + })); + monaco.languages.registerDefinitionProvider("newt", { + provideDefinition(model, position, token) { + const info = model.getWordAtPosition(position); + if (!info) return; + let entry = language.getEntry( + info.word, + position.lineNumber, + info.startColumn + ); + if (!entry) return; + + // HACK - we don't know our filename which was generated from `module` decl, so + // assume the last context entry is in our file. + + let file = language.getFileName(); + if (!entry || entry.fc.file !== file) return; + let lineNumber = entry.fc.line + 1; + let column = entry.fc.col + 1; + let word = model.getWordAtPosition({ lineNumber, column }); + let range = new monaco.Range(lineNumber, column, lineNumber, column); + if (word) { + range = new monaco.Range( + lineNumber, + word.startColumn, + lineNumber, + word.endColumn + ); + } + return { uri: model.uri, range }; + }, + }); + monaco.languages.registerHoverProvider("newt", { + provideHover(model, position, token, context) { + const info = model.getWordAtPosition(position); + if (!info) return; + let entry = language.getEntry( + info.word, + position.lineNumber, + info.startColumn + ); + if (!entry) return; + return { + range: new monaco.Range( + position.lineNumber, + info.startColumn, + position.lineNumber, + info.endColumn + ), + contents: [{ value: `${entry.name} : ${entry.type}` }], + }; + }, + }); + editor.onDidChangeModelContent((ev) => { + this.delegate.onChange(editor.getValue()); + + let last = ev.changes[ev.changes.length - 1]; + const model = editor.getModel(); + // figure out heuristics here, what chars do we want to trigger? + // the lean one will only be active if it sees you type \ + // and bail if it decides you've gone elsewhere + // it maintains an underline annotation, too. + if (model && last.text && " ')\\".includes(last.text)) { + console.log("LAST", last); + let { startLineNumber, startColumn } = last.range; + const text = model.getValueInRange( + new monaco.Range( + startLineNumber, + Math.max(1, startColumn - 10), + startLineNumber, + startColumn + ) + ); + const m = text.match(/(\\[^ ]+)$/); + if (m) { + let cand = m[0]; + console.log("GOT", cand); + let text = ABBREV[cand]; + if (text) { + const range = new monaco.Range( + startLineNumber, + startColumn - cand.length, + startLineNumber, + startColumn + ); + editor.executeEdits("replaceSequence", [{ range, text: text }]); + } + } + } + }); + } + setValue(value: string) { + this.editor.setValue(value); + } + getValue() { + return this.editor.getValue(); + } + setMarkers(markers: Marker[]) { + let model = this.editor.getModel()!; + const mapMarker = (marker: Marker): monaco.editor.IMarkerData => { + let severity = + marker.severity === "ERROR" + ? monaco.MarkerSeverity.Error + : monaco.MarkerSeverity.Info; + // translate to surrounding word + // FIXME - we have `getWord` in monaco, but probably belongs to the delegate + // and eventually we have better FC + let { message, startColumn, endColumn, startLineNumber, endLineNumber } = + marker; + let word = model.getWordAtPosition({ + column: startColumn, + lineNumber: startLineNumber, + }); + if (word) endColumn = word.endColumn; + return { + message, + startColumn, + endColumn, + startLineNumber, + endLineNumber, + severity, + }; + }; + monaco.editor.setModelMarkers(model, "newt", markers.map(mapMarker)); + } +} + +// scratch +const processOutput = ( + editor: monaco.editor.IStandaloneCodeEditor, + output: string +) => { + let model = editor.getModel()!; + let markers: monaco.editor.IMarkerData[] = []; + let lines = output.split("\n"); + let m = lines[0].match(/.*Process (.*)/); + let fn = ""; + if (m) fn = m[1]; + 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, file, line, col, message] = match; + let lineNumber = +line + 1; + let column = +col + 1; + // FIXME - pass the real path in + if (fn && fn !== file) { + lineNumber = column = 0; + } + 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; + + // 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(/^( )/)) { + message += "\n" + lines[++i]; + } + const severity = + kind === "ERROR" + ? monaco.MarkerSeverity.Error + : monaco.MarkerSeverity.Info; + if (kind === "ERROR" || lineNumber) + markers.push({ + severity, + message, + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: column, + endColumn, + }); + } + } + monaco.editor.setModelMarkers(model, "newt", markers); +}; diff --git a/playground/src/types.ts b/playground/src/types.ts index ec29e48..6cdd5f7 100644 --- a/playground/src/types.ts +++ b/playground/src/types.ts @@ -1,10 +1,15 @@ +import { EditorView } from "codemirror"; +import { linter, Diagnostic } from "@codemirror/lint"; + export interface CompileReq { + id: string type: "compileRequest"; fileName: string; src: string; } export interface CompileRes { + id: string type: "compileResult"; output: string; javascript: string; @@ -26,3 +31,44 @@ export interface ExecCode { } export type Message = CompileReq | CompileRes | ConsoleList | ConsoleItem | ExecCode +// editor.(createModel / setModel / getModels) to switch files +// TODO - remember files and allow switching? +// static zip filesystem with user changes overlaid via localStorage +// download individual files (we could use the cheap compression from the pdflib or no compression to make zip download) +// would need way to reset/delete +export interface FC { + file: string; + line: number; + col: number; +} + +interface TopEntry { + fc: FC; + name: string; + type: string; +} + +export interface TopData { + context: TopEntry[]; +} +export interface EditorDelegate { + getEntry(word: string, row: number, col: number): TopEntry | undefined + onChange(value: string): unknown + getFileName(): string + lint(view: EditorView): Promise | Diagnostic[] +} +export interface Marker { + severity: 'error' | 'info' | 'warning' + message: string + startColumn: number + startLineNumber: number + endColumn: number + endLineNumber: number +} +export interface AbstractEditor { + setValue: (_: string) => unknown; + getValue: () => string; + setMarkers: (_: Marker[]) => unknown + setDark(isDark: boolean): unknown +} + diff --git a/playground/src/worker.ts b/playground/src/worker.ts index 4860821..1875ff2 100644 --- a/playground/src/worker.ts +++ b/playground/src/worker.ts @@ -10,7 +10,7 @@ const handleMessage = async function (ev: { data: CompileReq }) { console.log("message", ev.data); await preload; shim.archive = archive; - let { src, fileName } = ev.data; + let { id, src, fileName } = ev.data; const outfile = "out.js"; shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"]; shim.files[fileName] = new TextEncoder().encode(src); @@ -29,7 +29,7 @@ const handleMessage = async function (ev: { data: CompileReq }) { console.log(`process ${fileName} in ${duration} ms`); let javascript = new TextDecoder().decode(shim.files[outfile]); let output = shim.stdout; - sendResponse({ type: 'compileResult', javascript, output, duration }); + sendResponse({ id, type: 'compileResult', javascript, output, duration }); }; // hooks for worker.html to override diff --git a/playground/style.css b/playground/style.css index 28c51eb..e5bdde2 100644 --- a/playground/style.css +++ b/playground/style.css @@ -1,4 +1,7 @@ - +body { + overflow: hidden; + font-size: 12px; +} svg.icon path { stroke: black; fill: none; @@ -87,3 +90,6 @@ svg.icon path { white-space: pre; padding: 5px; } +.tooltip { + padding: 5px; +} diff --git a/playground/tsconfig.json b/playground/tsconfig.json index a0eb396..b0f79b8 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -5,7 +5,7 @@ "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, - + "allowJs": true, "outDir": "out", "sourceMap": true,