organize & comment code
This commit is contained in:
parent
2ef5f85099
commit
db8c26cd38
@ -6,7 +6,6 @@ import Editor, { BeforeMount, OnMount } from "@monaco-editor/react";
|
|||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useClerk } from "@clerk/nextjs";
|
import { useClerk } from "@clerk/nextjs";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
|
||||||
|
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import LiveblocksProvider from "@liveblocks/yjs";
|
import LiveblocksProvider from "@liveblocks/yjs";
|
||||||
@ -19,38 +18,32 @@ import {
|
|||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
import {
|
import { FileJson, Loader2, TerminalSquare } from "lucide-react";
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
FileJson,
|
|
||||||
Loader2,
|
|
||||||
Plus,
|
|
||||||
RotateCw,
|
|
||||||
Shell,
|
|
||||||
SquareTerminal,
|
|
||||||
TerminalSquare,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Tab from "../ui/tab";
|
import Tab from "../ui/tab";
|
||||||
import Sidebar from "./sidebar";
|
import Sidebar from "./sidebar";
|
||||||
import EditorTerminal from "./terminal";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import GenerateInput from "./generate";
|
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 { processFileType, validateName } from "@/lib/utils";
|
||||||
import { Cursors } from "./live/cursors";
|
import { Cursors } from "./live/cursors";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import DisableAccessModal from "./live/disableModal";
|
import DisableAccessModal from "./live/disableModal";
|
||||||
import Loading from "./loading";
|
import Loading from "./loading";
|
||||||
|
import PreviewWindow from "./preview";
|
||||||
|
import Terminals from "./terminals";
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
isSharedUser,
|
}: // isSharedUser,
|
||||||
}: {
|
{
|
||||||
userData: User;
|
userData: User;
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox;
|
||||||
isSharedUser: boolean;
|
isSharedUser: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const socket = io(
|
||||||
|
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
|
||||||
|
);
|
||||||
|
|
||||||
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 [editorLanguage, setEditorLanguage] = useState("plaintext");
|
||||||
@ -90,7 +83,6 @@ export default function CodeEditor({
|
|||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
|
||||||
|
|
||||||
// const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
|
|
||||||
const [editorRef, setEditorRef] =
|
const [editorRef, setEditorRef] =
|
||||||
useState<monaco.editor.IStandaloneCodeEditor>();
|
useState<monaco.editor.IStandaloneCodeEditor>();
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -98,16 +90,27 @@ export default function CodeEditor({
|
|||||||
const generateRef = useRef<HTMLDivElement>(null);
|
const generateRef = useRef<HTMLDivElement>(null);
|
||||||
const generateWidgetRef = useRef<HTMLDivElement>(null);
|
const generateWidgetRef = useRef<HTMLDivElement>(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) => {
|
const handleEditorWillMount: BeforeMount = (monaco) => {
|
||||||
monaco.editor.addKeybindingRules([
|
monaco.editor.addKeybindingRules([
|
||||||
{
|
{
|
||||||
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
|
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyG,
|
||||||
command: "null",
|
command: "null",
|
||||||
// when: "textInputFocus",
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Post-mount editor keybindings and actions
|
||||||
const handleEditorMount: OnMount = (editor, monaco) => {
|
const handleEditorMount: OnMount = (editor, monaco) => {
|
||||||
setEditorRef(editor);
|
setEditorRef(editor);
|
||||||
monacoRef.current = monaco;
|
monacoRef.current = monaco;
|
||||||
@ -167,6 +170,7 @@ export default function CodeEditor({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generate widget effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ai) {
|
if (!ai) {
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
@ -239,6 +243,7 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
}, [generate.show]);
|
}, [generate.show]);
|
||||||
|
|
||||||
|
// Decorations effect for generate widget tips
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (decorations.options.length === 0) {
|
if (decorations.options.length === 0) {
|
||||||
decorations.instance?.clear();
|
decorations.instance?.clear();
|
||||||
@ -261,10 +266,7 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
}, [decorations.options]);
|
}, [decorations.options]);
|
||||||
|
|
||||||
const socket = io(
|
// Save file keybinding logic effect
|
||||||
`http://localhost:4000?userId=${userData.id}&sandboxId=${sandboxData.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||||
@ -286,15 +288,7 @@ export default function CodeEditor({
|
|||||||
};
|
};
|
||||||
}, [tabs, activeFileId]);
|
}, [tabs, activeFileId]);
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
// Liveblocks live collaboration setup effect
|
||||||
for (const entry of entries) {
|
|
||||||
const { width } = entry.contentRect;
|
|
||||||
setGenerate((prev) => {
|
|
||||||
return { ...prev, width };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tab = tabs.find((t) => t.id === activeFileId);
|
const tab = tabs.find((t) => t.id === activeFileId);
|
||||||
const model = editorRef?.getModel();
|
const model = editorRef?.getModel();
|
||||||
@ -317,8 +311,6 @@ export default function CodeEditor({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Yjs content is not synchronized
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -341,7 +333,7 @@ export default function CodeEditor({
|
|||||||
};
|
};
|
||||||
}, [editorRef, room, activeFileContent]);
|
}, [editorRef, room, activeFileContent]);
|
||||||
|
|
||||||
// connection/disconnection effect + resizeobserver
|
// Connection/disconnection effect + resizeobserver
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.connect();
|
socket.connect();
|
||||||
|
|
||||||
@ -352,23 +344,14 @@ export default function CodeEditor({
|
|||||||
return () => {
|
return () => {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
|
|
||||||
// terminals.forEach((term) => {
|
|
||||||
// if (term.terminal) {
|
|
||||||
// term.terminal.dispose();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// event listener effect
|
// Socket event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onConnect = () => {
|
const onConnect = () => {};
|
||||||
console.log("connected");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDisconnect = () => {
|
const onDisconnect = () => {
|
||||||
console.log("disconnected");
|
|
||||||
setTerminals([]);
|
setTerminals([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -412,23 +395,9 @@ export default function CodeEditor({
|
|||||||
// }, []);
|
// }, []);
|
||||||
}, [terminals]);
|
}, [terminals]);
|
||||||
|
|
||||||
// Helper functions:
|
// Helper functions for tabs:
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Select file and load content
|
||||||
const selectFile = (tab: TTab) => {
|
const selectFile = (tab: TTab) => {
|
||||||
if (tab.id === activeFileId) return;
|
if (tab.id === activeFileId) return;
|
||||||
const exists = tabs.find((t) => t.id === tab.id);
|
const exists = tabs.find((t) => t.id === tab.id);
|
||||||
@ -448,6 +417,7 @@ export default function CodeEditor({
|
|||||||
setActiveFileId(tab.id);
|
setActiveFileId(tab.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close tab and remove from tabs
|
||||||
const closeTab = (tab: TFile) => {
|
const closeTab = (tab: TFile) => {
|
||||||
const numTabs = tabs.length;
|
const numTabs = tabs.length;
|
||||||
const index = tabs.findIndex((t) => t.id === tab.id);
|
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 = (
|
const handleRename = (
|
||||||
id: string,
|
id: string,
|
||||||
newName: 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 (
|
||||||
<>
|
<>
|
||||||
<DisableAccessModal
|
<DisableAccessModal
|
||||||
@ -553,10 +490,10 @@ export default function CodeEditor({
|
|||||||
<Loading />
|
<Loading />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Copilot DOM elements */}
|
||||||
<div ref={generateRef} />
|
<div ref={generateRef} />
|
||||||
<div className="z-50 p-1" ref={generateWidgetRef}>
|
<div className="z-50 p-1" ref={generateWidgetRef}>
|
||||||
{generate.show && ai ? (
|
{generate.show && ai ? (
|
||||||
@ -606,6 +543,7 @@ export default function CodeEditor({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Main editor components */}
|
||||||
<Sidebar
|
<Sidebar
|
||||||
files={files}
|
files={files}
|
||||||
selectFile={selectFile}
|
selectFile={selectFile}
|
||||||
@ -624,9 +562,12 @@ export default function CodeEditor({
|
|||||||
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
|
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
// AI Copilot Toggle
|
||||||
ai={ai}
|
ai={ai}
|
||||||
setAi={setAi}
|
setAi={setAi}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Shadcn resizeable panels: https://ui.shadcn.com/docs/components/resizable */}
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
@ -635,6 +576,7 @@ export default function CodeEditor({
|
|||||||
defaultSize={60}
|
defaultSize={60}
|
||||||
>
|
>
|
||||||
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
|
<div className="h-10 w-full flex gap-2 overflow-auto tab-scroll">
|
||||||
|
{/* File tabs */}
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@ -649,6 +591,7 @@ export default function CodeEditor({
|
|||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Monaco editor */}
|
||||||
<div
|
<div
|
||||||
ref={editorContainerRef}
|
ref={editorContainerRef}
|
||||||
className="grow w-full overflow-hidden rounded-md relative"
|
className="grow w-full overflow-hidden rounded-md relative"
|
||||||
@ -660,7 +603,8 @@ export default function CodeEditor({
|
|||||||
No file selected.
|
No file selected.
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : clerk.loaded ? (
|
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||||
|
clerk.loaded ? (
|
||||||
<>
|
<>
|
||||||
{provider ? <Cursors yProvider={provider} /> : null}
|
{provider ? <Cursors yProvider={provider} /> : null}
|
||||||
<Editor
|
<Editor
|
||||||
@ -719,26 +663,7 @@ export default function CodeEditor({
|
|||||||
minSize={20}
|
minSize={20}
|
||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
>
|
>
|
||||||
<div className="h-10 select-none w-full flex gap-2">
|
<PreviewWindow />
|
||||||
<div className="h-8 rounded-md px-3 text-xs bg-secondary flex items-center w-full justify-between">
|
|
||||||
Preview
|
|
||||||
<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">
|
|
||||||
<TerminalSquare 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">
|
|
||||||
<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">
|
|
||||||
<ChevronRight 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">
|
|
||||||
<RotateCw className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full grow rounded-md bg-foreground"></div>
|
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
@ -747,74 +672,18 @@ export default function CodeEditor({
|
|||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
>
|
>
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
<>
|
<Terminals
|
||||||
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
|
terminals={terminals}
|
||||||
{terminals.map((term) => (
|
setTerminals={setTerminals}
|
||||||
<Tab
|
activeTerminalId={activeTerminalId}
|
||||||
key={term.id}
|
setActiveTerminalId={setActiveTerminalId}
|
||||||
onClick={() => setActiveTerminalId(term.id)}
|
socket={socket}
|
||||||
onClose={() => closeTerminal(term)}
|
activeTerminal={activeTerminal}
|
||||||
selected={activeTerminalId === term.id}
|
creatingTerminal={creatingTerminal}
|
||||||
>
|
setCreatingTerminal={setCreatingTerminal}
|
||||||
<SquareTerminal className="w-4 h-4 mr-2" />
|
closingTerminal={closingTerminal}
|
||||||
Shell
|
setClosingTerminal={setClosingTerminal}
|
||||||
</Tab>
|
/>
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
disabled={creatingTerminal}
|
|
||||||
onClick={() => {
|
|
||||||
if (terminals.length >= 4) {
|
|
||||||
toast.error(
|
|
||||||
"You reached the maximum # of terminals."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createTerminal();
|
|
||||||
}}
|
|
||||||
size="smIcon"
|
|
||||||
variant={"secondary"}
|
|
||||||
className={`font-normal shrink-0 select-none text-muted-foreground`}
|
|
||||||
>
|
|
||||||
{creatingTerminal ? (
|
|
||||||
<Loader2 className="animate-spin w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{socket && activeTerminal ? (
|
|
||||||
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
|
||||||
{terminals.map((term) => (
|
|
||||||
<EditorTerminal
|
|
||||||
key={term.id}
|
|
||||||
socket={socket}
|
|
||||||
id={term.id}
|
|
||||||
term={term.terminal}
|
|
||||||
setTerm={(t: Terminal) => {
|
|
||||||
// console.log(
|
|
||||||
// "setting terminal",
|
|
||||||
// activeTerminalId,
|
|
||||||
// t.options
|
|
||||||
// );
|
|
||||||
setTerminals((prev) =>
|
|
||||||
prev.map((term) =>
|
|
||||||
term.id === activeTerminalId
|
|
||||||
? { ...term, terminal: t }
|
|
||||||
: term
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
visible={activeTerminalId === term.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
|
||||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
|
||||||
No terminals open.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||||
|
35
frontend/components/editor/preview/index.tsx
Normal file
35
frontend/components/editor/preview/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
RotateCw,
|
||||||
|
TerminalSquare,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export default function PreviewWindow() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="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">
|
||||||
|
Preview
|
||||||
|
<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">
|
||||||
|
<TerminalSquare 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">
|
||||||
|
<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">
|
||||||
|
<ChevronRight 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">
|
||||||
|
<RotateCw className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full grow rounded-md bg-foreground"></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
126
frontend/components/editor/terminals/index.tsx
Normal file
126
frontend/components/editor/terminals/index.tsx
Normal file
@ -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<React.SetStateAction<string>>;
|
||||||
|
socket: Socket;
|
||||||
|
activeTerminal:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
terminal: Terminal | null;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
creatingTerminal: boolean;
|
||||||
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
closingTerminal: string;
|
||||||
|
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="h-10 w-full overflow-auto flex gap-2 shrink-0 tab-scroll">
|
||||||
|
{terminals.map((term) => (
|
||||||
|
<Tab
|
||||||
|
key={term.id}
|
||||||
|
onClick={() => setActiveTerminalId(term.id)}
|
||||||
|
onClose={() =>
|
||||||
|
closeTerminal({
|
||||||
|
term,
|
||||||
|
terminals,
|
||||||
|
setTerminals,
|
||||||
|
setActiveTerminalId,
|
||||||
|
setClosingTerminal,
|
||||||
|
socket,
|
||||||
|
activeTerminalId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
closing={closingTerminal === term.id}
|
||||||
|
selected={activeTerminalId === term.id}
|
||||||
|
>
|
||||||
|
<SquareTerminal className="w-4 h-4 mr-2" />
|
||||||
|
Shell
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
disabled={creatingTerminal}
|
||||||
|
onClick={() => {
|
||||||
|
if (terminals.length >= 4) {
|
||||||
|
toast.error("You reached the maximum # of terminals.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createTerminal({
|
||||||
|
setTerminals,
|
||||||
|
setActiveTerminalId,
|
||||||
|
setCreatingTerminal,
|
||||||
|
socket,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="smIcon"
|
||||||
|
variant={"secondary"}
|
||||||
|
className={`font-normal shrink-0 select-none text-muted-foreground`}
|
||||||
|
>
|
||||||
|
{creatingTerminal ? (
|
||||||
|
<Loader2 className="animate-spin w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{socket && activeTerminal ? (
|
||||||
|
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
||||||
|
{terminals.map((term) => (
|
||||||
|
<EditorTerminal
|
||||||
|
key={term.id}
|
||||||
|
socket={socket}
|
||||||
|
id={term.id}
|
||||||
|
term={term.terminal}
|
||||||
|
setTerm={(t: Terminal) => {
|
||||||
|
setTerminals((prev) =>
|
||||||
|
prev.map((term) =>
|
||||||
|
term.id === activeTerminalId
|
||||||
|
? { ...term, terminal: t }
|
||||||
|
: term
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
visible={activeTerminalId === term.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||||
|
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||||
|
No terminals open.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { X } from "lucide-react"
|
import { Loader2, X } from "lucide-react";
|
||||||
import { Button } from "./button"
|
import { Button } from "./button";
|
||||||
import { MouseEvent, MouseEventHandler, useEffect } from "react"
|
import { MouseEvent, MouseEventHandler, useEffect } from "react";
|
||||||
|
|
||||||
export default function Tab({
|
export default function Tab({
|
||||||
children,
|
children,
|
||||||
@ -10,12 +10,14 @@ export default function Tab({
|
|||||||
selected = false,
|
selected = false,
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
|
closing = false,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
saved?: boolean
|
saved?: boolean;
|
||||||
selected?: boolean
|
selected?: boolean;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
onClose?: () => void
|
onClose?: () => void;
|
||||||
|
closing?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -31,17 +33,19 @@ export default function Tab({
|
|||||||
{children}
|
{children}
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={
|
||||||
onClose
|
onClose && !closing
|
||||||
? (e) => {
|
? (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
onClose()
|
onClose();
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
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"
|
||||||
>
|
>
|
||||||
{saved ? (
|
{closing ? (
|
||||||
|
<Loader2 className="animate-spin w-3 h-3" />
|
||||||
|
) : saved ? (
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -51,5 +55,5 @@ export default function Tab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
91
frontend/lib/terminal.ts
Normal file
91
frontend/lib/terminal.ts
Normal file
@ -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<React.SetStateAction<{
|
||||||
|
id: string;
|
||||||
|
terminal: Terminal | null;
|
||||||
|
}[]>>;
|
||||||
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
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<React.SetStateAction<{
|
||||||
|
id: string;
|
||||||
|
terminal: Terminal | null
|
||||||
|
}[]>>
|
||||||
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user