From 527c52abbc597aa361a59c5b6f93b72031407a5e Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Wed, 5 Mar 2025 00:33:19 +0800 Subject: [PATCH] feat(editor): refactor code editor with LSP support and state management --- src/components/code-editor.tsx | 295 +++++++++-------------------- src/components/core-editor-lsp.tsx | 127 ------------- 2 files changed, 89 insertions(+), 333 deletions(-) delete mode 100644 src/components/core-editor-lsp.tsx diff --git a/src/components/code-editor.tsx b/src/components/code-editor.tsx index 1f9b4a6..778e313 100644 --- a/src/components/code-editor.tsx +++ b/src/components/code-editor.tsx @@ -1,240 +1,123 @@ "use client"; -import { - toSocket, - WebSocketMessageReader, - WebSocketMessageWriter, -} from "vscode-ws-jsonrpc"; import dynamic from "next/dynamic"; -import normalizeUrl from "normalize-url"; +import { Skeleton } from "./ui/skeleton"; import { highlighter } from "@/lib/shiki"; -import { useEffect, useRef } from "react"; +import type { editor } from "monaco-editor"; import { shikiToMonaco } from "@shikijs/monaco"; -import { Skeleton } from "@/components/ui/skeleton"; +import type { Monaco } from "@monaco-editor/react"; +import { useCallback, useEffect, useRef } from "react"; import { useMonacoTheme } from "@/hooks/use-monaco-theme"; -import { DEFAULT_EDITOR_PATH } from "@/config/editor/path"; -import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value"; +import { connectToLanguageServer } from "@/lib/language-server"; +import { useCodeEditorStore } from "@/store/useCodeEditorStore"; import type { MonacoLanguageClient } from "monaco-languageclient"; -import { DefaultEditorOptionConfig } from "@/config/editor-option"; -import { SUPPORTED_LANGUAGE_SERVERS } from "@/config/lsp/language-server"; -import { useCodeEditorOptionStore, useCodeEditorStore } from "@/store/useCodeEditorStore"; +// Skeleton component for loading state +const CodeEditorLoadingSkeleton = () => ( +
+ +
+); + +// Dynamically import Monaco Editor with SSR disabled const Editor = dynamic( async () => { await import("vscode"); - const monaco = await import("monaco-editor"); const { loader } = await import("@monaco-editor/react"); loader.config({ monaco }); - return (await import("@monaco-editor/react")).Editor; }, { ssr: false, - loading: () => ( -
- -
- ), + loading: () => , } ); -type ConnectionHandle = { - client: MonacoLanguageClient | null; - socket: WebSocket | null; - controller: AbortController; -}; - export default function CodeEditor() { + const { + hydrated, + language, + path, + value, + lspConfig, + editorConfig, + isLspEnabled, + setEditor, + } = useCodeEditorStore(); const { monacoTheme } = useMonacoTheme(); - const connectionRef = useRef({ - client: null, - socket: null, - controller: new AbortController(), - }); - const { fontSize, lineHeight } = useCodeEditorOptionStore(); - const { language, setEditor } = useCodeEditorStore(); + const editorRef = useRef(null); + const monacoLanguageClientRef = useRef(null); - useEffect(() => { - const currentHandle: ConnectionHandle = { - client: null, - socket: null, - controller: new AbortController(), - }; - const signal = currentHandle.controller.signal; - connectionRef.current = currentHandle; + // Connect to LSP only if enabled + const connectLSP = useCallback(async () => { + if (!(isLspEnabled && language && lspConfig && editorRef.current)) return; - const cleanupConnection = async (handle: ConnectionHandle) => { - try { - // Cleanup Language Client - if (handle.client) { - console.log("Stopping language client..."); - await handle.client.stop(250).catch(() => { }); - handle.client.dispose(); - } - } catch (e) { - console.log("Client cleanup error:", e); - } finally { - handle.client = null; - } - - // Cleanup WebSocket - if (handle.socket) { - console.log("Closing WebSocket..."); - const socket = handle.socket; - socket.onopen = null; - socket.onerror = null; - socket.onclose = null; - socket.onmessage = null; - - try { - if ( - [WebSocket.OPEN, WebSocket.CONNECTING].includes( - socket.readyState as WebSocket["OPEN"] | WebSocket["CONNECTING"] - ) - ) { - socket.close(1000, "Connection replaced"); - } - } catch (e) { - console.log("Socket close error:", e); - } finally { - handle.socket = null; - } - } - }; - - const initialize = async () => { - try { - // Cleanup old connection - await cleanupConnection(connectionRef.current); - - const serverConfig = SUPPORTED_LANGUAGE_SERVERS.find( - (s) => s.id === language - ); - if (!serverConfig || signal.aborted) return; - - // Create WebSocket connection - const lspUrl = `${serverConfig.protocol}://${serverConfig.hostname}${serverConfig.port ? `:${serverConfig.port}` : ""}${serverConfig.path || ""}`; - const webSocket = new WebSocket(normalizeUrl(lspUrl)); - currentHandle.socket = webSocket; - - // Wait for connection to establish or timeout - await Promise.race([ - new Promise((resolve, reject) => { - webSocket.onopen = () => { - if (signal.aborted) reject(new Error("Connection aborted")); - else resolve(); - }; - webSocket.onerror = () => reject(new Error("WebSocket error")); - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Connection timeout")), 5000) - ), - ]); - - if (signal.aborted) { - webSocket.close(1001, "Connection aborted"); - return; - } - - // Initialize Language Client - const { MonacoLanguageClient } = await import("monaco-languageclient"); - const { ErrorAction, CloseAction } = await import("vscode-languageclient"); - - const socket = toSocket(webSocket); - const client = new MonacoLanguageClient({ - name: `${serverConfig.label} Client`, - clientOptions: { - documentSelector: [serverConfig.id], - errorHandler: { - error: () => ({ action: ErrorAction.Continue }), - closed: () => ({ action: CloseAction.DoNotRestart }), - }, - }, - connectionProvider: { - get: () => - Promise.resolve({ - reader: new WebSocketMessageReader(socket), - writer: new WebSocketMessageWriter(socket), - }), - }, - }); - - client.start(); - currentHandle.client = client; - - // Bind WebSocket close event - webSocket.onclose = (event) => { - if (!signal.aborted) { - console.log("WebSocket closed:", event); - client.stop(); - } - }; - } catch (error) { - if (!signal.aborted) { - console.error("Connection failed:", error); - } - cleanupConnection(currentHandle); - } - }; - - initialize(); - - return () => { - console.log("Cleanup triggered"); - currentHandle.controller.abort(); - cleanupConnection(currentHandle); - }; - }, [language]); - - const mergeOptions = { - ...DefaultEditorOptionConfig, - fontSize, - lineHeight, - }; - - function handleEditorChange(value: string | undefined) { - if (typeof window !== "undefined") { - localStorage.setItem(`code-editor-value-${language}`, value ?? ""); + // If there's an existing language client, stop it first + if (monacoLanguageClientRef.current) { + monacoLanguageClientRef.current.stop(); + monacoLanguageClientRef.current = null; } + + // Create a new language client + try { + const monacoLanguageClient = await connectToLanguageServer( + lspConfig.protocol, + lspConfig.hostname, + lspConfig.port, + lspConfig.path, + lspConfig.lang + ); + monacoLanguageClientRef.current = monacoLanguageClient; + } catch (error) { + console.error("Failed to connect to LSP:", error); + } + }, [isLspEnabled, language, lspConfig]); + + // Connect to LSP once the editor has mounted + const handleEditorDidMount = useCallback( + async (editor: editor.IStandaloneCodeEditor) => { + editorRef.current = editor; + await connectLSP(); + setEditor(editor); + }, + [connectLSP] + ); + + // Reconnect to the LSP whenever language or lspConfig changes + useEffect(() => { + connectLSP(); + }, [connectLSP]); + + // Cleanup the LSP connection when the component unmounts + useEffect(() => { + return () => { + if (monacoLanguageClientRef.current) { + monacoLanguageClientRef.current.stop(); + monacoLanguageClientRef.current = null; + } + }; + }, []); + + if (!hydrated) { + return ; } - const editorValue = - typeof window !== "undefined" - ? localStorage.getItem(`code-editor-value-${language}`) || - DEFAULT_EDITOR_VALUE[language] - : DEFAULT_EDITOR_VALUE[language]; + function handleEditorWillMount(monaco: Monaco) { + shikiToMonaco(highlighter, monaco); + } return ( { - shikiToMonaco(highlighter, monaco); - }} - onMount={(editor) => { - setEditor(editor); - }} - onChange={handleEditorChange} - // onValidate={(markers) => { - // markers.forEach((marker) => { - // console.log(marker.severity); - // console.log(marker.startLineNumber); - // console.log(marker.startColumn); - // console.log(marker.endLineNumber); - // console.log(marker.endColumn); - // console.log(marker.message); - // }); - // }} - loading={ -
- -
- } + language={language} + theme={monacoTheme.id} + path={path} + value={value} + beforeMount={handleEditorWillMount} + onMount={handleEditorDidMount} + options={editorConfig} + loading={} + className="h-full w-full py-2" /> ); } diff --git a/src/components/core-editor-lsp.tsx b/src/components/core-editor-lsp.tsx deleted file mode 100644 index 718fd0d..0000000 --- a/src/components/core-editor-lsp.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; -import { Skeleton } from "./ui/skeleton"; -import { highlighter } from "@/lib/shiki"; -import type { editor } from "monaco-editor"; -import { shikiToMonaco } from "@shikijs/monaco"; -import type { Monaco } from "@monaco-editor/react"; -import { useCallback, useEffect, useRef } from "react"; -import { connectToLanguageServer } from "@/lib/language-server"; -import { LanguageServerMetadata } from "@/types/language-server"; -import type { MonacoLanguageClient } from "monaco-languageclient"; - -// Dynamically import Monaco Editor with SSR disabled -const Editor = dynamic( - async () => { - await import("vscode"); - const monaco = await import("monaco-editor"); - const { loader } = await import("@monaco-editor/react"); - loader.config({ monaco }); - return (await import("@monaco-editor/react")).Editor; - }, - { - ssr: false, - loading: () => ( -
- -
- ), - } -); - -interface CoreEditorLspProps { - language?: string; - theme?: string; - path?: string; - value?: string; - className?: string; - lspConfig?: LanguageServerMetadata; - editorConfig?: editor.IEditorConstructionOptions; - enableLSP?: boolean; -} - -export default function CoreEditorLsp({ - language, - theme, - path, - value, - className, - lspConfig, - editorConfig, - enableLSP = true, -}: CoreEditorLspProps) { - const editorRef = useRef(null); - const monacoLanguageClientRef = useRef(null); - - // Connect to LSP only if enabled - const connectLSP = useCallback(async () => { - if (!enableLSP || !language || !lspConfig || !editorRef.current) return; - - // If there's an existing language client, stop it first - if (monacoLanguageClientRef.current) { - monacoLanguageClientRef.current.stop(); - monacoLanguageClientRef.current = null; - } - - // Create a new language client - try { - const monacoLanguageClient = await connectToLanguageServer( - lspConfig.protocol, - lspConfig.hostname, - lspConfig.port, - lspConfig.path, - lspConfig.lang - ); - monacoLanguageClientRef.current = monacoLanguageClient; - } catch (error) { - console.error("Failed to connect to LSP:", error); - } - }, [language, lspConfig, enableLSP]); - - // Connect to LSP once the editor has mounted - const handleEditorDidMount = useCallback( - async (editor: editor.IStandaloneCodeEditor) => { - editorRef.current = editor; - await connectLSP(); - }, - [connectLSP] - ); - - // Reconnect to the LSP whenever language or lspConfig changes - useEffect(() => { - connectLSP(); - }, [lspConfig, language, connectLSP]); - - // Cleanup the LSP connection when the component unmounts - useEffect(() => { - return () => { - if (monacoLanguageClientRef.current) { - monacoLanguageClientRef.current.stop(); - monacoLanguageClientRef.current = null; - } - }; - }, []); - - function handleEditorWillMount(monaco: Monaco) { - shikiToMonaco(highlighter, monaco); - } - - return ( - - - - } - /> - ); -}