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 { 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'; const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const currPath = editor.getModel()?.uri.path; if (apiHeaderFiles.find(h => currPath?.endsWith(h))) { editor.updateOptions({ readOnly: true }); } else { editor.updateOptions({ readOnly: false }); } }; 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(); 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]; return ( {snap.files.length > 0 && router.isReady ? ( (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(".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;