123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- import Editor, { Monaco } from '@monaco-editor/react'
- import useTheme from 'hooks/useTheme'
- import libImport from './es5-lib'
- import typesImport from './types-import'
- import React, { useCallback, useEffect, useRef } from 'react'
- import styled from 'styles'
- import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
- import { getFormattedCode } from 'utils/code'
-
- export type IMonaco = typeof monaco
-
- export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
-
- const modifierKeys = ['Escape', 'Meta', 'Control', 'Shift', 'Option', 'Alt']
-
- interface Props {
- value: string
- error: { line: number; column: 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): JSX.Element {
- 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
-
- // Set the compiler options.
-
- monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
- allowJs: true,
- checkJs: true,
- strict: true,
- noLib: true,
- lib: ['es6'],
- target: monaco.languages.typescript.ScriptTarget.ES2016,
- allowNonTsExtensions: true,
- })
-
- // Sync the intellisense on load.
-
- monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
-
- // Run both semantic and syntax validation.
-
- monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
- noSemanticValidation: false,
- noSyntaxValidation: false,
- })
-
- // Add custom types
-
- monaco.languages.typescript.typescriptDefaults.addExtraLib(
- typesImport.content
- )
-
- // Add es5 library types
-
- monaco.languages.typescript.typescriptDefaults.addExtraLib(
- libImport.content
- )
-
- // Use prettier as a formatter
-
- monaco.languages.registerDocumentFormattingEditProvider('typescript', {
- async provideDocumentFormattingEdits(model) {
- try {
- const text = getFormattedCode(model.getValue())
-
- return [
- {
- range: model.getFullModelRange(),
- text,
- },
- ]
- } catch (e) {
- return [
- {
- range: model.getFullModelRange(),
- text: model.getValue(),
- },
- ]
- }
- },
- })
- }, [])
-
- const handleMount = useCallback((editor: IMonacoEditor) => {
- if (editorRef) {
- editorRef.current = editor
- }
- rEditor.current = editor
-
- editor.updateOptions({
- fontSize,
- fontFamily: "'Recursive Mono', monospace",
- 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>) => {
- e.stopPropagation()
- !modifierKeys.includes(e.key) && onKey?.()
- 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="typescript"
- value={value}
- theme={theme === 'dark' ? 'vs-dark' : 'light'}
- beforeMount={handleBeforeMount}
- onMount={handleMount}
- onChange={handleChange}
- defaultPath="index.ts"
- />
- </EditorContainer>
- )
- }
-
- const EditorContainer = styled('div', {
- height: '100%',
- pointerEvents: 'all',
- userSelect: 'all',
-
- '& > *': {
- userSelect: 'all',
- pointerEvents: 'all',
- },
-
- '.editorLineError': {
- backgroundColor: '$lineError',
- },
- })
|