├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── components.json ├── eslint.config.mjs ├── examples ├── brainfuck.wa ├── closure.wa ├── complex.wa ├── count.wa ├── defer.wa ├── heart.wa ├── hello-zh.wz ├── hello.wa ├── iface.wa ├── iter.wa └── map.wa ├── global.d.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── examples.json ├── fonts │ ├── RobotoMono-Medium.ttf │ └── RobotoMono-Regular.ttf ├── logo.svg ├── preview.png ├── wa-app.js └── wasm-exec.js ├── scripts └── gen-code.ts ├── src ├── App.tsx ├── components │ ├── editor │ │ └── editor-pane.tsx │ ├── header.tsx │ ├── main-content.tsx │ ├── preview │ │ ├── memory.tsx │ │ ├── output.tsx │ │ ├── preview-pane.tsx │ │ └── wat.tsx │ ├── skeleton-code.tsx │ ├── skeleton-preview.tsx │ └── ui │ │ ├── button.tsx │ │ ├── mode-toggle.tsx │ │ ├── resizable.tsx │ │ ├── select.tsx │ │ ├── skeleton.tsx │ │ └── theme-provider.tsx ├── constants │ └── lang.ts ├── hooks │ ├── useDebounce.ts │ ├── useEditorEvents.ts │ ├── useIsMobile.ts │ ├── useWaMonaco.ts │ └── useWasmMonaco.ts ├── lib │ ├── idb.ts │ ├── import-obj.ts │ ├── memory.ts │ ├── utils.ts │ └── wawasm.ts ├── main.tsx ├── monaco │ ├── actions.ts │ ├── config.ts │ ├── hovers.ts │ ├── shiki.ts │ ├── suggestions.ts │ └── wa.tmLanguage.json ├── stores │ ├── config.ts │ ├── wasm.ts │ └── withSelectors.ts ├── tailwind.css └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy playground to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build-and-deploy: 15 | runs-on: ubuntu-latest 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: '19' 26 | 27 | - name: Setup pnpm 28 | uses: pnpm/action-setup@v2 29 | with: 30 | version: 9 31 | 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 37 | 38 | - name: Setup pnpm cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 42 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pnpm-store- 45 | 46 | - name: Install dependencies 47 | run: pnpm install 48 | 49 | - name: Build project 50 | run: pnpm run build 51 | 52 | - name: Deploy to GitHub Pages 53 | uses: peaceiris/actions-gh-pages@v4 54 | with: 55 | github_token: ${{ secrets.GITHUB_TOKEN }} 56 | publish_dir: ./dist 57 | cname: ${{ secrets.CNAME }} 58 | force_orphan: true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | public/wa.wasm.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Wa Payground 2 |

凹语言™ 游乐场

