(({ 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 |
--------------------------------------------------------------------------------