import React, { useEffect, useRef, useState } 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' import { Link } from '.' import Markdown from './Markdown' 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() const monacoRef = useRef() const subscriptionRef = useRef(null) const snap = useSnapshot(state) const router = useRouter() const { theme } = useTheme() const [isMdPreview, setIsMdPreview] = useState(true) 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 = () => ( (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 })} ) const previewToggle = ( { if (!isMdPreview) { saveFile(false) } setIsMdPreview(!isMdPreview) }} css={{ position: 'absolute', right: 0, bottom: 0, zIndex: 10, m: '$1', fontSize: '$sm' }} > {isMdPreview ? 'Exit Preview' : 'View Preview'} ) return ( {file?.language === 'markdown' && previewToggle} {snap.files.length > 0 && router.isReady ? ( isMdPreview && file?.language === 'markdown' ? ( ( {children} ) }} > {file.content} ) : ( (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'] }) monaco.languages.register({ id: 'text', extensions: ['.txt'], 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('.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'} /> ) ) : ( {!snap.loading && router.isReady && ( Click the link above to create your file )} )} ) } export default HooksEditor