mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-18 07:16:34 +00:00
feat(editor): refactor code editor with LSP support and state management
This commit is contained in:
parent
0c94bb2fa3
commit
527c52abbc
@ -1,240 +1,123 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
|
||||||
toSocket,
|
|
||||||
WebSocketMessageReader,
|
|
||||||
WebSocketMessageWriter,
|
|
||||||
} from "vscode-ws-jsonrpc";
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import normalizeUrl from "normalize-url";
|
import { Skeleton } from "./ui/skeleton";
|
||||||
import { highlighter } from "@/lib/shiki";
|
import { highlighter } from "@/lib/shiki";
|
||||||
import { useEffect, useRef } from "react";
|
import type { editor } from "monaco-editor";
|
||||||
import { shikiToMonaco } from "@shikijs/monaco";
|
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 { useMonacoTheme } from "@/hooks/use-monaco-theme";
|
||||||
import { DEFAULT_EDITOR_PATH } from "@/config/editor/path";
|
import { connectToLanguageServer } from "@/lib/language-server";
|
||||||
import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value";
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
import type { MonacoLanguageClient } from "monaco-languageclient";
|
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 = () => (
|
||||||
|
<div className="h-full w-full p-2">
|
||||||
|
<Skeleton className="h-full w-full rounded-3xl" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dynamically import Monaco Editor with SSR disabled
|
||||||
const Editor = dynamic(
|
const Editor = dynamic(
|
||||||
async () => {
|
async () => {
|
||||||
await import("vscode");
|
await import("vscode");
|
||||||
|
|
||||||
const monaco = await import("monaco-editor");
|
const monaco = await import("monaco-editor");
|
||||||
const { loader } = await import("@monaco-editor/react");
|
const { loader } = await import("@monaco-editor/react");
|
||||||
loader.config({ monaco });
|
loader.config({ monaco });
|
||||||
|
|
||||||
return (await import("@monaco-editor/react")).Editor;
|
return (await import("@monaco-editor/react")).Editor;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => <CodeEditorLoadingSkeleton />,
|
||||||
<div className="h-full w-full p-4">
|
|
||||||
<Skeleton className="h-full w-full rounded-3xl" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type ConnectionHandle = {
|
|
||||||
client: MonacoLanguageClient | null;
|
|
||||||
socket: WebSocket | null;
|
|
||||||
controller: AbortController;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CodeEditor() {
|
export default function CodeEditor() {
|
||||||
|
const {
|
||||||
|
hydrated,
|
||||||
|
language,
|
||||||
|
path,
|
||||||
|
value,
|
||||||
|
lspConfig,
|
||||||
|
editorConfig,
|
||||||
|
isLspEnabled,
|
||||||
|
setEditor,
|
||||||
|
} = useCodeEditorStore();
|
||||||
const { monacoTheme } = useMonacoTheme();
|
const { monacoTheme } = useMonacoTheme();
|
||||||
const connectionRef = useRef<ConnectionHandle>({
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||||
client: null,
|
const monacoLanguageClientRef = useRef<MonacoLanguageClient | null>(null);
|
||||||
socket: null,
|
|
||||||
controller: new AbortController(),
|
|
||||||
});
|
|
||||||
const { fontSize, lineHeight } = useCodeEditorOptionStore();
|
|
||||||
const { language, setEditor } = useCodeEditorStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Connect to LSP only if enabled
|
||||||
const currentHandle: ConnectionHandle = {
|
const connectLSP = useCallback(async () => {
|
||||||
client: null,
|
if (!(isLspEnabled && language && lspConfig && editorRef.current)) return;
|
||||||
socket: null,
|
|
||||||
controller: new AbortController(),
|
|
||||||
};
|
|
||||||
const signal = currentHandle.controller.signal;
|
|
||||||
connectionRef.current = currentHandle;
|
|
||||||
|
|
||||||
const cleanupConnection = async (handle: ConnectionHandle) => {
|
// 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 {
|
try {
|
||||||
// Cleanup Language Client
|
const monacoLanguageClient = await connectToLanguageServer(
|
||||||
if (handle.client) {
|
lspConfig.protocol,
|
||||||
console.log("Stopping language client...");
|
lspConfig.hostname,
|
||||||
await handle.client.stop(250).catch(() => { });
|
lspConfig.port,
|
||||||
handle.client.dispose();
|
lspConfig.path,
|
||||||
}
|
lspConfig.lang
|
||||||
} 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;
|
monacoLanguageClientRef.current = monacoLanguageClient;
|
||||||
|
|
||||||
// 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<void>((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) {
|
} catch (error) {
|
||||||
if (!signal.aborted) {
|
console.error("Failed to connect to LSP:", error);
|
||||||
console.error("Connection failed:", error);
|
|
||||||
}
|
}
|
||||||
cleanupConnection(currentHandle);
|
}, [isLspEnabled, language, lspConfig]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initialize();
|
// 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 () => {
|
return () => {
|
||||||
console.log("Cleanup triggered");
|
if (monacoLanguageClientRef.current) {
|
||||||
currentHandle.controller.abort();
|
monacoLanguageClientRef.current.stop();
|
||||||
cleanupConnection(currentHandle);
|
monacoLanguageClientRef.current = null;
|
||||||
};
|
|
||||||
}, [language]);
|
|
||||||
|
|
||||||
const mergeOptions = {
|
|
||||||
...DefaultEditorOptionConfig,
|
|
||||||
fontSize,
|
|
||||||
lineHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleEditorChange(value: string | undefined) {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem(`code-editor-value-${language}`, value ?? "");
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hydrated) {
|
||||||
|
return <CodeEditorLoadingSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorValue =
|
function handleEditorWillMount(monaco: Monaco) {
|
||||||
typeof window !== "undefined"
|
shikiToMonaco(highlighter, monaco);
|
||||||
? localStorage.getItem(`code-editor-value-${language}`) ||
|
}
|
||||||
DEFAULT_EDITOR_VALUE[language]
|
|
||||||
: DEFAULT_EDITOR_VALUE[language];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editor
|
<Editor
|
||||||
defaultLanguage={language}
|
language={language}
|
||||||
value={editorValue}
|
theme={monacoTheme.id}
|
||||||
path={DEFAULT_EDITOR_PATH[language]}
|
path={path}
|
||||||
theme={monacoTheme}
|
value={value}
|
||||||
className="h-full"
|
beforeMount={handleEditorWillMount}
|
||||||
options={mergeOptions}
|
onMount={handleEditorDidMount}
|
||||||
beforeMount={(monaco) => {
|
options={editorConfig}
|
||||||
shikiToMonaco(highlighter, monaco);
|
loading={<CodeEditorLoadingSkeleton />}
|
||||||
}}
|
className="h-full w-full py-2"
|
||||||
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={
|
|
||||||
<div className="h-full w-full p-4">
|
|
||||||
<Skeleton className="h-full w-full rounded-3xl" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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: () => (
|
|
||||||
<div className="h-full w-full p-2">
|
|
||||||
<Skeleton className="h-full w-full rounded-3xl" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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<editor.IStandaloneCodeEditor | null>(null);
|
|
||||||
const monacoLanguageClientRef = useRef<MonacoLanguageClient | null>(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 (
|
|
||||||
<Editor
|
|
||||||
language={language}
|
|
||||||
theme={theme}
|
|
||||||
path={path}
|
|
||||||
value={value}
|
|
||||||
className={className}
|
|
||||||
beforeMount={handleEditorWillMount}
|
|
||||||
onMount={handleEditorDidMount}
|
|
||||||
options={editorConfig}
|
|
||||||
loading={
|
|
||||||
<div className="h-full w-full p-2">
|
|
||||||
<Skeleton className="h-full w-full rounded-3xl" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user