val 0.3.6

An arbitrary precision calculator language
Documentation
import { AstNode } from '@/components/ast-node';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import type { AstNode as AstNodeType, ValError } from '@/lib/types';
import { EditorView } from '@codemirror/view';
import { Radius } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import init, { parse } from 'val-wasm';

import { Editor, EditorRef } from './components/editor';
import { EditorSettingsDialog } from './components/editor-settings-dialog';
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from './components/ui/resizable';
import EXAMPLES from './lib/examples';

const STORAGE_KEY_CODE = 'val-editor-code';
const STORAGE_KEY_EXAMPLE = 'val-editor-example';

function App() {
  const [ast, setAst] = useState<AstNodeType | null>(null);

  const [code, setCode] = useState(() => {
    const savedCode = localStorage.getItem(STORAGE_KEY_CODE);
    return savedCode || EXAMPLES.factorial;
  });

  const [currentExample, setCurrentExample] = useState(() => {
    const savedExample = localStorage.getItem(STORAGE_KEY_EXAMPLE);
    return savedExample || 'factorial';
  });

  const [editorView, setEditorView] = useState<EditorView | null>(null);
  const [errors, setErrors] = useState<ValError[]>([]);
  const [wasmLoaded, setWasmLoaded] = useState(false);

  const editorRef = useRef<EditorRef>(null);

  const handleEditorReady = (view: EditorView) => {
    setEditorView(view);
  };

  useEffect(() => {
    init()
      .then(() => {
        setWasmLoaded(true);
      })
      .catch((error) => {
        toast.error(error);
      });
  }, []);

  useEffect(() => {
    if (!wasmLoaded) return;

    try {
      setAst(parse(code));
    } catch (error) {
      setErrors(error as ValError[]);
    }
  }, [code, wasmLoaded]);

  useEffect(() => {
    if (editorRef.current?.view && !editorView) {
      setEditorView(editorRef.current.view);
    }
  }, [editorRef.current?.view, editorView]);

  useEffect(() => {
    localStorage.setItem(STORAGE_KEY_CODE, code);
  }, [code]);

  useEffect(() => {
    localStorage.setItem(STORAGE_KEY_EXAMPLE, currentExample);
  }, [currentExample]);

  const handleExampleChange = (value: string) => {
    setCurrentExample(value);
    setCode(EXAMPLES[value as keyof typeof EXAMPLES]);
  };

  if (!wasmLoaded) return null;

  return (
    <div className='flex h-screen flex-col p-4'>
      <div className='mb-4 flex items-center'>
        <a href='/val' className='font-semibold'>
          <div className='flex items-center gap-x-1'>
            <Radius className='h-4 w-4' />
            <p>val</p>
          </div>
        </a>
      </div>
      <ResizablePanelGroup
        direction='horizontal'
        className='min-h-0 flex-grow overflow-hidden rounded border'
      >
        <ResizablePanel
          defaultSize={50}
          minSize={30}
          className='flex min-h-0 flex-col overflow-hidden'
        >
          <div className='flex h-full flex-col overflow-hidden'>
            <div className='flex h-full min-h-0 flex-col overflow-hidden'>
              <div className='flex items-center justify-between border-b bg-gray-50 px-2 py-1'>
                <div className='flex items-center'>
                  <Select
                    value={currentExample}
                    onValueChange={handleExampleChange}
                  >
                    <SelectTrigger className='h-7 w-36 bg-white text-sm'>
                      <SelectValue placeholder='Select example' />
                    </SelectTrigger>
                    <SelectContent>
                      {Object.keys(EXAMPLES).map((key) => (
                        <SelectItem key={key} value={key}>
                          {key}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </div>
                <EditorSettingsDialog />
              </div>
              <div className='h-full min-h-0 flex-grow overflow-hidden'>
                <Editor
                  errors={errors}
                  onChange={setCode}
                  onEditorReady={handleEditorReady}
                  ref={editorRef}
                  value={code}
                />
              </div>
            </div>
          </div>
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel
          defaultSize={50}
          minSize={30}
          className='min-h-0 overflow-hidden'
        >
          <div className='h-full overflow-auto p-2'>
            {ast ? (
              <AstNode node={ast} editorView={editorView} />
            ) : (
              <div className='text-muted-foreground p-2'>No AST available</div>
            )}
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

export default App;