You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import Editor, { Monaco } from '@monaco-editor/react'
  2. import useTheme from 'hooks/useTheme'
  3. import libImport from './es5-lib'
  4. import typesImport from './types-import'
  5. import React, { useCallback, useEffect, useRef } from 'react'
  6. import styled from 'styles'
  7. import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
  8. import { getFormattedCode } from 'utils/code'
  9. export type IMonaco = typeof monaco
  10. export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
  11. const modifierKeys = ['Escape', 'Meta', 'Control', 'Shift', 'Option', 'Alt']
  12. interface Props {
  13. value: string
  14. error: { line: number; column: 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. // Set the compiler options.
  46. monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
  47. allowJs: true,
  48. checkJs: true,
  49. strict: true,
  50. noLib: true,
  51. lib: ['es6'],
  52. target: monaco.languages.typescript.ScriptTarget.ES2016,
  53. allowNonTsExtensions: true,
  54. })
  55. // Sync the intellisense on load.
  56. monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
  57. // Run both semantic and syntax validation.
  58. monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
  59. noSemanticValidation: false,
  60. noSyntaxValidation: false,
  61. })
  62. // Add custom types
  63. monaco.languages.typescript.typescriptDefaults.addExtraLib(
  64. typesImport.content
  65. )
  66. // Add es5 library types
  67. monaco.languages.typescript.typescriptDefaults.addExtraLib(
  68. libImport.content
  69. )
  70. // Use prettier as a formatter
  71. monaco.languages.registerDocumentFormattingEditProvider('typescript', {
  72. async provideDocumentFormattingEdits(model) {
  73. try {
  74. const text = getFormattedCode(model.getValue())
  75. return [
  76. {
  77. range: model.getFullModelRange(),
  78. text,
  79. },
  80. ]
  81. } catch (e) {
  82. return [
  83. {
  84. range: model.getFullModelRange(),
  85. text: model.getValue(),
  86. },
  87. ]
  88. }
  89. },
  90. })
  91. }, [])
  92. const handleMount = useCallback((editor: IMonacoEditor) => {
  93. if (editorRef) {
  94. editorRef.current = editor
  95. }
  96. rEditor.current = editor
  97. editor.updateOptions({
  98. fontSize,
  99. fontFamily: "'Recursive Mono', monospace",
  100. wordBasedSuggestions: false,
  101. minimap: { enabled: false },
  102. lightbulb: {
  103. enabled: false,
  104. },
  105. readOnly,
  106. })
  107. }, [])
  108. const handleChange = useCallback((code: string | undefined) => {
  109. onChange(code, rEditor.current)
  110. }, [])
  111. const handleKeydown = useCallback(
  112. (e: React.KeyboardEvent<HTMLDivElement>) => {
  113. e.stopPropagation()
  114. !modifierKeys.includes(e.key) && onKey?.()
  115. const metaKey = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
  116. if (e.key === 's' && metaKey) {
  117. const editor = rEditor.current
  118. if (!editor) return
  119. editor
  120. .getAction('editor.action.formatDocument')
  121. .run()
  122. .then(() =>
  123. onSave(rEditor.current?.getModel().getValue(), rEditor.current)
  124. )
  125. e.preventDefault()
  126. }
  127. if (e.key === 'p' && metaKey) {
  128. e.preventDefault()
  129. }
  130. if (e.key === 'd' && metaKey) {
  131. e.preventDefault()
  132. }
  133. },
  134. []
  135. )
  136. const handleKeyUp = useCallback(
  137. (e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
  138. []
  139. )
  140. const rDecorations = useRef<any>([])
  141. useEffect(() => {
  142. const monaco = rMonaco.current
  143. if (!monaco) return
  144. const editor = rEditor.current
  145. if (!editor) return
  146. if (!error) {
  147. rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
  148. return
  149. }
  150. if (!error.line) return
  151. rDecorations.current = editor.deltaDecorations(rDecorations.current, [
  152. {
  153. range: new monaco.Range(
  154. Number(error.line) - 1,
  155. 0,
  156. Number(error.line) - 1,
  157. 0
  158. ),
  159. options: {
  160. isWholeLine: true,
  161. className: 'editorLineError',
  162. },
  163. },
  164. ])
  165. }, [error])
  166. useEffect(() => {
  167. const monaco = rMonaco.current
  168. if (!monaco) return
  169. monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'light')
  170. }, [theme])
  171. useEffect(() => {
  172. const editor = rEditor.current
  173. if (!editor) return
  174. editor.updateOptions({
  175. fontSize,
  176. })
  177. }, [fontSize])
  178. return (
  179. <EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
  180. <Editor
  181. height="100%"
  182. language="typescript"
  183. value={value}
  184. theme={theme === 'dark' ? 'vs-dark' : 'light'}
  185. beforeMount={handleBeforeMount}
  186. onMount={handleMount}
  187. onChange={handleChange}
  188. defaultPath="index.ts"
  189. />
  190. </EditorContainer>
  191. )
  192. }
  193. const EditorContainer = styled('div', {
  194. height: '100%',
  195. pointerEvents: 'all',
  196. userSelect: 'all',
  197. '& > *': {
  198. userSelect: 'all',
  199. pointerEvents: 'all',
  200. },
  201. '.editorLineError': {
  202. backgroundColor: '$lineError',
  203. },
  204. })