/* eslint-disable react/display-name */ import ReactCodeMirror, { EditorView, ReactCodeMirrorRef, keymap, } from "@uiw/react-codemirror"; import { javascript } from "@codemirror/lang-javascript"; import { css } from "@codemirror/lang-css"; import { html } from "@codemirror/lang-html"; import { json } from "@codemirror/lang-json"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night"; import { vscodeKeymap } from "@replit/codemirror-vscode-keymap"; import prettier from "prettier/standalone"; import prettierHtmlPlugin from "prettier/plugins/html"; import prettierCssPlugin from "prettier/plugins/postcss"; import prettierBabelPlugin from "prettier/plugins/babel"; import * as prettierPluginEstree from "prettier/plugins/estree"; import { abbreviationTracker } from "@emmetio/codemirror6-plugin"; import { useDebounce } from "~/hooks/useDebounce"; import useCommandKey from "~/hooks/useCommandKey"; type Props = { lang?: string; value: string; wordWrap?: boolean; onChange: (val: string) => void; formatOnSave?: boolean; }; const CodeEditor = (props: Props) => { const codeMirror = useRef(null); const { lang, value, formatOnSave, wordWrap, onChange } = props; const [data, setData] = useState(value); const [debounceChange, resetDebounceChange] = useDebounce(onChange, 3000); const langMetadata = useMemo(() => getLangMetadata(lang || "plain"), [lang]); const onSave = useCallback(async () => { try { const cm = codeMirror.current?.view; const content = cm ? cm.state.doc.toString() : data; const formatter = langMetadata.formatter; if (formatOnSave && cm && formatter != null) { const [parser, ...plugins] = formatter; const cursor = cm.state.selection.main.head || 0; const { formatted, cursorOffset } = await prettier.formatWithCursor( content, { parser, plugins, cursorOffset: cursor, printWidth: 64, } ); setData(formatted); onChange(formatted); if (cm) { cm.dispatch({ changes: { from: 0, to: cm?.state.doc.length, insert: formatted }, }); cm.dispatch({ selection: { anchor: cursorOffset }, }); } } else { onChange(content); } } catch (err) { console.log("prettier error", err); } setTimeout(() => resetDebounceChange(), 100); }, [ data, langMetadata.formatter, formatOnSave, onChange, resetDebounceChange, ]); useCommandKey("s", onSave); useEffect(() => { setData(value); }, [value]); const extensions = [...langMetadata.extensions, keymap.of(vscodeKeymap)]; if (wordWrap) { extensions.push(EditorView.lineWrapping); } return ( { setData(val); debounceChange(val); }} height="100%" theme={tokyoNight} /> ); }; function getLangMetadata(lang: string) { let extensions: any[] = []; let formatter: any = null; switch (lang) { case "html": extensions = [html({ selfClosingTags: true }), abbreviationTracker()]; formatter = ["html", prettierHtmlPlugin]; break; case "css": extensions = [css()]; formatter = ["css", prettierCssPlugin]; break; case "json": extensions = [json()]; formatter = ["json", prettierBabelPlugin, prettierPluginEstree]; break; case "jsx": case "js": case "ts": case "tsx": extensions = [ javascript({ jsx: ["jsx", "tsx"].includes(lang), typescript: ["tsx", "ts"].includes(lang), }), ]; formatter = ["babel", prettierBabelPlugin, prettierPluginEstree]; break; } return { extensions, formatter }; } export default CodeEditor;