tldraw/components/code-panel/code-editor.tsx
2021-05-14 23:56:41 +01:00

214 lines
5.3 KiB
TypeScript

import Editor, { Monaco } from "@monaco-editor/react"
import useTheme from "hooks/useTheme"
import prettier from "prettier/standalone"
import parserTypeScript from "prettier/parser-typescript"
import codeAsString from "./code-as-string"
import React, { useCallback, useEffect, useRef } from "react"
import styled from "styles"
import { IMonaco, IMonacoEditor } from "types"
interface Props {
value: string
error: { line: number }
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,
}: Props) {
const { theme } = useTheme()
const rEditor = useRef<IMonacoEditor>(null)
const rMonaco = useRef<IMonaco>(null)
const handleBeforeMount = useCallback((monaco: Monaco) => {
if (monacoRef) {
monacoRef.current = monaco
}
rMonaco.current = monaco
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
allowJs: true,
checkJs: false,
strict: false,
noLib: true,
lib: ["es6"],
target: monaco.languages.typescript.ScriptTarget.ES2015,
allowNonTsExtensions: true,
})
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: true,
})
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: true,
})
monaco.languages.typescript.javascriptDefaults.addExtraLib(codeAsString)
monaco.languages.registerDocumentFormattingEditProvider("javascript", {
async provideDocumentFormattingEdits(model) {
const text = prettier.format(model.getValue(), {
parser: "typescript",
plugins: [parserTypeScript],
singleQuote: true,
trailingComma: "es5",
semi: false,
})
return [
{
range: model.getFullModelRange(),
text,
},
]
},
})
}, [])
const handleMount = useCallback((editor: IMonacoEditor) => {
if (editorRef) {
editorRef.current = editor
}
rEditor.current = editor
editor.updateOptions({
fontSize,
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>) => {
onKey && onKey()
e.stopPropagation()
const metaKey = navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey
if (e.key === "s" && metaKey) {
const editor = rEditor.current
if (!editor) return
editor
.getAction("editor.action.formatDocument")
.run()
.then(() =>
onSave(rEditor.current?.getModel().getValue(), rEditor.current)
)
e.preventDefault()
}
if (e.key === "p" && metaKey) {
e.preventDefault()
}
if (e.key === "d" && metaKey) {
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",
},
},
])
}, [error])
useEffect(() => {
const monaco = rMonaco.current
if (!monaco) return
monaco.editor.setTheme(theme === "dark" ? "vs-dark" : "light")
}, [theme])
useEffect(() => {
const editor = rEditor.current
if (!editor) return
editor.updateOptions({
fontSize,
})
}, [fontSize])
return (
<EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
<Editor
height="100%"
language="javascript"
value={value}
theme={theme === "dark" ? "vs-dark" : "light"}
beforeMount={handleBeforeMount}
onMount={handleMount}
onChange={handleChange}
/>
</EditorContainer>
)
}
const EditorContainer = styled("div", {
height: "100%",
pointerEvents: "all",
userSelect: "all",
".editorLineError": {
backgroundColor: "$lineError",
},
})