From c74e59485d3f11998ba47884d696d6a5276e34ab Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 29 Jun 2026 18:18:31 +0800 Subject: [PATCH] feat(editor): add codemirror-editor subproject + Rust bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新建 libs/codemirror-editor(pnpm/Vite IIFE/@catppuccin/codemirror Catppuccin Latte+Mocha 主题/@codemirror/lang-sql schema 补全/@replit/codemirror-vim), 输出 public/codemirror/。Rust 桥接 codemirror_bridge.rs 镜像 tiptap_bridge.rs (Reflect::get 取对象字面量 + EditorHandle 持有闭包 + Drop→destroy)。 接入 Makefile/Dioxus.toml/.gitignore。 注意:thememirror 不含 catppuccin 主题,改用官方 @catppuccin/codemirror@1.0.3 (导出 catppuccinLatte/catppuccinMocha)。 --- .gitignore | 1 + Dioxus.toml | 4 +- Makefile | 16 +- libs/codemirror-editor/package.json | 30 + libs/codemirror-editor/pnpm-lock.yaml | 1041 +++++++++++++++++ .../src/__tests__/editor.test.ts | 67 ++ libs/codemirror-editor/src/editor.ts | 127 ++ libs/codemirror-editor/src/index.ts | 31 + libs/codemirror-editor/src/themes.ts | 11 + libs/codemirror-editor/tsconfig.json | 19 + libs/codemirror-editor/vite.config.ts | 27 + libs/codemirror-editor/vitest.config.ts | 8 + src/codemirror_bridge.rs | 187 +++ src/main.rs | 3 + 14 files changed, 1568 insertions(+), 4 deletions(-) create mode 100644 libs/codemirror-editor/package.json create mode 100644 libs/codemirror-editor/pnpm-lock.yaml create mode 100644 libs/codemirror-editor/src/__tests__/editor.test.ts create mode 100644 libs/codemirror-editor/src/editor.ts create mode 100644 libs/codemirror-editor/src/index.ts create mode 100644 libs/codemirror-editor/src/themes.ts create mode 100644 libs/codemirror-editor/tsconfig.json create mode 100644 libs/codemirror-editor/vite.config.ts create mode 100644 libs/codemirror-editor/vitest.config.ts create mode 100644 src/codemirror_bridge.rs diff --git a/.gitignore b/.gitignore index 182a940..001d4e9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ others/ public/style.css public/highlight.css public/tiptap +public/codemirror public/lightbox public/yggdrasil-core public/doc diff --git a/Dioxus.toml b/Dioxus.toml index b4bdecb..a4333d2 100644 --- a/Dioxus.toml +++ b/Dioxus.toml @@ -11,8 +11,8 @@ watch_path = ["src", "Cargo.toml"] [web.resource] style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css", "/yggdrasil-core/yggdrasil-core.css"] -script = ["/tiptap/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"] +script = ["/tiptap/editor.js", "/codemirror/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"] [web.resource.dev] style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css", "/yggdrasil-core/yggdrasil-core.css"] -script = ["/tiptap/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"] +script = ["/tiptap/editor.js", "/codemirror/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"] diff --git a/Makefile b/Makefile index ebd869b..fba3b7e 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ -.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental highlight-css test doc doc-open start clippy fix +.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental build-codemirror build-codemirror-incremental highlight-css test doc doc-open start clippy fix build: @$(MAKE) build-editor @$(MAKE) build-lightbox @$(MAKE) build-core + @$(MAKE) build-codemirror @$(MAKE) highlight-css @tailwindcss -i input.css -o public/style.css --minify @$(MAKE) doc @@ -13,6 +14,7 @@ build-linux: @$(MAKE) build-editor @$(MAKE) build-lightbox @$(MAKE) build-core + @$(MAKE) build-codemirror @$(MAKE) highlight-css @tailwindcss -i input.css -o public/style.css --minify @dx build @client --release --debug-symbols=false --wasm-js-cfg false @@ -53,7 +55,16 @@ build-core: build-core-incremental: @cd libs/yggdrasil-core && pnpm run build -dev: build-editor-incremental build-lightbox-incremental build-core-incremental +build-codemirror: + @echo "Building CodeMirror editor..." + @cd libs/codemirror-editor && pnpm ci --include=dev && pnpm run build + @echo "CodeMirror editor built." + +# dev 用的增量构建:跳过 pnpm ci(假设 node_modules 已存在),仅 vite build。 +build-codemirror-incremental: + @cd libs/codemirror-editor && pnpm run build + +dev: build-editor-incremental build-lightbox-incremental build-core-incremental build-codemirror-incremental @echo "Cleaning static/..." @rm -rf static/ @echo "Building Tiptap editor (incremental)..." @@ -74,6 +85,7 @@ test: @cd libs/tiptap-editor && pnpm test @cd libs/lightbox && pnpm test @cd libs/yggdrasil-core && pnpm test + @cd libs/codemirror-editor && pnpm test clippy: @cargo clippy --all-targets --all-features -- -D warnings diff --git a/libs/codemirror-editor/package.json b/libs/codemirror-editor/package.json new file mode 100644 index 0000000..062f587 --- /dev/null +++ b/libs/codemirror-editor/package.json @@ -0,0 +1,30 @@ +{ + "name": "@yggdrasil/codemirror-editor", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc --noEmit && vite build", + "dev": "vite", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@catppuccin/codemirror": "^1.0.3", + "@codemirror/autocomplete": "^6.18.0", + "@codemirror/commands": "^6.8.0", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/language": "^6.11.0", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.36.0", + "@replit/codemirror-vim": "^6.3.0", + "codemirror": "^6.0.1" + }, + "devDependencies": { + "happy-dom": "^20.10.6", + "typescript": "^6.0.3", + "vite": "^8.1.0", + "vitest": "^4.1.9" + } +} diff --git a/libs/codemirror-editor/pnpm-lock.yaml b/libs/codemirror-editor/pnpm-lock.yaml new file mode 100644 index 0000000..8d24057 --- /dev/null +++ b/libs/codemirror-editor/pnpm-lock.yaml @@ -0,0 +1,1041 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@catppuccin/codemirror': + specifier: ^1.0.3 + version: 1.0.3 + '@codemirror/autocomplete': + specifier: ^6.18.0 + version: 6.20.3 + '@codemirror/commands': + specifier: ^6.8.0 + version: 6.10.4 + '@codemirror/lang-sql': + specifier: ^6.10.0 + version: 6.10.0 + '@codemirror/language': + specifier: ^6.11.0 + version: 6.12.4 + '@codemirror/state': + specifier: ^6.5.0 + version: 6.7.0 + '@codemirror/view': + specifier: ^6.36.0 + version: 6.43.4 + '@replit/codemirror-vim': + specifier: ^6.3.0 + version: 6.3.0(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4) + codemirror: + specifier: ^6.0.1 + version: 6.0.2 + devDependencies: + happy-dom: + specifier: ^20.10.6 + version: 20.10.6 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.1) + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@26.0.1)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.1)) + +packages: + + '@catppuccin/codemirror@1.0.3': + resolution: {integrity: sha512-P1ZCj4DZVLqr/TNz28m3aaF2Elrikpb8OOnzN0Vyf95Un4pTWTkCSvhbskbnJbnNJ87Rfvt3fXoaUj4o55X30Q==} + + '@catppuccin/palette@1.8.0': + resolution: {integrity: sha512-qXhwKiLzQomUygUJYB36YAFgs+dET5bIocfkiaFIatQF5Pwc7L112TlF9P8J5Oqs3x3XTjYSucG0ncHXSCuk7Q==} + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.0': + resolution: {integrity: sha512-Zbl9NyscLMZkfXPQnNAIIAFftidrA1UbcJEIMp24C0Bukc2I5T8wJS0wsXYsnDOqCFJUeJ1BITGNs5CqPDSmSg==} + + '@codemirror/view@6.43.4': + resolution: {integrity: sha512-YImu23iyKfncJzT7sRy+rEqEhSc8RhOHqDxwy4WzXRKJwYm6iwf/9OJk5ctCAdZ6yi2ZqaGEvmf55fSVqMDrgg==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@replit/codemirror-vim@6.3.0': + resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==} + peerDependencies: + '@codemirror/commands': 6.x.x + '@codemirror/language': 6.x.x + '@codemirror/search': 6.x.x + '@codemirror/state': 6.x.x + '@codemirror/view': 6.x.x + + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.0.1': + resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + happy-dom@20.10.6: + resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} + engines: {node: '>=20.0.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@catppuccin/codemirror@1.0.3': + dependencies: + '@catppuccin/palette': 1.8.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + '@lezer/highlight': 1.2.3 + + '@catppuccin/palette@1.8.0': {} + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + '@lezer/common': 1.5.2 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + crelt: 1.0.7 + + '@codemirror/state@6.7.0': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.4': + dependencies: + '@codemirror/state': 6.7.0 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.3': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.137.0': {} + + '@replit/codemirror-vim@6.3.0(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)': + dependencies: + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + + '@rolldown/binding-android-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@26.0.1': + dependencies: + undici-types: 8.3.0 + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.0.1 + + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.0(@types/node@26.0.1) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + assertion-error@2.0.1: {} + + buffer-image-size@0.6.4: + dependencies: + '@types/node': 26.0.1 + + chai@6.2.2: {} + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.0 + '@codemirror/view': 6.43.4 + + convert-source-map@2.0.0: {} + + crelt@1.0.7: {} + + detect-libc@2.1.2: {} + + entities@7.0.1: {} + + es-module-lexer@2.1.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + happy-dom@20.10.6: + dependencies: + '@types/node': 26.0.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.15: {} + + obug@2.1.3: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.1.3: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + style-mod@4.1.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tslib@2.8.1: + optional: true + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + vite@8.1.0(@types/node@26.0.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.0.1 + fsevents: 2.3.3 + + vitest@4.1.9(@types/node@26.0.1)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.1)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.0(@types/node@26.0.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.0.1 + happy-dom: 20.10.6 + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.0: {} diff --git a/libs/codemirror-editor/src/__tests__/editor.test.ts b/libs/codemirror-editor/src/__tests__/editor.test.ts new file mode 100644 index 0000000..9bbcf84 --- /dev/null +++ b/libs/codemirror-editor/src/__tests__/editor.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { EditorOptions, CodeMirrorInstance } from '../editor'; + +describe('CodeMirrorInstance', () => { + let container: HTMLElement; + + beforeEach(() => { + container = document.createElement('div'); + container.id = 'test-cm'; + document.body.appendChild(container); + }); + + it('getValue/setValue 往返', () => { + const inst = new CodeMirrorInstance(container, new EditorOptions()); + inst.setValue('SELECT 1'); + expect(inst.getValue()).toBe('SELECT 1'); + inst.destroy(); + }); + + it('初始 value 正确', () => { + const opts = new EditorOptions(); + opts.value = 'SELECT * FROM posts'; + const inst = new CodeMirrorInstance(container, opts); + expect(inst.getValue()).toBe('SELECT * FROM posts'); + inst.destroy(); + }); + + it('setTheme 不抛错(走 Compartment reconfigure)', () => { + const inst = new CodeMirrorInstance(container, new EditorOptions()); + expect(() => inst.setTheme('dark')).not.toThrow(); + expect(() => inst.setTheme('light')).not.toThrow(); + inst.destroy(); + }); + + it('setSchema 更新 lang-sql 配置', () => { + const inst = new CodeMirrorInstance(container, new EditorOptions()); + expect(() => + inst.setSchema({ tables: [{ name: 'posts', columns: ['id', 'title'] }] }), + ).not.toThrow(); + inst.destroy(); + }); + + it('vim 开关:vim:true 注入,false 不注入', () => { + const optsOn = new EditorOptions(); + optsOn.vim = true; + const instOn = new CodeMirrorInstance(container, optsOn); + instOn.destroy(); + + const optsOff = new EditorOptions(); + optsOff.vim = false; + const instOff = new CodeMirrorInstance(container, optsOff); + instOff.destroy(); + // happy-dom 无法验证 keymap 行为,仅验证配置加载不抛错 + }); + + it('onChange 在内容变更时触发', () => { + let captured = ''; + const opts = new EditorOptions(); + opts.onChange = (v) => { + captured = v; + }; + const inst = new CodeMirrorInstance(container, opts); + inst.setValue('hello'); + expect(captured).toBe('hello'); + inst.destroy(); + }); +}); diff --git a/libs/codemirror-editor/src/editor.ts b/libs/codemirror-editor/src/editor.ts new file mode 100644 index 0000000..f96a319 --- /dev/null +++ b/libs/codemirror-editor/src/editor.ts @@ -0,0 +1,127 @@ +import { EditorState, Compartment } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; +import { basicSetup } from 'codemirror'; +import { sql, PostgreSQL } from '@codemirror/lang-sql'; +import { vim } from '@replit/codemirror-vim'; +import { themeExtension, type ThemeName } from './themes'; + +/** SQL 补全用 schema 数据(由 Rust 侧从实时库拉取注入)。 */ +export interface SqlSchema { + tables: { name: string; columns: string[] }[]; +} + +/** + * 传给 CodeMirrorEditor.create 的配置。 + * 必须是 class(非 interface),以便 TS 擦除后存活, + * wasm 侧能用 `new EditorOptions()` 构造,并通过 setter 填充字段。 + */ +export class EditorOptions { + language?: string; + theme?: ThemeName; + vim?: boolean; + schema?: SqlSchema; + value?: string; + onChange?: (value: string) => void; + onReady?: () => void; +} + +/** + * CodeMirror 实例封装。 + * create() 时用三个 Compartment 注入 theme/schema/vim, + * 支持后续热切换(reconfigure)而不重建实例——保留 Vim 状态、光标、撤销栈。 + */ +export class CodeMirrorInstance { + private view: EditorView; + private themeCompartment = new Compartment(); + private schemaCompartment = new Compartment(); + private vimCompartment = new Compartment(); + + constructor(container: HTMLElement, options: EditorOptions) { + const theme: ThemeName = options.theme ?? 'light'; + const schema = options.schema ?? { tables: [] }; + + this.view = new EditorView({ + state: EditorState.create({ + doc: options.value ?? '', + extensions: [ + basicSetup, + // vim 必须在 keymap 最前(@replit/codemirror-vim 仓库要求) + this.vimCompartment.of(options.vim ? [vim()] : []), + this.themeCompartment.of(themeExtension(theme)), + this.schemaCompartment.of( + sql({ + dialect: PostgreSQL, + schema: schemaToCompletions(schema), + upperCaseKeywords: true, + }), + ), + EditorView.updateListener.of((v) => { + if (v.docChanged) { + options.onChange?.(this.view.state.doc.toString()); + } + }), + ], + }), + parent: container, + }); + + options.onReady?.(); + } + + getValue(): string { + return this.view.state.doc.toString(); + } + + setValue(s: string): void { + this.view.dispatch({ + changes: { from: 0, to: this.view.state.doc.length, insert: s }, + }); + } + + /** 热切换主题,不重建实例(Compartment.reconfigure)。 */ + setTheme(theme: ThemeName): void { + this.view.dispatch({ + effects: this.themeCompartment.reconfigure(themeExtension(theme)), + }); + } + + /** 更新 SQL 补全 schema(Compartment.reconfigure)。 */ + setSchema(schema: SqlSchema): void { + this.view.dispatch({ + effects: this.schemaCompartment.reconfigure( + sql({ + dialect: PostgreSQL, + schema: schemaToCompletions(schema), + upperCaseKeywords: true, + }), + ), + }); + } + + focus(): void { + this.view.focus(); + } + + destroy(): void { + this.view.destroy(); + } +} + +/** 把 SqlSchema 转成 @codemirror/lang-sql 期望的补全结构。 */ +function schemaToCompletions(schema: SqlSchema) { + return schema.tables.map((t) => ({ + label: t.name, + type: 'table', + detail: 'table', + columns: t.columns.map((c) => ({ label: c, type: 'column' })), + })); +} + +// 暴露 EditorOptions 到 window,供 wasm-bindgen 用 new EditorOptions()。 +// IIFE 的 name 只能挂一个全局(CodeMirrorEditor),故手动 hoist EditorOptions。 +declare global { + interface Window { + EditorOptions: typeof EditorOptions; + } +} +window.EditorOptions = EditorOptions; diff --git a/libs/codemirror-editor/src/index.ts b/libs/codemirror-editor/src/index.ts new file mode 100644 index 0000000..445b3c9 --- /dev/null +++ b/libs/codemirror-editor/src/index.ts @@ -0,0 +1,31 @@ +import { CodeMirrorInstance, EditorOptions } from './editor'; + +/** + * 模块入口:暴露对象字面量 { create } 作为默认导出。 + * IIFE 产物挂在 window.CodeMirrorEditor 上,由 Rust 侧用 Reflect::get 取 + * (对象字面量,不能用 wasm-bindgen 的 extern fn——那会被编成函数调用而失败)。 + */ +const CodeMirrorEditor = { + _instances: new Map(), + + create( + containerId: string, + options: EditorOptions = new EditorOptions(), + ): CodeMirrorInstance | null { + const container = document.getElementById(containerId); + if (!container) return null; + + // 销毁同 id 的旧实例 + const existing = this._instances.get(containerId); + if (existing) { + existing.destroy(); + this._instances.delete(containerId); + } + + const instance = new CodeMirrorInstance(container, options); + this._instances.set(containerId, instance); + return instance; + }, +}; + +export default CodeMirrorEditor; diff --git a/libs/codemirror-editor/src/themes.ts b/libs/codemirror-editor/src/themes.ts new file mode 100644 index 0000000..5632f99 --- /dev/null +++ b/libs/codemirror-editor/src/themes.ts @@ -0,0 +1,11 @@ +// @catppuccin/codemirror 提供现成的 Catppuccin 主题 Extension, +// 与项目 themes/ 下的 Catppuccin Latte/Mocha .tmTheme 视觉一致。 +import { catppuccinLatte, catppuccinMocha } from '@catppuccin/codemirror'; +import type { Extension } from '@codemirror/state'; + +export type ThemeName = 'light' | 'dark'; + +/** 根据主题名返回对应的 CodeMirror 主题 Extension。 */ +export function themeExtension(name: ThemeName): Extension { + return name === 'light' ? catppuccinLatte : catppuccinMocha; +} diff --git a/libs/codemirror-editor/tsconfig.json b/libs/codemirror-editor/tsconfig.json new file mode 100644 index 0000000..fd15971 --- /dev/null +++ b/libs/codemirror-editor/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "types": [] + }, + "include": ["src"] +} diff --git a/libs/codemirror-editor/vite.config.ts b/libs/codemirror-editor/vite.config.ts new file mode 100644 index 0000000..3682af9 --- /dev/null +++ b/libs/codemirror-editor/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + build: { + // 输出直写 public/codemirror/,Dioxus 直接托管,无需拷贝步骤。 + outDir: resolve(__dirname, '../../public/codemirror'), + emptyOutDir: true, + lib: { + entry: resolve(__dirname, 'src/index.ts'), + // IIFE 产物挂在 window.CodeMirrorEditor 上,Rust 侧用 Reflect::get 取。 + name: 'CodeMirrorEditor', + fileName: () => 'editor.js', + formats: ['iife'], + }, + rolldownOptions: { + output: { + // 默认导出(对象字面量 { create() })成为 window.CodeMirrorEditor。 + exports: 'default', + assetFileNames: 'editor.[ext]', + }, + }, + cssCodeSplit: false, + minify: true, + sourcemap: true, + }, +}); diff --git a/libs/codemirror-editor/vitest.config.ts b/libs/codemirror-editor/vitest.config.ts new file mode 100644 index 0000000..cbfbbf4 --- /dev/null +++ b/libs/codemirror-editor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'happy-dom', + include: ['src/**/*.test.ts'], + }, +}) diff --git a/src/codemirror_bridge.rs b/src/codemirror_bridge.rs new file mode 100644 index 0000000..5b9fc0a --- /dev/null +++ b/src/codemirror_bridge.rs @@ -0,0 +1,187 @@ +//! CodeMirror 编辑器的 wasm-bindgen 绑定层。 +//! +//! 封装与 `window.CodeMirrorEditor`(IIFE 暴露的全局对象字面量)的全部交互, +//! 严格镜像 [`crate::tiptap_bridge`] 的结构:共享纯数据类型双目标编译, +//! wasm-bindgen extern + [`EditorHandle`] 仅在 WASM 前端编译(server 构建无 window)。 +//! +//! 与 tiptap 一样,`CodeMirrorEditor` 是 IIFE 挂在 window 上的**对象字面量** +//! (`{ create }`),不是函数——因此用 `js_sys::Reflect::get` 做属性访问拿到, +//! 不能用 wasm-bindgen 的 extern fn(那会被编成函数调用,"not a function")。 + +use serde::{Deserialize, Serialize}; + +/// SQL 补全用 schema 数据,由 `get_db_schema` server function 填充。 +// 暂未被消费(Task 5 的 get_db_schema 会用到),先 allow dead_code 避免 -D warnings 阻断 clippy。 +#[allow(dead_code)] +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct SqlSchema { + pub tables: Vec, +} + +/// 单张表的补全数据:表名 + 列名列表。 +#[allow(dead_code)] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SqlTable { + pub name: String, + pub columns: Vec, +} + +// ============================================================================ +// 以下全部仅在 WASM 前端编译:wasm-bindgen extern + EditorHandle + 闭包。 +// 放在 #[cfg] 子模块内,避免 server 构建尝试编译引用 JS 对象的 extern。 +// ============================================================================ +#[cfg(target_arch = "wasm32")] +pub mod wasm { + use super::SqlSchema; + use wasm_bindgen::prelude::*; + use wasm_bindgen::JsCast; + + // —— window.CodeMirrorEditor 模块对象 —— + // + // CodeMirrorEditor 是 IIFE 产物挂在 window 上的对象字面量(含 create 方法), + // 不是函数。wasm-bindgen 对 `fn get_module() -> T` 形式的 extern 会生成 + // `window.CodeMirrorEditor()`(函数调用),会因 "not a function" 失败。 + // 因此用 js_sys::Reflect::get 做属性访问拿到模块对象,再 unchecked_into。 + #[wasm_bindgen] + extern "C" { + /// `window.CodeMirrorEditor` 模块对象的 Rust 映射(IIFE 产物挂在 window 上的对象字面量)。 + /// 不是函数——通过 [`get_module`] 用 Reflect::get 取属性而非 extern fn 调用拿到。 + pub type CodeMirrorEditorModule; + + /// 调用 `CodeMirrorEditor.create(containerId, opts)`。 + /// 找不到容器返回 null(被 Option 捕获);构造失败抛异常(被 catch 捕获)。 + #[wasm_bindgen(method, catch)] + pub fn create( + this: &CodeMirrorEditorModule, + container_id: &str, + opts: &EditorOptions, + ) -> Result, JsValue>; + } + + /// 读取 `window.CodeMirrorEditor`(IIFE 默认导出,顶层 var 即 window 属性)。 + /// 用 Reflect::get 做属性访问——extern fn 形式会被 wasm-bindgen 编成函数调用。 + /// + /// 用 unchecked_into 而非 dyn_into:CodeMirrorEditor 是 JS 对象字面量, + /// 不是 wasm-bindgen 注册的构造函数实例,dyn_into 的 instanceof 检查必然失败。 + /// unchecked_into 只做编译期类型标注,不做运行时校验 + /// (Reflect.get 已保证拿到的是目标对象)。 + pub fn get_module() -> CodeMirrorEditorModule { + let window = web_sys::window().expect("no window"); + let val = js_sys::Reflect::get(&window, &"CodeMirrorEditor".into()) + .expect("window.CodeMirrorEditor missing"); + val.unchecked_into::() + } + + // —— 编辑器实例(CodeMirrorInstance)—— + #[wasm_bindgen] + extern "C" { + /// `CodeMirrorEditor.create` 返回的编辑器实例对象,承载 CodeMirror EditorView。 + pub type EditorInstance; + + /// 返回当前文档全文。 + #[wasm_bindgen(method, js_name = getValue)] + pub fn get_value(this: &EditorInstance) -> String; + + /// 替换整个文档内容(dispatch changes,触发 onChange)。 + #[wasm_bindgen(method, js_name = setValue)] + pub fn set_value(this: &EditorInstance, s: &str); + + /// 热切换主题(Compartment.reconfigure,不重建实例)。 + #[wasm_bindgen(method, js_name = setTheme)] + pub fn set_theme(this: &EditorInstance, theme: &str); + + /// 更新 SQL 补全 schema(Compartment.reconfigure)。 + #[wasm_bindgen(method, js_name = setSchema)] + pub fn set_schema(this: &EditorInstance, schema: &SqlSchema); + + /// 让编辑器获取焦点。 + #[wasm_bindgen(method)] + pub fn focus(this: &EditorInstance); + + /// 销毁编辑器,释放 JS 侧资源。 + #[wasm_bindgen(method)] + pub fn destroy(this: &EditorInstance); + } + + // —— EditorOptions:用 builder 模式(setter)构造 JS 对象 —— + #[wasm_bindgen] + extern "C" { + /// 传给 `CodeMirrorEditor.create` 的配置对象,对应 JS 侧的 EditorOptions。 + /// 用 `new()` 创建空对象后通过 setter 链式设置字段。 + pub type EditorOptions; + + /// 构造一个空的 EditorOptions,随后用各 setter 填充。 + #[wasm_bindgen(constructor)] + pub fn new() -> EditorOptions; + + /// 语言(默认 'sql')。 + #[wasm_bindgen(method, setter, js_name = language)] + pub fn set_language(this: &EditorOptions, v: &str); + + /// 主题:'light'(Catppuccin Latte)或 'dark'(Catppuccin Mocha)。 + #[wasm_bindgen(method, setter, js_name = theme)] + pub fn set_theme(this: &EditorOptions, v: &str); + + /// 是否启用 Vim keymap。 + #[wasm_bindgen(method, setter, js_name = vim)] + pub fn set_vim(this: &EditorOptions, v: bool); + + /// SQL 补全 schema(表/列数据)。 + #[wasm_bindgen(method, setter, js_name = schema)] + pub fn set_schema(this: &EditorOptions, v: &SqlSchema); + + /// 初始文档内容。 + #[wasm_bindgen(method, setter, js_name = value)] + pub fn set_value(this: &EditorOptions, v: &str); + + /// 文档变更回调(参数为最新全文)。 + #[wasm_bindgen(method, setter, js_name = onChange)] + pub fn set_on_change(this: &EditorOptions, cb: &Closure); + + /// 编辑器就绪回调(构造末尾同步触发一次)。 + #[wasm_bindgen(method, setter, js_name = onReady)] + pub fn set_on_ready(this: &EditorOptions, cb: &Closure); + } + + /// 编辑器实例句柄:持有 instance + 所有 Closure,Drop 时销毁实例并释放闭包。 + /// + /// 闭包字段 `_` 前缀表示仅用于保持生命周期——它们被注入 JS 后,JS 侧持有 + /// 函数引用;只要 [`EditorHandle`] 存活,闭包就不会被回收。Drop 时随结构释放。 + pub struct EditorHandle { + instance: EditorInstance, + _on_change: Closure, + _on_ready: Closure, + } + + impl EditorHandle { + /// 调用方须先把各 closure set 进 EditorOptions,再 create, + /// 然后把返回的 instance + 同名 closure 一起传入 new。 + pub fn new( + instance: EditorInstance, + on_change: Closure, + on_ready: Closure, + ) -> Self { + Self { + instance, + _on_change: on_change, + _on_ready: on_ready, + } + } + + /// 借用底层实例,供宿主调 getValue/setTheme/setSchema 等。 + pub fn instance(&self) -> &EditorInstance { + &self.instance + } + } + + impl Drop for EditorHandle { + fn drop(&mut self) { + // 销毁 JS 侧编辑器;随后 _on_change/_on_ready 字段按声明顺序释放, + // 释放 wasm-bindgen 函数表槽位。 + let _ = self.instance.destroy(); + } + } +} + +#[cfg(target_arch = "wasm32")] +pub use wasm::*; diff --git a/src/main.rs b/src/main.rs index 4f4a174..773542e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,9 @@ mod theme; // tiptap_bridge:共享类型(UploadsInFlight/UploadErrorEntry)两端都编译; // wasm-bindgen extern 与 EditorHandle 在内部的 #[cfg(wasm32)] 子模块里。 mod tiptap_bridge; +// codemirror_bridge:SQL 编辑器的 wasm-bindgen 绑定,结构镜像 tiptap_bridge。 +// 共享类型(SqlSchema/SqlTable)两端都编译;extern 与 EditorHandle 在 #[cfg(wasm32)] 子模块里。 +mod codemirror_bridge; mod utils; mod webp;