import React, { useState, useEffect, useRef } from 'react'; import { FileCode, Plus, X, FolderOpen, FileText, Save, Download, Copy, Trash2, Eye, EyeOff, Play, RotateCw } from 'lucide-react'; import * as monaco from 'monaco-editor'; // Import workers import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; // Configure Monaco Environment self.MonacoEnvironment = { getWorker(_, label) { if (label === 'json') return new jsonWorker(); if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker(); if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker(); if (label === 'typescript' || label === 'javascript') return new tsWorker(); return new editorWorker(); } }; const MonacoEditor = () => { const [files, setFiles] = useState(() => { // Load saved files from localStorage on mount try { const saved = localStorage.getItem('monaco-editor-files'); if (saved) { const parsed = JSON.parse(saved); console.log('[Storage] Restored', parsed.length, 'files from localStorage'); return parsed; } } catch (err) { console.error('[Storage] Error loading files:', err); } return []; }); const [activeFileId, setActiveFileId] = useState(() => { // Load active file ID from localStorage try { const saved = localStorage.getItem('monaco-editor-active-file'); if (saved) { console.log('[Storage] Restored active file:', saved); return JSON.parse(saved); } } catch (err) { console.error('[Storage] Error loading active file:', err); } return null; }); const [showPreview, setShowPreview] = useState(false); const [previewContent, setPreviewContent] = useState(''); const [pythonOutput, setPythonOutput] = useState(''); const editorRef = useRef(null); const containerRef = useRef(null); const modelsRef = useRef({}); const iframeRef = useRef(null); const [error, setError] = useState(null); const [selectedPreviewFile, setSelectedPreviewFile] = useState(null); const [filePaths, setFilePaths] = useState({}); // Store file paths: { fileId: realPath } // Add these state variables with your other useState declarations const [showPrompt, setShowPrompt] = useState(false); const [promptValue, setPromptValue] = useState(''); const [promptCallback, setPromptCallback] = useState(null); const [workspaceFolder, setWorkspaceFolder] = useState(null); const [folderContents, setFolderContents] = useState([]); const [expandedFolders, setExpandedFolders] = useState(new Set()); const [showExplorer, setShowExplorer] = useState(true); const [contextMenu, setContextMenu] = useState(null); const [draggedItem, setDraggedItem] = useState(null); // Add this custom prompt function const customPrompt = (message, defaultValue = '') => { return new Promise((resolve) => { setPromptValue(defaultValue); setShowPrompt(true); setPromptCallback(() => resolve); }); }; // Save files to localStorage whenever they change useEffect(() => { if (files.length > 0) { try { localStorage.setItem('monaco-editor-files', JSON.stringify(files)); console.log('[Storage] Saved', files.length, 'files to localStorage'); } catch (err) { console.error('[Storage] Error saving files:', err); } } }, [files]); // Save active file ID whenever it changes useEffect(() => { if (activeFileId !== null) { try { localStorage.setItem('monaco-editor-active-file', JSON.stringify(activeFileId)); console.log('[Storage] Saved active file ID:', activeFileId); } catch (err) { console.error('[Storage] Error saving active file ID:', err); } } }, [activeFileId]); useEffect(() => { if (!containerRef.current || editorRef.current) return; try { const editor = monaco.editor.create(containerRef.current, { value: '// Start typing...', language: 'javascript', theme: 'vs-dark', automaticLayout: true, fontSize: 14, minimap: { enabled: true }, scrollbar: { vertical: 'visible', horizontal: 'visible' } }); editorRef.current = editor; editor.onDidChangeModelContent(() => { if (activeFileId && modelsRef.current[activeFileId]) { const content = editor.getValue(); setFiles(prev => prev.map(f => f.id === activeFileId ? { ...f, content, modified: true } : f )); } }); } catch (err) { setError(`Failed to create editor: ${err.message}`); } return () => { if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null; } Object.values(modelsRef.current).forEach(model => { if (model && !model.isDisposed()) model.dispose(); }); }; }, [files.length]); useEffect(() => { if (!editorRef.current) { console.log('[Monaco] Editor not ready, skipping model update'); return; } const activeFile = files.find(f => f.id === activeFileId); console.log('[Monaco] Switching to file:', activeFile?.name, 'ID:', activeFileId); if (!activeFile) { console.log('[Monaco] No active file'); return; } try { // Create model if it doesn't exist if (!modelsRef.current[activeFile.id]) { console.log('[Monaco] Creating new model for:', activeFile.name); const uri = monaco.Uri.parse(`file:///${activeFile.name}`); // Check if a model with this URI already exists const existingModel = monaco.editor.getModel(uri); if (existingModel) { console.log('[Monaco] Model already exists, reusing it'); modelsRef.current[activeFile.id] = existingModel; } else { modelsRef.current[activeFile.id] = monaco.editor.createModel( activeFile.content || '', activeFile.language, uri ); console.log('[Monaco] Model created successfully'); } } const model = modelsRef.current[activeFile.id]; // Check if model is disposed if (model.isDisposed()) { console.error('[Monaco] Model is disposed, recreating...'); const uri = monaco.Uri.parse(`file:///${activeFile.name}`); modelsRef.current[activeFile.id] = monaco.editor.createModel( activeFile.content || '', activeFile.language, uri ); } console.log('[Monaco] Setting model to editor'); editorRef.current.setModel(modelsRef.current[activeFile.id]); editorRef.current.focus(); console.log('[Monaco] ✅ Model switched successfully'); } catch (err) { console.error('[Monaco] ❌ Error switching model:', err); setError(`Error switching file: ${err.message}`); } }, [activeFileId, files]); // Add keyboard shortcut handler useEffect(() => { const handleKeyDown = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveFile(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [activeFileId, files, filePaths]); const getLanguageFromExtension = (filename) => { const ext = filename.split('.').pop().toLowerCase(); const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'py': 'python', 'java': 'java', 'cpp': 'cpp', 'c': 'c', 'cs': 'csharp', 'php': 'php', 'rb': 'ruby', 'go': 'go', 'rs': 'rust', 'html': 'html', 'css': 'css', 'scss': 'scss', 'json': 'json', 'xml': 'xml', 'md': 'markdown', 'sql': 'sql', 'sh': 'shell', 'yaml': 'yaml', 'yml': 'yaml', 'txt': 'plaintext' }; return languageMap[ext] || 'plaintext'; }; const canPreview = (file) => { return ['html', 'javascript', 'python'].includes(file?.language); }; const buildPreview = () => { console.log('[Preview] Building preview...'); // Get LATEST content from Monaco models, not from state const currentFiles = files.map(f => { const model = modelsRef.current[f.id]; return { ...f, content: model && !model.isDisposed() ? model.getValue() : f.content }; }); console.log('[Preview] Files available:', currentFiles.map(f => `${f.name} (${f.language})`)); // Use selected file or fall back to first HTML file // Use selected file for preview const selectedFile = currentFiles.find(f => f.id === selectedPreviewFile); // If selected file is HTML, use it. If it's JS/JSX, show it as standalone. Otherwise find first HTML. let htmlFile; if (selectedFile?.language === 'html') { htmlFile = selectedFile; } else if (selectedFile?.language === 'javascript') { // For JS/JSX files, create a basic HTML wrapper just for this file htmlFile = null; // Will be handled below } else { htmlFile = currentFiles.find(f => f.language === 'html'); } const jsFiles = currentFiles.filter(f => f.language === 'javascript'); const cssFiles = currentFiles.filter(f => f.language === 'css'); // If viewing a specific JS file, only include that one if (selectedFile?.language === 'javascript') { const jsFilesToUse = [selectedFile]; let html = '
'; const jsContent = jsFilesToUse.map(f => f.content).join('\n'); html = html.replace('', `\n`); const timestamp = Date.now(); html = html.replace('
', `
`); setPreviewContent(html); return; } console.log('[Preview] HTML file:', htmlFile?.name); console.log('[Preview] JS files:', jsFiles.map(f => f.name)); console.log('[Preview] CSS files:', cssFiles.map(f => f.name)); if (!htmlFile && jsFiles.length === 0) { const noContentHtml = '