feat: refactor language server connection handling and improve undo/redo functionality in code editor

This commit is contained in:
ngc2207 2025-01-11 00:41:22 +08:00
parent 3587cfe009
commit d2ce0acfe5
2 changed files with 125 additions and 133 deletions

View File

@ -3,6 +3,18 @@ import {
DEFAULT_EDITOR_THEME, DEFAULT_EDITOR_THEME,
DEFAULT_EDITOR_LANGUAGE, DEFAULT_EDITOR_LANGUAGE,
} from "@/config"; } from "@/config";
import {
Paintbrush,
Palette,
Redo2,
Undo2,
} from "lucide-react";
import {
redoChange,
undoChange,
formatCode,
moveCursorToEnd,
} from "@/lib/editor";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -10,41 +22,26 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Copy,
FileDown,
Paintbrush,
Palette,
Redo2,
Undo2,
} from "lucide-react";
import * as monaco from "monaco-editor";
import { highlighter } from "@/lib/shiki";
import { Bot, CodeXml } from "lucide-react";
import { shikiToMonaco } from "@shikijs/monaco";
import { useEffect, useRef, useState } from "react";
import { SUPPORTED_EDITOR_THEMES } from "@/constants/themes";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { DiffEditor, Editor, Monaco, loader } from "@monaco-editor/react";
import { SUPPORTED_EDITOR_LANGUAGES_CONFIG } from "@/constants/languages";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import { connectToLanguageServer, SUPPORTED_LSP_LANGUAGES } from "@/lib/lsp";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { import { CodeXml } from "lucide-react";
redoChange, import * as monaco from "monaco-editor";
undoChange, import { highlighter } from "@/lib/shiki";
formatCode, import { Button } from "@/components/ui/button";
downloadCode, import { shikiToMonaco } from "@shikijs/monaco";
copyCode, import { useEffect, useRef, useState } from "react";
moveCursorToEnd, import { SUPPORTED_EDITOR_THEMES } from "@/constants/themes";
} from "@/lib/editor"; import { MonacoLanguageClient } from "monaco-languageclient";
import { Editor, Monaco, loader } from "@monaco-editor/react";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { SUPPORTED_EDITOR_LANGUAGES_CONFIG } from "@/constants/languages";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import { connectToLanguageServer, SUPPORTED_LSP_LANGUAGES } from "@/lib/lsp";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
self.MonacoEnvironment = { self.MonacoEnvironment = {
getWorker(_, _label) { getWorker(_, _label) {
@ -59,42 +56,45 @@ function App() {
const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME); const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME);
const file = DEFAULT_FILES[language]; const file = DEFAULT_FILES[language];
const webSocketRef = useRef<WebSocket | null>(null); const webSocketRef = useRef<WebSocket | null>(null);
const languageClientRef = useRef<MonacoLanguageClient | null>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null); const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
useEffect(() => { useEffect(() => {
const closeWebSocket = () => { const handleLanguageServer = async () => {
if (webSocketRef.current) { if (languageClientRef.current && language in SUPPORTED_LSP_LANGUAGES) {
if ( await languageClientRef.current.stop();
webSocketRef.current.readyState === WebSocket.OPEN || languageClientRef.current = null;
webSocketRef.current.readyState === WebSocket.CONNECTING
) {
webSocketRef.current.onclose = () => {
webSocketRef.current = null;
};
webSocketRef.current.close();
} else {
webSocketRef.current = null;
}
} }
}; if (
webSocketRef.current &&
const connectWebSocket = async () => { webSocketRef.current.readyState === WebSocket.OPEN
closeWebSocket(); ) {
webSocketRef.current.close();
if (editorRef.current && language in SUPPORTED_LSP_LANGUAGES) { webSocketRef.current = null;
try { }
const webSocket = await connectToLanguageServer(language); if (language in SUPPORTED_LSP_LANGUAGES) {
webSocket.onopen = () => { connectToLanguageServer(language)
.then(([webSocket, languageClient]) => {
webSocketRef.current = webSocket; webSocketRef.current = webSocket;
}; languageClientRef.current = languageClient;
} catch (error) { })
console.error("Failed to connect to language server", error); .catch((error) => {
} console.error(error);
});
} }
}; };
connectWebSocket(); if (editorRef.current) {
return closeWebSocket; const value = editorRef.current.getModel()?.getValue();
if (value !== undefined) {
window.parent.postMessage(
{ type: "editor-info", language, code: value },
"*"
);
}
moveCursorToEnd(editorRef.current);
handleLanguageServer();
}
}, [language]); }, [language]);
return ( return (
@ -153,24 +153,6 @@ function App() {
</ScrollArea> </ScrollArea>
<TabsContent value="code-editor" className="h-full mt-0"> <TabsContent value="code-editor" className="h-full mt-0">
<div className="flex items-center justify-end gap-x-2 pr-4"> <div className="flex items-center justify-end gap-x-2 pr-4">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Redo"
className="bg-white"
onClick={() => redoChange(editorRef.current!)}
>
<Redo2 size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -189,6 +171,24 @@ function App() {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Redo"
className="bg-white"
onClick={() => redoChange(editorRef.current!)}
>
<Redo2 size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -207,42 +207,6 @@ function App() {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Copy"
className="bg-white"
onClick={() => copyCode(editorRef.current!)}
>
<Copy size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Download"
className="bg-white"
onClick={() => downloadCode(editorRef.current!)}
>
<FileDown size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
<Editor <Editor
theme={theme} theme={theme}
@ -274,11 +238,15 @@ function App() {
"*" "*"
); );
} }
if (language in SUPPORTED_LSP_LANGUAGES) { moveCursorToEnd(editor);
connectToLanguageServer(language).then((webSocket) => { connectToLanguageServer(language)
.then(([webSocket, languageClient]) => {
webSocketRef.current = webSocket; webSocketRef.current = webSocket;
languageClientRef.current = languageClient;
})
.catch((error) => {
console.error(error);
}); });
}
}} }}
onChange={(value) => { onChange={(value) => {
if (value !== undefined) { if (value !== undefined) {

View File

@ -57,26 +57,50 @@ function createLanguageClient(
}); });
} }
function createWebSocket(url: string) { // function createWebSocket(url: string) {
const webSocket = new WebSocket(url); // const webSocket = new WebSocket(url);
webSocket.onopen = () => { // webSocket.onopen = () => {
const socket = toSocket(webSocket); // const socket = toSocket(webSocket);
const reader = new WebSocketMessageReader(socket); // const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket); // const writer = new WebSocketMessageWriter(socket);
const languageClient = createLanguageClient({ // const languageClient = createLanguageClient({
reader, // reader,
writer, // writer,
}); // });
languageClient.start(); // languageClient.start();
reader.onClose(() => languageClient.stop()); // reader.onClose(() => languageClient.stop());
}; // };
return webSocket; // return webSocket;
} // }
export async function connectToLanguageServer( export async function connectToLanguageServer(
language: string language: string
): Promise<WebSocket> { ): Promise<[WebSocket, MonacoLanguageClient]> {
const { hostname, port, path } = SUPPORTED_LSP_LANGUAGES[language]; return new Promise((resolve, reject) => {
const url = createUrl(hostname, port, path); if (!(language in SUPPORTED_LSP_LANGUAGES)) {
return createWebSocket(url); reject(new Error("Unsupported language"));
return;
}
const { hostname, port, path } = SUPPORTED_LSP_LANGUAGES[language];
const url = createUrl(hostname, port, path);
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());
resolve([webSocket, languageClient]);
};
webSocket.onerror = (error) => {
reject(error);
};
webSocket.onclose = () => {
reject(new Error("WebSocket closed"));
};
});
} }