Files
xrpl-hooks-ide/components/HooksEditor.tsx
2022-08-17 11:50:49 +05:30

291 lines
9.3 KiB
TypeScript

import React, { useEffect, useRef } from 'react'
import { useSnapshot, ref } from 'valtio'
import type monaco from 'monaco-editor'
import { ArrowBendLeftUp } from 'phosphor-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import Box from './Box'
import Container from './Container'
import { createNewFile, saveFile } from '../state/actions'
import { apiHeaderFiles } from '../state/constants'
import state from '../state'
import EditorNavigation from './EditorNavigation'
import Text from './Text'
import { MonacoServices } from '@codingame/monaco-languageclient'
import { createLanguageClient, createWebSocket } from '../utils/languageClient'
import { listen } from '@codingame/monaco-jsonrpc'
import ReconnectingWebSocket from 'reconnecting-websocket'
import docs from '../xrpl-hooks-docs/docs'
import Monaco from './Monaco'
import { saveAllFiles } from '../state/actions/saveFile'
import { Tab, Tabs } from './Tabs'
import { renameFile } from '../state/actions/createNewFile'
const checkWritable = (filename?: string): boolean => {
if (apiHeaderFiles.find(file => file === filename)) {
return false
}
return true
}
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const filename = editor.getModel()?.uri.path.split('/').pop()
const isWritable = checkWritable(filename)
editor.updateOptions({ readOnly: !isWritable })
}
let decorations: { [key: string]: string[] } = {}
const setMarkers = (monacoE: typeof monaco) => {
// Get all the markers that are active at the moment,
// Also if same error is there twice, we can show the content
// only once (that's why we're using uniqBy)
const markers = monacoE.editor
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
marker =>
typeof marker?.code === 'string' &&
// Take only markers that starts with "hooks-"
marker?.code?.includes('hooks-')
)
// Get the active model (aka active file you're editing)
// const model = monacoE.editor?.getModel(
// monacoE.Uri.parse(`file:///work/c/${state.files?.[state.active]?.name}`)
// );
// console.log(state.active);
// Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are
const models = monacoE.editor.getModels()
models.forEach(model => {
decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [],
markers
.filter(marker =>
marker?.resource.path.split('/').includes(`${state.files?.[state.active]?.name}`)
)
.map(marker => ({
range: new monacoE.Range(
marker.startLineNumber,
marker.startColumn,
marker.endLineNumber,
marker.endColumn
),
options: {
hoverMessage: {
value:
// Find the related hover message markdown from the
// /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
// which was generated from rst files
(typeof marker.code === 'string' && docs[marker?.code]?.toString()) || '',
supportHtml: true,
isTrusted: true
}
}
}))
)
})
}
const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
const monacoRef = useRef<typeof monaco>()
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null)
const snap = useSnapshot(state)
const router = useRouter()
const { theme } = useTheme()
useEffect(() => {
if (editorRef.current) validateWritability(editorRef.current)
}, [snap.active])
useEffect(() => {
return () => {
subscriptionRef?.current?.close()
}
}, [])
useEffect(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current)
}
}, [snap.active])
useEffect(() => {
return () => {
saveAllFiles()
}
}, [])
const file = snap.files[snap.active]
const renderNav = () => (
<Tabs
label="File"
activeIndex={snap.active}
onChangeActive={idx => (state.active = idx)}
extensionRequired
onCreateNewTab={createNewFile}
onCloseTab={idx => state.files.splice(idx, 1)}
onRenameTab={(idx, nwName, oldName = '') => renameFile(oldName, nwName)}
headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
error: 'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"'
}}
>
{snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} renameDisabled={!checkWritable(file.name)} />
})}
</Tabs>
)
return (
<Box
css={{
flex: 1,
flexShrink: 1,
display: 'flex',
position: 'relative',
flexDirection: 'column',
backgroundColor: '$mauve2',
width: '100%'
}}
>
<EditorNavigation renderNav={renderNav} />
{snap.files.length > 0 && router.isReady ? (
<Monaco
keepCurrentModel
defaultLanguage={file?.language}
language={file?.language}
path={`file:///work/c/${file?.name}`}
defaultValue={file?.content}
// onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
if (!snap.editorCtx) {
snap.files.forEach(file =>
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.parse(`file:///work/c/${file.name}`)
)
)
}
// create the web socket
if (!subscriptionRef.current) {
monaco.languages.register({
id: 'c',
extensions: ['.c', '.h'],
aliases: ['C', 'c', 'H', 'h'],
mimetypes: ['text/plain']
})
MonacoServices.install(monaco)
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
)
subscriptionRef.current = webSocket
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: connection => {
// create and start the language client
const languageClient = createLanguageClient(connection)
const disposable = languageClient.start()
connection.onClose(() => {
try {
disposable.dispose()
} catch (err) {
console.log('err', err)
}
})
}
})
}
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor)
}
}}
onMount={(editor, monaco) => {
editorRef.current = editor
monacoRef.current = monaco
editor.updateOptions({
glyphMargin: true,
lightbulb: {
enabled: true
}
})
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
saveFile()
})
// When the markers (errors/warnings from clangd language server) change
// Lets improve the markers by adding extra content to them from related
// md files
monaco.editor.onDidChangeMarkers(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current)
}
})
// Hacky way to hide Peek menu
editor.onContextMenu(e => {
const host = document.querySelector<HTMLElement>('.shadow-root-host')
const contextMenuItems = host?.shadowRoot?.querySelectorAll('li.action-item')
contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it
if (k.querySelector('.action-label')?.textContent === 'Peek') {
// @ts-expect-error
k['style'].display = 'none'
}
})
})
validateWritability(editor)
}}
theme={theme === 'dark' ? 'dark' : 'light'}
/>
) : (
<Container>
{!snap.loading && router.isReady && (
<Box
css={{
flexDirection: 'row',
width: '$spaces$wide',
gap: '$3',
display: 'inline-flex'
}}
>
<Box css={{ display: 'inline-flex', pl: '35px' }}>
<ArrowBendLeftUp size={30} />
</Box>
<Box css={{ pl: '0px', pt: '15px', flex: 1, display: 'inline-flex' }}>
<Text
css={{
fontSize: '14px',
maxWidth: '220px',
fontFamily: '$monospace'
}}
>
Click the link above to create your file
</Text>
</Box>
</Box>
)}
</Container>
)}
</Box>
)
}
export default HooksEditor