From db8c26cd386ce10a451b019a059c724a0946da65 Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Wed, 8 May 2024 23:52:08 -0700 Subject: [PATCH] organize & comment code --- frontend/components/editor/index.tsx | 243 ++++-------------- frontend/components/editor/preview/index.tsx | 35 +++ .../components/editor/terminals/index.tsx | 126 +++++++++ .../index.tsx => terminals/terminal.tsx} | 0 .../editor/{terminal => terminals}/xterm.css | 0 frontend/components/ui/tab.tsx | 34 +-- frontend/lib/terminal.ts | 91 +++++++ 7 files changed, 327 insertions(+), 202 deletions(-) create mode 100644 frontend/components/editor/preview/index.tsx create mode 100644 frontend/components/editor/terminals/index.tsx rename frontend/components/editor/{terminal/index.tsx => terminals/terminal.tsx} (100%) rename frontend/components/editor/{terminal => terminals}/xterm.css (100%) create mode 100644 frontend/lib/terminal.ts diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 302670b..33f08c1 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -6,7 +6,6 @@ import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"; import { io } from "socket.io-client"; import { toast } from "sonner"; import { useClerk } from "@clerk/nextjs"; -import { createId } from "@paralleldrive/cuid2"; import * as Y from "yjs"; import LiveblocksProvider from "@liveblocks/yjs"; @@ -19,38 +18,32 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; -import { - ChevronLeft, - ChevronRight, - FileJson, - Loader2, - Plus, - RotateCw, - Shell, - SquareTerminal, - TerminalSquare, -} from "lucide-react"; +import { FileJson, Loader2, TerminalSquare } from "lucide-react"; import Tab from "../ui/tab"; import Sidebar from "./sidebar"; -import EditorTerminal from "./terminal"; -import { Button } from "../ui/button"; import GenerateInput from "./generate"; -import { Sandbox, User, TFile, TFileData, TFolder, TTab } from "@/lib/types"; +import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"; import { processFileType, validateName } from "@/lib/utils"; import { Cursors } from "./live/cursors"; import { Terminal } from "@xterm/xterm"; import DisableAccessModal from "./live/disableModal"; import Loading from "./loading"; +import PreviewWindow from "./preview"; +import Terminals from "./terminals"; export default function CodeEditor({ userData, sandboxData, - isSharedUser, -}: { +}: // isSharedUser, +{ userData: User; sandboxData: Sandbox; isSharedUser: boolean; }) { + const socket = io( + `http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}` + ); + const [files, setFiles] = useState<(TFolder | TFile)[]>([]); const [tabs, setTabs] = useState([]); const [editorLanguage, setEditorLanguage] = useState("plaintext"); @@ -90,7 +83,6 @@ export default function CodeEditor({ const room = useRoom(); const activeTerminal = terminals.find((t) => t.id === activeTerminalId); - // const editorRef = useRef(null) const [editorRef, setEditorRef] = useState(); const editorContainerRef = useRef(null); @@ -98,16 +90,27 @@ export default function CodeEditor({ const generateRef = useRef(null); const generateWidgetRef = useRef(null); + // Resize observer tracks editor width for generate widget + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width } = entry.contentRect; + setGenerate((prev) => { + return { ...prev, width }; + }); + } + }); + + // Pre-mount editor keybindings const handleEditorWillMount: BeforeMount = (monaco) => { monaco.editor.addKeybindingRules([ { keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG, command: "null", - // when: "textInputFocus", }, ]); }; + // Post-mount editor keybindings and actions const handleEditorMount: OnMount = (editor, monaco) => { setEditorRef(editor); monacoRef.current = monaco; @@ -167,6 +170,7 @@ export default function CodeEditor({ }); }; + // Generate widget effect useEffect(() => { if (!ai) { setGenerate((prev) => { @@ -239,6 +243,7 @@ export default function CodeEditor({ } }, [generate.show]); + // Decorations effect for generate widget tips useEffect(() => { if (decorations.options.length === 0) { decorations.instance?.clear(); @@ -261,10 +266,7 @@ export default function CodeEditor({ } }, [decorations.options]); - const socket = io( - `http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}` - ); - + // Save file keybinding logic effect useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "s" && (e.metaKey || e.ctrlKey)) { @@ -286,15 +288,7 @@ export default function CodeEditor({ }; }, [tabs, activeFileId]); - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const { width } = entry.contentRect; - setGenerate((prev) => { - return { ...prev, width }; - }); - } - }); - + // Liveblocks live collaboration setup effect useEffect(() => { const tab = tabs.find((t) => t.id === activeFileId); const model = editorRef?.getModel(); @@ -317,8 +311,6 @@ export default function CodeEditor({ }, 0); } } - } else { - // Yjs content is not synchronized } }; @@ -341,7 +333,7 @@ export default function CodeEditor({ }; }, [editorRef, room, activeFileContent]); - // connection/disconnection effect + resizeobserver + // Connection/disconnection effect + resizeobserver useEffect(() => { socket.connect(); @@ -352,23 +344,14 @@ export default function CodeEditor({ return () => { socket.disconnect(); resizeObserver.disconnect(); - - // terminals.forEach((term) => { - // if (term.terminal) { - // term.terminal.dispose(); - // } - // }); }; }, []); - // event listener effect + // Socket event listener effect useEffect(() => { - const onConnect = () => { - console.log("connected"); - }; + const onConnect = () => {}; const onDisconnect = () => { - console.log("disconnected"); setTerminals([]); }; @@ -412,23 +395,9 @@ export default function CodeEditor({ // }, []); }, [terminals]); - // Helper functions: - - const createTerminal = () => { - setCreatingTerminal(true); - const id = createId(); - console.log("creating terminal, id:", id); - - setTerminals((prev) => [...prev, { id, terminal: null }]); - setActiveTerminalId(id); - - setTimeout(() => { - socket.emit("createTerminal", id, () => { - setCreatingTerminal(false); - }); - }, 1000); - }; + // Helper functions for tabs: + // Select file and load content const selectFile = (tab: TTab) => { if (tab.id === activeFileId) return; const exists = tabs.find((t) => t.id === tab.id); @@ -448,6 +417,7 @@ export default function CodeEditor({ setActiveFileId(tab.id); }; + // Close tab and remove from tabs const closeTab = (tab: TFile) => { const numTabs = tabs.length; const index = tabs.findIndex((t) => t.id === tab.id); @@ -475,40 +445,6 @@ export default function CodeEditor({ } }; - const closeTerminal = (term: { id: string; terminal: Terminal | null }) => { - const numTerminals = terminals.length; - const index = terminals.findIndex((t) => t.id === term.id); - if (index === -1) return; - - setClosingTerminal(term.id); - - socket.emit("closeTerminal", term.id, () => { - setClosingTerminal(""); - - const nextId = - activeTerminalId === term.id - ? numTerminals === 1 - ? null - : index < numTerminals - 1 - ? terminals[index + 1].id - : terminals[index - 1].id - : activeTerminalId; - - // if (activeTerminal && activeTerminal.terminal) - // activeTerminal.terminal.dispose(); - setTerminals((prev) => prev.filter((t) => t.id !== term.id)); - - if (!nextId) { - setActiveTerminalId(""); - } else { - const nextTerminal = terminals.find((t) => t.id === nextId); - if (nextTerminal) { - setActiveTerminalId(nextTerminal.id); - } - } - }); - }; - const handleRename = ( id: string, newName: string, @@ -542,7 +478,8 @@ export default function CodeEditor({ // }) }; - if (disableAccess.isDisabled) { + // On disabled access for shared users, show un-interactable loading placeholder + info modal + if (disableAccess.isDisabled) return ( <> ); - } return ( <> + {/* Copilot DOM elements */}
{generate.show && ai ? ( @@ -606,6 +543,7 @@ export default function CodeEditor({ ) : null}
+ {/* Main editor components */} [...prev, { id, name, type: "folder", children: [] }]) } }} + // AI Copilot Toggle ai={ai} setAi={setAi} /> + + {/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
+ {/* File tabs */} {tabs.map((tab) => ( ))}
+ {/* Monaco editor */}
- ) : clerk.loaded ? ( + ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 + clerk.loaded ? ( <> {provider ? : null} -
-
- Preview -
-
- -
-
- -
-
- -
-
- -
-
-
-
-
+ {isOwner ? ( - <> -
- {terminals.map((term) => ( - setActiveTerminalId(term.id)} - onClose={() => closeTerminal(term)} - selected={activeTerminalId === term.id} - > - - Shell - - ))} - -
- {socket && activeTerminal ? ( -
- {terminals.map((term) => ( - { - // console.log( - // "setting terminal", - // activeTerminalId, - // t.options - // ); - setTerminals((prev) => - prev.map((term) => - term.id === activeTerminalId - ? { ...term, terminal: t } - : term - ) - ); - }} - visible={activeTerminalId === term.id} - /> - ))} -
- ) : ( -
- - No terminals open. -
- )} - + ) : (
diff --git a/frontend/components/editor/preview/index.tsx b/frontend/components/editor/preview/index.tsx new file mode 100644 index 0000000..db8734b --- /dev/null +++ b/frontend/components/editor/preview/index.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { + ChevronLeft, + ChevronRight, + RotateCw, + TerminalSquare, +} from "lucide-react"; + +export default function PreviewWindow() { + return ( + <> +
+
+ Preview +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + ); +} diff --git a/frontend/components/editor/terminals/index.tsx b/frontend/components/editor/terminals/index.tsx new file mode 100644 index 0000000..f28f18c --- /dev/null +++ b/frontend/components/editor/terminals/index.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import Tab from "@/components/ui/tab"; +import { closeTerminal, createTerminal } from "@/lib/terminal"; +import { Terminal } from "@xterm/xterm"; +import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"; +import { Socket } from "socket.io-client"; +import { toast } from "sonner"; +import EditorTerminal from "./terminal"; + +export default function Terminals({ + terminals, + setTerminals, + activeTerminalId, + setActiveTerminalId, + socket, + activeTerminal, + creatingTerminal, + setCreatingTerminal, + closingTerminal, + setClosingTerminal, +}: { + terminals: { id: string; terminal: Terminal | null }[]; + setTerminals: React.Dispatch< + React.SetStateAction< + { + id: string; + terminal: Terminal | null; + }[] + > + >; + activeTerminalId: string; + setActiveTerminalId: React.Dispatch>; + socket: Socket; + activeTerminal: + | { + id: string; + terminal: Terminal | null; + } + | undefined; + creatingTerminal: boolean; + setCreatingTerminal: React.Dispatch>; + closingTerminal: string; + setClosingTerminal: React.Dispatch>; +}) { + return ( + <> +
+ {terminals.map((term) => ( + setActiveTerminalId(term.id)} + onClose={() => + closeTerminal({ + term, + terminals, + setTerminals, + setActiveTerminalId, + setClosingTerminal, + socket, + activeTerminalId, + }) + } + closing={closingTerminal === term.id} + selected={activeTerminalId === term.id} + > + + Shell + + ))} + +
+ {socket && activeTerminal ? ( +
+ {terminals.map((term) => ( + { + setTerminals((prev) => + prev.map((term) => + term.id === activeTerminalId + ? { ...term, terminal: t } + : term + ) + ); + }} + visible={activeTerminalId === term.id} + /> + ))} +
+ ) : ( +
+ + No terminals open. +
+ )} + + ); +} diff --git a/frontend/components/editor/terminal/index.tsx b/frontend/components/editor/terminals/terminal.tsx similarity index 100% rename from frontend/components/editor/terminal/index.tsx rename to frontend/components/editor/terminals/terminal.tsx diff --git a/frontend/components/editor/terminal/xterm.css b/frontend/components/editor/terminals/xterm.css similarity index 100% rename from frontend/components/editor/terminal/xterm.css rename to frontend/components/editor/terminals/xterm.css diff --git a/frontend/components/ui/tab.tsx b/frontend/components/ui/tab.tsx index 8cd4cb6..0338f53 100644 --- a/frontend/components/ui/tab.tsx +++ b/frontend/components/ui/tab.tsx @@ -1,8 +1,8 @@ -"use client" +"use client"; -import { X } from "lucide-react" -import { Button } from "./button" -import { MouseEvent, MouseEventHandler, useEffect } from "react" +import { Loader2, X } from "lucide-react"; +import { Button } from "./button"; +import { MouseEvent, MouseEventHandler, useEffect } from "react"; export default function Tab({ children, @@ -10,12 +10,14 @@ export default function Tab({ selected = false, onClick, onClose, + closing = false, }: { - children: React.ReactNode - saved?: boolean - selected?: boolean - onClick?: MouseEventHandler - onClose?: () => void + children: React.ReactNode; + saved?: boolean; + selected?: boolean; + onClick?: MouseEventHandler; + onClose?: () => void; + closing?: boolean; }) { return (
- ) + ); } diff --git a/frontend/lib/terminal.ts b/frontend/lib/terminal.ts new file mode 100644 index 0000000..fb3200f --- /dev/null +++ b/frontend/lib/terminal.ts @@ -0,0 +1,91 @@ +// Helper functions for terminal instances + +import { createId } from "@paralleldrive/cuid2"; +import { Terminal } from "@xterm/xterm"; +import { Socket } from "socket.io-client"; + +export const createTerminal = ({ + setTerminals, + setActiveTerminalId, + setCreatingTerminal, + socket, +}: { + setTerminals: React.Dispatch>; + setActiveTerminalId: React.Dispatch>; + setCreatingTerminal: React.Dispatch>; + socket: Socket; + +}) => { + setCreatingTerminal(true); + const id = createId(); + console.log("creating terminal, id:", id); + + setTerminals((prev) => [...prev, { id, terminal: null }]); + setActiveTerminalId(id); + + setTimeout(() => { + socket.emit("createTerminal", id, () => { + setCreatingTerminal(false); + }); + }, 1000); +}; + +export const closeTerminal = ({ + term, + terminals, + setTerminals, + setActiveTerminalId, + setClosingTerminal, + socket, + activeTerminalId, +} : { + term: { + id: string; + terminal: Terminal | null + } + terminals: { + id: string; + terminal: Terminal | null + }[] + setTerminals: React.Dispatch> + setActiveTerminalId: React.Dispatch> + setClosingTerminal: React.Dispatch> + socket: Socket + activeTerminalId: string +}) => { + const numTerminals = terminals.length; + const index = terminals.findIndex((t) => t.id === term.id); + if (index === -1) return; + + setClosingTerminal(term.id); + + socket.emit("closeTerminal", term.id, () => { + setClosingTerminal(""); + + const nextId = + activeTerminalId === term.id + ? numTerminals === 1 + ? null + : index < numTerminals - 1 + ? terminals[index + 1].id + : terminals[index - 1].id + : activeTerminalId; + + setTerminals((prev) => prev.filter((t) => t.id !== term.id)); + + if (!nextId) { + setActiveTerminalId(""); + } else { + const nextTerminal = terminals.find((t) => t.id === nextId); + if (nextTerminal) { + setActiveTerminalId(nextTerminal.id); + } + } + }); +}; \ No newline at end of file