tldraw/components/code-panel/code-editor.tsx

249 lines
5.8 KiB
TypeScript
Raw Normal View History

import Editor, { Monaco } from '@monaco-editor/react'
import useTheme from 'hooks/useTheme'
import libImport from './es5-lib'
2021-06-23 22:32:21 +00:00
import typesImport from './types-import'
import React, { useCallback, useEffect, useRef } from 'react'
import styled from 'styles'
2021-06-23 22:32:21 +00:00
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
import { getFormattedCode } from 'utils/code'
2021-07-10 16:28:44 +00:00
import { metaKey } from 'utils'
2021-06-23 22:32:21 +00:00
export type IMonaco = typeof monaco
export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
2021-05-14 22:56:41 +00:00
const modifierKeys = ['Escape', 'Meta', 'Control', 'Shift', 'Option', 'Alt']
2021-05-14 22:56:41 +00:00
interface Props {
value: string
error: { line: number; column: number }
2021-05-14 22:56:41 +00:00
fontSize: number
monacoRef?: React.MutableRefObject<IMonaco>
editorRef?: React.MutableRefObject<IMonacoEditor>
readOnly?: boolean
onMount?: (value: string, editor: IMonacoEditor) => void
onUnmount?: (editor: IMonacoEditor) => void
onChange?: (value: string, editor: IMonacoEditor) => void
onSave?: (value: string, editor: IMonacoEditor) => void
onError?: (error: Error, line: number, col: number) => void
onKey?: () => void
}
export default function CodeEditor({
editorRef,
monacoRef,
fontSize,
value,
error,
readOnly,
onChange,
onSave,
onKey,
2021-06-21 21:35:28 +00:00
}: Props): JSX.Element {
2021-05-14 22:56:41 +00:00
const { theme } = useTheme()
2021-07-10 16:14:49 +00:00
2021-05-14 22:56:41 +00:00
const rEditor = useRef<IMonacoEditor>(null)
const rMonaco = useRef<IMonaco>(null)
const handleBeforeMount = useCallback((monaco: Monaco) => {
2021-06-25 12:09:53 +00:00
if (monacoRef) {
monacoRef.current = monaco
}
2021-05-14 22:56:41 +00:00
rMonaco.current = monaco
// Set the compiler options.
2021-05-14 22:56:41 +00:00
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
allowJs: true,
2021-06-25 12:09:53 +00:00
checkJs: true,
strict: true,
noLib: true,
lib: ['es6'],
2021-06-25 12:09:53 +00:00
target: monaco.languages.typescript.ScriptTarget.ES2016,
allowNonTsExtensions: true,
2021-05-14 22:56:41 +00:00
})
// Sync the intellisense on load.
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
2021-05-14 22:56:41 +00:00
// Run both semantic and syntax validation.
2021-06-25 12:09:53 +00:00
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
})
// Add custom types
2021-06-23 22:32:21 +00:00
monaco.languages.typescript.typescriptDefaults.addExtraLib(
typesImport.content
)
2021-06-25 12:09:53 +00:00
// Add es5 library types
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libImport.content
2021-06-23 22:32:21 +00:00
)
2021-05-14 22:56:41 +00:00
// Use prettier as a formatter
2021-06-23 22:32:21 +00:00
monaco.languages.registerDocumentFormattingEditProvider('typescript', {
2021-05-14 22:56:41 +00:00
async provideDocumentFormattingEdits(model) {
2021-06-24 14:59:56 +00:00
try {
const text = getFormattedCode(model.getValue())
2021-06-24 14:59:56 +00:00
return [
{
range: model.getFullModelRange(),
text,
},
]
} catch (e) {
return [
{
range: model.getFullModelRange(),
text: model.getValue(),
},
]
}
2021-05-14 22:56:41 +00:00
},
})
}, [])
const handleMount = useCallback((editor: IMonacoEditor) => {
if (editorRef) {
editorRef.current = editor
}
rEditor.current = editor
editor.updateOptions({
fontSize,
2021-06-27 21:30:37 +00:00
fontFamily: "'Recursive Mono', monospace",
2021-05-14 22:56:41 +00:00
wordBasedSuggestions: false,
minimap: { enabled: false },
lightbulb: {
enabled: false,
},
readOnly,
})
}, [])
const handleChange = useCallback((code: string | undefined) => {
onChange(code, rEditor.current)
}, [])
const handleKeydown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
e.stopPropagation()
2021-07-10 16:28:44 +00:00
!modifierKeys.includes(e.key) && onKey?.()
2021-07-10 16:28:44 +00:00
if ((e.key === 's' || e.key === 'Enter') && metaKey(e)) {
2021-05-14 22:56:41 +00:00
const editor = rEditor.current
2021-07-10 16:28:44 +00:00
2021-05-14 22:56:41 +00:00
if (!editor) return
2021-07-10 16:28:44 +00:00
2021-05-14 22:56:41 +00:00
editor
.getAction('editor.action.formatDocument')
2021-05-14 22:56:41 +00:00
.run()
.then(() =>
onSave(rEditor.current?.getModel().getValue(), rEditor.current)
)
e.preventDefault()
}
2021-07-10 16:28:44 +00:00
if (e.key === 'p' && metaKey(e)) {
2021-05-14 22:56:41 +00:00
e.preventDefault()
}
2021-07-10 16:28:44 +00:00
if (e.key === 'd' && metaKey(e)) {
2021-05-14 22:56:41 +00:00
e.preventDefault()
}
},
[]
)
const handleKeyUp = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
[]
)
const rDecorations = useRef<any>([])
useEffect(() => {
const monaco = rMonaco.current
if (!monaco) return
const editor = rEditor.current
if (!editor) return
if (!error) {
rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
return
}
if (!error.line) return
rDecorations.current = editor.deltaDecorations(rDecorations.current, [
{
range: new monaco.Range(
Number(error.line) - 1,
0,
Number(error.line) - 1,
0
),
options: {
isWholeLine: true,
className: 'editorLineError',
2021-05-14 22:56:41 +00:00
},
},
])
}, [error])
useEffect(() => {
const monaco = rMonaco.current
if (!monaco) return
monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'light')
2021-05-14 22:56:41 +00:00
}, [theme])
useEffect(() => {
const editor = rEditor.current
if (!editor) return
editor.updateOptions({
fontSize,
})
}, [fontSize])
return (
<EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
<Editor
height="100%"
2021-06-23 22:32:21 +00:00
language="typescript"
2021-05-14 22:56:41 +00:00
value={value}
theme={theme === 'dark' ? 'vs-dark' : 'light'}
2021-05-14 22:56:41 +00:00
beforeMount={handleBeforeMount}
onMount={handleMount}
onChange={handleChange}
defaultPath="index.ts"
2021-05-14 22:56:41 +00:00
/>
</EditorContainer>
)
}
const EditorContainer = styled('div', {
height: '100%',
pointerEvents: 'all',
userSelect: 'all',
2021-05-14 22:56:41 +00:00
'& > *': {
userSelect: 'all',
pointerEvents: 'all',
},
'.editorLineError': {
backgroundColor: '$lineError',
2021-05-14 22:56:41 +00:00
},
})