import { Box, BoxProps, HStack } from '@chakra-ui/react' import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup' import { json, jsonParseLinter } from '@codemirror/lang-json' import { css } from '@codemirror/lang-css' import { javascript } from '@codemirror/lang-javascript' import { html } from '@codemirror/lang-html' import { useEffect, useRef, useState } from 'react' import { useDebouncedCallback } from 'use-debounce' import { linter } from '@codemirror/lint' import { VariablesButton } from './buttons/VariablesButton' import { Variable } from 'models' import { isEmpty } from 'utils' const linterExtension = linter(jsonParseLinter()) type Props = { value: string lang?: 'css' | 'json' | 'js' | 'html' isReadOnly?: boolean debounceTimeout?: number withVariableButton?: boolean onChange?: (value: string) => void } export const CodeEditor = ({ value, lang, onChange, withVariableButton = true, isReadOnly = false, debounceTimeout = 1000, ...props }: Props & Omit) => { const editorContainer = useRef(null) const editorView = useRef(null) const [, setPlainTextValue] = useState(value) const [carretPosition, setCarretPosition] = useState(0) const isVariableButtonDisplayed = withVariableButton && !isReadOnly const debounced = useDebouncedCallback( (value) => { setPlainTextValue(value) onChange && onChange(value) }, isEmpty(process.env.NEXT_PUBLIC_E2E_TEST) ? debounceTimeout : 0 ) useEffect( () => () => { debounced.flush() }, [debounced] ) useEffect(() => { if (!editorView.current || !isReadOnly) return editorView.current.dispatch({ changes: { from: 0, to: editorView.current.state.doc.length, insert: value, }, }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]) useEffect(() => { const editor = initEditor(value) return () => { editor?.destroy() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const initEditor = (value: string) => { if (!editorContainer.current) return const updateListenerExtension = EditorView.updateListener.of((update) => { if (update.docChanged && onChange) debounced(update.state.doc.toJSON().join('\n')) }) const extensions = [ updateListenerExtension, basicSetup, EditorState.readOnly.of(isReadOnly), ] if (lang === 'json') { extensions.push(json()) extensions.push(linterExtension) } if (lang === 'css') extensions.push(css()) if (lang === 'js') extensions.push(javascript()) if (lang === 'html') extensions.push(html()) extensions.push( EditorView.theme({ '&': { maxHeight: '500px' }, '.cm-gutter,.cm-content': { minHeight: isReadOnly ? '0' : '250px' }, '.cm-scroller': { overflow: 'auto' }, }) ) const editor = new EditorView({ state: EditorState.create({ extensions, }), parent: editorContainer.current, }) editor.dispatch({ changes: { from: 0, insert: value }, }) editorView.current = editor return editor } const handleVariableSelected = (variable?: Pick) => { editorView.current?.focus() const insert = `{{${variable?.name}}}` editorView.current?.dispatch({ changes: { from: carretPosition, insert, }, selection: { anchor: carretPosition + insert.length }, }) } const handleKeyUp = () => { if (!editorContainer.current) return setCarretPosition(editorView.current?.state.selection.main.from ?? 0) } return ( {isVariableButtonDisplayed && ( )} ) }