9 changed files with 146 additions and 17 deletions
+2 -1
View File
@@ -2,6 +2,7 @@
"extends": ["next/core-web-vitals", "next/typescript"], "extends": ["next/core-web-vitals", "next/typescript"],
"rules": { "rules": {
"@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-empty-object-type": "warn" "@typescript-eslint/no-empty-object-type": "warn",
"@typescript-eslint/no-require-imports": "warn"
} }
} }
+3
View File
@@ -10,6 +10,9 @@
}, },
"ligaturesCheckbox": { "ligaturesCheckbox": {
"label": "Font Ligatures" "label": "Font Ligatures"
},
"minimapCheckbox": {
"label": "Open Minimap"
} }
} }
} }
+3
View File
@@ -10,6 +10,9 @@
}, },
"ligaturesCheckbox": { "ligaturesCheckbox": {
"label": "连字" "label": "连字"
},
"minimapCheckbox": {
"label": "打开缩略图"
} }
} }
} }
+4
View File
@@ -30,14 +30,18 @@
"gitea-js": "^1.22.0", "gitea-js": "^1.22.0",
"jotai": "^2.10.3", "jotai": "^2.10.3",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"monaco-languageclient": "^8.8.3",
"next": "15.0.4", "next": "15.0.4",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-intl": "^3.26.1", "next-intl": "^3.26.1",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"normalize-url": "^8.0.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"reconnecting-websocket": "^4.4.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vscode-ws-jsonrpc": "^3.3.2",
"zustand": "^5.0.2" "zustand": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
+97 -15
View File
@@ -1,12 +1,27 @@
"use client"; "use client";
import {
toSocket,
WebSocketMessageReader,
WebSocketMessageWriter,
} from "vscode-ws-jsonrpc";
import {
CloseAction,
ErrorAction,
MessageTransports,
} from "vscode-languageclient";
import "@fontsource-variable/fira-code"; import "@fontsource-variable/fira-code";
import normalizeUrl from "normalize-url";
import { createHighlighter } from "shiki"; import { createHighlighter } from "shiki";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
import { shikiToMonaco } from "@shikijs/monaco"; import { shikiToMonaco } from "@shikijs/monaco";
import { useEffect, useRef, useMemo } from "react"; import { useEffect, useRef, useMemo } from "react";
import { useCodeEditorStore } from "@/store/codeEditorStore"; import { useCodeEditorStore } from "@/store/codeEditorStore";
import { MonacoLanguageClient } from "monaco-languageclient";
import MonacoEditor, { type Monaco } from "@monaco-editor/react"; import MonacoEditor, { type Monaco } from "@monaco-editor/react";
import { initServices } from "monaco-languageclient/vscode/services";
const ReconnectingWebSocket = require("reconnecting-websocket");
const ADDITIONAL_THEMES = [ const ADDITIONAL_THEMES = [
"andromeeda", "andromeeda",
@@ -26,14 +41,17 @@ const ADDITIONAL_THEMES = [
const ADDITIONAL_LANGUAGES = [ const ADDITIONAL_LANGUAGES = [
"c", "c",
"cpp",
"java", "java",
"python", "python",
"typescript",
] as const satisfies Parameters<typeof createHighlighter>[0]["langs"]; ] as const satisfies Parameters<typeof createHighlighter>[0]["langs"];
export function CodeEditor() { export function CodeEditor() {
const monacoRef = useRef<Monaco | null>(null); const monacoRef = useRef<Monaco | null>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null); const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const { lang, path, theme, value, isLigature } = useCodeEditorStore(); const { lang, path, theme, value, isMinimap, isLigature } =
useCodeEditorStore();
useEffect(() => { useEffect(() => {
if (monacoRef.current && editorRef.current) { if (monacoRef.current && editorRef.current) {
@@ -42,22 +60,84 @@ export function CodeEditor() {
}, [theme]); }, [theme]);
const options = useMemo( const options = useMemo(
() => ({ () =>
minimap: { enabled: false }, ({
fontSize: 14, minimap: { enabled: isMinimap, size: "fit" },
fontFamily: "Fira Code Variable, monospace", fontSize: 14,
tabSize: 4, fontFamily: "Fira Code Variable, monospace",
showFoldingControls: "always" as const, tabSize: 4,
fontLigatures: isLigature, showFoldingControls: "always",
automaticLayout: true, fontLigatures: isLigature,
guides: { automaticLayout: true,
bracketPairs: true, autoIndent: "full",
indentation: true, guides: {
}, bracketPairs: true,
}), indentation: true,
[isLigature] },
} as editor.IStandaloneEditorConstructionOptions),
[isMinimap, isLigature]
); );
const createLanguageClient = (
transports: MessageTransports
): MonacoLanguageClient => {
return new MonacoLanguageClient({
name: "LSP Language Client",
clientOptions: {
documentSelector: ADDITIONAL_LANGUAGES,
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart }),
},
},
connectionProvider: {
get: () => {
return Promise.resolve(transports);
},
},
});
};
useEffect(() => {
if (!monacoRef.current) return;
const url = normalizeUrl(`ws://172.20.0.13:3000`);
const socketOptions = {
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 10000,
maxRetries: Infinity,
debug: true,
};
const webSocket: WebSocket = new ReconnectingWebSocket.default(
url,
[],
socketOptions
);
webSocket.onopen = () => {
console.log("WebSocket connection opened");
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());
};
webSocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
webSocket.onclose = (event) => {
console.log("WebSocket connection closed:", event);
};
return () => {
webSocket.close();
};
}, [monacoRef]);
const handleEditorMount = async ( const handleEditorMount = async (
editor: editor.IStandaloneCodeEditor, editor: editor.IStandaloneCodeEditor,
monaco: Monaco monaco: Monaco
@@ -75,6 +155,8 @@ export function CodeEditor() {
}); });
shikiToMonaco(highlighter, monacoRef.current); shikiToMonaco(highlighter, monacoRef.current);
await initServices({});
}; };
return ( return (
@@ -0,0 +1,18 @@
"use client";
import { useTranslations } from "next-intl";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useCodeEditorStore } from "@/store/codeEditorStore";
export default function MinimapCheckbox() {
const t = useTranslations("Page.PlaygroundPage.Components.minimapCheckbox");
const { setIsMinimap } = useCodeEditorStore();
return (
<div className="flex items-center gap-2">
<Checkbox onCheckedChange={setIsMinimap} />
<Label>{t("label")}</Label>
</div>
);
}
@@ -1,5 +1,12 @@
"use client"; "use client";
import {
COriginal,
CplusplusOriginal,
JavaOriginal,
PythonOriginal,
TypescriptOriginal,
} from "devicons-react";
import * as React from "react"; import * as React from "react";
import { import {
Sidebar, Sidebar,
@@ -22,7 +29,6 @@ import {
import * as actions from "@/actions"; import * as actions from "@/actions";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { COriginal, JavaOriginal, PythonOriginal } from "devicons-react";
import { useCodeEditorStore } from "@/store/codeEditorStore"; import { useCodeEditorStore } from "@/store/codeEditorStore";
import { ChevronRight, File, Folder, FolderOpen } from "lucide-react"; import { ChevronRight, File, Folder, FolderOpen } from "lucide-react";
@@ -137,10 +143,14 @@ function Tree({ item }: { item: FileTree }) {
if (fileExtension === "c") { if (fileExtension === "c") {
fileIcon = <COriginal />; fileIcon = <COriginal />;
} else if (fileExtension === "cpp") {
fileIcon = <CplusplusOriginal />;
} else if (fileExtension === "java") { } else if (fileExtension === "java") {
fileIcon = <JavaOriginal />; fileIcon = <JavaOriginal />;
} else if (fileExtension === "py") { } else if (fileExtension === "py") {
fileIcon = <PythonOriginal />; fileIcon = <PythonOriginal />;
} else if (fileExtension === "ts") {
fileIcon = <TypescriptOriginal />;
} }
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
@@ -148,8 +158,10 @@ function Tree({ item }: { item: FileTree }) {
const langMap: { [key: string]: string } = { const langMap: { [key: string]: string } = {
c: "c", c: "c",
cpp: "cpp",
java: "java", java: "java",
py: "python", py: "python",
ts: "typescript",
}; };
const handleFileClick = async () => { const handleFileClick = async () => {
+2
View File
@@ -10,6 +10,7 @@ import { ModeSwitcher } from "@/components/mode-switcher";
import LanguageSwitcher from "@/components/language-switcher"; import LanguageSwitcher from "@/components/language-switcher";
import LigaturesCheckbox from "./components/ligatures-checkbox"; import LigaturesCheckbox from "./components/ligatures-checkbox";
import { PlaygroundSidebar } from "@/app/playground/components/playground-sidebar"; import { PlaygroundSidebar } from "@/app/playground/components/playground-sidebar";
import MinimapCheckbox from "./components/minimap-checkbox";
export default function PlaygroundLayout({ export default function PlaygroundLayout({
children, children,
@@ -26,6 +27,7 @@ export default function PlaygroundLayout({
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
<ThemeSelector /> <ThemeSelector />
<LigaturesCheckbox /> <LigaturesCheckbox />
<MinimapCheckbox />
</div> </div>
<div className="ml-auto px-3"> <div className="ml-auto px-3">
<LanguageSwitcher /> <LanguageSwitcher />
+4
View File
@@ -5,11 +5,13 @@ interface CodeEditorState {
path: string; path: string;
theme: string; theme: string;
value: string; value: string;
isMinimap: boolean;
isLigature: boolean; isLigature: boolean;
setLang: (lang: string) => void; setLang: (lang: string) => void;
setPath: (path: string) => void; setPath: (path: string) => void;
setTheme: (theme: string) => void; setTheme: (theme: string) => void;
setValue: (value: string) => void; setValue: (value: string) => void;
setIsMinimap: (isMinimap: boolean) => void;
setIsLigature: (isLigature: boolean) => void; setIsLigature: (isLigature: boolean) => void;
} }
@@ -18,10 +20,12 @@ export const useCodeEditorStore = create<CodeEditorState>((set) => ({
path: "", path: "",
theme: "one-dark-pro", theme: "one-dark-pro",
value: "", value: "",
isMinimap: false,
isLigature: false, isLigature: false,
setLang: (lang) => set({ lang }), setLang: (lang) => set({ lang }),
setPath: (path) => set({ path }), setPath: (path) => set({ path }),
setTheme: (theme) => set({ theme }), setTheme: (theme) => set({ theme }),
setValue: (value) => set({ value }), setValue: (value) => set({ value }),
setIsMinimap: (isMinimap) => set({ isMinimap }),
setIsLigature: (isLigature) => set({ isLigature }), setIsLigature: (isLigature) => set({ isLigature }),
})); }));