val 0.3.6

An arbitrary precision calculator language
Documentation
import { highlightExtension } from '@/lib/highlight';
import { ValError } from '@/lib/types';
import { useEditorSettings } from '@/providers/editor-settings-provider';
import { rust } from '@codemirror/lang-rust';
import {
  bracketMatching,
  defaultHighlightStyle,
  indentOnInput,
  syntaxHighlighting,
} from '@codemirror/language';
import { Diagnostic, linter } from '@codemirror/lint';
import { vim } from '@replit/codemirror-vim';
import CodeMirror, { EditorState, EditorView } from '@uiw/react-codemirror';
import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
} from 'react';

interface EditorProps {
  errors: ValError[];
  onChange?: (value: string, viewUpdate: any) => void;
  onEditorReady?: (view: EditorView) => void;
  value: string;
}

export interface EditorRef {
  view: EditorView | null;
}

export const Editor = forwardRef<EditorRef, EditorProps>(
  ({ value, errors, onChange, onEditorReady }, ref) => {
    const { settings } = useEditorSettings();

    const viewRef = useRef<EditorView | null>(null);

    useImperativeHandle(ref, () => ({
      get view() {
        return viewRef.current;
      },
    }));

    useEffect(() => {
      if (viewRef.current && onEditorReady) {
        onEditorReady(viewRef.current);
      }
    }, [viewRef.current, onEditorReady]);

    const createExtensions = useCallback(() => {
      const extensions = [
        EditorState.tabSize.of(settings.tabSize),
        bracketMatching(),
        highlightExtension,
        indentOnInput(),
        linter(diagnostics()),
        rust(),
        syntaxHighlighting(defaultHighlightStyle),
      ];

      if (settings.lineWrapping) {
        extensions.push(EditorView.lineWrapping);
      }

      if (settings.keybindings === 'vim') {
        extensions.push(vim());
      }

      return extensions;
    }, [settings]);

    const createTheme = useCallback(
      () =>
        EditorView.theme({
          '&': {
            height: '100%',
            fontSize: `${settings.fontSize}px`,
            display: 'flex',
            flexDirection: 'column',
          },
          '&.cm-editor': {
            height: '100%',
          },
          '.cm-scroller': {
            overflow: 'auto',
            flex: '1 1 auto',
            fontFamily:
              'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
          },
          '.cm-line': {
            padding: '0 10px',
          },
          '.cm-content': {
            padding: '10px 0',
          },
          '.cm-gutters': {
            backgroundColor: 'transparent',
            borderRight: 'none',
            paddingRight: '8px',
          },
          '.cm-activeLineGutter': {
            backgroundColor: 'rgba(59, 130, 246, 0.1)',
          },
          '.cm-activeLine': {
            backgroundColor: 'rgba(59, 130, 246, 0.1)',
          },
          '.cm-fat-cursor': {
            backgroundColor: 'rgba(59, 130, 246, 0.5)',
            borderLeft: 'none',
            width: '0.6em',
          },
          '.cm-cursor-secondary': {
            backgroundColor: 'rgba(59, 130, 246, 0.3)',
          },
        }),
      [settings]
    );

    const diagnostics = () =>
      useCallback(
        (_view: EditorView): Diagnostic[] => {
          return errors.map((error) => {
            try {
              return {
                from: error.range.start,
                to: error.range.end,
                severity: 'error',
                message: error.message,
                source: error.kind.toString(),
              };
            } catch (e) {
              console.warn('Failed to create diagnostic:', e, error);

              return {
                from: 0,
                to: 0,
                severity: 'error',
                message: error.message,
                source: error.kind.toString(),
              };
            }
          });
        },
        [errors]
      );

    const handleEditorCreate = (view: EditorView) => {
      viewRef.current = view;

      if (onEditorReady) {
        onEditorReady(view);
      }
    };

    return (
      <CodeMirror
        value={value}
        theme={createTheme()}
        basicSetup={{
          foldGutter: false,
          highlightActiveLineGutter: false,
          lineNumbers: settings.lineNumbers,
        }}
        height='100%'
        extensions={createExtensions()}
        onCreateEditor={handleEditorCreate}
        onChange={onChange}
        className='h-full'
      />
    );
  }
);

Editor.displayName = 'Editor';