import React, { useMemo } from 'react'; import _CodeMirror from '@uiw/react-codemirror'; import { EditorView, Decoration } from '@codemirror/view'; import { StateField, StateEffect } from '@codemirror/state'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import strudelTheme from '../themes/strudel-theme'; import './style.css'; import { useCallback } from 'react'; import { autocompletion } from '@codemirror/autocomplete'; import { strudelAutocomplete } from './Autocomplete'; import { vim } from '@replit/codemirror-vim'; import { emacs } from '@replit/codemirror-emacs'; export const setFlash = StateEffect.define(); const flashField = StateField.define({ create() { return Decoration.none; }, update(flash, tr) { try { for (let e of tr.effects) { if (e.is(setFlash)) { if (e.value) { const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } }); flash = Decoration.set([mark.range(0, tr.newDoc.length)]); } else { flash = Decoration.set([]); } } } return flash; } catch (err) { console.warn('flash error', err); return flash; } }, provide: (f) => EditorView.decorations.from(f), }); export const flash = (view) => { view.dispatch({ effects: setFlash.of(true) }); setTimeout(() => { view.dispatch({ effects: setFlash.of(false) }); }, 200); }; export const setHighlights = StateEffect.define(); const highlightField = StateField.define({ create() { return Decoration.none; }, update(highlights, tr) { try { for (let e of tr.effects) { if (e.is(setHighlights)) { const { haps } = e.value; const marks = haps .map((hap) => (hap.context.locations || []).map(({ start, end }) => { const color = hap.context.color || e.value.color; let from = tr.newDoc.line(start.line).from + start.column; let to = tr.newDoc.line(end.line).from + end.column; const l = tr.newDoc.length; if (from > l || to > l) { return; // dont mark outside of range, as it will throw an error } let mark; if (color) { mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } }); } else { mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } }); } return mark.range(from, to); }), ) .flat() .filter(Boolean) || []; highlights = Decoration.set(marks, true); } } return highlights; } catch (err) { // console.warn('highlighting error', err); return Decoration.set([]); } }, provide: (f) => EditorView.decorations.from(f), }); const staticExtensions = [javascript(), highlightField, flashField]; export default function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, theme, keybindings, isLineNumbersDisplayed, isAutoCompletionEnabled, isLineWrappingEnabled, fontSize = 18, fontFamily = 'monospace', options, editorDidMount, }) { const handleOnChange = useCallback( (value) => { onChange?.(value); }, [onChange], ); const handleOnCreateEditor = useCallback( (view) => { onViewChanged?.(view); }, [onViewChanged], ); const handleOnUpdate = useCallback( (viewUpdate) => { if (viewUpdate.selectionSet && onSelectionChange) { onSelectionChange?.(viewUpdate.state.selection); } }, [onSelectionChange], ); const extensions = useMemo(() => { let _extensions = [...staticExtensions]; let bindings = { vim, emacs, }; if (bindings[keybindings]) { _extensions.push(bindings[keybindings]()); } if (isAutoCompletionEnabled) { _extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete })); } else { _extensions.push(autocompletion({ override: [] })); } if (isLineWrappingEnabled) { _extensions.push(EditorView.lineWrapping); } return _extensions; }, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]); return (