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_LANGUAGE,
} from "@/config";
import {
Paintbrush,
Palette,
Redo2,
Undo2,
} from "lucide-react";
import {
redoChange,
undoChange,
formatCode,
moveCursorToEnd,
} from "@/lib/editor";
import {
Select,
SelectContent,
@ -10,41 +22,26 @@ import {
SelectTrigger,
SelectValue,
} 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 {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
redoChange,
undoChange,
formatCode,
downloadCode,
copyCode,
moveCursorToEnd,
} from "@/lib/editor";
import { CodeXml } from "lucide-react";
import * as monaco from "monaco-editor";
import { highlighter } from "@/lib/shiki";
import { Button } from "@/components/ui/button";
import { shikiToMonaco } from "@shikijs/monaco";
import { useEffect, useRef, useState } from "react";
import { SUPPORTED_EDITOR_THEMES } from "@/constants/themes";
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 = {
getWorker(_, _label) {
@ -59,42 +56,45 @@ function App() {
const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME);
const file = DEFAULT_FILES[language];
const webSocketRef = useRef<WebSocket | null>(null);
const languageClientRef = useRef<MonacoLanguageClient | null>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
useEffect(() => {
const closeWebSocket = () => {
if (webSocketRef.current) {
if (
webSocketRef.current.readyState === WebSocket.OPEN ||
webSocketRef.current.readyState === WebSocket.CONNECTING
) {
webSocketRef.current.onclose = () => {
webSocketRef.current = null;
};
webSocketRef.current.close();
} else {
webSocketRef.current = null;
}
const handleLanguageServer = async () => {
if (languageClientRef.current && language in SUPPORTED_LSP_LANGUAGES) {
await languageClientRef.current.stop();
languageClientRef.current = null;
}
};
const connectWebSocket = async () => {
closeWebSocket();
if (editorRef.current && language in SUPPORTED_LSP_LANGUAGES) {
try {
const webSocket = await connectToLanguageServer(language);
webSocket.onopen = () => {
if (
webSocketRef.current &&
webSocketRef.current.readyState === WebSocket.OPEN
) {
webSocketRef.current.close();
webSocketRef.current = null;
}
if (language in SUPPORTED_LSP_LANGUAGES) {
connectToLanguageServer(language)
.then(([webSocket, languageClient]) => {
webSocketRef.current = webSocket;
};
} catch (error) {
console.error("Failed to connect to language server", error);
}
languageClientRef.current = languageClient;
})
.catch((error) => {
console.error(error);
});
}
};
connectWebSocket();
return closeWebSocket;
if (editorRef.current) {
const value = editorRef.current.getModel()?.getValue();
if (value !== undefined) {
window.parent.postMessage(
{ type: "editor-info", language, code: value },
"*"
);
}
moveCursorToEnd(editorRef.current);
handleLanguageServer();
}
}, [language]);
return (
@ -153,24 +153,6 @@ function App() {
</ScrollArea>
<TabsContent value="code-editor" className="h-full mt-0">
<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}>
<Tooltip>
<TooltipTrigger asChild>
@ -189,6 +171,24 @@ function App() {
</TooltipContent>
</Tooltip>
</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}>
<Tooltip>
<TooltipTrigger asChild>
@ -207,42 +207,6 @@ function App() {
</TooltipContent>
</Tooltip>
</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>
<Editor
theme={theme}
@ -274,11 +238,15 @@ function App() {
"*"
);
}
if (language in SUPPORTED_LSP_LANGUAGES) {
connectToLanguageServer(language).then((webSocket) => {
moveCursorToEnd(editor);
connectToLanguageServer(language)
.then(([webSocket, languageClient]) => {
webSocketRef.current = webSocket;
languageClientRef.current = languageClient;
})
.catch((error) => {
console.error(error);
});
}
}}
onChange={(value) => {
if (value !== undefined) {

View File

@ -57,26 +57,50 @@ function createLanguageClient(
});
}
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;
}
// 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<WebSocket> {
const { hostname, port, path } = SUPPORTED_LSP_LANGUAGES[language];
const url = createUrl(hostname, port, path);
return createWebSocket(url);
): Promise<[WebSocket, MonacoLanguageClient]> {
return new Promise((resolve, reject) => {
if (!(language in SUPPORTED_LSP_LANGUAGES)) {
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"));
};
});
}