start multi terminal logic

This commit is contained in:
Ishaan Dey 2024-05-06 23:34:45 -07:00
parent 4e42555887
commit 91feeffc5a
5 changed files with 139 additions and 39 deletions

View File

@ -222,7 +222,7 @@ io.on("connection", async (socket) => {
} }
}) })
socket.on("createTerminal", ({ id }: { id: string }) => { socket.on("createTerminal", (id: string, callback) => {
console.log("creating terminal", id) console.log("creating terminal", id)
if (terminals[id]) { if (terminals[id]) {
console.log("Terminal already exists.") console.log("Terminal already exists.")
@ -243,6 +243,7 @@ io.on("connection", async (socket) => {
console.log("ondata") console.log("ondata")
socket.emit("terminalResponse", { socket.emit("terminalResponse", {
// data: Buffer.from(data, "utf-8").toString("base64"), // data: Buffer.from(data, "utf-8").toString("base64"),
id,
data, data,
}) })
}) })
@ -256,6 +257,8 @@ io.on("connection", async (socket) => {
onData, onData,
onExit, onExit,
} }
callback(true)
}) })
socket.on("terminalData", (id: string, data: string) => { socket.on("terminalData", (id: string, data: string) => {
@ -271,6 +274,19 @@ io.on("connection", async (socket) => {
} }
}) })
socket.on("closeTerminal", (id: string, callback) => {
if (!terminals[id]) {
console.log("tried to close, but term does not exist. terminals", terminals)
return
}
terminals[id].onData.dispose()
terminals[id].onExit.dispose()
delete terminals[id]
callback(true)
})
socket.on( socket.on(
"generateCode", "generateCode",
async ( async (
@ -311,7 +327,6 @@ io.on("connection", async (socket) => {
if (data.isOwner) { if (data.isOwner) {
Object.entries(terminals).forEach((t) => { Object.entries(terminals).forEach((t) => {
const { terminal, onData, onExit } = t[1] const { terminal, onData, onExit } = t[1]
if (os.platform() !== "win32") terminal.kill()
onData.dispose() onData.dispose()
onExit.dispose() onExit.dispose()
delete terminals[t[0]] delete terminals[t[0]]

View File

@ -6,6 +6,7 @@ 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";
@ -52,9 +53,7 @@ export default function CodeEditor({
const [tabs, setTabs] = useState<TTab[]>([]); const [tabs, setTabs] = useState<TTab[]>([]);
const [editorLanguage, setEditorLanguage] = useState("plaintext"); const [editorLanguage, setEditorLanguage] = useState("plaintext");
const [activeFileId, setActiveFileId] = useState<string>(""); const [activeFileId, setActiveFileId] = useState<string>("");
const [activeFileContent, setActiveFileContent] = useState<string | null>( const [activeFileContent, setActiveFileContent] = useState("");
null
);
const [cursorLine, setCursorLine] = useState(0); const [cursorLine, setCursorLine] = useState(0);
const [generate, setGenerate] = useState<{ const [generate, setGenerate] = useState<{
show: boolean; show: boolean;
@ -74,12 +73,16 @@ export default function CodeEditor({
terminal: Terminal | null; terminal: Terminal | null;
}[] }[]
>([]); >([]);
const [activeTerminalId, setActiveTerminalId] = useState("");
const [creatingTerminal, setCreatingTerminal] = useState(false);
const [provider, setProvider] = useState<TypedLiveblocksProvider>(); const [provider, setProvider] = useState<TypedLiveblocksProvider>();
const [ai, setAi] = useState(false); const [ai, setAi] = useState(false);
const isOwner = sandboxData.userId === userData.id; const isOwner = sandboxData.userId === userData.id;
const clerk = useClerk(); const clerk = useClerk();
const room = useRoom(); const room = useRoom();
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
console.log("activeTerminal", activeTerminal ? activeTerminal.id : "none");
// const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null) // const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const [editorRef, setEditorRef] = const [editorRef, setEditorRef] =
@ -354,9 +357,8 @@ export default function CodeEditor({
useEffect(() => { useEffect(() => {
const onConnect = () => { const onConnect = () => {
console.log("connected"); console.log("connected");
setTimeout(() => {
socket.emit("createTerminal", { id: "testId" }); createTerminal();
}, 1000);
}; };
const onDisconnect = () => {}; const onDisconnect = () => {};
@ -395,9 +397,17 @@ export default function CodeEditor({
// Helper functions: // Helper functions:
const createTerminal = () => { const createTerminal = () => {
const id = "testId"; setCreatingTerminal(true);
const id = createId();
socket.emit("createTerminal", { id }); setActiveTerminalId(id);
setTimeout(() => {
socket.emit("createTerminal", id, (res: boolean) => {
if (res) {
setTerminals((prev) => [...prev, { id, terminal: null }]);
}
});
}, 1000);
setCreatingTerminal(false);
}; };
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
@ -446,6 +456,36 @@ 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;
socket.emit("closeTerminal", term.id, (res: boolean) => {
if (res) {
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);
}
}
}
});
};
const handleRename = ( const handleRename = (
id: string, id: string,
newName: string, newName: string,
@ -624,7 +664,7 @@ export default function CodeEditor({
fontFamily: "var(--font-geist-mono)", fontFamily: "var(--font-geist-mono)",
}} }}
theme="vs-dark" theme="vs-dark"
value={activeFileContent ?? ""} value={activeFileContent}
/> />
</> </>
) : ( ) : (
@ -673,29 +713,56 @@ export default function CodeEditor({
{isOwner ? ( {isOwner ? (
<> <>
<div className="h-10 w-full flex gap-2 shrink-0"> <div className="h-10 w-full flex gap-2 shrink-0">
<Tab selected> {terminals.map((term) => (
<Tab
key={term.id}
onClick={() => setActiveTerminalId(term.id)}
onClose={() => closeTerminal(term)}
selected={activeTerminalId === term.id}
>
<SquareTerminal className="w-4 h-4 mr-2" /> <SquareTerminal className="w-4 h-4 mr-2" />
Shell Shell
</Tab> </Tab>
))}
<Button <Button
disabled={creatingTerminal}
onClick={() => { onClick={() => {
if (terminals.length >= 4) { if (terminals.length >= 4) {
toast.error( toast.error(
"You reached the maximum # of terminals." "You reached the maximum # of terminals."
); );
return;
} }
createTerminal();
}} }}
size="smIcon" size="smIcon"
variant={"secondary"} variant={"secondary"}
className={`font-normal select-none text-muted-foreground`} className={`font-normal select-none text-muted-foreground`}
> >
{creatingTerminal ? (
<Loader2 className="animate-spin w-4 h-4" />
) : (
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
)}
</Button> </Button>
</div> </div>
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary"> <div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
{/* {socket ? <EditorTerminal socket={socket} term={ {socket && activeTerminal ? (
<EditorTerminal
} /> : null} */} socket={socket}
id={activeTerminal.id}
term={activeTerminal.terminal}
setTerm={(t: Terminal) => {
setTerminals((prev) =>
prev.map((term) =>
term.id === activeTerminal.id
? { ...term, terminal: t }
: term
)
);
}}
/>
) : null}
</div> </div>
</> </>
) : ( ) : (

View File

@ -10,10 +10,12 @@ import { Loader2 } from "lucide-react";
export default function EditorTerminal({ export default function EditorTerminal({
socket, socket,
id,
term, term,
setTerm, setTerm,
}: { }: {
socket: Socket; socket: Socket;
id: string;
term: Terminal | null; term: Terminal | null;
setTerm: (term: Terminal) => void; setTerm: (term: Terminal) => void;
}) { }) {
@ -41,14 +43,7 @@ export default function EditorTerminal({
useEffect(() => { useEffect(() => {
if (!term) return; if (!term) return;
// const onTerminalResponse = (response: { data: string }) => {
// const res = response.data;
// term.write(res);
// };
if (terminalRef.current) { if (terminalRef.current) {
// socket.on("terminalResponse", onTerminalResponse);
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
term.open(terminalRef.current); term.open(terminalRef.current);
@ -57,7 +52,7 @@ export default function EditorTerminal({
} }
const disposable = term.onData((data) => { const disposable = term.onData((data) => {
console.log("sending data", data); console.log("sending data", data);
socket.emit("terminalData", "testId", data); socket.emit("terminalData", id, data);
}); });
// socket.emit("terminalData", "\n"); // socket.emit("terminalData", "\n");
@ -68,6 +63,7 @@ export default function EditorTerminal({
}, [term, terminalRef.current]); }, [term, terminalRef.current]);
return ( return (
<>
<div ref={terminalRef} className="w-full h-full text-left"> <div ref={terminalRef} className="w-full h-full text-left">
{term === null ? ( {term === null ? (
<div className="flex items-center text-muted-foreground p-2"> <div className="flex items-center text-muted-foreground p-2">
@ -76,5 +72,6 @@ export default function EditorTerminal({
</div> </div>
) : null} ) : null}
</div> </div>
</>
); );
} }

View File

@ -16,6 +16,7 @@
"@liveblocks/react": "^1.12.0", "@liveblocks/react": "^1.12.0",
"@liveblocks/yjs": "^1.12.0", "@liveblocks/yjs": "^1.12.0",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
@ -609,6 +610,17 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@noble/hashes": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -641,6 +653,14 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@peculiar/asn1-schema": { "node_modules/@peculiar/asn1-schema": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",

View File

@ -17,6 +17,7 @@
"@liveblocks/react": "^1.12.0", "@liveblocks/react": "^1.12.0",
"@liveblocks/yjs": "^1.12.0", "@liveblocks/yjs": "^1.12.0",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",