import Editor, { Monaco } from "@monaco-editor/react" import useTheme from "hooks/useTheme" import prettier from "prettier/standalone" import parserTypeScript from "prettier/parser-typescript" import codeAsString from "./code-as-string" import React, { useCallback, useEffect, useRef } from "react" import styled from "styles" import { IMonaco, IMonacoEditor } from "types" interface Props { value: string error: { line: number } fontSize: number monacoRef?: React.MutableRefObject editorRef?: React.MutableRefObject readOnly?: boolean onMount?: (value: string, editor: IMonacoEditor) => void onUnmount?: (editor: IMonacoEditor) => void onChange?: (value: string, editor: IMonacoEditor) => void onSave?: (value: string, editor: IMonacoEditor) => void onError?: (error: Error, line: number, col: number) => void onKey?: () => void } export default function CodeEditor({ editorRef, monacoRef, fontSize, value, error, readOnly, onChange, onSave, onKey, }: Props) { const { theme } = useTheme() const rEditor = useRef(null) const rMonaco = useRef(null) const handleBeforeMount = useCallback((monaco: Monaco) => { if (monacoRef) { monacoRef.current = monaco } rMonaco.current = monaco monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ allowJs: true, checkJs: false, strict: false, noLib: true, lib: ["es6"], target: monaco.languages.typescript.ScriptTarget.ES2015, allowNonTsExtensions: true, }) monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true) monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true) monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ noSemanticValidation: true, noSyntaxValidation: true, }) monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ noSemanticValidation: true, noSyntaxValidation: true, }) monaco.languages.typescript.javascriptDefaults.addExtraLib(codeAsString) monaco.languages.registerDocumentFormattingEditProvider("javascript", { async provideDocumentFormattingEdits(model) { const text = prettier.format(model.getValue(), { parser: "typescript", plugins: [parserTypeScript], singleQuote: true, trailingComma: "es5", semi: false, }) return [ { range: model.getFullModelRange(), text, }, ] }, }) }, []) const handleMount = useCallback((editor: IMonacoEditor) => { if (editorRef) { editorRef.current = editor } rEditor.current = editor editor.updateOptions({ fontSize, wordBasedSuggestions: false, minimap: { enabled: false }, lightbulb: { enabled: false, }, readOnly, }) }, []) const handleChange = useCallback((code: string | undefined) => { onChange(code, rEditor.current) }, []) const handleKeydown = useCallback( (e: React.KeyboardEvent) => { onKey && onKey() e.stopPropagation() const metaKey = navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey if (e.key === "s" && metaKey) { const editor = rEditor.current if (!editor) return editor .getAction("editor.action.formatDocument") .run() .then(() => onSave(rEditor.current?.getModel().getValue(), rEditor.current) ) e.preventDefault() } if (e.key === "p" && metaKey) { e.preventDefault() } if (e.key === "d" && metaKey) { e.preventDefault() } }, [] ) const handleKeyUp = useCallback( (e: React.KeyboardEvent) => e.stopPropagation(), [] ) const rDecorations = useRef([]) useEffect(() => { const monaco = rMonaco.current if (!monaco) return const editor = rEditor.current if (!editor) return if (!error) { rDecorations.current = editor.deltaDecorations(rDecorations.current, []) return } if (!error.line) return rDecorations.current = editor.deltaDecorations(rDecorations.current, [ { range: new monaco.Range( Number(error.line) - 1, 0, Number(error.line) - 1, 0 ), options: { isWholeLine: true, className: "editorLineError", }, }, ]) }, [error]) useEffect(() => { const monaco = rMonaco.current if (!monaco) return monaco.editor.setTheme(theme === "dark" ? "vs-dark" : "light") }, [theme]) useEffect(() => { const editor = rEditor.current if (!editor) return editor.updateOptions({ fontSize, }) }, [fontSize]) return ( ) } const EditorContainer = styled("div", { height: "100%", pointerEvents: "all", userSelect: "all", ".editorLineError": { backgroundColor: "$lineError", }, })