diff --git a/Frontend/README.md b/Frontend/README.md
index 10f1307..03e7d9f 100644
--- a/Frontend/README.md
+++ b/Frontend/README.md
@@ -1,13 +1,24 @@
-# VSCode Clone with React and Vite
+# VS Code Clone Project
-This project is a VSCode-like code editor built with React and Vite. It features a customizable UI with an activity bar, sidebar, editor area, panel, and status bar, mimicking the look and feel of Visual Studio Code.
+## Authors
+- Arnab Bhowmik
+- Ishika Bhoyar
-## Features
+## Description
+This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
-- **Activity Bar**: Switch between different views like Explorer, Search, Source Control, etc.
-- **Sidebar**: Displays file explorer, search results, and source control information.
-- **Editor Area**: Code editor with syntax highlighting and multiple tabs.
-- **Panel**: Terminal, Problems, and Output views.
-- **Status Bar**: Displays status information and provides quick actions.
+## Frontend Functionalities
+- Built with React and Monaco Editor.
+- File tree navigation for managing files and folders.
+- Tab management for opening multiple files simultaneously.
+- Code editing with syntax highlighting and language support.
+- Terminal panel for running code and viewing output.
+- Persistent file structure and content using localStorage.
-## Project Structure
+## Backend Functionalities
+- Built with Go and Docker for secure code execution.
+- Supports multiple programming languages (Python, Java, C/C++).
+- Executes code in isolated Docker containers with resource limits.
+- RESTful API for submitting code, checking status, and retrieving results.
+- Job queue system for managing concurrent executions.
+- Enforces timeouts and resource limits for security and performance.
diff --git a/Frontend/src/components/EditorArea.jsx b/Frontend/src/components/EditorArea.jsx
index 89b954b..caf0b2c 100644
--- a/Frontend/src/components/EditorArea.jsx
+++ b/Frontend/src/components/EditorArea.jsx
@@ -9,6 +9,32 @@ import {
import Sidebar from "./Sidebar";
import Panel from "./Panel"; // Import Panel component
+// Add this function to map file extensions to language identifiers
+const getLanguageFromExtension = (extension) => {
+ const extensionMap = {
+ 'js': 'javascript',
+ 'jsx': 'javascript',
+ 'ts': 'typescript',
+ 'tsx': 'typescript',
+ 'py': 'python',
+ 'java': 'java',
+ 'c': 'c',
+ 'cpp': 'cpp',
+ 'h': 'c',
+ 'hpp': 'cpp',
+ 'cs': 'csharp',
+ 'go': 'go',
+ 'rb': 'ruby',
+ 'php': 'php',
+ 'html': 'html',
+ 'css': 'css',
+ 'json': 'json',
+ 'md': 'markdown'
+ };
+
+ return extensionMap[extension] || 'text';
+};
+
const EditorArea = ({
sidebarVisible = true,
activeView = "explorer",
@@ -62,8 +88,8 @@ const EditorArea = ({
// Add a new state for user input
const [userInput, setUserInput] = useState("");
- // Add a new state for waiting for input
- const [waitingForInput, setWaitingForInput] = useState(false);
+ // Add socket state to track the connection
+ const [activeSocket, setActiveSocket] = useState(null);
// Focus the input when new file modal opens
useEffect(() => {
@@ -132,6 +158,41 @@ const EditorArea = ({
}
}, [panelVisible]);
+ // Add this useEffect for cleanup
+ useEffect(() => {
+ // Cleanup function to close socket when component unmounts
+ return () => {
+ if (activeSocket) {
+ activeSocket.close();
+ }
+ };
+ }, []);
+
+ // Add interval to poll execution status
+ useEffect(() => {
+ const checkInterval = setInterval(() => {
+ // Poll execution status
+ if (activeSocket && activeRunningFile) {
+ // Check if socket is still connected
+ if (activeSocket.readyState !== WebSocket.OPEN) {
+ console.warn("Socket not in OPEN state:", activeSocket.readyState);
+ setTerminalOutput(prev => [...prev, {
+ type: 'warning',
+ content: `Terminal connection lost, attempting to reconnect...`
+ }]);
+ // Could implement reconnection logic here
+ }
+ }
+ }, 5000);
+
+ // Clean up interval when component unmounts
+ return () => {
+ if (checkInterval) {
+ clearInterval(checkInterval);
+ }
+ };
+ }, [activeSocket, activeRunningFile]);
+
const handleEditorDidMount = (editor) => {
editorRef.current = editor;
};
@@ -479,21 +540,31 @@ const EditorArea = ({
case "README.md":
return `# VS Code Clone Project
-## Overview
-This is a simple VS Code clone built with React and Monaco Editor.
+## Authors
+- Arnab Bhowmik
+- Ishika Bhoyar
-## Features
-- File tree navigation
-- Tab management
-- Code editing with Monaco Editor
-- Syntax highlighting
+## Description
+This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
+
+## Frontend Functionalities
+- Built with React and Monaco Editor.
+- File tree navigation for managing files and folders.
+- Tab management for opening multiple files simultaneously.
+- Code editing with syntax highlighting and language support.
+- Terminal panel for running code and viewing output.
+- Persistent file structure and content using localStorage.
+
+## Backend Functionalities
+- Built with Go and Docker for secure code execution.
+- Supports multiple programming languages (Python, Java, C/C++).
+- Executes code in isolated Docker containers with resource limits.
+- RESTful API for submitting code, checking status, and retrieving results.
+- Job queue system for managing concurrent executions.
+- Enforces timeouts and resource limits for security and performance.
+`;
-## Getting Started
-1. Create a new file using the + button in the sidebar
-2. Edit your code in the editor
-3. Save changes using the save button
-Happy coding!`;
default:
return "";
}
@@ -507,7 +578,7 @@ Happy coding!`;
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
};
- // Modify the handleRunCode function to prompt for input first
+ // Update the handleRunCode function
const handleRunCode = async () => {
if (!activeFile) return;
@@ -517,49 +588,36 @@ Happy coding!`;
setPanelVisible(true);
}
- // Set state to waiting for input
- setWaitingForInput(true);
- setActiveRunningFile(activeFile.id);
-
// Clear previous output and add new command
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
const language = getLanguageFromExtension(fileExtension);
const newOutput = [
{ type: 'command', content: `$ run ${activeFile.id}` },
- { type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' }
+ { type: 'output', content: 'Submitting code...' }
];
setTerminalOutput(newOutput);
- };
-
- // Add a new function to handle input submission
- const handleInputSubmit = async () => {
- if (!activeFile || !waitingForInput) return;
- // Set running state
- setIsRunning(true);
- setWaitingForInput(false);
-
- // Add message that we're running with the input
- setTerminalOutput(prev => [
- ...prev,
- { type: 'output', content: userInput ? `Using input: "${userInput}"` : 'Running without input...' }
- ]);
-
- // Use API URL from environment variable
- const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
-
try {
- // Now make the API call with the input that was entered
+ // Close any existing socket
+ if (activeSocket) {
+ activeSocket.close();
+ setActiveSocket(null);
+ }
+
+ // Use API URL from environment variable
+ const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
+
+ // Submit the code to get an execution ID
const submitResponse = await fetch(`${apiUrl}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
- language: getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase()),
+ language: language,
code: activeFile.content,
- input: userInput
+ input: "" // Explicitly passing empty input, no user input handling
}),
});
@@ -570,87 +628,189 @@ Happy coding!`;
const { id } = await submitResponse.json();
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
- // Step 2: Poll for status until completed or failed
- let status = 'pending';
- while (status !== 'completed' && status !== 'failed') {
- // Add a small delay between polls
- await new Promise(resolve => setTimeout(resolve, 1000));
+ // Set active running file
+ setActiveRunningFile(activeFile.id);
+
+ // Connect to WebSocket with the execution ID
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsBaseUrl = apiUrl.replace(/^https?:\/\//, '');
+ const wsUrl = `${wsProtocol}//${wsBaseUrl}/ws/terminal?id=${id}`;
+
+ setTerminalOutput(prev => [...prev, { type: 'output', content: `Connecting to: ${wsUrl}` }]);
+
+ // Create a new WebSocket
+ const newSocket = new WebSocket(wsUrl);
+
+ // Set up event handlers
+ newSocket.onopen = () => {
+ console.log("WebSocket connected");
+ setTerminalOutput(prev => [...prev, { type: 'output', content: 'Connected to execution terminal' }]);
+ setIsRunning(true);
+ };
+
+ newSocket.onmessage = (event) => {
+ console.log("WebSocket message received:", event.data);
+ setTerminalOutput(prev => [...prev, { type: 'output', content: event.data }]);
- const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
- if (!statusResponse.ok) {
- throw new Error(`Status check failed: ${statusResponse.status}`);
+ // Check if this message is likely asking for input (prompt detection)
+ const isPrompt =
+ event.data.includes("input") ||
+ event.data.includes("?") ||
+ event.data.endsWith(":") ||
+ event.data.endsWith("> ");
+
+ if (isPrompt) {
+ console.log("Input prompt detected, focusing terminal");
+ // Force terminal to focus after a prompt is detected
+ setTimeout(() => {
+ document.querySelector('.panel-terminal')?.focus();
+ }, 100);
}
+ };
+
+ // Add polling for job status
+ let statusCheckInterval;
+ if (id) {
+ // Start polling the status endpoint every 2 seconds
+ statusCheckInterval = setInterval(async () => {
+ try {
+ const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
+ if (statusResponse.ok) {
+ const statusData = await statusResponse.json();
+
+ // If the process is completed or failed, stop polling and update UI
+ if (statusData.status === 'completed' || statusData.status === 'failed') {
+ clearInterval(statusCheckInterval);
+ console.log("Process status:", statusData.status);
+
+ // Update the UI to show process is no longer running
+ setIsRunning(false);
+
+ // Display the final result if WebSocket didn't capture it
+ if (statusData.output && statusData.output.length > 0) {
+ setTerminalOutput(prev => {
+ // Check if the output is already in the terminal
+ const lastOutput = prev[prev.length - 1]?.content || "";
+ if (!lastOutput.includes(statusData.output)) {
+ return [...prev, {
+ type: 'output',
+ content: `\n[System] Final output:\n${statusData.output}`
+ }];
+ }
+ return prev;
+ });
+ }
+
+ // Close socket if it's still open
+ if (newSocket && newSocket.readyState === WebSocket.OPEN) {
+ newSocket.close();
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Status check error:", error);
+ }
+ }, 2000);
- const statusData = await statusResponse.json();
- status = statusData.status;
-
- // Update terminal with status (for any status type)
- setTerminalOutput(prev => {
- // Update the last status message or add a new one
- const hasStatus = prev.some(line => line.content.includes('Status:'));
- if (hasStatus) {
- return prev.map(line =>
- line.content.includes('Status:')
- ? { ...line, content: `Status: ${status}` }
- : line
- );
- } else {
- return [...prev, { type: 'output', content: `Status: ${status}` }];
+ // Clean up interval when component unmounts or when socket closes
+ newSocket.addEventListener('close', () => {
+ if (statusCheckInterval) {
+ clearInterval(statusCheckInterval);
}
});
}
- // Get the result for both completed and failed status
- const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
- if (!resultResponse.ok) {
- throw new Error(`Result fetch failed: ${resultResponse.status}`);
- }
+ newSocket.onclose = (event) => {
+ console.log("WebSocket closed:", event);
+ setIsRunning(false);
+ setActiveSocket(null);
+
+ const reason = event.reason ? `: ${event.reason}` : '';
+ const code = event.code ? ` (code: ${event.code})` : '';
+
+ setTerminalOutput(prev => [...prev, {
+ type: 'warning',
+ content: `Terminal connection closed${reason}${code}`
+ }]);
+
+ // Clean up interval
+ if (statusCheckInterval) {
+ clearInterval(statusCheckInterval);
+ }
+ };
- const { output } = await resultResponse.json();
+ newSocket.onerror = (event) => {
+ console.error("WebSocket error:", event);
+ setTerminalOutput(prev => [...prev, {
+ type: 'warning',
+ content: `WebSocket error occurred`
+ }]);
+ };
- // Format and display output
- const outputLines = output.split('\n').map(line => ({
- type: status === 'failed' ? 'warning' : 'output',
- content: line
- }));
-
- setTerminalOutput(prev => [
- ...prev,
- {
- type: status === 'failed' ? 'warning' : 'output',
- content: status === 'failed'
- ? '------- EXECUTION FAILED -------'
- : '------- EXECUTION RESULT -------'
- },
- ...outputLines
- ]);
-
- if (status === 'failed') {
- console.error('Code execution failed:', output);
- }
+ // Set the active socket after all handlers are defined
+ setActiveSocket(newSocket);
} catch (error) {
+ console.error("Run code error:", error);
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
- } finally {
- // Set running state to false
setIsRunning(false);
+
+ // Also add cleanup in the error handler
+ if (statusCheckInterval) {
+ clearInterval(statusCheckInterval);
+ }
}
};
-
- // Helper function to convert file extension to language identifier for API
- const getLanguageFromExtension = (extension) => {
- const languageMap = {
- 'java': 'java',
- 'c': 'c',
- 'cpp': 'cpp',
- 'py': 'python',
- 'js': 'javascript',
- 'jsx': 'javascript',
- 'ts': 'typescript',
- 'tsx': 'typescript'
- };
+
+ // Update handleInputSubmit to ensure the input is sent properly
+ const handleInputSubmit = (input) => {
+ // Use the direct input parameter instead of relying on userInput state
+ const textToSend = input || userInput;
- return languageMap[extension] || extension;
+ console.log("Input submit called, active socket state:",
+ activeSocket ? activeSocket.readyState : "no socket",
+ "input:", textToSend);
+
+ if (!activeSocket) {
+ console.warn("Cannot send input: No active socket");
+ setTerminalOutput(prev => [...prev, {
+ type: 'warning',
+ content: `Cannot send input: No active connection`
+ }]);
+ return;
+ }
+
+ if (activeSocket.readyState !== WebSocket.OPEN) {
+ console.warn("Socket not in OPEN state:", activeSocket.readyState);
+ setTerminalOutput(prev => [...prev, {
+ type: 'warning',
+ content: `Cannot send input: Connection not open (state: ${activeSocket.readyState})`
+ }]);
+ return;
+ }
+
+ if (!textToSend.trim()) {
+ console.warn("Cannot send empty input");
+ return;
+ }
+
+ try {
+ // Add the input to the terminal display
+ setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${textToSend}` }]);
+
+ // Send the input via WebSocket with a newline character
+ console.log("Sending input:", textToSend);
+ activeSocket.send(textToSend + "\n");
+
+ // Clear the input field
+ setUserInput("");
+ } catch (error) {
+ console.error("Error sending input:", error);
+ setTerminalOutput(prev => [...prev, {
+ type: 'warning',
+ content: `Error sending input: ${error.message}`
+ }]);
+ }
};
// Update this function to also update parent state
@@ -834,18 +994,17 @@ Happy coding!`;
document.addEventListener("mouseup", onMouseUp);
}}
/>
-
+
>
)}
diff --git a/Frontend/src/components/Navbar.jsx b/Frontend/src/components/Navbar.jsx
deleted file mode 100644
index 3ab701c..0000000
--- a/Frontend/src/components/Navbar.jsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import React from "react"
-"use client"
-
-import Link from "next/link"
-import { ChevronDown } from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import {
- NavigationMenu,
- NavigationMenuContent,
- NavigationMenuItem,
- NavigationMenuLink,
- NavigationMenuList,
- NavigationMenuTrigger,
-} from "@/components/ui/navigation-menu"
-
-export function Navbar() {
- return (
-
- )
-}
-
-export default Navbar
\ No newline at end of file
diff --git a/Frontend/src/components/Panel.jsx b/Frontend/src/components/Panel.jsx
index 29c7b18..a76a640 100644
--- a/Frontend/src/components/Panel.jsx
+++ b/Frontend/src/components/Panel.jsx
@@ -1,8 +1,7 @@
-import React from "react";
-import { useState, useEffect } from "react";
-import { X } from "lucide-react";
+import React, { useState, useEffect, useRef } from "react";
+import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
-const Panel = ({
+const Panel = ({
height,
terminalOutput = [],
isRunning = false,
@@ -12,85 +11,135 @@ const Panel = ({
onClose,
userInput = "",
onUserInputChange,
- onInputSubmit
+ onInputSubmit,
}) => {
const [activeTab, setActiveTab] = useState(initialTab);
+ const terminalRef = useRef(null);
+ const [inputBuffer, setInputBuffer] = useState("");
- // Set active tab when initialTab changes
+ // Update active tab when initialTab changes
useEffect(() => {
setActiveTab(initialTab);
}, [initialTab]);
- const renderTerminal = () => {
- return (
-
- {terminalOutput.length > 0 ? (
- // Render output from EditorArea when available
- <>
- {terminalOutput.map((line, index) => (
-
- {line.type === 'command' ?
$ : ''} {line.content}
+ // Auto-scroll terminal to the bottom when content changes
+ useEffect(() => {
+ if (terminalRef.current) {
+ terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
+ }
+ }, [terminalOutput]);
+
+ // Handle keyboard input for the terminal
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (!isRunning) return;
+
+ if (e.key === "Enter") {
+ if (inputBuffer.trim() && onInputSubmit) {
+ e.preventDefault();
+ // Update parent's userInput state directly and call submit in the same function
+ // instead of using setTimeout which creates a race condition
+ onUserInputChange(inputBuffer);
+ onInputSubmit(inputBuffer); // Pass inputBuffer directly to avoid race condition
+ setInputBuffer("");
+ }
+ } else if (e.key === "Backspace") {
+ setInputBuffer((prev) => prev.slice(0, -1));
+ } else if (e.key.length === 1) {
+ setInputBuffer((prev) => prev + e.key);
+ }
+ };
+
+ const terminalElement = terminalRef.current;
+ terminalElement?.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ terminalElement?.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]);
+
+ // Render the terminal tab
+ const renderTerminal = () => (
+
terminalRef.current?.focus()} // Focus when clicked
+ >
+ {terminalOutput.length > 0 ? (
+ <>
+ {terminalOutput.map((line, index) => {
+ const typeClass =
+ line.type === "warning"
+ ? "terminal-warning"
+ : line.type === "error"
+ ? "terminal-error"
+ : "terminal-output";
+
+ return (
+
+ {line.timestamp && (
+ {line.timestamp}
+ )}
+ {line.type === "command" && $}
+ {line.content}
- ))}
- {waitingForInput && (
-
- Input:
- onUserInputChange && onUserInputChange(e.target.value)}
- placeholder="Enter input for your program here..."
- onKeyDown={(e) => {
- if (e.key === 'Enter' && onInputSubmit) {
- onInputSubmit();
- }
- }}
- autoFocus
- />
-
- )}
- >
- ) : (
- // Default terminal content when no output
- <>
-
-
$ npm start
+ );
+ })}
+
+ {isRunning && (
+
+ $ {inputBuffer}
+
-
Starting the development server...
-
Compiled successfully!
-
You can now view vscode-clone in the browser.
-
Local: http://localhost:3000
-
On Your Network: http://192.168.1.5:3000
-
- $
-
- >
- )}
-
- );
- };
+ )}
+ >
+ ) : (
+
+ $
+
+
+ )}
+
+ );
- const renderProblems = () => {
- return (
-
-
No problems have been detected in the workspace.
-
- );
- };
+ // Render other tabs
+ const renderProblems = () => (
+
+
No problems have been detected in the workspace.
+
+ );
- const renderOutput = () => {
- return (
-
-
[Extension Host] Extension host started.
-
[Language Server] Language server started.
- {activeRunningFile && (
-
[Running] {activeRunningFile}
- )}
-
- );
- };
+ const renderOutput = () => (
+
+
[Extension Host] Extension host started.
+
[Language Server] Language server started.
+ {activeRunningFile && (
+
[Running] {activeRunningFile}
+ )}
+
+ );
+ const renderDebugConsole = () => (
+
+
Debug session not yet started.
+
Press F5 to start debugging.
+
+ );
+
+ const renderPorts = () => (
+
+
No forwarded ports detected.
+
+ );
+
+ const renderComments = () => (
+
+
No comments have been added to this workspace.
+
+ );
+
+ // Get content for the active tab
const getTabContent = () => {
switch (activeTab) {
case "terminal":
@@ -99,6 +148,12 @@ const Panel = ({
return renderProblems();
case "output":
return renderOutput();
+ case "debug":
+ return renderDebugConsole();
+ case "ports":
+ return renderPorts();
+ case "comments":
+ return renderComments();
default:
return
Unknown tab
;
}
@@ -107,76 +162,29 @@ const Panel = ({
return (
-
setActiveTab("problems")}
- >
-
-
-
- Problems
-
-
setActiveTab("output")}>
-
-
-
- Output
-
-
setActiveTab("terminal")}
- >
-
-
-
-
Terminal
-
+ {["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
+
setActiveTab(tab)}
+ >
+ {tab.toUpperCase()}
+
+ ))}
- {/* Add close button */}
+ {/*
+
+
*/}
@@ -186,5 +194,4 @@ const Panel = ({
);
};
-export default Panel;
-
+export default Panel;
\ No newline at end of file
diff --git a/Frontend/src/components/Sidebar.jsx b/Frontend/src/components/Sidebar.jsx
index 1f26a69..c0002d1 100644
--- a/Frontend/src/components/Sidebar.jsx
+++ b/Frontend/src/components/Sidebar.jsx
@@ -31,10 +31,10 @@ const Sidebar = ({
const renderExplorer = () => {
const renderFileTree = (structure, path = "") => {
if (!structure) return null;
-
+
return Object.entries(structure).map(([name, item]) => {
const currentPath = path ? `${path}/${name}` : name;
-
+
if (item.type === "folder") {
const isExpanded = expandedFolders[currentPath];
return (
@@ -75,21 +75,6 @@ const Sidebar = ({
)}
-
-
-
{name}
{isExpanded && (
@@ -137,7 +122,7 @@ const Sidebar = ({
}
});
};
-
+
return (
@@ -184,10 +169,38 @@ const Sidebar = ({
);
};
-
const getFileIcon = (fileName) => {
const extension = fileName.split('.').pop().toLowerCase();
-
+
+ if (fileName.toLowerCase() === 'readme.md') {
+ return (
+
+ );
+ }
+
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
return (