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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import Editor, { Monaco } from '@monaco-editor/react'
  2. import { useTheme } from 'next-themes'
  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. import { metaKey } from 'utils'
  10. export type IMonaco = typeof monaco
  11. export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
  12. const modifierKeys = ['Escape', 'Meta', 'Control', 'Shift', 'Option', 'Alt']
  13. interface Props {
  14. value: string
  15. error: { line: number; column: number }
  16. fontSize: number
  17. monacoRef?: React.MutableRefObject<IMonaco>
  18. editorRef?: React.MutableRefObject<IMonacoEditor>
  19. readOnly?: boolean
  20. onMount?: (value: string, editor: IMonacoEditor) => void
  21. onUnmount?: (editor: IMonacoEditor) => void
  22. onChange?: (value: string, editor: IMonacoEditor) => void
  23. onSave?: (value: string, editor: IMonacoEditor) => void
  24. onError?: (error: Error, line: number, col: number) => void
  25. onKey?: () => void
  26. }
  27. export default function CodeEditor({
  28. editorRef,
  29. monacoRef,
  30. fontSize,
  31. value,
  32. error,
  33. readOnly,
  34. onChange,
  35. onSave,
  36. onKey,
  37. }: Props): JSX.Element {
  38. const { theme } = useTheme()
  39. const rEditor = useRef<IMonacoEditor>(null)
  40. const rMonaco = useRef<IMonaco>(null)
  41. const handleBeforeMount = useCallback((monaco: Monaco) => {
  42. if (monacoRef) {
  43. monacoRef.current = monaco
  44. }
  45. rMonaco.current = monaco
  46. // Set the compiler options.
  47. monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
  48. allowJs: true,
  49. checkJs: true,
  50. strict: true,
  51. noLib: true,
  52. lib: ['es6'],
  53. target: monaco.languages.typescript.ScriptTarget.ES2016,
  54. allowNonTsExtensions: true,
  55. })
  56. // Sync the intellisense on load.
  57. monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
  58. // Run both semantic and syntax validation.
  59. monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
  60. noSemanticValidation: false,
  61. noSyntaxValidation: false,
  62. })
  63. // Add custom types
  64. monaco.languages.typescript.typescriptDefaults.addExtraLib(
  65. typesImport.content
  66. )
  67. // Add es5 library types
  68. monaco.languages.typescript.typescriptDefaults.addExtraLib(
  69. libImport.content
  70. )
  71. // Use prettier as a formatter
  72. monaco.languages.registerDocumentFormattingEditProvider('typescript', {
  73. async provideDocumentFormattingEdits(model) {
  74. try {
  75. const text = getFormattedCode(model.getValue())
  76. return [
  77. {
  78. range: model.getFullModelRange(),
  79. text,
  80. },
  81. ]
  82. } catch (e) {
  83. return [
  84. {
  85. range: model.getFullModelRange(),
  86. text: model.getValue(),
  87. },
  88. ]
  89. }
  90. },
  91. })
  92. }, [])
  93. const handleMount = useCallback((editor: IMonacoEditor) => {
  94. if (editorRef) {
  95. editorRef.current = editor
  96. }
  97. rEditor.current = editor
  98. editor.updateOptions({
  99. fontSize,
  100. fontFamily: "'Recursive Mono', monospace",
  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. if ((e.key === 's' || e.key === 'Enter') && metaKey(e)) {
  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(e)) {
  128. e.preventDefault()
  129. }
  130. if (e.key === 'd' && metaKey(e)) {
  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. })