From 668145a9c782ff49eb6f5b4c37889dc0614580de Mon Sep 17 00:00:00 2001 From: ngc2207 Date: Sun, 15 Dec 2024 12:33:39 +0800 Subject: [PATCH] feat(playground): add code editor component and integrate file retrieval functionality --- package.json | 12 +++-- src/actions/index.ts | 21 +++++++- src/app/playground/components/code-editor.tsx | 52 +++++++++++++++++++ .../components/playground-sidebar.tsx | 34 +++++++++++- src/app/playground/page.tsx | 11 ++-- src/store/codeEditorStore.ts | 23 ++++++++ 6 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 src/app/playground/components/code-editor.tsx create mode 100644 src/store/codeEditorStore.ts diff --git a/package.json b/package.json index 99012cd..deb835d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@fontsource-variable/fira-code": "^5.1.0", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1", @@ -37,13 +39,15 @@ "zustand": "^5.0.2" }, "devDependencies": { - "typescript": "^5", + "@shikijs/monaco": "^1.24.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "postcss": "^8", - "tailwindcss": "^3.4.1", "eslint": "^8", - "eslint-config-next": "15.0.4" + "eslint-config-next": "15.0.4", + "postcss": "^8", + "shiki": "^1.24.2", + "tailwindcss": "^3.4.1", + "typescript": "^5" } } diff --git a/src/actions/index.ts b/src/actions/index.ts index b7af701..e376878 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -15,10 +15,29 @@ export async function retrieveTreeStructure( return response.data.tree || []; } catch (error) { if ((error as APIError).message) { - console.error("Gitea API Error", error); + console.error("Gitea API [getTree] Error", error); } else { console.error("Unexpected Error", error); } throw new Error("Failed to retrieve tree structure"); } } + +export async function retrieveFileContent( + owner: string, + repo: string, + path: string +): Promise<{ encoding: string; content: string }> { + try { + const response = await api.repos.repoGetContents(owner, repo, path); + const { encoding, content } = response.data; + return { encoding: encoding || "", content: content || "" }; + } catch (error) { + if ((error as APIError).message) { + console.error("Gitea API [repoGetContents] Error", error); + } else { + console.error("Unexpected Error", error); + } + throw new Error("Failed to retrieve file contents"); + } +} diff --git a/src/app/playground/components/code-editor.tsx b/src/app/playground/components/code-editor.tsx new file mode 100644 index 0000000..7829d4a --- /dev/null +++ b/src/app/playground/components/code-editor.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useRef } from "react"; +import "@fontsource-variable/fira-code"; +import { createHighlighter } from "shiki"; +import type { editor } from "monaco-editor"; +import { shikiToMonaco } from "@shikijs/monaco"; +import { useCodeEditorStore } from "@/store/codeEditorStore"; +import MonacoEditor, { type Monaco } from "@monaco-editor/react"; + +export function CodeEditor() { + const monacoRef = useRef(null); + const editorRef = useRef(null); + const { lang, theme, value, isLigature } = useCodeEditorStore(); + return ( + { + editorRef.current = editor; + monacoRef.current = monaco; + void (async () => { + const ADDITIONAL_LANGUAGES = [ + "c", + "java", + ] as const satisfies Parameters[0]["langs"]; + + for (const lang of ADDITIONAL_LANGUAGES) { + monacoRef.current?.languages.register({ id: lang }); + } + + const highlighter = await createHighlighter({ + themes: [theme], + langs: ADDITIONAL_LANGUAGES, + }); + + shikiToMonaco(highlighter, monacoRef.current); + })(); + }} + /> + ); +} diff --git a/src/app/playground/components/playground-sidebar.tsx b/src/app/playground/components/playground-sidebar.tsx index 82dbc32..60061b0 100644 --- a/src/app/playground/components/playground-sidebar.tsx +++ b/src/app/playground/components/playground-sidebar.tsx @@ -22,6 +22,7 @@ import { import * as actions from "@/actions"; import { useSession } from "next-auth/react"; import { COriginal, JavaOriginal } from "devicons-react"; +import { useCodeEditorStore } from "@/store/codeEditorStore"; import { ChevronRight, File, Folder, FolderOpen } from "lucide-react"; interface FileTree { @@ -125,7 +126,9 @@ export function PlaygroundSidebar({ } function Tree({ item }: { item: FileTree }) { - const { name, type, children } = item; + const { data: session } = useSession(); + const username = session?.user?.name ?? ""; + const { name, path, type, children } = item; const fileExtension = name.split(".").pop(); let fileIcon = ; @@ -137,10 +140,37 @@ function Tree({ item }: { item: FileTree }) { } const [isOpen, setIsOpen] = React.useState(false); + const { setLang, setValue } = useCodeEditorStore(); + + const langMap: { [key: string]: string } = { + c: "c", + java: "java", + }; + + const handleFileClick = async () => { + if (type === "blob") { + const fileContentsResponse = await actions.retrieveFileContent( + username, + "playground", + path + ); + const { encoding, content } = fileContentsResponse; + const decodedContent = Buffer.from( + content, + encoding as BufferEncoding + ).toString("utf-8"); + const lang = langMap[fileExtension ?? ""] || "plaintext"; + setLang(lang); + setValue(decodedContent); + } + }; if (type === "blob") { return ( - + {fileIcon} {name} diff --git a/src/app/playground/page.tsx b/src/app/playground/page.tsx index d7862c8..8feab07 100644 --- a/src/app/playground/page.tsx +++ b/src/app/playground/page.tsx @@ -1,12 +1,9 @@ +import { CodeEditor } from "./components/code-editor"; + export default function PlaygroundPage() { return ( -
-
-
-
-
-
-
+
+
); } diff --git a/src/store/codeEditorStore.ts b/src/store/codeEditorStore.ts new file mode 100644 index 0000000..88cb8b5 --- /dev/null +++ b/src/store/codeEditorStore.ts @@ -0,0 +1,23 @@ +import { create } from "zustand"; + +interface CodeEditorState { + lang: string; + theme: string; + value: string; + isLigature: boolean; + setLang: (lang: string) => void; + setTheme: (theme: string) => void; + setValue: (value: string) => void; + setIsLigature: (isLigature: boolean) => void; +} + +export const useCodeEditorStore = create((set) => ({ + lang: "c", + theme: "one-dark-pro", + value: "", + isLigature: false, + setLang: (lang) => set({ lang }), + setTheme: (theme) => set({ theme }), + setValue: (value) => set({ value }), + setIsLigature: (isLigature) => set({ isLigature }), +}));