"use client"; import { toSocket, WebSocketMessageReader, WebSocketMessageWriter, } from "vscode-ws-jsonrpc"; import dynamic from "next/dynamic"; import normalizeUrl from "normalize-url"; import type { editor } from "monaco-editor"; import { getHighlighter } from "@/lib/shiki"; import { Loading } from "@/components/loading"; import { shikiToMonaco } from "@shikijs/monaco"; import type { Monaco } from "@monaco-editor/react"; import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor"; import { useMonacoTheme } from "@/hooks/use-monaco-theme"; import { LanguageServerConfig } from "@/generated/client"; import type { MessageTransports } from "vscode-languageclient"; import { useCallback, useEffect, useRef, useState } from "react"; import type { MonacoLanguageClient } from "monaco-languageclient"; const MonacoEditor = dynamic( async () => { const [react, monaco] = await Promise.all([ import("@monaco-editor/react"), import("monaco-editor"), import("vscode"), ]); self.MonacoEnvironment = { getWorker() { return new Worker( new URL( "monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url ) ); }, }; react.loader.config({ monaco }); return react.Editor; }, { ssr: false, loading: () => , } ); interface CoreEditorProps { language?: string; value?: string; path?: string; languageServerConfigs?: LanguageServerConfig[]; onEditorReady?: (editor: editor.IStandaloneCodeEditor) => void; onLspWebSocketReady?: (lspWebSocket: WebSocket) => void; onChange?: (value: string) => void; onMarkersReady?: (markers: editor.IMarker[]) => void; className?: string; } export const CoreEditor = ({ language, value, path, languageServerConfigs, onEditorReady, onLspWebSocketReady, onChange, onMarkersReady, className, }: CoreEditorProps) => { const { theme } = useMonacoTheme(); const [isEditorMounted, setIsEditorMounted] = useState(false); const editorRef = useRef(null); const lspClientRef = useRef(null); const webSocketRef = useRef(null); const activeLanguageServerConfig = languageServerConfigs?.find( (config) => config.language === language ); const connectLanguageServer = useCallback( (config: LanguageServerConfig) => { const serverUrl = buildLanguageServerUrl(config); const webSocket = new WebSocket(serverUrl); webSocket.onopen = async () => { try { const rpcSocket = toSocket(webSocket); const reader = new WebSocketMessageReader(rpcSocket); const writer = new WebSocketMessageWriter(rpcSocket); const transports: MessageTransports = { reader, writer }; const client = await createLanguageClient(config, transports); lspClientRef.current = client; await client.start(); } catch (error) { console.error("Failed to initialize language client:", error); } webSocketRef.current = webSocket; onLspWebSocketReady?.(webSocket); }; }, [onLspWebSocketReady] ); useEffect(() => { if (isEditorMounted && activeLanguageServerConfig) { connectLanguageServer(activeLanguageServerConfig); } return () => { if (lspClientRef.current) { lspClientRef.current.stop(); lspClientRef.current = null; } }; }, [activeLanguageServerConfig, connectLanguageServer, isEditorMounted]); const handleBeforeMount = useCallback((monaco: Monaco) => { const highlighter = getHighlighter(); shikiToMonaco(highlighter, monaco); }, []); const handleOnMount = useCallback( (editor: editor.IStandaloneCodeEditor) => { editorRef.current = editor; onEditorReady?.(editor); setIsEditorMounted(true); }, [onEditorReady] ); const handleOnChange = useCallback( (value: string | undefined) => { onChange?.(value ?? ""); }, [onChange] ); const handleOnValidate = useCallback( (markers: editor.IMarker[]) => { onMarkersReady?.(markers); }, [onMarkersReady] ); return ( } className={className} /> ); }; const buildLanguageServerUrl = (config: LanguageServerConfig) => { return normalizeUrl( `${config.protocol}://${config.hostname}${ config.port ? `:${config.port}` : "" }${config.path ?? ""}` ); }; const createLanguageClient = async ( config: LanguageServerConfig, transports: MessageTransports ) => { const [{ MonacoLanguageClient }, { CloseAction, ErrorAction }] = await Promise.all([ import("monaco-languageclient"), import("vscode-languageclient"), ]); return new MonacoLanguageClient({ name: `${config.language} language client`, clientOptions: { documentSelector: [config.language], errorHandler: { error: (error, message, count) => { console.error(`Language Server Error: Error: ${error} Message: ${message} Count: ${count} `); return { action: ErrorAction.Continue }; }, closed: () => ({ action: CloseAction.DoNotRestart }), }, }, connectionProvider: { get: () => Promise.resolve(transports), }, }); };