add resizing logic

This commit is contained in:
Ishaan Dey 2024-05-09 22:16:56 -07:00
parent 1500e84724
commit e86e86dbe2
8 changed files with 150 additions and 50 deletions

View File

@ -263,8 +263,15 @@ io.on("connection", async (socket) => {
callback() callback()
}) })
socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => {
console.log("resizeTerminal", dimensions)
Object.values(terminals).forEach((t) => {
t.terminal.resize(dimensions.cols, dimensions.rows)
})
})
socket.on("terminalData", (id: string, data: string) => { socket.on("terminalData", (id: string, data: string) => {
console.log("terminalData", id, data)
if (!terminals[id]) { if (!terminals[id]) {
console.log("terminal not found", id) console.log("terminal not found", id)
return return
@ -282,13 +289,10 @@ io.on("connection", async (socket) => {
return return
} }
console.log("closing terminal", id)
terminals[id].onData.dispose() terminals[id].onData.dispose()
terminals[id].onExit.dispose() terminals[id].onExit.dispose()
delete terminals[id] delete terminals[id]
console.log("terminals:", Object.keys(terminals))
callback() callback()
}) })
@ -330,7 +334,7 @@ io.on("connection", async (socket) => {
socket.on("disconnect", async () => { socket.on("disconnect", async () => {
if (data.isOwner) { if (data.isOwner) {
console.log("deleting all terminals") // console.log("deleting all terminals")
Object.entries(terminals).forEach((t) => { Object.entries(terminals).forEach((t) => {
const { terminal, onData, onExit } = t[1] const { terminal, onData, onExit } = t[1]
onData.dispose() onData.dispose()

View File

@ -120,7 +120,12 @@ export default function NewProjectModal({
} }
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog
open={open}
onOpenChange={(open: boolean) => {
if (!loading) setOpen(open);
}}
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create A Sandbox</DialogTitle> <DialogTitle>Create A Sandbox</DialogTitle>

View File

@ -9,6 +9,7 @@ import Link from "next/link";
import { Card } from "../ui/card"; import { Card } from "../ui/card";
import { deleteSandbox, updateSandbox } from "@/lib/actions"; import { deleteSandbox, updateSandbox } from "@/lib/actions";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState } from "react";
export default function DashboardProjects({ export default function DashboardProjects({
sandboxes, sandboxes,
@ -17,9 +18,16 @@ export default function DashboardProjects({
sandboxes: Sandbox[]; sandboxes: Sandbox[];
q: string | null; q: string | null;
}) { }) {
const [deletingId, setDeletingId] = useState<string>("");
const onDelete = async (sandbox: Sandbox) => { const onDelete = async (sandbox: Sandbox) => {
setDeletingId(sandbox.id);
toast(`Project ${sandbox.name} deleted.`); toast(`Project ${sandbox.name} deleted.`);
await deleteSandbox(sandbox.id); await deleteSandbox(sandbox.id);
setTimeout(() => {
// timeout to wait for revalidatePath and avoid flashing
setDeletingId("");
}, 200);
}; };
const onVisibilityChange = async (sandbox: Sandbox) => { const onVisibilityChange = async (sandbox: Sandbox) => {
@ -50,7 +58,11 @@ export default function DashboardProjects({
<Link <Link
key={sandbox.id} key={sandbox.id}
href={`/code/${sandbox.id}`} href={`/code/${sandbox.id}`}
className="cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg" className={`${
deletingId === sandbox.id
? "pointer-events-none opacity-50"
: ""
} cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
> >
<Card className="p-4 h-48 flex flex-col justify-between items-start hover:border-foreground transition-all"> <Card className="p-4 h-48 flex flex-col justify-between items-start hover:border-foreground transition-all">
{/* <ProjectCard key={sandbox.id} id={sandbox.id}> */} {/* <ProjectCard key={sandbox.id} id={sandbox.id}> */}

View File

@ -30,6 +30,7 @@ import DisableAccessModal from "./live/disableModal";
import Loading from "./loading"; import Loading from "./loading";
import PreviewWindow from "./preview"; import PreviewWindow from "./preview";
import Terminals from "./terminals"; import Terminals from "./terminals";
import { ImperativePanelHandle } from "react-resizable-panels";
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -44,12 +45,28 @@ export default function CodeEditor({
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}` `http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
); );
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(
sandboxData.type !== "react"
);
const [disableAccess, setDisableAccess] = useState({
isDisabled: false,
message: "",
});
// File state
const [files, setFiles] = useState<(TFolder | TFile)[]>([]); const [files, setFiles] = useState<(TFolder | TFile)[]>([]);
const [tabs, setTabs] = useState<TTab[]>([]); const [tabs, setTabs] = useState<TTab[]>([]);
const [editorLanguage, setEditorLanguage] = useState("plaintext");
const [activeFileId, setActiveFileId] = useState<string>(""); const [activeFileId, setActiveFileId] = useState<string>("");
const [activeFileContent, setActiveFileContent] = useState(""); const [activeFileContent, setActiveFileContent] = useState("");
// Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext");
const [cursorLine, setCursorLine] = useState(0); const [cursorLine, setCursorLine] = useState(0);
const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>();
// AI Copilot state
const [ai, setAi] = useState(false);
const [generate, setGenerate] = useState<{ const [generate, setGenerate] = useState<{
show: boolean; show: boolean;
id: string; id: string;
@ -62,6 +79,8 @@ export default function CodeEditor({
options: monaco.editor.IModelDeltaDecoration[]; options: monaco.editor.IModelDeltaDecoration[];
instance: monaco.editor.IEditorDecorationsCollection | undefined; instance: monaco.editor.IEditorDecorationsCollection | undefined;
}>({ options: [], instance: undefined }); }>({ options: [], instance: undefined });
// Terminal state
const [terminals, setTerminals] = useState< const [terminals, setTerminals] = useState<
{ {
id: string; id: string;
@ -71,24 +90,21 @@ export default function CodeEditor({
const [activeTerminalId, setActiveTerminalId] = useState(""); const [activeTerminalId, setActiveTerminalId] = useState("");
const [creatingTerminal, setCreatingTerminal] = useState(false); const [creatingTerminal, setCreatingTerminal] = useState(false);
const [closingTerminal, setClosingTerminal] = useState(""); const [closingTerminal, setClosingTerminal] = useState("");
const [provider, setProvider] = useState<TypedLiveblocksProvider>(); const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
const [ai, setAi] = useState(false);
const [disableAccess, setDisableAccess] = useState({
isDisabled: false,
message: "",
});
const isOwner = sandboxData.userId === userData.id; const isOwner = sandboxData.userId === userData.id;
const clerk = useClerk(); const clerk = useClerk();
const room = useRoom();
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
const [editorRef, setEditorRef] = // Liveblocks hooks
useState<monaco.editor.IStandaloneCodeEditor>(); const room = useRoom();
const [provider, setProvider] = useState<TypedLiveblocksProvider>();
// Refs for libraries / features
const editorContainerRef = useRef<HTMLDivElement>(null); const editorContainerRef = useRef<HTMLDivElement>(null);
const monacoRef = useRef<typeof monaco | null>(null); const monacoRef = useRef<typeof monaco | null>(null);
const generateRef = useRef<HTMLDivElement>(null); const generateRef = useRef<HTMLDivElement>(null);
const generateWidgetRef = useRef<HTMLDivElement>(null); const generateWidgetRef = useRef<HTMLDivElement>(null);
const previewPanelRef = useRef<ImperativePanelHandle>(null);
// Resize observer tracks editor width for generate widget // Resize observer tracks editor width for generate widget
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
@ -371,10 +387,11 @@ export default function CodeEditor({
}; };
const onDisableAccess = (message: string) => { const onDisableAccess = (message: string) => {
setDisableAccess({ if (!isOwner)
isDisabled: true, setDisableAccess({
message, isDisabled: true,
}); message,
});
}; };
socket.on("connect", onConnect); socket.on("connect", onConnect);
@ -571,7 +588,7 @@ export default function CodeEditor({
<ResizablePanelGroup direction="horizontal"> <ResizablePanelGroup direction="horizontal">
<ResizablePanel <ResizablePanel
className="p-2 flex flex-col" className="p-2 flex flex-col"
maxSize={75} maxSize={80}
minSize={30} minSize={30}
defaultSize={60} defaultSize={60}
> >
@ -659,11 +676,22 @@ export default function CodeEditor({
<ResizablePanel defaultSize={40}> <ResizablePanel defaultSize={40}>
<ResizablePanelGroup direction="vertical"> <ResizablePanelGroup direction="vertical">
<ResizablePanel <ResizablePanel
ref={previewPanelRef}
defaultSize={50} defaultSize={50}
minSize={20} collapsedSize={4}
minSize={25}
collapsible
className="p-2 flex flex-col" className="p-2 flex flex-col"
onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)}
> >
<PreviewWindow /> <PreviewWindow
collapsed={isPreviewCollapsed}
open={() => {
previewPanelRef.current?.expand();
setIsPreviewCollapsed(false);
}}
/>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel <ResizablePanel

View File

@ -5,31 +5,69 @@ import {
ChevronRight, ChevronRight,
RotateCw, RotateCw,
TerminalSquare, TerminalSquare,
UnfoldVertical,
} from "lucide-react"; } from "lucide-react";
export default function PreviewWindow() { export default function PreviewWindow({
collapsed,
open,
}: {
collapsed: boolean;
open: () => void;
}) {
return ( return (
<> <>
<div className="h-10 select-none w-full flex gap-2"> <div
className={`${
collapsed ? "h-full" : "h-10"
} select-none w-full flex gap-2`}
>
<div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between"> <div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between">
Preview Preview
<div className="flex space-x-1 translate-x-1"> <div className="flex space-x-1 translate-x-1">
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"> {collapsed ? (
<TerminalSquare className="w-4 h-4" /> <PreviewButton onClick={open}>
</div> <UnfoldVertical className="w-4 h-4" />
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"> </PreviewButton>
<ChevronLeft className="w-4 h-4" /> ) : (
</div> <>
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"> <PreviewButton onClick={() => console.log("Terminal")}>
<ChevronRight className="w-4 h-4" /> <TerminalSquare className="w-4 h-4" />
</div> </PreviewButton>
<div className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"> <PreviewButton onClick={() => console.log("Back")}>
<RotateCw className="w-3 h-3" /> <ChevronLeft className="w-4 h-4" />
</div> </PreviewButton>
<PreviewButton onClick={() => console.log("Forward")}>
<ChevronRight className="w-4 h-4" />
</PreviewButton>
<PreviewButton onClick={() => console.log("Reload")}>
<RotateCw className="w-3 h-3" />
</PreviewButton>
</>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="w-full grow rounded-md bg-foreground"></div> {collapsed ? null : (
<div className="w-full grow rounded-md bg-foreground"></div>
)}
</> </>
); );
} }
function PreviewButton({
children,
onClick,
}: {
children: React.ReactNode;
onClick: () => void;
}) {
return (
<div
className="p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"
onClick={onClick}
>
{children}
</div>
);
}

View File

@ -50,6 +50,7 @@ export default function Terminals({
{terminals.map((term) => ( {terminals.map((term) => (
<Tab <Tab
key={term.id} key={term.id}
creating={creatingTerminal}
onClick={() => setActiveTerminalId(term.id)} onClick={() => setActiveTerminalId(term.id)}
onClose={() => onClose={() =>
closeTerminal({ closeTerminal({
@ -85,7 +86,7 @@ export default function Terminals({
}} }}
size="smIcon" size="smIcon"
variant={"secondary"} variant={"secondary"}
className={`font-normal shrink-0 select-none text-muted-foreground`} className={`font-normal shrink-0 select-none text-muted-foreground disabled:opacity-50`}
> >
{creatingTerminal ? ( {creatingTerminal ? (
<Loader2 className="animate-spin w-4 h-4" /> <Loader2 className="animate-spin w-4 h-4" />

View File

@ -48,19 +48,29 @@ export default function EditorTerminal({
useEffect(() => { useEffect(() => {
if (!term) return; if (!term) return;
if (terminalRef.current) { if (!terminalRef.current) return;
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
term.open(terminalRef.current); term.open(terminalRef.current);
fitAddon.fit(); fitAddon.fit();
}
const disposable = term.onData((data) => { const disposableOnData = term.onData((data) => {
console.log("terminalData", id, data); console.log("terminalData", id, data);
socket.emit("terminalData", id, data); socket.emit("terminalData", id, data);
}); });
const disposableOnResize = term.onResize((dimensions) => {
// const terminal_size = {
// width: dimensions.cols,
// height: dimensions.rows,
// };
fitAddon.fit();
socket.emit("terminalResize", dimensions);
});
return () => { return () => {
disposable.dispose(); disposableOnData.dispose();
disposableOnResize.dispose();
}; };
}, [term, terminalRef.current]); }, [term, terminalRef.current]);

View File

@ -6,6 +6,7 @@ import { MouseEvent, MouseEventHandler, useEffect } from "react";
export default function Tab({ export default function Tab({
children, children,
creating = false,
saved = true, saved = true,
selected = false, selected = false,
onClick, onClick,
@ -13,6 +14,7 @@ export default function Tab({
closing = false, closing = false,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
creating?: boolean;
saved?: boolean; saved?: boolean;
selected?: boolean; selected?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>; onClick?: MouseEventHandler<HTMLButtonElement>;
@ -43,7 +45,7 @@ export default function Tab({
} }
className="h-5 w-5 ml-0.5 group flex items-center justify-center translate-x-1 transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm" className="h-5 w-5 ml-0.5 group flex items-center justify-center translate-x-1 transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm"
> >
{closing ? ( {closing || creating ? (
<Loader2 className="animate-spin w-3 h-3" /> <Loader2 className="animate-spin w-3 h-3" />
) : saved ? ( ) : saved ? (
<X className="w-3 h-3" /> <X className="w-3 h-3" />