import React from "react"; import { useState, useRef, useEffect } from "react"; import Editor from "@monaco-editor/react"; import { X, Plus, Save, FileCode, FileText, Folder, ChevronDown, ChevronRight, File, FilePlus, FolderPlus, Trash2, Edit, MoreHorizontal, Play, Terminal, Loader } from "lucide-react"; 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", panelVisible, setPanelVisible }) => { // Store files with their content in state - start with just README.md const [files, setFiles] = useState([ { id: "README.md", language: "markdown", content: getDefaultCode("README.md") }, ]); const [activeTab, setActiveTab] = useState(files[0]?.id || ""); const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false); const [newFileName, setNewFileName] = useState(""); const [newFileType, setNewFileType] = useState("javascript"); const [unsavedChanges, setUnsavedChanges] = useState({}); // Sidebar state - now receives visibility from props const [sidebarWidth, setSidebarWidth] = useState(250); const [expandedFolders, setExpandedFolders] = useState({}); const [fileStructure, setFileStructure] = useState({ 'src': { type: 'folder', children: {} }, 'README.md': { type: 'file', language: 'markdown', id: 'README.md' } }); // Context menu state const [showContextMenu, setShowContextMenu] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const [contextMenuTarget, setContextMenuTarget] = useState(null); const editorRef = useRef(null); const newFileInputRef = useRef(null); const renameInputRef = useRef(null); const [isRenaming, setIsRenaming] = useState(false); const [renamePath, setRenamePath] = useState(''); const [renameValue, setRenameValue] = useState(''); // Replace terminal states with panel states const [isRunning, setIsRunning] = useState(false); const [showPanel, setShowPanel] = useState(false); const [panelHeight, setPanelHeight] = useState(200); const [terminalOutput, setTerminalOutput] = useState([]); const [activeRunningFile, setActiveRunningFile] = useState(null); // Add a new state for user input const [userInput, setUserInput] = useState(""); // Add socket state to track the connection const [activeSocket, setActiveSocket] = useState(null); // Focus the input when new file modal opens useEffect(() => { if (isNewFileModalOpen && newFileInputRef.current) { newFileInputRef.current.focus(); } }, [isNewFileModalOpen]); // Focus rename input when renaming useEffect(() => { if (isRenaming && renameInputRef.current) { renameInputRef.current.focus(); } }, [isRenaming]); // Load files and file structure from localStorage on component mount useEffect(() => { const savedFiles = localStorage.getItem("vscode-clone-files"); const savedFileStructure = localStorage.getItem("vscode-clone-structure"); if (savedFiles) { try { const parsedFiles = JSON.parse(savedFiles); setFiles(parsedFiles); if (parsedFiles.length > 0) { setActiveTab(parsedFiles[0].id); } } catch (error) { console.error("Failed to load saved files:", error); } } if (savedFileStructure) { try { const parsedStructure = JSON.parse(savedFileStructure); setFileStructure(parsedStructure); } catch (error) { console.error("Failed to load file structure:", error); } } }, []); // Save files and file structure to localStorage whenever they change useEffect(() => { localStorage.setItem("vscode-clone-files", JSON.stringify(files)); }, [files]); useEffect(() => { localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure)); }, [fileStructure]); // Add this effect to handle editor resize when sidebar changes useEffect(() => { // Force editor to readjust layout when sidebar visibility changes if (editorRef.current) { setTimeout(() => { editorRef.current.layout(); }, 300); // Small delay to allow transition to complete } }, [sidebarVisible]); // Add this effect to sync the panel state with parent component useEffect(() => { if (panelVisible !== undefined) { setShowPanel(panelVisible); } }, [panelVisible]); // Add this useEffect for cleanup useEffect(() => { // Cleanup function to close socket when component unmounts return () => { if (activeSocket) { activeSocket.close(); } }; }, []); const handleEditorDidMount = (editor) => { editorRef.current = editor; }; const handleEditorChange = (value) => { // Mark the current file as having unsaved changes setUnsavedChanges(prev => ({ ...prev, [activeTab]: true })); // Update the file content in the files array setFiles(files.map(file => file.id === activeTab ? { ...file, content: value } : file )); }; const handleCloseTab = (e, fileId) => { e.stopPropagation(); if (unsavedChanges[fileId]) { if (!confirm(`You have unsaved changes in ${fileId}. Close anyway?`)) { return; } } // Remove the file from the files array const newFiles = files.filter(file => file.id !== fileId); setFiles(newFiles); // Update unsavedChanges const newUnsavedChanges = { ...unsavedChanges }; delete newUnsavedChanges[fileId]; setUnsavedChanges(newUnsavedChanges); // If the active tab is closed, set a new active tab if (activeTab === fileId && newFiles.length > 0) { setActiveTab(newFiles[0].id); } }; const handleCreateNewFile = (e, path = '') => { e?.preventDefault(); if (!newFileName) return; const filePath = path ? `${path}/${newFileName}` : newFileName; // Check if file already exists if (files.some(file => file.id === filePath)) { alert(`A file named "${filePath}" already exists.`); return; } // Determine language based on file extension let language = newFileType; const extension = newFileName.split('.').pop().toLowerCase(); if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) { language = 'javascript'; } else if (['css', 'scss', 'less'].includes(extension)) { language = 'css'; } else if (['html', 'htm'].includes(extension)) { language = 'html'; } else if (['json'].includes(extension)) { language = 'json'; } else if (['md', 'markdown'].includes(extension)) { language = 'markdown'; } // Create new file const newFile = { id: filePath, language, content: '' }; setFiles([...files, newFile]); setActiveTab(filePath); // Update file structure updateFileStructure(filePath, 'file', language); setNewFileName(''); setIsNewFileModalOpen(false); }; const updateFileStructure = (path, type, language = null) => { const parts = path.split('/'); const fileName = parts.pop(); let current = fileStructure; // Navigate to the correct folder if (parts.length > 0) { for (const part of parts) { if (!current[part]) { current[part] = { type: 'folder', children: {} }; } current = current[part].children; } } // Add the new item to the structure if (type === 'file') { current[fileName] = { type: 'file', language, id: path }; } else if (type === 'folder') { current[fileName] = { type: 'folder', children: {} }; } // Update the state with the new structure setFileStructure({...fileStructure}); }; const createNewFolder = (path = '') => { const folderName = prompt("Enter folder name:"); if (!folderName) return; const folderPath = path ? `${path}/${folderName}` : folderName; updateFileStructure(folderPath, 'folder'); // If the folder is inside another folder, expand the parent if (path) { setExpandedFolders({ ...expandedFolders, [path]: true }); } }; const handleSaveFile = () => { // Mark current file as saved setUnsavedChanges(prev => ({ ...prev, [activeTab]: false })); // In a real app, you would save to the server here console.log(`File ${activeTab} saved!`); }; const toggleFolder = (folderPath) => { setExpandedFolders({ ...expandedFolders, [folderPath]: !expandedFolders[folderPath] }); }; const openFile = (fileId) => { // Check if file exists in files array const fileExists = files.some(file => file.id === fileId); if (!fileExists) { // Determine language from file structure let language = 'text'; const parts = fileId.split('/'); const fileName = parts.pop(); const extension = fileName.split('.').pop().toLowerCase(); if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) { language = 'javascript'; } else if (['css', 'scss', 'less'].includes(extension)) { language = 'css'; } else if (['html', 'htm'].includes(extension)) { language = 'html'; } else if (['json'].includes(extension)) { language = 'json'; } else if (['md', 'markdown'].includes(extension)) { language = 'markdown'; } // Create new file entry const newFile = { id: fileId, language, content: '' }; setFiles([...files, newFile]); } setActiveTab(fileId); }; const handleContextMenu = (e, path, type) => { e.preventDefault(); setContextMenuPosition({ x: e.clientX, y: e.clientY }); setContextMenuTarget({ path, type }); setShowContextMenu(true); }; const closeContextMenu = () => { setShowContextMenu(false); setContextMenuTarget(null); }; const deleteItem = (path, type) => { const confirmDelete = confirm(`Are you sure you want to delete ${path}?`); if (!confirmDelete) return; if (type === 'file') { // Remove from files array setFiles(files.filter(file => file.id !== path)); // If it was active, set a new active tab if (activeTab === path) { const newActiveTab = files.find(file => file.id !== path)?.id || ''; setActiveTab(newActiveTab); } // Remove from unsavedChanges const newUnsavedChanges = { ...unsavedChanges }; delete newUnsavedChanges[path]; setUnsavedChanges(newUnsavedChanges); } // Remove from file structure const parts = path.split('/'); const itemName = parts.pop(); let current = fileStructure; let parent = null; // Navigate to the correct folder if (parts.length > 0) { for (const part of parts) { parent = current; current = current[part].children; } // Delete the item delete current[itemName]; } else { // Delete top-level item delete fileStructure[itemName]; } // Update the state setFileStructure({...fileStructure}); }; const startRenaming = (path, type) => { setRenamePath(path); const parts = path.split('/'); const currentName = parts.pop(); setRenameValue(currentName); setIsRenaming(true); }; const handleRename = (e) => { e.preventDefault(); if (!renameValue.trim()) return; const parts = renamePath.split('/'); const oldName = parts.pop(); const parentPath = parts.join('/'); const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue; // Check if this would overwrite an existing file or folder const parts2 = newPath.split('/'); const newName = parts2.pop(); let current = fileStructure; // Navigate to parent folder for (let i = 0; i < parts2.length; i++) { current = current[parts2[i]].children; } if (current[newName] && renamePath !== newPath) { alert(`An item named "${newName}" already exists at this location.`); return; } // Get the object data const pathParts = renamePath.split('/'); let curr = fileStructure; for (let i = 0; i < pathParts.length - 1; i++) { curr = curr[pathParts[i]].children; } const item = curr[pathParts[pathParts.length - 1]]; // Delete from old location delete curr[pathParts[pathParts.length - 1]]; // Add to new location const newParts = newPath.split('/'); curr = fileStructure; for (let i = 0; i < newParts.length - 1; i++) { curr = curr[newParts[i]].children; } curr[newParts[newParts.length - 1]] = item; // If it's a file, update the files array if (item.type === 'file') { const fileIndex = files.findIndex(file => file.id === renamePath); if (fileIndex !== -1) { const updatedFiles = [...files]; updatedFiles[fileIndex] = { ...updatedFiles[fileIndex], id: newPath }; setFiles(updatedFiles); // Update active tab if necessary if (activeTab === renamePath) { setActiveTab(newPath); } // Update unsavedChanges if (unsavedChanges[renamePath]) { const newUnsavedChanges = { ...unsavedChanges }; newUnsavedChanges[newPath] = newUnsavedChanges[renamePath]; delete newUnsavedChanges[renamePath]; setUnsavedChanges(newUnsavedChanges); } } } setFileStructure({...fileStructure}); setIsRenaming(false); }; const cancelRename = () => { setIsRenaming(false); setRenamePath(''); setRenameValue(''); }; const getFileIcon = (fileName) => { const extension = fileName.split('.').pop().toLowerCase(); if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) { return ; } else if (['css', 'scss', 'less'].includes(extension)) { return ; } else if (['html', 'htm'].includes(extension)) { return ; } else if (['md', 'markdown'].includes(extension)) { return ; } return ; }; function getDefaultCode(tabId) { switch (tabId) { case "README.md": return `# VS Code Clone Project ## Overview This is a simple VS Code clone built with React and Monaco Editor. ## Features - File tree navigation - Tab management - Code editing with Monaco Editor - Syntax highlighting ## 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 ""; } } const activeFile = files.find(file => file.id === activeTab); // Calculate editor area style based on sidebar visibility const editorAreaStyle = { marginLeft: sidebarVisible ? `${sidebarWidth}px` : '0px', width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)` }; // Update the handleRunCode function const handleRunCode = async () => { if (!activeFile) return; // Show the panel setShowPanel(true); if (setPanelVisible) { setPanelVisible(true); } // 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: 'Submitting code...' } ]; setTerminalOutput(newOutput); try { // 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: language, code: activeFile.content, input: "" // Explicitly passing empty input, no user input handling }), }); if (!submitResponse.ok) { throw new Error(`Server error: ${submitResponse.status}`); } const { id } = await submitResponse.json(); setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]); // 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 }]); // 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); // Clean up interval when component unmounts or when socket closes newSocket.addEventListener('close', () => { if (statusCheckInterval) { clearInterval(statusCheckInterval); } }); } 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); } }; newSocket.onerror = (event) => { console.error("WebSocket error:", event); setTerminalOutput(prev => [...prev, { type: 'warning', content: `WebSocket error occurred` }]); }; // 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}` }]); setIsRunning(false); // Also add cleanup in the error handler if (statusCheckInterval) { clearInterval(statusCheckInterval); } } }; // Update handleInputSubmit to ensure the input is sent properly const handleInputSubmit = () => { // Log more detail for debugging console.log("Input submit called, active socket:", !!activeSocket, "userInput:", userInput); if (!activeSocket || !userInput.trim()) { console.warn("Cannot send input: No active socket or empty input"); return; } try { // Add the input to the terminal display setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${userInput}` }]); // Send the input via WebSocket with a newline character to ensure it's processed console.log("Sending input:", userInput); activeSocket.send(userInput + "\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 const togglePanel = () => { const newState = !showPanel; setShowPanel(newState); if (setPanelVisible) { setPanelVisible(newState); } }; // Add this function above the return statement const handleDownloadFile = () => { if (!activeFile) return; // Create a blob with the file content const blob = new Blob([activeFile.content], { type: 'text/plain' }); // Create a URL for the blob const url = URL.createObjectURL(blob); // Create a temporary anchor element const a = document.createElement('a'); a.href = url; // Get just the filename without path const fileName = activeFile.id.includes('/') ? activeFile.id.split('/').pop() : activeFile.id; // Set the download attribute with the filename a.download = fileName; // Append to the document, click it, and then remove it document.body.appendChild(a); a.click(); document.body.removeChild(a); // Release the object URL URL.revokeObjectURL(url); }; return (
{sidebarVisible && ( )}
{files.map((file) => { // Extract just the filename without path for display const displayName = file.id.includes('/') ? file.id.split('/').pop() : file.id; return (
setActiveTab(file.id)} title={file.id} // Show full path on hover > {getFileIcon(file.id)} {displayName} {/* Show just filename, not full path */} {unsavedChanges[file.id] && ' •'}
); })}
{/* Run controls */}
{activeFile && ( <> )}
{activeFile ? ( ) : (

No file open. Create a new file or select a file to start editing.

)}
{/* Use Panel component instead of internal terminal */} {showPanel && ( <>
{ const startY = e.clientY; const startHeight = panelHeight; const onMouseMove = (moveEvent) => { const newHeight = startHeight - (moveEvent.clientY - startY); if (newHeight > 100 && newHeight < 500) { setPanelHeight(newHeight); } }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }} /> )} {/* Modify the editor-actions div to include the download button */}
{/* Add download button */}
{isNewFileModalOpen && (

Create New File

setNewFileName(e.target.value)} />
)} {/* Context Menu */} {showContextMenu && (
{contextMenuTarget?.type === 'folder' && ( <>
{ createNewFolder(contextMenuTarget.path); closeContextMenu(); }}> New Folder
{ setNewFileName(''); setIsNewFileModalOpen(true); closeContextMenu(); }}> New File
)}
{ startRenaming(contextMenuTarget.path, contextMenuTarget.type); closeContextMenu(); }}> Rename
{/* Add download option - only show for files */} {contextMenuTarget?.type === 'file' && (
{ // Find the file in the files array const file = files.find(f => f.id === contextMenuTarget.path); if (file) { // Create a blob with the file content const blob = new Blob([file.content], { type: 'text/plain' }); // Create a URL for the blob const url = URL.createObjectURL(blob); // Create a temporary anchor element const a = document.createElement('a'); a.href = url; // Get just the filename without path const fileName = file.id.includes('/') ? file.id.split('/').pop() : file.id; // Set the download attribute with the filename a.download = fileName; // Append to the document, click it, and then remove it document.body.appendChild(a); a.click(); document.body.removeChild(a); // Release the object URL URL.revokeObjectURL(url); } closeContextMenu(); }}> Download
)}
{ deleteItem(contextMenuTarget.path, contextMenuTarget.type); closeContextMenu(); }}> Delete
)}
{showContextMenu && (
)}
); }; export default EditorArea;