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
No file open. Create a new file or select a file to start editing.