diff --git a/code-editor/bun.lockb b/code-editor/bun.lockb index 0b2d609..2793933 100755 Binary files a/code-editor/bun.lockb and b/code-editor/bun.lockb differ diff --git a/code-editor/package.json b/code-editor/package.json index 6e3f00c..ba41223 100644 --- a/code-editor/package.json +++ b/code-editor/package.json @@ -18,10 +18,15 @@ "clsx": "^2.1.1", "devicons-react": "^1.4.0", "lucide-react": "^0.469.0", + "monaco-editor": "0.36.1", + "monaco-languageclient": "5.0.1", + "normalize-url": "~8.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "vscode-languageclient": "~8.1.0", + "vscode-ws-jsonrpc": "3.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/code-editor/src/App.tsx b/code-editor/src/App.tsx index 284c505..e30fbe1 100644 --- a/code-editor/src/App.tsx +++ b/code-editor/src/App.tsx @@ -3,19 +3,38 @@ import { DEFAULT_EDITOR_THEME, DEFAULT_EDITOR_LANGUAGE, } from "@/config"; -import { useState } from "react"; import * as monaco from "monaco-editor"; import { highlighter } from "@/lib/shiki"; import { Bot, CodeXml } from "lucide-react"; import { shikiToMonaco } from "@shikijs/monaco"; -import { DiffEditor, Editor, Monaco } from "@monaco-editor/react"; +import { connectToLanguageServer } from "@/lib/lsp"; +import { useEffect, useRef, useState } from "react"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { DiffEditor, Editor, Monaco, loader } from "@monaco-editor/react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +loader.config({ monaco }); + function App() { const [language, setLanguage] = useState(DEFAULT_EDITOR_LANGUAGE); const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME); const file = DEFAULT_FILES[language]; + const webSocketRef = useRef(null); + + useEffect(() => { + if (webSocketRef.current) { + webSocketRef.current.close(); + } + connectToLanguageServer(language).then((webSocket) => { + webSocketRef.current = webSocket; + }); + + return () => { + if (webSocketRef.current) { + webSocketRef.current.close(); + } + }; + }, [language]); return (
@@ -71,10 +90,7 @@ function App() { beforeMount={async (monaco: Monaco) => { shikiToMonaco(await highlighter, monaco); }} - onMount={( - editor: monaco.editor.IStandaloneCodeEditor, - monaco: Monaco - ) => { + onMount={(editor: monaco.editor.IStandaloneCodeEditor) => { const value = editor.getModel()?.getValue(); if (value !== undefined) { window.parent.postMessage( @@ -82,6 +98,9 @@ function App() { "*" ); } + connectToLanguageServer(language).then((webSocket) => { + webSocketRef.current = webSocket; + }); }} onChange={(value) => { if (value !== undefined) { diff --git a/code-editor/src/lib/lsp.ts b/code-editor/src/lib/lsp.ts new file mode 100644 index 0000000..cdbfcd6 --- /dev/null +++ b/code-editor/src/lib/lsp.ts @@ -0,0 +1,82 @@ +import { + toSocket, + WebSocketMessageReader, + WebSocketMessageWriter, +} from "vscode-ws-jsonrpc"; +import { + CloseAction, + ErrorAction, + MessageTransports, +} from "vscode-languageclient"; +import normalizeUrl from "normalize-url"; +import { MonacoLanguageClient } from "monaco-languageclient"; + +export const SUPPORTED_LSP_LANGUAGES: { + [key: string]: { hostname: string; port: number | null; path: string }; +} = { + c: { + hostname: "c.litchi.icu", + port: null, + path: "/clangd", + }, + cpp: { + hostname: "cpp.litchi.icu", + port: null, + path: "/clangd", + }, +}; + +function createUrl( + hostname: string, + port: number | null, + path: string +): string { + const protocol = location.protocol === "https:" ? "wss" : "ws"; + return port !== null + ? normalizeUrl(`${protocol}://${hostname}:${port}${path}`) + : normalizeUrl(`${protocol}://${hostname}${path}`); +} + +function createLanguageClient( + transports: MessageTransports +): MonacoLanguageClient { + return new MonacoLanguageClient({ + name: "Judge4c Language Client", + clientOptions: { + documentSelector: ["c", "cpp"], + errorHandler: { + error: () => ({ action: ErrorAction.Continue }), + closed: () => ({ action: CloseAction.DoNotRestart }), + }, + }, + connectionProvider: { + get: () => { + return Promise.resolve(transports); + }, + }, + }); +} + +function createWebSocket(url: string) { + const webSocket = new WebSocket(url); + webSocket.onopen = () => { + 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()); + }; + return webSocket; +} + +export async function connectToLanguageServer( + language: string +): Promise { + const { hostname, port, path } = SUPPORTED_LSP_LANGUAGES[language]; + const url = createUrl(hostname, port, path); + return createWebSocket(url); +}