| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 | 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<IMonaco>
  editorRef?: React.MutableRefObject<IMonacoEditor>
  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<IMonacoEditor>(null)
  const rMonaco = useRef<IMonaco>(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<HTMLDivElement>) => {
      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<HTMLDivElement>) => e.stopPropagation(),
    []
  )
  const rDecorations = useRef<any>([])
  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 (
    <EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
      <Editor
        height="100%"
        language="javascript"
        value={value}
        theme={theme === "dark" ? "vs-dark" : "light"}
        beforeMount={handleBeforeMount}
        onMount={handleMount}
        onChange={handleChange}
      />
    </EditorContainer>
  )
}
const EditorContainer = styled("div", {
  height: "100%",
  pointerEvents: "all",
  userSelect: "all",
  ".editorLineError": {
    backgroundColor: "$lineError",
  },
})
 |