3 | 4 | > 不依赖后台 Web 服务,纯 WASM 环境编译并执行 5 | 6 | [在线地址](https://wa-lang.org/playground/) 7 | 8 | [![](./public/preview.png)](https://wa-lang.org/playground/) 9 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/tailwind.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu() 4 | -------------------------------------------------------------------------------- /examples/brainfuck.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2019 凹语言 作者。保留所有权利。 2 | 3 | func main { 4 | // print hi 5 | const code = "++++++++++[>++++++++++<-]>++++.+." 6 | vm := NewBrainFuck(code) 7 | vm.Run() 8 | } 9 | 10 | type BrainFuck :struct { 11 | mem: [30000]byte 12 | code: string 13 | pos: int 14 | pc: int 15 | } 16 | 17 | func NewBrainFuck(code: string) => *BrainFuck { 18 | return &BrainFuck{code: code} 19 | } 20 | 21 | func BrainFuck.Run { 22 | for ; this.pc != len(this.code); this.pc++ { 23 | switch x := this.code[this.pc]; x { 24 | case '>': 25 | this.pos++ 26 | case '<': 27 | this.pos-- 28 | case '+': 29 | this.mem[this.pos]++ 30 | case '-': 31 | this.mem[this.pos]-- 32 | case '[': 33 | if this.mem[this.pos] == 0 { 34 | this.loop(1) 35 | } 36 | case ']': 37 | if this.mem[this.pos] != 0 { 38 | this.loop(-1) 39 | } 40 | case '.': 41 | print(rune(this.mem[this.pos])) 42 | case ',': 43 | // TODO: support read byte 44 | } 45 | } 46 | return 47 | } 48 | 49 | func BrainFuck.loop(inc: int) { 50 | for i := inc; i != 0; this.pc += inc { 51 | switch this.code[this.pc+inc] { 52 | case '[': 53 | i++ 54 | case ']': 55 | i-- 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/closure.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2019 凹语言 作者。保留所有权利。 2 | 3 | type FP :func(i: i32) => i32 4 | 5 | type ST :struct { 6 | i: i32 7 | } 8 | 9 | func ST.meth(p: i32) => i32 { 10 | this.i += p 11 | return this.i 12 | } 13 | 14 | global g_f: FP 15 | 16 | func main { 17 | o: ST 18 | o.i = 11 19 | g_f = o.meth 20 | println(g_f(11)) // 22 21 | println(o.i) // 22 22 | 23 | n := i32(21) 24 | g_f = func(i: i32) => i32 { 25 | n += i 26 | return n 27 | } 28 | println(g_f(22)) // 43 29 | println(n) // 43 30 | 31 | func(i: i32) { 32 | n += i 33 | }(22) 34 | println(n) // 65 35 | 36 | g_f = Double 37 | println(g_f(13)) // 26 38 | } 39 | 40 | func Double(i: i32) => i32 { 41 | return i * 2 42 | } 43 | -------------------------------------------------------------------------------- /examples/complex.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2024 凹语言 作者。保留所有权利。 2 | 3 | func main { 4 | x: complex128 = complex(1, 2) // 1+2i 5 | y: complex128 = complex(3, 4) // 3+4i 6 | println(x * y) // "(-5+10i)" 7 | println(real(x * y)) // "-5" 8 | println(imag(x * y)) // "10" 9 | } 10 | -------------------------------------------------------------------------------- /examples/count.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2019 凹语言 作者。保留所有权利。 2 | 3 | func main { 4 | print("30以内的质数:") 5 | for n := 2; n <= 30; n = n + 1 { 6 | isPrime: int = 1 7 | for i := 2; i*i <= n; i = i + 1 { 8 | if x := n % i; x == 0 { 9 | isPrime = 0 10 | } 11 | } 12 | if isPrime != 0 { 13 | print(n) 14 | if n != 29 { 15 | print("、") 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/defer.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2024 凹语言 作者。保留所有权利。 2 | 3 | func main { 4 | defer println("a1") 5 | defer println("a2") 6 | println("a3") 7 | 8 | for i := 0; i < 3; i++ { 9 | defer println("i.v0:", i) 10 | } 11 | 12 | for i := 0; i < 3; i++ { 13 | defer func { println("i.v1:", i) }() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/heart.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2019 凹语言 作者。保留所有权利。 2 | 3 | func main { 4 | a := 0.0 5 | for y := 1.5; y > -1.5; y = y - 0.15 { 6 | for x := -1.5; x < 1.5; x = x + 0.07 { 7 | a = x*x + y*y - 1.0 8 | if a*a*a < x*x*y*y*y { 9 | print("@") 10 | } else { 11 | print(" ") 12 | } 13 | } 14 | println() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/hello-zh.wz: -------------------------------------------------------------------------------- 1 | // 版权 @2022 _examples/hello-zh 作者。保留所有权利。 2 | 3 | #syntax=wz 4 | 5 | 引于 "书" 6 | 7 | 【启】: 8 | 书·说:"你好,凹语言中文版!" 9 | 。 10 | -------------------------------------------------------------------------------- /examples/hello.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2019 凹语言 作者。保留所有权利。 2 | 3 | import "fmt" 4 | import "runtime" 5 | 6 | func main { 7 | println("你好,凹语言!", runtime.WAOS) 8 | println(add(40, 2)) 9 | 10 | fmt.Println(1 + 1) 11 | } 12 | 13 | func add(a: i32, b: i32) => i32 { 14 | return a + b 15 | } 16 | -------------------------------------------------------------------------------- /examples/iface.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2021 凹语言 作者。保留所有权利。 2 | 3 | type S1 :struct { 4 | a: i32 5 | } 6 | 7 | type S2 :struct { 8 | a: i32 9 | } 10 | 11 | type i1 :interface { 12 | f() 13 | } 14 | 15 | func S1.f { 16 | println("This is S1, this.a==", this.a) 17 | } 18 | 19 | func S2.f { 20 | println("This is S2, this.a==", this.a) 21 | } 22 | 23 | func main { 24 | v1 := S1{a: 13} 25 | v2 := S2{a: 42} 26 | 27 | i: i1 = &v1 28 | i.f() 29 | 30 | i = &v2 31 | i.f() 32 | } 33 | -------------------------------------------------------------------------------- /examples/iter.wa: -------------------------------------------------------------------------------- 1 | type MyObject :struct { 2 | elems: []string 3 | } 4 | 5 | func New(a: ...string) => *MyObject { 6 | return &MyObject{elems: a} 7 | } 8 | 9 | func MyObject.Iter => func => (ok: bool, k: int, v: string) { 10 | idx: int 11 | return func => (ok: bool, k: int, v: string) { 12 | if ok = idx < len(this.elems); ok { 13 | k, v = idx, this.elems[idx] 14 | idx++ 15 | } 16 | return 17 | } 18 | } 19 | 20 | func main { 21 | for i, v := range New("aa", "bb", "cc").Iter() { 22 | println(i, v) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/map.wa: -------------------------------------------------------------------------------- 1 | // 版权 @2024 凹语言 作者。保留所有权利。 2 | 3 | func main { 4 | m := make(map[string]int) 5 | m["a"] = 13 6 | m["b"] = 42 7 | m["c"] = 29 8 | 9 | for k, v := range m { 10 | println(k, v) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | class Go { 3 | importObject: WebAssembly.Imports 4 | run(instance: WebAssembly.Instance): Promise 5 | init(wasmInst: any): void 6 | } 7 | 8 | interface Window { 9 | __WA_WASM__: ArrayBuffer 10 | __WA_WAT__: string 11 | __WA_CODE__: string 12 | __WA_PRINT__: string 13 | __WA_ERROR__: string 14 | __WA_FMT_CODE__: string 15 | __WA_APP__: { 16 | getString: (ptr: number, len: number) => string 17 | init: (wasmInst: any) => void 18 | } 19 | Go: typeof Go 20 | } 21 | interface ISyscallJS { 22 | print_bool: (v: boolean) => void 23 | print_i32: (i: number) => void 24 | print_u32: (i: number) => void 25 | print_ptr: (i: number) => void 26 | print_i64: (i: bigint) => void 27 | print_u64: (i: bigint) => void 28 | print_f32: (i: number) => void 29 | print_f64: (i: number) => void 30 | print_rune: (c: number) => void 31 | print_str: (ptr: number, len: number) => void 32 | proc_exit: (i: number) => void 33 | } 34 | } 35 | 36 | export {} 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wa Playground 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wa-tutorial", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "npm run gen-code && vite", 8 | "build": "npm run gen-code && tsc -b && vite build", 9 | "preview": "npm run gen-code && vite preview", 10 | "lint": "eslint .", 11 | "lint:fix": "eslint . --fix", 12 | "gen-code": "tsx scripts/gen-code.ts" 13 | }, 14 | "dependencies": { 15 | "@monaco-editor/react": "^4.6.0", 16 | "@radix-ui/react-select": "^2.1.4", 17 | "@radix-ui/react-slot": "^1.1.1", 18 | "class-variance-authority": "^0.7.1", 19 | "clsx": "^2.1.1", 20 | "idb-keyval": "^6.2.1", 21 | "lucide-react": "^0.469.0", 22 | "monaco-editor": "^0.52.2", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-resizable-panels": "^2.1.7", 26 | "tailwind-merge": "^2.6.0", 27 | "tailwindcss-animate": "^1.0.7", 28 | "zustand": "^5.0.3" 29 | }, 30 | "devDependencies": { 31 | "@antfu/eslint-config": "^3.12.2", 32 | "@eslint/js": "^9.17.0", 33 | "@shikijs/markdown-it": "^1.26.1", 34 | "@shikijs/monaco": "^1.26.1", 35 | "@types/markdown-it": "^14.1.2", 36 | "@types/node": "^22.10.5", 37 | "@types/react": "^18.3.18", 38 | "@types/react-dom": "^18.3.5", 39 | "@vitejs/plugin-react": "^4.3.4", 40 | "autoprefixer": "^10.4.20", 41 | "eslint": "^9.17.0", 42 | "eslint-plugin-react-hooks": "^5.0.0", 43 | "eslint-plugin-react-refresh": "^0.4.16", 44 | "esno": "^4.8.0", 45 | "globals": "^15.14.0", 46 | "jszip": "^3.10.1", 47 | "markdown-it": "^14.1.0", 48 | "postcss": "^8.4.49", 49 | "shiki": "^1.26.1", 50 | "tailwindcss": "^3.4.17", 51 | "tsx": "^4.19.3", 52 | "typescript": "~5.6.2", 53 | "typescript-eslint": "^8.18.2", 54 | "vite": "^6.0.5", 55 | "vite-plugin-svgr": "^4.3.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/examples.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "hello", 4 | "code": "// 版权 @2019 凹语言 作者。保留所有权利。\n\nimport \"fmt\"\nimport \"runtime\"\n\nfunc main {\n\tprintln(\"你好,凹语言!\", runtime.WAOS)\n\tprintln(add(40, 2))\n\n\tfmt.Println(1 + 1)\n}\n\nfunc add(a: i32, b: i32) => i32 {\n\treturn a + b\n}\n" 5 | }, 6 | { 7 | "name": "count", 8 | "code": "// 版权 @2019 凹语言 作者。保留所有权利。\n\nfunc main {\n\tprint(\"30以内的质数:\")\n\tfor n := 2; n <= 30; n = n + 1 {\n\t\tisPrime: int = 1\n\t\tfor i := 2; i*i <= n; i = i + 1 {\n\t\t\tif x := n % i; x == 0 {\n\t\t\t\tisPrime = 0\n\t\t\t}\n\t\t}\n\t\tif isPrime != 0 {\n\t\t\tprint(n)\n\t\t\tif n != 29 {\n\t\t\t\tprint(\"、\")\n\t\t\t}\n\t\t}\n\t}\n}\n" 9 | }, 10 | { 11 | "name": "heart", 12 | "code": "// 版权 @2019 凹语言 作者。保留所有权利。\n\nfunc main {\n\ta := 0.0\n\tfor y := 1.5; y > -1.5; y = y - 0.15 {\n\t\tfor x := -1.5; x < 1.5; x = x + 0.07 {\n\t\t\ta = x*x + y*y - 1.0\n\t\t\tif a*a*a < x*x*y*y*y {\n\t\t\t\tprint(\"@\")\n\t\t\t} else {\n\t\t\t\tprint(\" \")\n\t\t\t}\n\t\t}\n\t\tprintln()\n\t}\n}\n" 13 | }, 14 | { 15 | "name": "brainfuck", 16 | "code": "// 版权 @2019 凹语言 作者。保留所有权利。\n\nfunc main {\n\t// print hi\n\tconst code = \"++++++++++[>++++++++++<-]>++++.+.\"\n\tvm := NewBrainFuck(code)\n\tvm.Run()\n}\n\ntype BrainFuck :struct {\n\tmem: [30000]byte\n\tcode: string\n\tpos: int\n\tpc: int\n}\n\nfunc NewBrainFuck(code: string) => *BrainFuck {\n\treturn &BrainFuck{code: code}\n}\n\nfunc BrainFuck.Run {\n\tfor ; this.pc != len(this.code); this.pc++ {\n\t\tswitch x := this.code[this.pc]; x {\n\t\tcase '>':\n\t\t\tthis.pos++\n\t\tcase '<':\n\t\t\tthis.pos--\n\t\tcase '+':\n\t\t\tthis.mem[this.pos]++\n\t\tcase '-':\n\t\t\tthis.mem[this.pos]--\n\t\tcase '[':\n\t\t\tif this.mem[this.pos] == 0 {\n\t\t\t\tthis.loop(1)\n\t\t\t}\n\t\tcase ']':\n\t\t\tif this.mem[this.pos] != 0 {\n\t\t\t\tthis.loop(-1)\n\t\t\t}\n\t\tcase '.':\n\t\t\tprint(rune(this.mem[this.pos]))\n\t\tcase ',':\n\t\t\t// TODO: support read byte\n\t\t}\n\t}\n\treturn\n}\n\nfunc BrainFuck.loop(inc: int) {\n\tfor i := inc; i != 0; this.pc += inc {\n\t\tswitch this.code[this.pc+inc] {\n\t\tcase '[':\n\t\t\ti++\n\t\tcase ']':\n\t\t\ti--\n\t\t}\n\t}\n}\n" 17 | }, 18 | { 19 | "name": "closure", 20 | "code": "// 版权 @2019 凹语言 作者。保留所有权利。\n\ntype FP :func(i: i32) => i32\n\ntype ST :struct {\n\ti: i32\n}\n\nfunc ST.meth(p: i32) => i32 {\n\tthis.i += p\n\treturn this.i\n}\n\nglobal g_f: FP\n\nfunc main {\n\to: ST\n\to.i = 11\n\tg_f = o.meth\n\tprintln(g_f(11)) // 22\n\tprintln(o.i) // 22\n\n\tn := i32(21)\n\tg_f = func(i: i32) => i32 {\n\t\tn += i\n\t\treturn n\n\t}\n\tprintln(g_f(22)) // 43\n\tprintln(n) // 43\n\n\tfunc(i: i32) {\n\t\tn += i\n\t}(22)\n\tprintln(n) // 65\n\n\tg_f = Double\n\tprintln(g_f(13)) // 26\n}\n\nfunc Double(i: i32) => i32 {\n\treturn i * 2\n}\n" 21 | }, 22 | { 23 | "name": "iface", 24 | "code": "// 版权 @2021 凹语言 作者。保留所有权利。\n\ntype S1 :struct {\n\ta: i32\n}\n\ntype S2 :struct {\n\ta: i32\n}\n\ntype i1 :interface {\n\tf()\n}\n\nfunc S1.f {\n\tprintln(\"This is S1, this.a==\", this.a)\n}\n\nfunc S2.f {\n\tprintln(\"This is S2, this.a==\", this.a)\n}\n\nfunc main {\n\tv1 := S1{a: 13}\n\tv2 := S2{a: 42}\n\n\ti: i1 = &v1\n\ti.f()\n\n\ti = &v2\n\ti.f()\n}\n" 25 | }, 26 | { 27 | "name": "map", 28 | "code": "// 版权 @2024 凹语言 作者。保留所有权利。\n\nfunc main {\n\tm := make(map[string]int)\n\tm[\"a\"] = 13\n\tm[\"b\"] = 42\n\tm[\"c\"] = 29\n\n\tfor k, v := range m {\n\t\tprintln(k, v)\n\t}\n}\n" 29 | }, 30 | { 31 | "name": "defer", 32 | "code": "// 版权 @2024 凹语言 作者。保留所有权利。\n\nfunc main {\n\tdefer println(\"a1\")\n\tdefer println(\"a2\")\n\tprintln(\"a3\")\n\n\tfor i := 0; i < 3; i++ {\n\t\tdefer println(\"i.v0:\", i)\n\t}\n\n\tfor i := 0; i < 3; i++ {\n\t\tdefer func { println(\"i.v1:\", i) }()\n\t}\n}\n" 33 | }, 34 | { 35 | "name": "complex", 36 | "code": "// 版权 @2024 凹语言 作者。保留所有权利。\n\nfunc main {\n\tx: complex128 = complex(1, 2) // 1+2i\n\ty: complex128 = complex(3, 4) // 3+4i\n\tprintln(x * y) // \"(-5+10i)\"\n\tprintln(real(x * y)) // \"-5\"\n\tprintln(imag(x * y)) // \"10\"\n}\n" 37 | }, 38 | { 39 | "name": "hello-zh", 40 | "code": "// 版权 @2022 _examples/hello-zh 作者。保留所有权利。\n\n#syntax=wz\n\n引于 \"书\"\n\n【启】:\n 书·说:\"你好,凹语言中文版!\"\n。\n" 41 | } 42 | ] -------------------------------------------------------------------------------- /public/fonts/RobotoMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa-lang/playground/caff949d55abeb2d6edde63ca0f7352e159ed85a/public/fonts/RobotoMono-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa-lang/playground/caff949d55abeb2d6edde63ca0f7352e159ed85a/public/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa-lang/playground/caff949d55abeb2d6edde63ca0f7352e159ed85a/public/preview.png -------------------------------------------------------------------------------- /public/wa-app.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | class WaApp { 3 | constructor() { 4 | this._inst = null 5 | } 6 | 7 | init(instance) { 8 | this._inst = instance 9 | } 10 | 11 | async run(instance) { 12 | this._inst = instance 13 | this._inst.exports.main() 14 | } 15 | 16 | mem() { 17 | return this._inst.exports.memory 18 | } 19 | 20 | memView(addr, len) { 21 | return new DataView(this._inst.exports.memory.buffer, addr, len) 22 | } 23 | 24 | memUint8Array(addr, len) { 25 | return new Uint8Array(this.Mem().buffer, addr, len) 26 | } 27 | 28 | getString(addr, len) { 29 | return new TextDecoder('utf-8').decode(this.memView(addr, len)) 30 | } 31 | 32 | setString(addr, len, s) { 33 | const bytes = new TextEncoder('utf-8').encode(s) 34 | if (len > bytes.length) { 35 | len = bytes.length 36 | } 37 | this.memUint8Array(addr, len).set(bytes) 38 | } 39 | } 40 | 41 | window.__WA_APP__ = new WaApp() 42 | })() 43 | -------------------------------------------------------------------------------- /public/wasm-exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substring(0, nl)); 23 | outputBuf = outputBuf.substring(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.path) { 77 | globalThis.path = { 78 | resolve(...pathSegments) { 79 | return pathSegments.join("/"); 80 | } 81 | } 82 | } 83 | 84 | if (!globalThis.crypto) { 85 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 86 | } 87 | 88 | if (!globalThis.performance) { 89 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 90 | } 91 | 92 | if (!globalThis.TextEncoder) { 93 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 94 | } 95 | 96 | if (!globalThis.TextDecoder) { 97 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 98 | } 99 | 100 | const encoder = new TextEncoder("utf-8"); 101 | const decoder = new TextDecoder("utf-8"); 102 | 103 | globalThis.Go = class { 104 | constructor() { 105 | this.argv = ["js"]; 106 | this.env = {}; 107 | this.exit = (code) => { 108 | if (code !== 0) { 109 | console.warn("exit code:", code); 110 | } 111 | }; 112 | this._exitPromise = new Promise((resolve) => { 113 | this._resolveExitPromise = resolve; 114 | }); 115 | this._pendingEvent = null; 116 | this._scheduledTimeouts = new Map(); 117 | this._nextCallbackTimeoutID = 1; 118 | 119 | const setInt64 = (addr, v) => { 120 | this.mem.setUint32(addr + 0, v, true); 121 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 122 | } 123 | 124 | const setInt32 = (addr, v) => { 125 | this.mem.setUint32(addr + 0, v, true); 126 | } 127 | 128 | const getInt64 = (addr) => { 129 | const low = this.mem.getUint32(addr + 0, true); 130 | const high = this.mem.getInt32(addr + 4, true); 131 | return low + high * 4294967296; 132 | } 133 | 134 | const loadValue = (addr) => { 135 | const f = this.mem.getFloat64(addr, true); 136 | if (f === 0) { 137 | return undefined; 138 | } 139 | if (!isNaN(f)) { 140 | return f; 141 | } 142 | 143 | const id = this.mem.getUint32(addr, true); 144 | return this._values[id]; 145 | } 146 | 147 | const storeValue = (addr, v) => { 148 | const nanHead = 0x7FF80000; 149 | 150 | if (typeof v === "number" && v !== 0) { 151 | if (isNaN(v)) { 152 | this.mem.setUint32(addr + 4, nanHead, true); 153 | this.mem.setUint32(addr, 0, true); 154 | return; 155 | } 156 | this.mem.setFloat64(addr, v, true); 157 | return; 158 | } 159 | 160 | if (v === undefined) { 161 | this.mem.setFloat64(addr, 0, true); 162 | return; 163 | } 164 | 165 | let id = this._ids.get(v); 166 | if (id === undefined) { 167 | id = this._idPool.pop(); 168 | if (id === undefined) { 169 | id = this._values.length; 170 | } 171 | this._values[id] = v; 172 | this._goRefCounts[id] = 0; 173 | this._ids.set(v, id); 174 | } 175 | this._goRefCounts[id]++; 176 | let typeFlag = 0; 177 | switch (typeof v) { 178 | case "object": 179 | if (v !== null) { 180 | typeFlag = 1; 181 | } 182 | break; 183 | case "string": 184 | typeFlag = 2; 185 | break; 186 | case "symbol": 187 | typeFlag = 3; 188 | break; 189 | case "function": 190 | typeFlag = 4; 191 | break; 192 | } 193 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 194 | this.mem.setUint32(addr, id, true); 195 | } 196 | 197 | const loadSlice = (addr) => { 198 | const array = getInt64(addr + 0); 199 | const len = getInt64(addr + 8); 200 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 201 | } 202 | 203 | const loadSliceOfValues = (addr) => { 204 | const array = getInt64(addr + 0); 205 | const len = getInt64(addr + 8); 206 | const a = new Array(len); 207 | for (let i = 0; i < len; i++) { 208 | a[i] = loadValue(array + i * 8); 209 | } 210 | return a; 211 | } 212 | 213 | const loadString = (addr) => { 214 | const saddr = getInt64(addr + 0); 215 | const len = getInt64(addr + 8); 216 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 217 | } 218 | 219 | const testCallExport = (a, b) => { 220 | this._inst.exports.testExport0(); 221 | return this._inst.exports.testExport(a, b); 222 | } 223 | 224 | const timeOrigin = Date.now() - performance.now(); 225 | this.importObject = { 226 | _gotest: { 227 | add: (a, b) => a + b, 228 | callExport: testCallExport, 229 | }, 230 | gojs: { 231 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 232 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 233 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 234 | // This changes the SP, thus we have to update the SP used by the imported function. 235 | 236 | // func wasmExit(code int32) 237 | "runtime.wasmExit": (sp) => { 238 | sp >>>= 0; 239 | const code = this.mem.getInt32(sp + 8, true); 240 | this.exited = true; 241 | delete this._inst; 242 | delete this._values; 243 | delete this._goRefCounts; 244 | delete this._ids; 245 | delete this._idPool; 246 | this.exit(code); 247 | }, 248 | 249 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 250 | "runtime.wasmWrite": (sp) => { 251 | sp >>>= 0; 252 | const fd = getInt64(sp + 8); 253 | const p = getInt64(sp + 16); 254 | const n = this.mem.getInt32(sp + 24, true); 255 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 256 | }, 257 | 258 | // func resetMemoryDataView() 259 | "runtime.resetMemoryDataView": (sp) => { 260 | sp >>>= 0; 261 | this.mem = new DataView(this._inst.exports.mem.buffer); 262 | }, 263 | 264 | // func nanotime1() int64 265 | "runtime.nanotime1": (sp) => { 266 | sp >>>= 0; 267 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 268 | }, 269 | 270 | // func walltime() (sec int64, nsec int32) 271 | "runtime.walltime": (sp) => { 272 | sp >>>= 0; 273 | const msec = (new Date).getTime(); 274 | setInt64(sp + 8, msec / 1000); 275 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 276 | }, 277 | 278 | // func scheduleTimeoutEvent(delay int64) int32 279 | "runtime.scheduleTimeoutEvent": (sp) => { 280 | sp >>>= 0; 281 | const id = this._nextCallbackTimeoutID; 282 | this._nextCallbackTimeoutID++; 283 | this._scheduledTimeouts.set(id, setTimeout( 284 | () => { 285 | this._resume(); 286 | while (this._scheduledTimeouts.has(id)) { 287 | // for some reason Go failed to register the timeout event, log and try again 288 | // (temporary workaround for https://github.com/golang/go/issues/28975) 289 | console.warn("scheduleTimeoutEvent: missed timeout event"); 290 | this._resume(); 291 | } 292 | }, 293 | getInt64(sp + 8), 294 | )); 295 | this.mem.setInt32(sp + 16, id, true); 296 | }, 297 | 298 | // func clearTimeoutEvent(id int32) 299 | "runtime.clearTimeoutEvent": (sp) => { 300 | sp >>>= 0; 301 | const id = this.mem.getInt32(sp + 8, true); 302 | clearTimeout(this._scheduledTimeouts.get(id)); 303 | this._scheduledTimeouts.delete(id); 304 | }, 305 | 306 | // func getRandomData(r []byte) 307 | "runtime.getRandomData": (sp) => { 308 | sp >>>= 0; 309 | crypto.getRandomValues(loadSlice(sp + 8)); 310 | }, 311 | 312 | // func finalizeRef(v ref) 313 | "syscall/js.finalizeRef": (sp) => { 314 | sp >>>= 0; 315 | const id = this.mem.getUint32(sp + 8, true); 316 | this._goRefCounts[id]--; 317 | if (this._goRefCounts[id] === 0) { 318 | const v = this._values[id]; 319 | this._values[id] = null; 320 | this._ids.delete(v); 321 | this._idPool.push(id); 322 | } 323 | }, 324 | 325 | // func stringVal(value string) ref 326 | "syscall/js.stringVal": (sp) => { 327 | sp >>>= 0; 328 | storeValue(sp + 24, loadString(sp + 8)); 329 | }, 330 | 331 | // func valueGet(v ref, p string) ref 332 | "syscall/js.valueGet": (sp) => { 333 | sp >>>= 0; 334 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 335 | sp = this._inst.exports.getsp() >>> 0; // see comment above 336 | storeValue(sp + 32, result); 337 | }, 338 | 339 | // func valueSet(v ref, p string, x ref) 340 | "syscall/js.valueSet": (sp) => { 341 | sp >>>= 0; 342 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 343 | }, 344 | 345 | // func valueDelete(v ref, p string) 346 | "syscall/js.valueDelete": (sp) => { 347 | sp >>>= 0; 348 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 349 | }, 350 | 351 | // func valueIndex(v ref, i int) ref 352 | "syscall/js.valueIndex": (sp) => { 353 | sp >>>= 0; 354 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 355 | }, 356 | 357 | // valueSetIndex(v ref, i int, x ref) 358 | "syscall/js.valueSetIndex": (sp) => { 359 | sp >>>= 0; 360 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 361 | }, 362 | 363 | // func valueCall(v ref, m string, args []ref) (ref, bool) 364 | "syscall/js.valueCall": (sp) => { 365 | sp >>>= 0; 366 | try { 367 | const v = loadValue(sp + 8); 368 | const m = Reflect.get(v, loadString(sp + 16)); 369 | const args = loadSliceOfValues(sp + 32); 370 | const result = Reflect.apply(m, v, args); 371 | sp = this._inst.exports.getsp() >>> 0; // see comment above 372 | storeValue(sp + 56, result); 373 | this.mem.setUint8(sp + 64, 1); 374 | } catch (err) { 375 | sp = this._inst.exports.getsp() >>> 0; // see comment above 376 | storeValue(sp + 56, err); 377 | this.mem.setUint8(sp + 64, 0); 378 | } 379 | }, 380 | 381 | // func valueInvoke(v ref, args []ref) (ref, bool) 382 | "syscall/js.valueInvoke": (sp) => { 383 | sp >>>= 0; 384 | try { 385 | const v = loadValue(sp + 8); 386 | const args = loadSliceOfValues(sp + 16); 387 | const result = Reflect.apply(v, undefined, args); 388 | sp = this._inst.exports.getsp() >>> 0; // see comment above 389 | storeValue(sp + 40, result); 390 | this.mem.setUint8(sp + 48, 1); 391 | } catch (err) { 392 | sp = this._inst.exports.getsp() >>> 0; // see comment above 393 | storeValue(sp + 40, err); 394 | this.mem.setUint8(sp + 48, 0); 395 | } 396 | }, 397 | 398 | // func valueNew(v ref, args []ref) (ref, bool) 399 | "syscall/js.valueNew": (sp) => { 400 | sp >>>= 0; 401 | try { 402 | const v = loadValue(sp + 8); 403 | const args = loadSliceOfValues(sp + 16); 404 | const result = Reflect.construct(v, args); 405 | sp = this._inst.exports.getsp() >>> 0; // see comment above 406 | storeValue(sp + 40, result); 407 | this.mem.setUint8(sp + 48, 1); 408 | } catch (err) { 409 | sp = this._inst.exports.getsp() >>> 0; // see comment above 410 | storeValue(sp + 40, err); 411 | this.mem.setUint8(sp + 48, 0); 412 | } 413 | }, 414 | 415 | // func valueLength(v ref) int 416 | "syscall/js.valueLength": (sp) => { 417 | sp >>>= 0; 418 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 419 | }, 420 | 421 | // valuePrepareString(v ref) (ref, int) 422 | "syscall/js.valuePrepareString": (sp) => { 423 | sp >>>= 0; 424 | const str = encoder.encode(String(loadValue(sp + 8))); 425 | storeValue(sp + 16, str); 426 | setInt64(sp + 24, str.length); 427 | }, 428 | 429 | // valueLoadString(v ref, b []byte) 430 | "syscall/js.valueLoadString": (sp) => { 431 | sp >>>= 0; 432 | const str = loadValue(sp + 8); 433 | loadSlice(sp + 16).set(str); 434 | }, 435 | 436 | // func valueInstanceOf(v ref, t ref) bool 437 | "syscall/js.valueInstanceOf": (sp) => { 438 | sp >>>= 0; 439 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 440 | }, 441 | 442 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 443 | "syscall/js.copyBytesToGo": (sp) => { 444 | sp >>>= 0; 445 | const dst = loadSlice(sp + 8); 446 | const src = loadValue(sp + 32); 447 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 448 | this.mem.setUint8(sp + 48, 0); 449 | return; 450 | } 451 | const toCopy = src.subarray(0, dst.length); 452 | dst.set(toCopy); 453 | setInt64(sp + 40, toCopy.length); 454 | this.mem.setUint8(sp + 48, 1); 455 | }, 456 | 457 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 458 | "syscall/js.copyBytesToJS": (sp) => { 459 | sp >>>= 0; 460 | const dst = loadValue(sp + 8); 461 | const src = loadSlice(sp + 16); 462 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 463 | this.mem.setUint8(sp + 48, 0); 464 | return; 465 | } 466 | const toCopy = src.subarray(0, dst.length); 467 | dst.set(toCopy); 468 | setInt64(sp + 40, toCopy.length); 469 | this.mem.setUint8(sp + 48, 1); 470 | }, 471 | 472 | "debug": (value) => { 473 | console.log(value); 474 | }, 475 | } 476 | }; 477 | } 478 | 479 | async run(instance) { 480 | if (!(instance instanceof WebAssembly.Instance)) { 481 | throw new Error("Go.run: WebAssembly.Instance expected"); 482 | } 483 | this._inst = instance; 484 | this.mem = new DataView(this._inst.exports.mem.buffer); 485 | this._values = [ // JS values that Go currently has references to, indexed by reference id 486 | NaN, 487 | 0, 488 | null, 489 | true, 490 | false, 491 | globalThis, 492 | this, 493 | ]; 494 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 495 | this._ids = new Map([ // mapping from JS values to reference ids 496 | [0, 1], 497 | [null, 2], 498 | [true, 3], 499 | [false, 4], 500 | [globalThis, 5], 501 | [this, 6], 502 | ]); 503 | this._idPool = []; // unused ids that have been garbage collected 504 | this.exited = false; // whether the Go program has exited 505 | 506 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 507 | let offset = 4096; 508 | 509 | const strPtr = (str) => { 510 | const ptr = offset; 511 | const bytes = encoder.encode(str + "\0"); 512 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 513 | offset += bytes.length; 514 | if (offset % 8 !== 0) { 515 | offset += 8 - (offset % 8); 516 | } 517 | return ptr; 518 | }; 519 | 520 | const argc = this.argv.length; 521 | 522 | const argvPtrs = []; 523 | this.argv.forEach((arg) => { 524 | argvPtrs.push(strPtr(arg)); 525 | }); 526 | argvPtrs.push(0); 527 | 528 | const keys = Object.keys(this.env).sort(); 529 | keys.forEach((key) => { 530 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 531 | }); 532 | argvPtrs.push(0); 533 | 534 | const argv = offset; 535 | argvPtrs.forEach((ptr) => { 536 | this.mem.setUint32(offset, ptr, true); 537 | this.mem.setUint32(offset + 4, 0, true); 538 | offset += 8; 539 | }); 540 | 541 | // The linker guarantees global data starts from at least wasmMinDataAddr. 542 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 543 | const wasmMinDataAddr = 4096 + 8192; 544 | if (offset >= wasmMinDataAddr) { 545 | throw new Error("total length of command line and environment variables exceeds limit"); 546 | } 547 | 548 | this._inst.exports.run(argc, argv); 549 | if (this.exited) { 550 | this._resolveExitPromise(); 551 | } 552 | await this._exitPromise; 553 | } 554 | 555 | _resume() { 556 | if (this.exited) { 557 | throw new Error("Go program has already exited"); 558 | } 559 | this._inst.exports.resume(); 560 | if (this.exited) { 561 | this._resolveExitPromise(); 562 | } 563 | } 564 | 565 | _makeFuncWrapper(id) { 566 | const go = this; 567 | return function () { 568 | const event = { id: id, this: this, args: arguments }; 569 | go._pendingEvent = event; 570 | go._resume(); 571 | return event.result; 572 | }; 573 | } 574 | } 575 | })(); 576 | -------------------------------------------------------------------------------- /scripts/gen-code.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs' 2 | import * as path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | interface IExample { 6 | name: string 7 | code: string 8 | } 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = path.dirname(__filename) 12 | 13 | const VALID_EXTENSIONS = ['.wa', '.wz'] 14 | const DEFAULT_ORDER = ['hello', 'count', 'heart', 'brainfuck', 'closure', 'iface', 'map', 'defer', 'complex', 'iter', 'hello-wz'] 15 | const EXAMPLES_DIR = path.resolve(__dirname, '../examples') 16 | const OUTPUT_PATH = path.resolve(__dirname, '../public/examples.json') 17 | 18 | function loadExamples(): IExample[] { 19 | const examples: IExample[] = [] 20 | 21 | try { 22 | const files = fs.readdirSync(EXAMPLES_DIR) 23 | 24 | for (const file of files) { 25 | const filePath = path.join(EXAMPLES_DIR, file) 26 | const extension = path.extname(filePath) 27 | 28 | if (VALID_EXTENSIONS.includes(extension)) { 29 | try { 30 | const name = path.basename(file, extension) 31 | const code = fs.readFileSync(filePath, 'utf-8') 32 | examples.push({ name, code }) 33 | } 34 | catch (err) { 35 | console.error(`Error reading file ${file}:`, err) 36 | } 37 | } 38 | } 39 | } 40 | catch (err) { 41 | console.error('Error reading examples directory:', err) 42 | } 43 | 44 | return examples 45 | } 46 | 47 | function sortExamples(examples: IExample[]): IExample[] { 48 | const orderedExamples = DEFAULT_ORDER 49 | .map(name => examples.find(example => example.name === name)) 50 | .filter((example): example is IExample => example !== undefined) 51 | 52 | const remainingExamples = examples.filter(example => !DEFAULT_ORDER.includes(example.name)) 53 | return [...orderedExamples, ...remainingExamples] 54 | } 55 | 56 | function main(): void { 57 | const examples = loadExamples() 58 | const sortedExamples = sortExamples(examples) 59 | 60 | const finalExamples = sortedExamples.map(({ name, code }) => ({ 61 | name, 62 | code, 63 | })) 64 | 65 | try { 66 | fs.writeFileSync(OUTPUT_PATH, JSON.stringify(finalExamples, null, 2)) 67 | console.log('Successfully generated examples.json') 68 | } 69 | catch (err) { 70 | console.error('Error writing examples.json:', err) 71 | } 72 | } 73 | 74 | main() 75 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@/components/header' 2 | import { MainContent } from '@/components/main-content' 3 | import { ThemeProvider } from '@/components/ui/theme-provider' 4 | 5 | function App() { 6 | return ( 7 | 8 |
9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | 16 | export default App 17 | -------------------------------------------------------------------------------- /src/components/editor/editor-pane.tsx: -------------------------------------------------------------------------------- 1 | import type * as MonacoType from 'monaco-editor' 2 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' 3 | import { useEditorEvents } from '@/hooks/useEditorEvents' 4 | import { useIsMobile } from '@/hooks/useIsMobile' 5 | import { useWaMonaco } from '@/hooks/useWaMonaco' 6 | import { runWa } from '@/lib/wawasm' 7 | import { monacoConfig } from '@/monaco/config' 8 | import { useConfigStore } from '@/stores/config' 9 | import Editor from '@monaco-editor/react' 10 | import { useEffect, useRef, useState } from 'react' 11 | import examples from '../../../public/examples.json' 12 | import { SkeletonCode } from '../skeleton-code' 13 | 14 | interface ICode { 15 | name: string 16 | code: string 17 | } 18 | 19 | export function EditorPane() { 20 | const isMobile = useIsMobile() 21 | const [current, setCurrent] = useState(examples[0]) 22 | const monacoInst = useWaMonaco() 23 | const { theme } = useConfigStore() 24 | const monacoTheme = theme === 'dark' ? 'vitesse-dark' : 'vitesse-light' 25 | const editorRef = useRef(null) 26 | 27 | const { isSaved, setIsSaved, handleError, handleFormatCode } = useEditorEvents({ editorRef, monacoInst }) 28 | 29 | const handleRunCode = () => { 30 | runWa() 31 | handleError() 32 | handleFormatCode() 33 | setIsSaved(true) 34 | } 35 | 36 | useEffect(() => { 37 | window.__WA_CODE__ = current?.code || '' 38 | handleRunCode() 39 | }, [current]) 40 | 41 | const handleEditorDidMount = (editor: MonacoType.editor.IStandaloneCodeEditor) => { 42 | editorRef.current = editor 43 | } 44 | 45 | const handleEditorChange = (value?: string) => { 46 | setIsSaved(false) 47 | window.__WA_CODE__ = value || '' 48 | } 49 | 50 | const handleSave = () => { 51 | if (editorRef.current) { 52 | window.__WA_CODE__ = editorRef.current.getValue() 53 | handleRunCode() 54 | } 55 | } 56 | 57 | return ( 58 |
59 |
60 | 83 |
84 |
85 | {!isMobile && ( 86 | 87 | {navigator.platform.includes('Mac') ? '⌘+S' : 'Ctrl+S'} 88 | 89 | )} 90 | 96 |
97 |
98 |
99 |
100 |
101 |
102 | } 104 | language="wa" 105 | {...monacoInst} 106 | height="100%" 107 | theme={monacoTheme} 108 | options={monacoConfig} 109 | value={current?.code} 110 | onMount={handleEditorDidMount} 111 | onChange={handleEditorChange} 112 | /> 113 |
114 |
115 |
116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Github, HomeIcon } from 'lucide-react' 2 | import Logo from '../../public/logo.svg?react' 3 | import { ModeToggle } from './ui/mode-toggle' 4 | 5 | export function Header() { 6 | return ( 7 |
8 | 9 |

Wa Playground

10 | window.open('https://wa-lang.org', '_blank')} 13 | /> 14 | window.open('https://github.com/wa-lang/wa', '_blank')} 17 | /> 18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/main-content.tsx: -------------------------------------------------------------------------------- 1 | import { useIsMobile } from '../hooks/useIsMobile' 2 | import { EditorPane } from './editor/editor-pane' 3 | import { PreviewPane } from './preview/preview-pane' 4 | import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './ui/resizable' 5 | 6 | export function MainContent() { 7 | const isMobile = useIsMobile() 8 | const direction = isMobile ? 'vertical' : 'horizontal' 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/preview/memory.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { formatMemory, getMemoryValue } from '@/lib/memory' 3 | import { memo, useCallback, useEffect, useMemo, useState } from 'react' 4 | 5 | interface IMemoryItem { 6 | address: string 7 | hex: string 8 | ascii: string 9 | } 10 | 11 | interface IMemoryCellProps { 12 | content: string 13 | isSelected: boolean 14 | onClick: () => void 15 | } 16 | 17 | const NUMBER_FORMAT_OPTIONS = { 18 | integer: ['dec', 'hex', 'oct'], 19 | float: ['dec', 'sci'], 20 | } as const 21 | 22 | const DEF_NUMBER_FORMATS = { 23 | 'Integer 8-bit': 'hex', 24 | 'Integer 16-bit': 'dec', 25 | 'Integer 32-bit': 'hex', 26 | 'Integer 64-bit': 'dec', 27 | 'Float 32-bit': 'sci', 28 | 'Float 64-bit': 'sci', 29 | 'Pointer 32-bit': 'hex', 30 | 'Pointer 64-bit': 'hex', 31 | } as Record 32 | 33 | const MemoryCell = memo(({ content, isSelected, onClick }) => ( 34 | 38 | {content} 39 | 40 | )) 41 | 42 | const MemoryAddress = memo<{ address: string, isSelected: boolean }>(({ address, isSelected }) => ( 43 |
44 | {address} 45 |
46 | )) 47 | 48 | const MemoryHex = memo<{ 49 | hex: string 50 | rowIndex: number 51 | selectedRow: number | null 52 | selectedCol: number | null 53 | onCellClick: (row: number, col: number) => void 54 | }>(({ hex, rowIndex, selectedRow, selectedCol, onCellClick }) => { 55 | const hexValues = useMemo(() => hex.split(' '), [hex]) 56 | 57 | return ( 58 |
59 | {hexValues.map((hexValue, hexIndex) => { 60 | const isSelected = selectedRow === rowIndex && selectedCol === hexIndex 61 | const handleClick = useCallback(() => { 62 | onCellClick(rowIndex, hexIndex) 63 | }, [rowIndex, hexIndex, onCellClick]) 64 | 65 | return ( 66 | 72 | ) 73 | })} 74 |
75 | ) 76 | }) 77 | 78 | const MemoryAscii = memo<{ 79 | ascii: string 80 | rowIndex: number 81 | selectedRow: number | null 82 | selectedCol: number | null 83 | onCellClick: (row: number, col: number) => void 84 | }>(({ ascii, rowIndex, selectedRow, selectedCol, onCellClick }) => { 85 | const asciiChars = useMemo(() => ascii.split(''), [ascii]) 86 | 87 | return ( 88 |
89 | {asciiChars.map((char, charIndex) => { 90 | const isSelected = selectedRow === rowIndex && selectedCol === charIndex 91 | const handleClick = useCallback(() => { 92 | onCellClick(rowIndex, charIndex) 93 | }, [rowIndex, charIndex, onCellClick]) 94 | 95 | return ( 96 | 102 | ) 103 | })} 104 |
105 | ) 106 | }) 107 | 108 | const MemoryRow = memo<{ 109 | item: IMemoryItem 110 | rowIndex: number 111 | selectedRow: number | null 112 | selectedCol: number | null 113 | onCellClick: (row: number, col: number) => void 114 | }>(({ item, rowIndex, selectedRow, selectedCol, onCellClick }) => ( 115 |
116 | 120 | 127 | 134 |
135 | ), (prevProps, nextProps) => { 136 | return ( 137 | prevProps.item === nextProps.item 138 | && prevProps.rowIndex === nextProps.rowIndex 139 | && (prevProps.selectedRow === nextProps.selectedRow 140 | || (prevProps.rowIndex !== prevProps.selectedRow && nextProps.rowIndex !== nextProps.selectedRow)) 141 | && (prevProps.selectedCol === nextProps.selectedCol 142 | || prevProps.rowIndex !== prevProps.selectedRow 143 | || nextProps.rowIndex !== nextProps.selectedRow) 144 | && prevProps.onCellClick === nextProps.onCellClick 145 | ) 146 | }) 147 | 148 | export const MemoryPreview: FC = () => { 149 | const [memory, setMemory] = useState([]) 150 | const [selectedRow, setSelectedRow] = useState(0) 151 | const [selectedCol, setSelectedCol] = useState(0) 152 | const [isLittleEndian, setIsLittleEndian] = useState(true) 153 | const [numberFormats, setNumberFormats] = useState(DEF_NUMBER_FORMATS) 154 | 155 | useEffect(() => { 156 | if (!window.__WA_WASM__) { 157 | setMemory([]) 158 | return 159 | } 160 | 161 | try { 162 | const buffer = window.__WA_WASM__ 163 | const formattedMemory = formatMemory(buffer.slice(0, 1024)) 164 | setMemory(formattedMemory) 165 | } 166 | catch (error) { 167 | console.error('Failed to get memory content:', error) 168 | setMemory([]) 169 | } 170 | }, []) 171 | 172 | const handleCellClick = useCallback((row: number, col: number) => { 173 | setSelectedRow(prev => prev === row ? prev : row) 174 | setSelectedCol(prev => prev === col ? prev : col) 175 | }, []) 176 | 177 | const getValueAtSelection = useCallback(() => { 178 | if (selectedRow === null || selectedCol === null || !memory[selectedRow]) 179 | return null 180 | 181 | const startIdx = selectedRow * 4 + selectedCol 182 | const buffer = window.__WA_WASM__ 183 | if (!buffer) 184 | return null 185 | 186 | return getMemoryValue(buffer, startIdx, isLittleEndian) 187 | }, [selectedRow, selectedCol, memory, isLittleEndian]) 188 | 189 | const endianValues = useMemo(() => getValueAtSelection(), [getValueAtSelection]) 190 | 191 | const currentAddress = useMemo(() => { 192 | if (selectedRow === null || selectedCol === null) 193 | return '0x00000000' 194 | const address = (selectedRow * 4 + selectedCol) 195 | return `0x${address.toString(16).padStart(8, '0').toUpperCase()}` 196 | }, [selectedRow, selectedCol]) 197 | 198 | const formatValue = useCallback((type: string, value: number | bigint | null) => { 199 | if (value === null) 200 | return 'N/A' 201 | 202 | if (type.startsWith('Pointer')) { 203 | return typeof value === 'bigint' 204 | ? `0x${value.toString(16)}` 205 | : `0x${value.toString(16)}` 206 | } 207 | 208 | if (type.startsWith('Float')) { 209 | const format = numberFormats[type as keyof typeof numberFormats] 210 | const num = Number(value) 211 | if (format === 'sci') { 212 | return num.toExponential(6) 213 | } 214 | return num.toFixed(2) 215 | } 216 | 217 | const format = numberFormats[type as keyof typeof numberFormats] 218 | switch (format) { 219 | case 'hex': return `0x${value.toString(16).toUpperCase()}` 220 | case 'oct': return `${value.toString(8)}` 221 | default: return value.toString() 222 | } 223 | }, [numberFormats]) 224 | 225 | const memoryRows = useMemo(() => { 226 | return memory.map((item, rowIndex) => ( 227 | 235 | )) 236 | }, [memory, selectedRow, selectedCol, handleCellClick]) 237 | 238 | return ( 239 |
240 |
241 |
242 | {currentAddress} 243 |
244 |
245 | {memoryRows} 246 |
247 |
248 | 249 |
250 |
251 | 257 | 263 |
264 |
265 | {endianValues && Object.entries(endianValues).map(([type, value]) => ( 266 |
267 |
268 | {type} 269 | {!type.startsWith('Pointer') && ( 270 | 287 | )} 288 |
289 | {formatValue(type, value)} 290 |
291 | ))} 292 |
293 |
294 |
295 | ) 296 | } 297 | -------------------------------------------------------------------------------- /src/components/preview/output.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | export const OutputPreview: FC<{ output: string }> = ({ output }) => { 4 | return ( 5 |
 6 |       {output || 'No output'}
 7 |     
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/preview/preview-pane.tsx: -------------------------------------------------------------------------------- 1 | import { useWasmMonaco } from '@/hooks/useWasmMonaco' 2 | import { initWaWasm } from '@/lib/wawasm' 3 | import { useConfigStore } from '@/stores/config' 4 | import { useWasmStore } from '@/stores/wasm' 5 | import { AppWindowMac, Cpu, FileType } from 'lucide-react' 6 | import { useEffect, useState } from 'react' 7 | import { SkeletonPreview } from '../skeleton-preview' 8 | import { MemoryPreview } from './memory' 9 | import { OutputPreview } from './output' 10 | import { WatPreview } from './wat' 11 | 12 | const TABS = [ 13 | { 14 | icon: , 15 | label: 'Preview', 16 | value: 'output', 17 | }, 18 | { 19 | icon: , 20 | label: 'WAT', 21 | value: 'wat', 22 | }, 23 | { 24 | icon: , 25 | label: 'Memory', 26 | value: 'memory', 27 | }, 28 | ] as const 29 | 30 | export function PreviewPane() { 31 | const [loading, setLoading] = useState(true) 32 | const [activeTab, setActiveTab] = useState<'output' | 'wat' | 'memory'>('output') 33 | const { output, wat } = useWasmStore() 34 | 35 | const monaco = useWasmMonaco() 36 | const { theme } = useConfigStore() 37 | 38 | useEffect(() => { 39 | initWaWasm().then(() => { 40 | setLoading(false) 41 | }) 42 | }, []) 43 | 44 | return ( 45 |
46 |
47 | {TABS.map(tab => ( 48 | 56 | ))} 57 |
58 | {loading 59 | ? 60 | : ( 61 |
62 | {activeTab === 'output' 63 | ? 64 | : activeTab === 'wat' 65 | ? 66 | : } 67 |
68 | )} 69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/components/preview/wat.tsx: -------------------------------------------------------------------------------- 1 | import type { useWaMonaco } from '@/hooks/useWaMonaco' 2 | import type { FC } from 'react' 3 | import { monacoConfig } from '@/monaco/config' 4 | import { Editor } from '@monaco-editor/react' 5 | 6 | export const WatPreview: FC<{ 7 | wat: string | null 8 | monaco: ReturnType 9 | theme: 'dark' | 'light' 10 | }> = ({ wat, monaco, theme }) => { 11 | const monacoTheme = theme === 'dark' ? 'vitesse-dark' : 'vitesse-light' 12 | 13 | return ( 14 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/skeleton-code.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from './ui/skeleton' 2 | 3 | export function SkeletonCode() { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/skeleton-preview.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from './ui/skeleton' 2 | 3 | export function SkeletonPreview() { 4 | return ( 5 |
6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import { Slot } from '@radix-ui/react-slot' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import * as React from 'react' 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 19 | ghost: 'hover:bg-accent hover:text-accent-foreground', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | sm: 'h-9 px-3', 25 | lg: 'h-11 px-8', 26 | icon: 'h-10 w-10', 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: 'default', 31 | size: 'default', 32 | }, 33 | }, 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : 'button' 45 | return ( 46 | 51 | ) 52 | }, 53 | ) 54 | Button.displayName = 'Button' 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useConfigStore } from '@/stores/config' 2 | import { Moon, Sun } from 'lucide-react' 3 | 4 | export function ModeToggle() { 5 | const { actions: { updateTheme } } = useConfigStore() 6 | 7 | return ( 8 |
9 | updateTheme('light')} 12 | /> 13 | updateTheme('dark')} 16 | /> 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import { GripVertical } from 'lucide-react' 3 | import * as ResizablePrimitive from 'react-resizable-panels' 4 | 5 | function ResizablePanelGroup({ 6 | className, 7 | ...props 8 | }: React.ComponentProps) { 9 | return ( 10 | 17 | ) 18 | } 19 | 20 | const ResizablePanel = ResizablePrimitive.Panel 21 | 22 | function ResizableHandle({ 23 | withHandle, 24 | className, 25 | ...props 26 | }: React.ComponentProps & { 27 | withHandle?: boolean 28 | }) { 29 | return ( 30 | div]:rotate-90', 33 | className, 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 |
39 | 40 |
41 | )} 42 |
43 | ) 44 | } 45 | 46 | export { ResizableHandle, ResizablePanel, ResizablePanelGroup } 47 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import * as SelectPrimitive from '@radix-ui/react-select' 3 | import { Check, ChevronDown, ChevronUp } from 'lucide-react' 4 | 5 | import * as React from 'react' 6 | 7 | const Select = SelectPrimitive.Root 8 | 9 | const SelectGroup = SelectPrimitive.Group 10 | 11 | const SelectValue = SelectPrimitive.Value 12 | 13 | const SelectTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, ref) => ( 17 | span]:line-clamp-1', 21 | className, 22 | )} 23 | {...props} 24 | > 25 | {children} 26 | 27 | 28 | 29 | 30 | )) 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 32 | 33 | const SelectScrollUpButton = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | 46 | 47 | )) 48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 49 | 50 | const SelectScrollDownButton = React.forwardRef< 51 | React.ElementRef, 52 | React.ComponentPropsWithoutRef 53 | >(({ className, ...props }, ref) => ( 54 | 62 | 63 | 64 | )) 65 | SelectScrollDownButton.displayName 66 | = SelectPrimitive.ScrollDownButton.displayName 67 | 68 | const SelectContent = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef 71 | >(({ className, children, position = 'popper', ...props }, ref) => ( 72 | 73 | 84 | 85 | 92 | {children} 93 | 94 | 95 | 96 | 97 | )) 98 | SelectContent.displayName = SelectPrimitive.Content.displayName 99 | 100 | const SelectLabel = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )) 110 | SelectLabel.displayName = SelectPrimitive.Label.displayName 111 | 112 | const SelectItem = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, children, ...props }, ref) => ( 116 | 124 | 125 | 126 | 127 | 128 | 129 | {children} 130 | 131 | )) 132 | SelectItem.displayName = SelectPrimitive.Item.displayName 133 | 134 | const SelectSeparator = React.forwardRef< 135 | React.ElementRef, 136 | React.ComponentPropsWithoutRef 137 | >(({ className, ...props }, ref) => ( 138 | 143 | )) 144 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 145 | 146 | export { 147 | Select, 148 | SelectContent, 149 | SelectGroup, 150 | SelectItem, 151 | SelectLabel, 152 | SelectScrollDownButton, 153 | SelectScrollUpButton, 154 | SelectSeparator, 155 | SelectTrigger, 156 | SelectValue, 157 | } 158 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/components/ui/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { useConfigStore } from '@/stores/config' 2 | import { useEffect } from 'react' 3 | 4 | interface ThemeProviderProps { 5 | children: React.ReactNode 6 | } 7 | 8 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 9 | const { theme } = useConfigStore() 10 | 11 | useEffect(() => { 12 | const root = window.document.documentElement 13 | root.classList.remove('light', 'dark') 14 | root.classList.add(theme) 15 | }, [theme]) 16 | 17 | return
{children}
18 | } 19 | -------------------------------------------------------------------------------- /src/constants/lang.ts: -------------------------------------------------------------------------------- 1 | export const LANG_KEYWORDS = [ 2 | 'break', 3 | 'defer', 4 | 'import', 5 | 'struct', 6 | 'case', 7 | 'else', 8 | 'interface', 9 | 'switch', 10 | 'const', 11 | 'for', 12 | 'map', 13 | 'type', 14 | 'continue', 15 | 'func', 16 | 'range', 17 | 'default', 18 | 'global', 19 | 'if', 20 | 'return', 21 | 'make', 22 | ] 23 | 24 | export const LANG_TYPES = [ 25 | 'bool', 26 | 'string', 27 | 'error', 28 | 'map', 29 | 'int', 30 | 'int8', 31 | 'int16', 32 | 'int32', 33 | 'int64', 34 | 'i8', 35 | 'i16', 36 | 'i32', 37 | 'i64', 38 | 'rune', 39 | 'uint', 40 | 'uint8', 41 | 'uint16', 42 | 'uint32', 43 | 'uint64', 44 | 'u8', 45 | 'u16', 46 | 'u32', 47 | 'u64', 48 | 'uintptr', 49 | 'byte', 50 | 'float32', 51 | 'float64', 52 | 'f32', 53 | 'f64', 54 | 'complex64', 55 | 'complex128', 56 | 'c64', 57 | 'c128', 58 | ] 59 | 60 | export const LANG_BOOL = ['true', 'false'] 61 | 62 | export const LANG_SNIPPETS = [ 63 | { 64 | label: 'im', 65 | insertText: 'import "${1:pkg}"', 66 | detail: 'Snippet for import statement', 67 | }, 68 | { 69 | label: 'ims', 70 | insertText: 'import (\n\t${1:pkg}\n)', 71 | detail: 'Snippet for a import block', 72 | }, 73 | { 74 | label: 'co', 75 | insertText: 'const ${1:name} = ${2:value}', 76 | detail: 'Snippet for a constant', 77 | }, 78 | { 79 | label: 'cos', 80 | insertText: 'const (\n\t${1:name} = ${2:value}\n)', 81 | detail: 'Snippet for a constant block', 82 | }, 83 | { 84 | label: 'tyf', 85 | insertText: 'type ${1:name} func($3) $4', 86 | detail: 'Snippet for a type function declaration', 87 | }, 88 | { 89 | label: 'tyi', 90 | insertText: 'type ${1:name} interface {\n\t$0\n}', 91 | detail: 'Snippet for a type interface', 92 | }, 93 | { 94 | label: 'tys', 95 | insertText: 'type ${1:name} struct {\n\t$0\n}', 96 | detail: 'Snippet for a struct declaration', 97 | }, 98 | { 99 | label: 'if', 100 | insertText: 'if ${1:cond} {\n\t$0\n}', 101 | detail: 'Snippet for if statement', 102 | }, 103 | { 104 | label: 'ife', 105 | insertText: 'if ${1:cond} {\n\t$0\n} else {\n\t$0\n}', 106 | detail: 'Snippet for if else statement', 107 | }, 108 | { 109 | label: 'iferr', 110 | insertText: 'if ${1:cond} != nil {\n\t$0\n}', 111 | detail: 'Snippet for if != nil statement', 112 | }, 113 | { 114 | label: 'for', 115 | insertText: 'for ${1:i} := ${2:0}; $1 < ${3:count}; $1${4:++} {\n\t$0\n}', 116 | detail: 'Snippet for for statement', 117 | }, 118 | { 119 | label: 'forr', 120 | insertText: 'for ${1:_, }${2:v} := range ${3:v} {\n\t$0\n}', 121 | detail: 'Snippet for for range statement', 122 | }, 123 | { 124 | label: 'sw', 125 | insertText: 'switch ${1:expr} {\n\t$0\n}', 126 | detail: 'Snippet for switch statement', 127 | }, 128 | { 129 | label: 'swc', 130 | insertText: 'switch ${1:expr} {\ncase ${2:cond}:\n\t$0\n}', 131 | detail: 'Snippet for switch case statement', 132 | }, 133 | { 134 | label: 'swd', 135 | insertText: 'switch ${1:expr} {\ndefault:\n\t$0\n}', 136 | detail: 'Snippet for switch default statement', 137 | }, 138 | { 139 | label: 'swcd', 140 | insertText: 'switch ${1:expr} {\ncase ${2:cond1}:\n\t$3\ndefault ${4:cond2}:\n\t$0\n}', 141 | detail: 'Snippet for switch default statement', 142 | }, 143 | { 144 | label: 'df', 145 | insertText: 'defer ${1:func}()', 146 | detail: 'Snippet for defer statement', 147 | }, 148 | { 149 | label: 'rt', 150 | insertText: 'return ${1:value}', 151 | detail: 'Snippet for return statement', 152 | }, 153 | { 154 | label: 'br', 155 | insertText: 'break', 156 | detail: 'Snippet for break statement', 157 | }, 158 | { 159 | label: 'cn', 160 | insertText: 'continue', 161 | detail: 'Snippet for continue statement', 162 | }, 163 | { 164 | label: 'f', 165 | insertText: 'func ${1:name}($2) $3 {\n\t$0\n}', 166 | detail: 'Snippet for function declaration', 167 | }, 168 | ] 169 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react' 2 | 3 | export function useDebounce void>(fn: T, delay: number) { 4 | const timerRef = useRef(null) 5 | 6 | return useCallback((...args: Parameters) => { 7 | if (timerRef.current) { 8 | clearTimeout(timerRef.current) 9 | } 10 | timerRef.current = setTimeout(() => { 11 | fn(...args) 12 | }, delay) 13 | }, [fn, delay]) 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/useEditorEvents.ts: -------------------------------------------------------------------------------- 1 | import type * as MonacoType from 'monaco-editor' 2 | import { runWa } from '@/lib/wawasm' 3 | import { useEffect, useState } from 'react' 4 | 5 | interface EditorEventsProps { 6 | editorRef: React.RefObject 7 | monacoInst: any 8 | } 9 | 10 | export function useEditorEvents({ 11 | editorRef, 12 | monacoInst, 13 | }: EditorEventsProps) { 14 | const [isSaved, setIsSaved] = useState(true) 15 | 16 | const handleError = () => { 17 | if (!editorRef.current || !monacoInst) 18 | return 19 | 20 | const model = editorRef.current.getModel() 21 | if (!model) 22 | return 23 | 24 | const err = window.__WA_ERROR__ as string 25 | if (!err || err === '') { 26 | monacoInst.editor.setModelMarkers(model, 'wa', []) 27 | return 28 | } 29 | 30 | const match = err.match(/(.+):(\d+):(\d+):\s*(.+)/) 31 | if (match) { 32 | const [, _file, line, column, message] = match 33 | const lineNum = Number.parseInt(line) 34 | const columnNum = Number.parseInt(column) 35 | 36 | const markers: MonacoType.editor.IMarkerData[] = [{ 37 | severity: monacoInst.MarkerSeverity.Error, 38 | message, 39 | startLineNumber: lineNum, 40 | startColumn: columnNum, 41 | endLineNumber: lineNum, 42 | endColumn: columnNum + 1, 43 | }] 44 | 45 | monacoInst.editor.setModelMarkers(model, 'wa', markers) 46 | } 47 | } 48 | 49 | const handleFormatCode = () => { 50 | if (window.__WA_FMT_CODE__ && editorRef.current) { 51 | const selection = editorRef.current.getSelection() 52 | 53 | editorRef.current.setValue(window.__WA_FMT_CODE__) 54 | 55 | if (selection) { 56 | editorRef.current.setSelection(selection) 57 | editorRef.current.revealPositionInCenter(selection.getPosition()) 58 | } 59 | } 60 | } 61 | 62 | const handleRunWaCode = (value?: string) => { 63 | window.__WA_CODE__ = value || '' 64 | runWa() 65 | handleFormatCode() 66 | handleError() 67 | } 68 | 69 | const handleToggleComment = (editor: MonacoType.editor.IStandaloneCodeEditor, selection: MonacoType.Selection) => { 70 | if (!editor || !monacoInst) 71 | return 72 | 73 | const model = editor.getModel() 74 | if (!model) 75 | return 76 | 77 | const oldSelection = editor.getSelection() 78 | 79 | const startLineNum = selection.startLineNumber 80 | const endLineNum = selection.endLineNumber 81 | 82 | const edits: MonacoType.editor.IIdentifiedSingleEditOperation[] = [] 83 | 84 | const firstLine = model.getLineContent(startLineNum) 85 | const isCommented = firstLine.trimStart().startsWith('//') 86 | 87 | for (let i = startLineNum; i <= endLineNum; i++) { 88 | const line = model.getLineContent(i) 89 | 90 | if (isCommented) { 91 | const trimmedLine = line.trimStart() 92 | const leadingSpaces = line.length - trimmedLine.length 93 | if (trimmedLine.startsWith('//')) { 94 | const commentContent = trimmedLine.substring(2) 95 | const newContent = commentContent.startsWith(' ') ? commentContent.substring(1) : commentContent 96 | const newLine = line.substring(0, leadingSpaces) + newContent 97 | edits.push({ 98 | range: new monacoInst.Range(i, 1, i, line.length + 1), 99 | text: newLine, 100 | }) 101 | } 102 | } 103 | else { 104 | edits.push({ 105 | range: new monacoInst.Range(i, 1, i, 1), 106 | text: '// ', 107 | }) 108 | } 109 | } 110 | 111 | editor.executeEdits('toggle-comment', edits) 112 | 113 | if (oldSelection) { 114 | let selectionStartCol = oldSelection.startColumn 115 | let selectionEndCol = oldSelection.endColumn 116 | 117 | if (!isCommented) { 118 | if (oldSelection.startLineNumber === oldSelection.endLineNumber) { 119 | selectionStartCol = Math.max(1, selectionStartCol + 3) 120 | selectionEndCol = Math.max(1, selectionEndCol + 3) 121 | } 122 | else { 123 | if (oldSelection.startColumn > 1) 124 | selectionStartCol = Math.max(1, selectionStartCol + 3) 125 | if (oldSelection.endColumn > 1) 126 | selectionEndCol = Math.max(1, selectionEndCol + 3) 127 | } 128 | } 129 | else { 130 | const commentPrefixLen = 3 131 | if (selectionStartCol > commentPrefixLen) 132 | selectionStartCol = Math.max(1, selectionStartCol - commentPrefixLen) 133 | if (selectionEndCol > commentPrefixLen) 134 | selectionEndCol = Math.max(1, selectionEndCol - commentPrefixLen) 135 | } 136 | 137 | const newSelection = new monacoInst.Selection( 138 | oldSelection.startLineNumber, 139 | selectionStartCol, 140 | oldSelection.endLineNumber, 141 | selectionEndCol, 142 | ) 143 | 144 | editor.setSelection(newSelection) 145 | editor.revealPositionInCenter(newSelection.getPosition()) 146 | } 147 | } 148 | 149 | const handleSaveEvent = (event: CustomEvent) => { 150 | const { value } = event.detail 151 | handleRunWaCode(value) 152 | setIsSaved(true) 153 | } 154 | 155 | const handleToggleCommentEvent = (event: CustomEvent) => { 156 | const { editor, selection } = event.detail 157 | handleToggleComment(editor, selection) 158 | } 159 | 160 | useEffect(() => { 161 | window.addEventListener('wa-editor-save', handleSaveEvent as EventListener) 162 | window.addEventListener('wa-editor-toggle-comment', handleToggleCommentEvent as EventListener) 163 | return () => { 164 | window.removeEventListener('wa-editor-save', handleSaveEvent as EventListener) 165 | window.removeEventListener('wa-editor-toggle-comment', handleToggleCommentEvent as EventListener) 166 | } 167 | }, [monacoInst]) 168 | 169 | return { 170 | handleError, 171 | handleRunWaCode, 172 | handleFormatCode, 173 | handleToggleComment, 174 | isSaved, 175 | setIsSaved, 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/hooks/useIsMobile.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export function useIsMobile(breakpoint: number = 768): boolean { 4 | const [isMobile, setIsMobile] = useState(false) 5 | 6 | useEffect(() => { 7 | const checkIsMobile = () => { 8 | setIsMobile(window.innerWidth < breakpoint) 9 | } 10 | 11 | checkIsMobile() 12 | 13 | window.addEventListener('resize', checkIsMobile) 14 | 15 | return () => window.removeEventListener('resize', checkIsMobile) 16 | }, [breakpoint]) 17 | 18 | return isMobile 19 | } -------------------------------------------------------------------------------- /src/hooks/useWaMonaco.ts: -------------------------------------------------------------------------------- 1 | import { registerEditorActions } from '@/monaco/actions' 2 | import { langConfig } from '@/monaco/config' 3 | import { registerHoverProvider } from '@/monaco/hovers' 4 | import { getShiki } from '@/monaco/shiki' 5 | import { registerLangSuggestions } from '@/monaco/suggestions' 6 | import { useConfigStore } from '@/stores/config' 7 | import { useMonaco } from '@monaco-editor/react' 8 | import { shikiToMonaco } from '@shikijs/monaco' 9 | import { useEffect } from 'react' 10 | 11 | export function useWaMonaco() { 12 | const { theme } = useConfigStore() 13 | const monaco = useMonaco() 14 | 15 | const registerLangHighlighter = async (monaco: typeof useMonaco) => { 16 | const highlighter = await getShiki(theme) 17 | shikiToMonaco(highlighter, monaco) 18 | } 19 | 20 | useEffect(() => { 21 | if (!monaco) 22 | return 23 | 24 | monaco.languages.register({ id: 'wa' }) 25 | monaco.languages.setLanguageConfiguration('wa', langConfig) 26 | 27 | registerLangHighlighter(monaco as unknown as typeof useMonaco) 28 | registerLangSuggestions(monaco) 29 | registerHoverProvider(monaco) 30 | registerEditorActions(monaco) 31 | }, [monaco]) 32 | 33 | return monaco 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/useWasmMonaco.ts: -------------------------------------------------------------------------------- 1 | import { langConfig } from '@/monaco/config' 2 | import { getShiki } from '@/monaco/shiki' 3 | import { useConfigStore } from '@/stores/config' 4 | import { useMonaco } from '@monaco-editor/react' 5 | import { shikiToMonaco } from '@shikijs/monaco' 6 | import { useEffect } from 'react' 7 | 8 | export function useWasmMonaco() { 9 | const { theme } = useConfigStore() 10 | const monaco = useMonaco() 11 | 12 | const registerLangHighlighter = async (monaco: typeof useMonaco) => { 13 | const highlighter = await getShiki(theme) 14 | shikiToMonaco(highlighter, monaco) 15 | } 16 | 17 | useEffect(() => { 18 | if (!monaco) 19 | return 20 | 21 | monaco.languages.register({ id: 'wasm' }) 22 | monaco.languages.setLanguageConfiguration('wasm', langConfig) 23 | 24 | registerLangHighlighter(monaco as unknown as typeof useMonaco) 25 | }, [monaco]) 26 | 27 | return monaco 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/idb.ts: -------------------------------------------------------------------------------- 1 | import type { StateStorage } from 'zustand/middleware' 2 | import { clear, del, get, set } from 'idb-keyval' 3 | 4 | export const IDB: StateStorage = { 5 | getItem: async (name: string): Promise => { 6 | return (await get(name)) || null 7 | }, 8 | setItem: async (name: string, value: string): Promise => { 9 | await set(name, value) 10 | }, 11 | removeItem: async (name: string): Promise => { 12 | await del(name) 13 | }, 14 | } 15 | 16 | export const clearIDB = () => clear() 17 | -------------------------------------------------------------------------------- /src/lib/import-obj.ts: -------------------------------------------------------------------------------- 1 | const importsObject = { 2 | syscall_js: new (function (this: ISyscallJS) { 3 | this.print_bool = (v: boolean): void => { 4 | if (v) { 5 | window.__WA_PRINT__ += 'true' 6 | } 7 | else { 8 | window.__WA_PRINT__ += 'false' 9 | } 10 | } 11 | 12 | this.print_i32 = (i: number): void => { 13 | window.__WA_PRINT__ += i 14 | } 15 | 16 | this.print_u32 = (i: number): void => { 17 | window.__WA_PRINT__ += i 18 | } 19 | 20 | this.print_ptr = (i: number): void => { 21 | window.__WA_PRINT__ += i 22 | } 23 | 24 | this.print_i64 = (i: bigint): void => { 25 | window.__WA_PRINT__ += i 26 | } 27 | 28 | this.print_u64 = (i: bigint): void => { 29 | window.__WA_PRINT__ += i 30 | } 31 | 32 | this.print_f32 = (i: number): void => { 33 | window.__WA_PRINT__ += i 34 | } 35 | 36 | this.print_f64 = (i: number): void => { 37 | window.__WA_PRINT__ += i 38 | } 39 | 40 | this.print_rune = (c: number): void => { 41 | const ch = String.fromCodePoint(c) 42 | if (ch === '\n') { 43 | window.__WA_PRINT__ += '\n' 44 | } 45 | else { 46 | window.__WA_PRINT__ += ch 47 | } 48 | } 49 | 50 | this.print_str = (ptr: number, len: number): void => { 51 | const s = window.__WA_APP__.getString(ptr, len) 52 | window.__WA_PRINT__ += s 53 | } 54 | 55 | this.proc_exit = (_i: number): void => { 56 | // exit(i); 57 | } 58 | } as any)(), 59 | } 60 | 61 | export { importsObject } 62 | -------------------------------------------------------------------------------- /src/lib/memory.ts: -------------------------------------------------------------------------------- 1 | export function formatMemory(buffer: ArrayBuffer, bytesPerRow: number = 4): { address: string, hex: string, ascii: string }[] { 2 | const bytes = new Uint8Array(buffer) 3 | const result: { address: string, hex: string, ascii: string }[] = [] 4 | 5 | for (let i = 0; i < bytes.length; i += bytesPerRow) { 6 | const address = i.toString(16).padStart(8, '0') 7 | 8 | const rowBytes = bytes.slice(i, i + bytesPerRow) 9 | const hex = Array.from(rowBytes) 10 | .map(byte => byte.toString(16).padStart(2, '0')) 11 | .join(' ') 12 | 13 | const ascii = Array.from(rowBytes) 14 | .map(byte => (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.') 15 | .join('') 16 | 17 | result.push({ 18 | address, 19 | hex, 20 | ascii, 21 | }) 22 | } 23 | 24 | return result 25 | } 26 | 27 | export interface IMemoryValue { 28 | 'Integer 8-bit': number 29 | 'Integer 16-bit': number 30 | 'Integer 32-bit': number 31 | 'Integer 64-bit': bigint 32 | 'Float 32-bit': number 33 | 'Float 64-bit': number 34 | 'Pointer 32-bit': number 35 | 'Pointer 64-bit': bigint 36 | } 37 | 38 | export function getMemoryValue( 39 | buffer: ArrayBuffer, 40 | startIdx: number, 41 | isLittleEndian: boolean, 42 | ): IMemoryValue | null { 43 | if (!buffer) 44 | return null 45 | 46 | const uint8Array = new Uint8Array(buffer) 47 | const tempBuffer = new ArrayBuffer(8) 48 | const tempUint8Array = new Uint8Array(tempBuffer) 49 | const view = new DataView(tempBuffer) 50 | 51 | for (let i = 0; i < 8; i++) { 52 | if (startIdx + i < uint8Array.length) { 53 | tempUint8Array[i] = uint8Array[startIdx + i] 54 | } 55 | } 56 | 57 | try { 58 | return { 59 | 'Integer 8-bit': view.getInt8(0), 60 | 'Integer 16-bit': view.getInt16(0, isLittleEndian), 61 | 'Integer 32-bit': view.getInt32(0, isLittleEndian), 62 | 'Integer 64-bit': view.getBigInt64(0, isLittleEndian), 63 | 'Float 32-bit': view.getFloat32(0, isLittleEndian), 64 | 'Float 64-bit': view.getFloat64(0, isLittleEndian), 65 | 'Pointer 32-bit': view.getUint32(0, isLittleEndian), 66 | 'Pointer 64-bit': view.getBigInt64(0, isLittleEndian), 67 | } 68 | } 69 | catch { 70 | return null 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/wawasm.ts: -------------------------------------------------------------------------------- 1 | import { useWasmStore } from '@/stores/wasm' 2 | import JSZip from 'jszip' 3 | import { importsObject } from './import-obj' 4 | 5 | type TWasmInst = WebAssembly.Instance & { 6 | exports: { 7 | '_start': () => void 8 | '__main__.main': () => void 9 | } 10 | } 11 | 12 | const zip = new JSZip() 13 | const isDEV = import.meta.env.DEV || import.meta.env.MODE === 'development' 14 | const WASM_ZIP_URL = isDEV 15 | ? './wa.wasm.zip' 16 | : 'https://wa-lang.org/wa/wa-js/wa.wasm.zip' 17 | 18 | export async function initWaWasm() { 19 | const { wasmInst, go, actions } = useWasmStore.getState() 20 | 21 | if (wasmInst) 22 | return 23 | 24 | const wasmZip = await (await fetch(WASM_ZIP_URL)).blob() 25 | const wasmFile = (await zip.loadAsync(wasmZip)).file('wa.wasm') 26 | if (!wasmFile) 27 | throw new Error('wa.wasm not found in zip') 28 | 29 | const wasmBinary = await wasmFile.async('arraybuffer') 30 | const wasmResponse = new Response(wasmBinary, { 31 | headers: { 32 | 'Content-Type': 'application/wasm', 33 | }, 34 | }) 35 | 36 | const result = await WebAssembly.instantiateStreaming(wasmResponse, go.importObject) 37 | actions.updateWasmInst(result.instance) 38 | actions.updateWasmMod(result.module) 39 | await runWa() 40 | } 41 | 42 | export async function runWa() { 43 | const { wasmInst, wasmMod, go, actions } = useWasmStore.getState() 44 | await go.run(wasmInst) 45 | const newWasmInst = await WebAssembly.instantiate(wasmMod as WebAssembly.Module, go.importObject) 46 | actions.updateWasmInst(newWasmInst) 47 | 48 | window.__WA_PRINT__ = '' 49 | 50 | const binary = window.__WA_WASM__ 51 | if (binary === null) 52 | return 53 | 54 | try { 55 | const module = await WebAssembly.compile(binary) 56 | const wasmInst = await WebAssembly.instantiate(module, importsObject) as TWasmInst 57 | window.__WA_APP__.init(wasmInst) 58 | wasmInst.exports._start() 59 | wasmInst.exports['__main__.main']() 60 | useWasmStore.getState().actions.updateOutput(window.__WA_PRINT__) 61 | useWasmStore.getState().actions.updateWat(window.__WA_WAT__) 62 | } 63 | catch (e) { 64 | console.error(e) 65 | useWasmStore.getState().actions.updateOutput('Code error') 66 | useWasmStore.getState().actions.updateWat(null) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App.tsx' 3 | import './tailwind.css' 4 | 5 | createRoot(document.getElementById('root')!).render() 6 | -------------------------------------------------------------------------------- /src/monaco/actions.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | 3 | function registerSaveAction(monaco: typeof Monaco) { 4 | monaco.editor.registerCommand('wa.editor.save', () => { 5 | const editorInst = monaco.editor.getEditors() 6 | if (!editorInst || editorInst.length === 0) 7 | return 8 | 9 | const editor = editorInst.find(e => e.hasTextFocus()) 10 | if (!editor) 11 | return 12 | 13 | const value = editor.getValue() 14 | 15 | const event = new CustomEvent('wa-editor-save', { 16 | detail: { 17 | value, 18 | editor, 19 | }, 20 | }) 21 | window.dispatchEvent(event) 22 | }) 23 | 24 | monaco.editor.addKeybindingRule({ 25 | keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, 26 | command: 'wa.editor.save', 27 | }) 28 | } 29 | 30 | function registerCommentAction(monaco: typeof Monaco) { 31 | monaco.editor.registerCommand('wa.editor.toggleComment', () => { 32 | const editorInst = monaco.editor.getEditors() 33 | if (!editorInst || editorInst.length === 0) 34 | return 35 | 36 | const editor = editorInst.find(e => e.hasTextFocus()) 37 | if (!editor) 38 | return 39 | 40 | const selection = editor.getSelection() 41 | if (!selection) 42 | return 43 | 44 | const event = new CustomEvent('wa-editor-toggle-comment', { 45 | detail: { 46 | editor, 47 | selection, 48 | }, 49 | }) 50 | window.dispatchEvent(event) 51 | }) 52 | 53 | monaco.editor.addKeybindingRule({ 54 | keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.Slash, 55 | command: 'wa.editor.toggleComment', 56 | }) 57 | } 58 | 59 | export function registerEditorActions(monaco: typeof Monaco) { 60 | registerSaveAction(monaco) 61 | registerCommentAction(monaco) 62 | } 63 | -------------------------------------------------------------------------------- /src/monaco/config.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | 3 | export const monacoConfig = { 4 | fontSize: 14, 5 | tabSize: 4, 6 | glyphMargin: false, 7 | automaticLayout: true, 8 | folding: true, 9 | lineDecorationsWidth: 10, 10 | lineNumbersMinChars: 3, 11 | fontFamily: 'RobotoMono, monospace', 12 | minimap: { enabled: false }, 13 | padding: { 14 | top: 8, 15 | }, 16 | overviewRulerLanes: 0, 17 | fixedOverflowWidgets: true, 18 | } 19 | 20 | export const langConfig = { 21 | brackets: [ 22 | ['{', '}'], 23 | ['[', ']'], 24 | ['(', ')'], 25 | ], 26 | autoClosingPairs: [ 27 | { open: '{', close: '}' }, 28 | { open: '[', close: ']' }, 29 | { open: '(', close: ')' }, 30 | { open: '"', close: '"', notIn: ['string', 'comment'] }, 31 | { open: '\'', close: '\'', notIn: ['string', 'comment'] }, 32 | ], 33 | surroundingPairs: [ 34 | { open: '{', close: '}' }, 35 | { open: '[', close: ']' }, 36 | { open: '(', close: ')' }, 37 | { open: '"', close: '"' }, 38 | { open: '\'', close: '\'' }, 39 | ], 40 | } satisfies Monaco.languages.LanguageConfiguration 41 | -------------------------------------------------------------------------------- /src/monaco/hovers.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | import { LANG_BOOL, LANG_KEYWORDS, LANG_TYPES } from '@/constants/lang' 3 | 4 | export function registerHoverProvider(monaco: typeof Monaco) { 5 | monaco.languages.registerHoverProvider('wa', { 6 | provideHover: (model, pos) => { 7 | const word = model.getWordAtPosition(pos) 8 | if (!word) 9 | return null 10 | 11 | if (LANG_KEYWORDS.includes(word.word)) { 12 | return { 13 | contents: [ 14 | { value: `**${word.word}**` }, 15 | { value: 'Wa Lang Keyword' }, 16 | ], 17 | } 18 | } 19 | 20 | if (LANG_TYPES.includes(word.word)) { 21 | let desc = 'Basic Type' 22 | if (word.word.startsWith('int') || word.word.startsWith('i')) { 23 | desc = 'Signed integer type' 24 | } 25 | else if (word.word.startsWith('uint') || word.word.startsWith('u')) { 26 | desc = 'Unsigned integer type' 27 | } 28 | else if (word.word.startsWith('float') || word.word.startsWith('f')) { 29 | desc = 'Floating-point number type' 30 | } 31 | else if (word.word.startsWith('complex') || word.word.startsWith('c')) { 32 | desc = 'Plural Types' 33 | } 34 | 35 | return { 36 | contents: [ 37 | { value: `**${word.word}**` }, 38 | { value: desc }, 39 | ], 40 | } 41 | } 42 | 43 | if (LANG_BOOL.includes(word.word)) { 44 | return { 45 | contents: [ 46 | { value: `**${word.word}**` }, 47 | { value: 'Boolean' }, 48 | ], 49 | } 50 | } 51 | 52 | return null 53 | }, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/monaco/shiki.ts: -------------------------------------------------------------------------------- 1 | import type { LanguageRegistration } from 'shiki' 2 | import { bundledLanguages, createHighlighter } from 'shiki' 3 | import waGrammar from './wa.tmLanguage.json' 4 | 5 | const wasm = bundledLanguages.wasm 6 | 7 | export async function getShiki(defaultTheme: 'light' | 'dark' = 'light') { 8 | const themes = defaultTheme === 'light' 9 | ? ['vitesse-light', 'vitesse-dark'] 10 | : ['vitesse-dark', 'vitesse-light'] 11 | 12 | const highlighter = await createHighlighter({ 13 | themes, 14 | langs: [wasm, waGrammar as unknown as LanguageRegistration], 15 | }) 16 | 17 | return highlighter 18 | } 19 | -------------------------------------------------------------------------------- /src/monaco/suggestions.ts: -------------------------------------------------------------------------------- 1 | import type * as Monaco from 'monaco-editor' 2 | import { LANG_BOOL, LANG_KEYWORDS, LANG_SNIPPETS, LANG_TYPES } from '@/constants/lang' 3 | 4 | export function registerLangSuggestions(monaco: typeof Monaco) { 5 | monaco.languages.registerCompletionItemProvider('wa', { 6 | triggerCharacters: ['.'], 7 | provideCompletionItems: (model, post, _context, _token) => { 8 | const wordInfo = model.getWordUntilPosition(post) 9 | const wordRange = new monaco.Range( 10 | post.lineNumber, 11 | wordInfo.startColumn, 12 | post.lineNumber, 13 | wordInfo.endColumn, 14 | ) 15 | 16 | const suggestions: Monaco.languages.CompletionItem[] = [] 17 | 18 | LANG_KEYWORDS.forEach((k) => { 19 | suggestions.push({ 20 | label: k, 21 | kind: monaco.languages.CompletionItemKind.Keyword, 22 | insertText: k, 23 | range: wordRange, 24 | detail: 'Keyword', 25 | }) 26 | }) 27 | 28 | LANG_TYPES.forEach((t) => { 29 | suggestions.push({ 30 | label: t, 31 | kind: monaco.languages.CompletionItemKind.Class, 32 | insertText: t, 33 | range: wordRange, 34 | detail: 'Type', 35 | }) 36 | }) 37 | 38 | LANG_BOOL.forEach((b) => { 39 | suggestions.push({ 40 | label: b, 41 | kind: monaco.languages.CompletionItemKind.Value, 42 | insertText: b, 43 | range: wordRange, 44 | detail: 'Boolean', 45 | }) 46 | }) 47 | 48 | LANG_SNIPPETS.forEach((snippet) => { 49 | suggestions.push({ 50 | label: snippet.label, 51 | kind: monaco.languages.CompletionItemKind.Snippet, 52 | insertText: snippet.insertText, 53 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 54 | range: wordRange, 55 | detail: snippet.detail, 56 | }) 57 | }) 58 | 59 | return { suggestions } 60 | }, 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/monaco/wa.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wa", 3 | "scopeName": "source.wa", 4 | "fileTypes": [ 5 | "wa" 6 | ], 7 | "patterns": [ 8 | { 9 | "include": "#comments" 10 | }, 11 | { 12 | "include": "#strings" 13 | }, 14 | { 15 | "include": "#keywords" 16 | }, 17 | { 18 | "include": "#operators" 19 | }, 20 | { 21 | "include": "#types" 22 | }, 23 | { 24 | "include": "#numbers" 25 | }, 26 | { 27 | "include": "#variables" 28 | }, 29 | { 30 | "include": "#support" 31 | } 32 | ], 33 | "repository": { 34 | "keywords": { 35 | "patterns": [ 36 | { 37 | "name": "keyword.control.wa", 38 | "match": "\\b(break|defer|import|struct|case|else|interface|switch|const|for|map|type|continue|func|range|default|global|if|return|make)\\b" 39 | } 40 | ] 41 | }, 42 | "types": { 43 | "patterns": [ 44 | { 45 | "comment": "Types", 46 | "name": "storage.type.wa", 47 | "match": "\\b(bool|string|error|map)\\b" 48 | }, 49 | { 50 | "comment": "Singed Ints", 51 | "name": "storage.type.singed.wa", 52 | "match": "\\b(int|int8|int16|int32|int64|i8|i16|i32|i64|rune)\\b" 53 | }, 54 | { 55 | "comment": "Unsigned Ints", 56 | "name": "storage.type.unsigned.wa", 57 | "match": "\\b(uint|uint8|uint16|uint32|uint64|u8|u16|u32|u64|uintptr|byte)\\b" 58 | }, 59 | { 60 | "comment": "Bool", 61 | "name": "storage.type.bool.wa", 62 | "match": "\\b(true|false)\\b" 63 | }, 64 | { 65 | "comment": "Floats", 66 | "name": "storage.type.floats.wa", 67 | "match": "\\b(float32|float64|f32|f64)\\b" 68 | }, 69 | { 70 | "comment": "Complex", 71 | "name": "storage.type.complex.wa", 72 | "match": "\\b(complex64|complex128|c64|c128)\\b" 73 | } 74 | ] 75 | }, 76 | "strings": { 77 | "patterns": [ 78 | { 79 | "name": "string.quoted.double.wa", 80 | "begin": "\"", 81 | "end": "\"", 82 | "patterns": [ 83 | { 84 | "include": "#stringcontent" 85 | } 86 | ] 87 | }, 88 | { 89 | "name": "string.multiline.wa", 90 | "begin": "\\\\\\\\", 91 | "end": "$" 92 | }, 93 | { 94 | "name": "string.quoted.single.wa", 95 | "match": "'([^'\\\\]|\\\\(x\\h{2}|[0-2][0-7]{,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.))'" 96 | } 97 | ] 98 | }, 99 | "stringcontent": { 100 | "patterns": [ 101 | { 102 | "name": "constant.character.escape.wa", 103 | "match": "\\\\([nrt'\"\\\\]|(x[0-9a-fA-F]{2})|(u\\{[0-9a-fA-F]+\\}))" 104 | }, 105 | { 106 | "name": "invalid.illegal.unrecognized-string-escape.wa", 107 | "match": "\\\\." 108 | } 109 | ] 110 | }, 111 | "numbers": { 112 | "patterns": [ 113 | { 114 | "name": "constant.numeric.float.wa", 115 | "match": "\\b[0-9][0-9_]*(\\.[0-9][0-9_]*)?([eE][+-]?[0-9_]+)?\\b" 116 | }, 117 | { 118 | "name": "constant.numeric.decimal.wa", 119 | "match": "\\b[0-9][0-9_]*\\b" 120 | }, 121 | { 122 | "name": "constant.numeric.hexadecimal.wa", 123 | "match": "\\b0x[a-fA-F0-9_]+\\b" 124 | }, 125 | { 126 | "name": "constant.numeric.octal.wa", 127 | "match": "\\b0o[0-7_]+\\b" 128 | }, 129 | { 130 | "name": "constant.numeric.binary.wa", 131 | "match": "\\b0b[01_]+\\b" 132 | } 133 | ] 134 | }, 135 | "variables": { 136 | "patterns": [ 137 | { 138 | "name": "meta.function.declaration.wa", 139 | "patterns": [ 140 | { 141 | "match": "\\b(func)\\s+([A-Z][a-zA-Z0-9]*)\\b", 142 | "captures": { 143 | "1": { 144 | "name": "storage.type.wa" 145 | }, 146 | "2": { 147 | "name": "entity.name.type.wa" 148 | } 149 | } 150 | }, 151 | { 152 | "match": "\\b(func)\\s+([_a-zA-Z][_a-zA-Z0-9]*)\\b", 153 | "captures": { 154 | "1": { 155 | "name": "storage.type.wa" 156 | }, 157 | "2": { 158 | "name": "entity.name.function.wa" 159 | } 160 | } 161 | }, 162 | { 163 | "begin": "\\b(func)\\s+", 164 | "end": "\"", 165 | "name": "entity.name.function.wa", 166 | "beginCaptures": { 167 | "1": { 168 | "name": "storage.type.wa" 169 | } 170 | }, 171 | "patterns": [ 172 | { 173 | "include": "#stringcontent" 174 | } 175 | ] 176 | }, 177 | { 178 | "name": "storage.type.wa", 179 | "match": "\\b(const|func)\\b" 180 | } 181 | ] 182 | }, 183 | { 184 | "name": "meta.function.call.wa", 185 | "patterns": [ 186 | { 187 | "match": "([A-Z][a-zA-Z0-9]*)(?=\\s*\\{)", 188 | "name": "entity.name.function.wa" 189 | }, 190 | { 191 | "match": "([A-Z][a-zA-Z0-9]*)(?=\\s*\\()", 192 | "name": "entity.name.function.wa" 193 | }, 194 | { 195 | "match": "([_a-zA-Z][_a-zA-Z0-9]*)(?=\\s*\\{)", 196 | "name": "entity.name.function.wa" 197 | }, 198 | { 199 | "match": "([_a-zA-Z][_a-zA-Z0-9]*)(?=\\s*\\()", 200 | "name": "entity.name.function.wa" 201 | }, 202 | { 203 | "match": "([\\u4e00-\\u9fa5]+)(?=\\s*\\()", 204 | "name": "entity.name.function.wa" 205 | } 206 | ] 207 | }, 208 | { 209 | "name": "meta.variable.wa", 210 | "patterns": [ 211 | { 212 | "match": "\\b[_A-Z][_A-Z0-9]+\\b", 213 | "name": "variable.constant.wa" 214 | }, 215 | { 216 | "match": "\\b[_a-zA-Z][_a-zA-Z0-9]*_t\\b", 217 | "name": "entity.name.type.wa" 218 | }, 219 | { 220 | "match": "\\b[A-Z][a-zA-Z0-9]*\\b", 221 | "name": "entity.name.type.wa" 222 | }, 223 | { 224 | "match": "\\b[_a-zA-Z][_a-zA-Z0-9]*\\b", 225 | "name": "variable.other.wa" 226 | } 227 | ] 228 | } 229 | ] 230 | }, 231 | "operators": { 232 | "patterns": [ 233 | { 234 | "name": "keyword.operator.comparison.wa", 235 | "match": "(==|!=|<=|>=|<|>)" 236 | }, 237 | { 238 | "name": "keyword.operator.arithmetic.wa", 239 | "match": "((\\+|-|\\*|/|\\%)=?)|(\\+\\+|--)" 240 | }, 241 | { 242 | "name": "keyword.operator.logical.wa", 243 | "match": "(!|&&|\\|\\|)" 244 | }, 245 | { 246 | "name": "keyword.operator.assignment.wa", 247 | "match": "(:=|=>|=)" 248 | }, 249 | { 250 | "name": "keyword.operator.bitwise.wa", 251 | "match": "((<<|>>|&|&\\^|\\^|\\|)=?)" 252 | } 253 | ] 254 | }, 255 | "comments": { 256 | "patterns": [ 257 | { 258 | "name": "comment.line.double-slash.wa", 259 | "begin": "//", 260 | "beginCaptures": { 261 | "0": { 262 | "name": "punctuation.definition.comment.begin.wa" 263 | } 264 | }, 265 | "end": "$" 266 | }, 267 | { 268 | "name": "comment.block.documentation.wa", 269 | "begin": "/\\*", 270 | "beginCaptures": { 271 | "0": { 272 | "name": "punctuation.definition.comment.begin.wa" 273 | } 274 | }, 275 | "end": "\\*/", 276 | "endCaptures": { 277 | "0": { 278 | "name": "punctuation.definition.comment.end.wa" 279 | } 280 | }, 281 | "patterns": [ 282 | { 283 | "include": "#comments" 284 | } 285 | ] 286 | } 287 | ] 288 | }, 289 | "commentContents": { 290 | "patterns": [ 291 | { 292 | "match": "\\b(TODO|FIXME|NOTE|INFO|IDEA|CHANGED|BUG|HACK)\\b:?", 293 | "name": "comment.line.todo.wa" 294 | } 295 | ] 296 | }, 297 | "support": { 298 | "patterns": [] 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/stores/config.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { createJSONStorage, persist } from 'zustand/middleware' 3 | import { createSelectors } from './withSelectors' 4 | 5 | type TTheme = 'light' | 'dark' 6 | 7 | interface IStore { 8 | theme: TTheme 9 | actions: { 10 | updateTheme: (theme: TTheme) => void 11 | } 12 | } 13 | 14 | const initialState: Omit = { 15 | theme: 'dark', 16 | } 17 | 18 | const configStore = create()( 19 | persist( 20 | set => ({ 21 | ...initialState, 22 | actions: { 23 | updateTheme: theme => set({ theme }), 24 | }, 25 | }), 26 | { 27 | name: 'WA_CONFIG_STORAGE', 28 | version: 1, 29 | storage: createJSONStorage(() => localStorage), 30 | partialize: ({ actions, ...rest }: IStore) => ({ ...rest }) as IStore, 31 | }, 32 | ), 33 | ) 34 | 35 | export const useConfigStore = createSelectors(configStore) 36 | -------------------------------------------------------------------------------- /src/stores/wasm.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { createSelectors } from './withSelectors' 3 | 4 | interface IStore { 5 | wasmInst: WebAssembly.Instance | null 6 | wasmMod: WebAssembly.Module | null 7 | go: any 8 | output: string 9 | wat: string | null 10 | actions: { 11 | updateWasmInst: (wasmInst: WebAssembly.Instance) => void 12 | updateWasmMod: (wasmMod: WebAssembly.Module) => void 13 | updateGo: (go: any) => void 14 | updateOutput: (output: string) => void 15 | updateWat: (wat: string | null) => void 16 | } 17 | } 18 | 19 | const initialState: Omit = { 20 | wasmInst: null, 21 | wasmMod: null, 22 | go: new window.Go(), 23 | output: '', 24 | wat: null, 25 | } 26 | 27 | const wasmStore = create()( 28 | set => ({ 29 | ...initialState, 30 | actions: { 31 | updateWasmInst: wasmInst => set({ wasmInst }), 32 | updateWasmMod: wasmMod => set({ wasmMod }), 33 | updateGo: go => set({ go }), 34 | updateOutput: output => set({ output }), 35 | updateWat: wat => set({ wat }), 36 | }, 37 | }), 38 | ) 39 | 40 | export const useWasmStore = createSelectors(wasmStore) 41 | -------------------------------------------------------------------------------- /src/stores/withSelectors.ts: -------------------------------------------------------------------------------- 1 | import type { StoreApi, UseBoundStore } from 'zustand' 2 | 3 | type WithSelectors = S extends { getState: () => infer T } 4 | ? S & { use: { [K in keyof T]: () => T[K] } } 5 | : never 6 | 7 | export function createSelectors>>(_store: S) { 8 | const store = _store as WithSelectors 9 | store.use = {} 10 | for (const k of Object.keys(store.getState())) { 11 | (store.use as any)[k] = () => store(s => s[k as keyof typeof s]) 12 | } 13 | 14 | return store 15 | } 16 | -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'RobotoMono'; 3 | src: url('/fonts/RobotoMono-Regular.ttf') format('truetype'); 4 | font-weight: 400; 5 | font-style: normal; 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: 'RobotoMono'; 11 | src: url('/fonts/RobotoMono-Medium.ttf') format('truetype'); 12 | font-weight: 500; 13 | font-style: normal; 14 | font-display: swap; 15 | } 16 | 17 | @tailwind base; 18 | @tailwind components; 19 | @tailwind utilities; 20 | @layer base { 21 | :root { 22 | --background: 0 0% 100%; 23 | --foreground: 240 10% 3.9%; 24 | --card: 0 0% 100%; 25 | --card-foreground: 240 10% 3.9%; 26 | --popover: 0 0% 100%; 27 | --popover-foreground: 240 10% 3.9%; 28 | --primary: 240 5.9% 10%; 29 | --primary-foreground: 0 0% 98%; 30 | --secondary: 240 4.8% 95.9%; 31 | --secondary-foreground: 240 5.9% 10%; 32 | --muted: 240 4.8% 95.9%; 33 | --muted-foreground: 240 3.8% 46.1%; 34 | --accent: 240 4.8% 95.9%; 35 | --accent-foreground: 240 5.9% 10%; 36 | --destructive: 0 84.2% 60.2%; 37 | --destructive-foreground: 0 0% 98%; 38 | --border: 240 5.9% 90%; 39 | --input: 240 5.9% 90%; 40 | --ring: 240 10% 3.9%; 41 | --chart-1: 12 76% 61%; 42 | --chart-2: 173 58% 39%; 43 | --chart-3: 197 37% 24%; 44 | --chart-4: 43 74% 66%; 45 | --chart-5: 27 87% 67%; 46 | --radius: 0.5rem 47 | } 48 | .dark { 49 | --background: 240 10% 3.9%; 50 | --foreground: 0 0% 98%; 51 | --card: 240 10% 3.9%; 52 | --card-foreground: 0 0% 98%; 53 | --popover: 240 10% 3.9%; 54 | --popover-foreground: 0 0% 98%; 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | --secondary: 240 3.7% 15.9%; 58 | --secondary-foreground: 0 0% 98%; 59 | --muted: 240 3.7% 15.9%; 60 | --muted-foreground: 240 5% 64.9%; 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: 0 0% 98%; 63 | --destructive: 0 62.8% 30.6%; 64 | --destructive-foreground: 0 0% 98%; 65 | --border: 240 3.7% 15.9%; 66 | --input: 240 3.7% 15.9%; 67 | --ring: 240 4.9% 83.9%; 68 | --chart-1: 220 70% 50%; 69 | --chart-2: 160 60% 45%; 70 | --chart-3: 30 80% 55%; 71 | --chart-4: 280 65% 60%; 72 | --chart-5: 340 75% 55% 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground font-mono font-normal; 82 | } 83 | 84 | ::-webkit-scrollbar { 85 | @apply w-2; 86 | } 87 | 88 | ::-webkit-scrollbar:horizontal { 89 | @apply h-2; 90 | } 91 | 92 | ::-webkit-scrollbar-thumb { 93 | @apply bg-foreground/5; 94 | } 95 | 96 | ::-webkit-scrollbar-thumb:hover { 97 | @apply bg-foreground/10; 98 | } 99 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | darkMode: ['class'], 4 | content: [ 5 | './index.html', 6 | './src/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | mono: ['RobotoMono', 'monospace'], 12 | sans: ['system-ui', 'sans-serif'], 13 | }, 14 | borderRadius: { 15 | lg: 'var(--radius)', 16 | md: 'calc(var(--radius) - 2px)', 17 | sm: 'calc(var(--radius) - 4px)', 18 | }, 19 | colors: { 20 | theme: { 21 | DEFAULT: '#00B5AB', 22 | foreground: '#00B5AB1a', 23 | }, 24 | background: 'hsl(var(--background))', 25 | foreground: 'hsl(var(--foreground))', 26 | card: { 27 | DEFAULT: 'hsl(var(--card))', 28 | foreground: 'hsl(var(--card-foreground))', 29 | }, 30 | popover: { 31 | DEFAULT: 'hsl(var(--popover))', 32 | foreground: 'hsl(var(--popover-foreground))', 33 | }, 34 | primary: { 35 | DEFAULT: 'hsl(var(--primary))', 36 | foreground: 'hsl(var(--primary-foreground))', 37 | }, 38 | secondary: { 39 | DEFAULT: 'hsl(var(--secondary))', 40 | foreground: 'hsl(var(--secondary-foreground))', 41 | }, 42 | muted: { 43 | DEFAULT: 'hsl(var(--muted))', 44 | foreground: 'hsl(var(--muted-foreground))', 45 | }, 46 | accent: { 47 | DEFAULT: 'hsl(var(--accent))', 48 | foreground: 'hsl(var(--accent-foreground))', 49 | }, 50 | destructive: { 51 | DEFAULT: 'hsl(var(--destructive))', 52 | foreground: 'hsl(var(--destructive-foreground))', 53 | }, 54 | border: 'hsl(var(--border))', 55 | input: 'hsl(var(--input))', 56 | ring: 'hsl(var(--ring))', 57 | chart: { 58 | 1: 'hsl(var(--chart-1))', 59 | 2: 'hsl(var(--chart-2))', 60 | 3: 'hsl(var(--chart-3))', 61 | 4: 'hsl(var(--chart-4))', 62 | 5: 'hsl(var(--chart-5))', 63 | }, 64 | }, 65 | }, 66 | }, 67 | plugins: [require('tailwindcss-animate')], 68 | } 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES2017", 5 | "jsx": "preserve", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "paths": { 14 | "@/*": [ 15 | "./src/*" 16 | ] 17 | }, 18 | "resolveJsonModule": true, 19 | "allowImportingTsExtensions": true, 20 | "allowJs": true, 21 | "strict": true, 22 | "noEmit": true, 23 | "esModuleInterop": true, 24 | "isolatedModules": true, 25 | "skipLibCheck": true 26 | }, 27 | "include": [ 28 | "**/*.ts", 29 | "**/*.tsx", 30 | "global.d.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | import svgr from 'vite-plugin-svgr' 5 | 6 | export default defineConfig({ 7 | base: '/playground/', 8 | plugins: [ 9 | react(), 10 | svgr(), 11 | ], 12 | resolve: { 13 | alias: { 14 | '@': path.resolve(__dirname, './src'), 15 | }, 16 | }, 17 | define: { 18 | 'process.env.VSCODE_TEXTMATE_DEBUG': 'false', 19 | }, 20 | }) 21 | --------------------------------------------------------------------------------