Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

code-editor.tsx 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import Editor, { Monaco } from '@monaco-editor/react'
  2. import useTheme from 'hooks/useTheme'
  3. import prettier from 'prettier/standalone'
  4. import parserTypeScript from 'prettier/parser-typescript'
  5. import codeAsString from './code-as-string'
  6. import React, { useCallback, useEffect, useRef } from 'react'
  7. import styled from 'styles'
  8. import { IMonaco, IMonacoEditor } from 'types'
  9. interface Props {
  10. value: string
  11. error: { line: number }
  12. fontSize: number
  13. monacoRef?: React.MutableRefObject<IMonaco>
  14. editorRef?: React.MutableRefObject<IMonacoEditor>
  15. readOnly?: boolean
  16. onMount?: (value: string, editor: IMonacoEditor) => void
  17. onUnmount?: (editor: IMonacoEditor) => void
  18. onChange?: (value: string, editor: IMonacoEditor) => void
  19. onSave?: (value: string, editor: IMonacoEditor) => void
  20. onError?: (error: Error, line: number, col: number) => void
  21. onKey?: () => void
  22. }
  23. export default function CodeEditor({
  24. editorRef,
  25. monacoRef,
  26. fontSize,
  27. value,
  28. error,
  29. readOnly,
  30. onChange,
  31. onSave,
  32. onKey,
  33. }: Props) {
  34. const { theme } = useTheme()
  35. const rEditor = useRef<IMonacoEditor>(null)
  36. const rMonaco = useRef<IMonaco>(null)
  37. const handleBeforeMount = useCallback((monaco: Monaco) => {
  38. if (monacoRef) {
  39. monacoRef.current = monaco
  40. }
  41. rMonaco.current = monaco
  42. monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
  43. allowJs: true,
  44. checkJs: false,
  45. strict: false,
  46. noLib: true,
  47. lib: ['es6'],
  48. target: monaco.languages.typescript.ScriptTarget.ES2015,
  49. allowNonTsExtensions: true,
  50. })
  51. monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
  52. monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
  53. monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
  54. noSemanticValidation: true,
  55. noSyntaxValidation: true,
  56. })
  57. monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
  58. noSemanticValidation: true,
  59. noSyntaxValidation: true,
  60. })
  61. monaco.languages.typescript.javascriptDefaults.addExtraLib(codeAsString)
  62. monaco.languages.registerDocumentFormattingEditProvider('javascript', {
  63. async provideDocumentFormattingEdits(model) {
  64. const text = prettier.format(model.getValue(), {
  65. parser: 'typescript',
  66. plugins: [parserTypeScript],
  67. singleQuote: true,
  68. trailingComma: 'es5',
  69. semi: false,
  70. })
  71. return [
  72. {
  73. range: model.getFullModelRange(),
  74. text,
  75. },
  76. ]
  77. },
  78. })
  79. }, [])
  80. const handleMount = useCallback((editor: IMonacoEditor) => {
  81. if (editorRef) {
  82. editorRef.current = editor
  83. }
  84. rEditor.current = editor
  85. editor.updateOptions({
  86. fontSize,
  87. wordBasedSuggestions: false,
  88. minimap: { enabled: false },
  89. lightbulb: {
  90. enabled: false,
  91. },
  92. readOnly,
  93. })
  94. }, [])
  95. const handleChange = useCallback((code: string | undefined) => {
  96. onChange(code, rEditor.current)
  97. }, [])
  98. const handleKeydown = useCallback(
  99. (e: React.KeyboardEvent<HTMLDivElement>) => {
  100. onKey && onKey()
  101. e.stopPropagation()
  102. const metaKey = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
  103. if (e.key === 's' && metaKey) {
  104. const editor = rEditor.current
  105. if (!editor) return
  106. editor
  107. .getAction('editor.action.formatDocument')
  108. .run()
  109. .then(() =>
  110. onSave(rEditor.current?.getModel().getValue(), rEditor.current)
  111. )
  112. e.preventDefault()
  113. }
  114. if (e.key === 'p' && metaKey) {
  115. e.preventDefault()
  116. }
  117. if (e.key === 'd' && metaKey) {
  118. e.preventDefault()
  119. }
  120. },
  121. []
  122. )
  123. const handleKeyUp = useCallback(
  124. (e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
  125. []
  126. )
  127. const rDecorations = useRef<any>([])
  128. useEffect(() => {
  129. const monaco = rMonaco.current
  130. if (!monaco) return
  131. const editor = rEditor.current
  132. if (!editor) return
  133. if (!error) {
  134. rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
  135. return
  136. }
  137. if (!error.line) return
  138. rDecorations.current = editor.deltaDecorations(rDecorations.current, [
  139. {
  140. range: new monaco.Range(
  141. Number(error.line) - 1,
  142. 0,
  143. Number(error.line) - 1,
  144. 0
  145. ),
  146. options: {
  147. isWholeLine: true,
  148. className: 'editorLineError',
  149. },
  150. },
  151. ])
  152. }, [error])
  153. useEffect(() => {
  154. const monaco = rMonaco.current
  155. if (!monaco) return
  156. monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'light')
  157. }, [theme])
  158. useEffect(() => {
  159. const editor = rEditor.current
  160. if (!editor) return
  161. editor.updateOptions({
  162. fontSize,
  163. })
  164. }, [fontSize])
  165. return (
  166. <EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
  167. <Editor
  168. height="100%"
  169. language="javascript"
  170. value={value}
  171. theme={theme === 'dark' ? 'vs-dark' : 'light'}
  172. beforeMount={handleBeforeMount}
  173. onMount={handleMount}
  174. onChange={handleChange}
  175. />
  176. </EditorContainer>
  177. )
  178. }
  179. const EditorContainer = styled('div', {
  180. height: '100%',
  181. pointerEvents: 'all',
  182. userSelect: 'all',
  183. '& > *': {
  184. userSelect: 'all',
  185. pointerEvents: 'all',
  186. },
  187. '.editorLineError': {
  188. backgroundColor: '$lineError',
  189. },
  190. })