feat(playground): implement language selection and theme customization in the editor

This commit is contained in:
ngc2207 2025-01-05 16:14:20 +08:00
parent 88b68d147e
commit 732ecd175f
4 changed files with 214 additions and 2 deletions

View File

@ -0,0 +1,148 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
SUPPORTED_THEMES,
highlightMonacoEditor,
} from "@/constants/editor/themes";
import { Palette } from "lucide-react";
import * as monaco from "monaco-editor";
import { files } from "@/constants/editor/files";
import { useEffect, useRef, useState } from "react";
import { Editor, loader } from "@monaco-editor/react";
import { MonacoLanguageClient } from "monaco-languageclient";
import { connectToLSP, LSP_SUPPORTED_LANGUAGES } from "@/lib/lsp";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { COriginal, CplusplusOriginal, JavaOriginal } from "devicons-react";
loader.config({ monaco });
function moveCursorToLastLine(editor: monaco.editor.IStandaloneCodeEditor) {
const model = editor.getModel();
if (model) {
const lineCount = model.getLineCount();
const lastLineLength = model.getLineLength(lineCount);
editor.setPosition({
lineNumber: lineCount,
column: lastLineLength + 1,
});
editor.focus();
}
}
export default function PlayPage() {
const [language, setLanguage] = useState<string>("c");
const [theme, setTheme] = useState<string>("vitesse-dark");
const file = files[language];
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const languageClientRef = useRef<MonacoLanguageClient | null>(null);
useEffect(() => {
if (editorRef.current) {
moveCursorToLastLine(editorRef.current);
if (languageClientRef.current) {
languageClientRef.current.stop();
languageClientRef.current = null;
}
if (language in LSP_SUPPORTED_LANGUAGES) {
connectToLSP(language).then((languageClient) => {
languageClientRef.current = languageClient;
});
}
}
}, [language]);
return (
<div className="h-full flex flex-col">
<header className="flex items-center gap-x-2 m-3 mt-0">
<Tabs defaultValue={language}>
<ScrollArea>
<TabsList>
<TabsTrigger
value="c"
onClick={() => setLanguage("c")}
disabled={language === "c"}
>
<COriginal
className="-ms-0.5 me-1.5"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
C
</TabsTrigger>
<TabsTrigger
value="cpp"
className="group"
onClick={() => setLanguage("cpp")}
disabled={language === "cpp"}
>
<CplusplusOriginal
className="-ms-0.5 me-1.5"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
C++
</TabsTrigger>
<TabsTrigger
value="java"
className="group"
onClick={() => setLanguage("java")}
disabled={language === "java"}
>
<JavaOriginal
className="-ms-0.5 me-1.5"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
Java
</TabsTrigger>
</TabsList>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</Tabs>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="relative ps-9 w-40">
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 group-has-[[disabled]]:opacity-50">
<Palette size={16} strokeWidth={2} aria-hidden="true" />
</div>
<SelectValue placeholder="Select Theme" />
</SelectTrigger>
<SelectContent>
{SUPPORTED_THEMES.map((theme) => (
<SelectItem key={theme.key} value={theme.value}>
<span className="truncate">{theme.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</header>
<Editor
theme={theme}
path={file.name}
defaultLanguage={file.language}
defaultValue={file.value}
options={{ automaticLayout: true }}
beforeMount={highlightMonacoEditor}
onMount={(editor) => {
editorRef.current = editor;
moveCursorToLastLine(editor);
if (language in LSP_SUPPORTED_LANGUAGES) {
connectToLSP(language).then((languageClient) => {
languageClientRef.current = languageClient;
});
}
}}
/>
</div>
);
}

View File

@ -110,14 +110,13 @@ export default function PlaygroundPage() {
const result = await runCode(code, language); const result = await runCode(code, language);
// 根据编译结果显示不同的信息
const statusMessage = result.success const statusMessage = result.success
? "Compilation successful" ? "Compilation successful"
: "Compilation failed"; : "Compilation failed";
const fullMessage = `${statusMessage}\n\n${result.output}`; const fullMessage = `${statusMessage}\n\n${result.output}`;
const highlighted = await codeToHtml(fullMessage, { const highlighted = await codeToHtml(fullMessage, {
lang: "log", // 或者根据你的需求选择合适的语言 lang: "log",
theme: "one-dark-pro", theme: "one-dark-pro",
}); });

View File

@ -0,0 +1,31 @@
export const files: {
[key: string]: { name: string; language: string; value: string };
} = {
c: {
name: "main.c",
language: "c",
value: `#include <stdio.h>
int main() {
printf("Hello, World!");
return 0;
}`,
},
cpp: {
name: "main.cpp",
language: "cpp",
value: `#include <iostream>
int main() {
std::cout << "Hello, World!";
return 0;
}`,
},
java: {
name: "Main.java",
language: "java",
value: `public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}`,
},
};

View File

@ -92,3 +92,37 @@ export async function connectToLanguageServer(
}; };
}); });
} }
export async function connectToLSP(
language: string
): Promise<MonacoLanguageClient> {
return new Promise((resolve, reject) => {
let url: string;
if (language in LSP_SUPPORTED_LANGUAGES) {
const { hostname, port, path } = LSP_SUPPORTED_LANGUAGES[language];
url = createUrl(hostname, port, path);
} else {
reject("Unsupported language");
return;
}
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();
resolve(languageClient);
};
webSocket.onclose = () => {
reject("WebSocket connection closed unexpectedly");
};
webSocket.onerror = (error) => {
reject(`WebSocket connection error: ${error}`);
};
});
}