start multi terminal logic
This commit is contained in:
parent
4e42555887
commit
91feeffc5a
@ -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]]
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user