Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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