diff --git a/.eslintrc.json b/.eslintrc.json index 319342c..c3a2c9f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["next/core-web-vitals", "next/typescript"], "rules": { "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/no-empty-object-type": "warn" + "@typescript-eslint/no-empty-object-type": "warn", + "@typescript-eslint/no-require-imports": "warn" } } diff --git a/package.json b/package.json index 1a2d871..f7bf0cb 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,18 @@ "gitea-js": "^1.22.0", "jotai": "^2.10.3", "lucide-react": "^0.468.0", + "monaco-languageclient": "^8.8.3", "next": "15.0.4", "next-auth": "^5.0.0-beta.25", "next-intl": "^3.26.1", "next-themes": "^0.4.4", + "normalize-url": "^8.0.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "reconnecting-websocket": "^4.4.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "vscode-ws-jsonrpc": "^3.3.2", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/src/app/playground/components/code-editor.tsx b/src/app/playground/components/code-editor.tsx index fe98e03..495d056 100644 --- a/src/app/playground/components/code-editor.tsx +++ b/src/app/playground/components/code-editor.tsx @@ -1,12 +1,27 @@ "use client"; +import { + toSocket, + WebSocketMessageReader, + WebSocketMessageWriter, +} from "vscode-ws-jsonrpc"; +import { + CloseAction, + ErrorAction, + MessageTransports, +} from "vscode-languageclient"; import "@fontsource-variable/fira-code"; +import normalizeUrl from "normalize-url"; import { createHighlighter } from "shiki"; import type { editor } from "monaco-editor"; import { shikiToMonaco } from "@shikijs/monaco"; import { useEffect, useRef, useMemo } from "react"; import { useCodeEditorStore } from "@/store/codeEditorStore"; +import { MonacoLanguageClient } from "monaco-languageclient"; import MonacoEditor, { type Monaco } from "@monaco-editor/react"; +import { initServices } from "monaco-languageclient/vscode/services"; + +const ReconnectingWebSocket = require("reconnecting-websocket"); const ADDITIONAL_THEMES = [ "andromeeda", @@ -63,6 +78,66 @@ export function CodeEditor() { [isMinimap, isLigature] ); + const createLanguageClient = ( + transports: MessageTransports + ): MonacoLanguageClient => { + return new MonacoLanguageClient({ + name: "LSP Language Client", + clientOptions: { + documentSelector: ADDITIONAL_LANGUAGES, + errorHandler: { + error: () => ({ action: ErrorAction.Continue }), + closed: () => ({ action: CloseAction.DoNotRestart }), + }, + }, + connectionProvider: { + get: () => { + return Promise.resolve(transports); + }, + }, + }); + }; + + useEffect(() => { + if (!monacoRef.current) return; + const url = normalizeUrl(`ws://172.20.0.13:3000`); + const socketOptions = { + maxReconnectionDelay: 10000, + minReconnectionDelay: 1000, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 10000, + maxRetries: Infinity, + debug: true, + }; + const webSocket: WebSocket = new ReconnectingWebSocket.default( + url, + [], + socketOptions + ); + + webSocket.onopen = () => { + console.log("WebSocket connection opened"); + const socket = toSocket(webSocket); + const reader = new WebSocketMessageReader(socket); + const writer = new WebSocketMessageWriter(socket); + const languageClient = createLanguageClient({ reader, writer }); + languageClient.start(); + reader.onClose(() => languageClient.stop()); + }; + + webSocket.onerror = (error) => { + console.error("WebSocket error:", error); + }; + + webSocket.onclose = (event) => { + console.log("WebSocket connection closed:", event); + }; + + return () => { + webSocket.close(); + }; + }, [monacoRef]); + const handleEditorMount = async ( editor: editor.IStandaloneCodeEditor, monaco: Monaco @@ -80,6 +155,8 @@ export function CodeEditor() { }); shikiToMonaco(highlighter, monacoRef.current); + + await initServices({}); }; return (