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.

code-editor.tsx 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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', monospace",
  100. fontWeight: '420',
  101. wordBasedSuggestions: false,
  102. minimap: { enabled: false },
  103. lightbulb: {
  104. enabled: false,
  105. },
  106. readOnly,
  107. })
  108. }, [])
  109. const handleChange = useCallback((code: string | undefined) => {
  110. onChange(code, rEditor.current)
  111. }, [])
  112. const handleKeydown = useCallback(
  113. (e: React.KeyboardEvent<HTMLDivElement>) => {
  114. e.stopPropagation()
  115. !modifierKeys.includes(e.key) && onKey?.()
  116. const metaKey = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
  117. if (e.key === 's' && metaKey) {
  118. const editor = rEditor.current
  119. if (!editor) return
  120. editor
  121. .getAction('editor.action.formatDocument')
  122. .run()
  123. .then(() =>
  124. onSave(rEditor.current?.getModel().getValue(), rEditor.current)
  125. )
  126. e.preventDefault()
  127. }
  128. if (e.key === 'p' && metaKey) {
  129. e.preventDefault()
  130. }
  131. if (e.key === 'd' && metaKey) {
  132. e.preventDefault()
  133. }
  134. },
  135. []
  136. )
  137. const handleKeyUp = useCallback(
  138. (e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
  139. []
  140. )
  141. const rDecorations = useRef<any>([])
  142. useEffect(() => {
  143. const monaco = rMonaco.current
  144. if (!monaco) return
  145. const editor = rEditor.current
  146. if (!editor) return
  147. if (!error) {
  148. rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
  149. return
  150. }
  151. if (!error.line) return
  152. rDecorations.current = editor.deltaDecorations(rDecorations.current, [
  153. {
  154. range: new monaco.Range(
  155. Number(error.line) - 1,
  156. 0,
  157. Number(error.line) - 1,
  158. 0
  159. ),
  160. options: {
  161. isWholeLine: true,
  162. className: 'editorLineError',
  163. },
  164. },
  165. ])
  166. }, [error])
  167. useEffect(() => {
  168. const monaco = rMonaco.current
  169. if (!monaco) return
  170. monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'light')
  171. }, [theme])
  172. useEffect(() => {
  173. const editor = rEditor.current
  174. if (!editor) return
  175. editor.updateOptions({
  176. fontSize,
  177. })
  178. }, [fontSize])
  179. return (
  180. <EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
  181. <Editor
  182. height="100%"
  183. language="typescript"
  184. value={value}
  185. theme={theme === 'dark' ? 'vs-dark' : 'light'}
  186. beforeMount={handleBeforeMount}
  187. onMount={handleMount}
  188. onChange={handleChange}
  189. defaultPath="index.ts"
  190. />
  191. </EditorContainer>
  192. )
  193. }
  194. const EditorContainer = styled('div', {
  195. height: '100%',
  196. pointerEvents: 'all',
  197. userSelect: 'all',
  198. '& > *': {
  199. userSelect: 'all',
  200. pointerEvents: 'all',
  201. },
  202. '.editorLineError': {
  203. backgroundColor: '$lineError',
  204. },
  205. })