1261 lines
50 KiB
JavaScript
1261 lines
50 KiB
JavaScript
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 = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body></body></html>';
|
|
|
|
const jsContent = jsFilesToUse.map(f => f.content).join('\n');
|
|
html = html.replace('</body>', `<script>\n${jsContent}\n</script>\n</body>`);
|
|
|
|
const timestamp = Date.now();
|
|
html = html.replace('<head>', `<head><!-- Updated: ${timestamp} -->`);
|
|
|
|
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 = '<html><body><h2 style="color: #888; text-align: center; margin-top: 50px;">No HTML or JS files to preview</h2></body></html>';
|
|
console.log('[Preview] No files to preview');
|
|
setPreviewContent(noContentHtml);
|
|
return;
|
|
}
|
|
|
|
let html = htmlFile?.content || '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body></body></html>';
|
|
|
|
// Inject CSS
|
|
if (cssFiles.length > 0) {
|
|
const cssContent = cssFiles.map(f => f.content).join('\n');
|
|
if (html.includes('</head>')) {
|
|
html = html.replace('</head>', `<style>\n${cssContent}\n</style>\n</head>`);
|
|
} else {
|
|
html = html.replace('<head>', `<head><style>\n${cssContent}\n</style>`);
|
|
}
|
|
}
|
|
|
|
// Inject JS
|
|
if (jsFiles.length > 0) {
|
|
const jsContent = jsFiles.map(f => f.content).join('\n');
|
|
if (html.includes('</body>')) {
|
|
html = html.replace('</body>', `<script>\n${jsContent}\n</script>\n</body>`);
|
|
} else {
|
|
html += `<script>\n${jsContent}\n</script>`;
|
|
}
|
|
}
|
|
|
|
const timestamp = Date.now();
|
|
html = html.replace('<head>', `<head><!-- Updated: ${timestamp} -->`);
|
|
|
|
setPreviewContent(html);
|
|
};
|
|
|
|
const runPython = () => {
|
|
const selectedFile = files.find(f => f.id === selectedPreviewFile);
|
|
const pythonFile = selectedFile?.language === 'python' ? selectedFile : files.find(f => f.language === 'python');
|
|
|
|
if (!pythonFile) {
|
|
setPythonOutput('No Python file found');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setPythonOutput('Note: This is a basic simulation. Use a real Python environment for actual execution.\n\n');
|
|
const code = pythonFile.content;
|
|
|
|
const printMatches = code.matchAll(/print\s*\((.*?)\)/g);
|
|
let output = '';
|
|
for (const match of printMatches) {
|
|
const content = match[1].trim();
|
|
const value = content.replace(/^["']|["']$/g, '');
|
|
output += value + '\n';
|
|
}
|
|
|
|
setPythonOutput(output || 'No print statements found');
|
|
} catch (err) {
|
|
setPythonOutput(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const runPreview = () => {
|
|
console.log('[Preview] Run preview clicked');
|
|
|
|
// Set active file as default if none selected
|
|
if (!selectedPreviewFile) {
|
|
setSelectedPreviewFile(activeFileId);
|
|
}
|
|
|
|
const activeFile = files.find(f => f.id === activeFileId);
|
|
if (activeFile?.language === 'python') {
|
|
runPython();
|
|
} else {
|
|
buildPreview();
|
|
}
|
|
setShowPreview(true);
|
|
};
|
|
|
|
const refreshPreview = () => {
|
|
console.log('[Preview] Refresh clicked');
|
|
|
|
const fileToPreview = files.find(f => f.id === selectedPreviewFile);
|
|
if (fileToPreview?.language === 'python') {
|
|
runPython();
|
|
} else {
|
|
setPreviewContent('');
|
|
setTimeout(() => {
|
|
buildPreview();
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
const promptForFilename = async () => {
|
|
const filename = await customPrompt('Enter filename (e.g., script.js, index.html, styles.css):', 'untitled.txt');
|
|
if (filename) {
|
|
const newFile = {
|
|
id: Date.now(),
|
|
name: filename,
|
|
content: '',
|
|
language: getLanguageFromExtension(filename),
|
|
modified: false
|
|
};
|
|
setFiles(prev => [...prev, newFile]);
|
|
setActiveFileId(newFile.id);
|
|
}
|
|
};
|
|
|
|
// Replace saveFile function
|
|
const saveFile = async () => {
|
|
const activeFile = files.find(f => f.id === activeFileId);
|
|
if (!activeFile) return;
|
|
|
|
if (!window.electronAPI) {
|
|
// Browser fallback
|
|
const blob = new Blob([activeFile.content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = activeFile.name;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
setFiles(prev => prev.map(f => f.id === activeFileId ? { ...f, modified: false } : f));
|
|
return;
|
|
}
|
|
|
|
// Electron save
|
|
const filePath = filePaths[activeFileId];
|
|
if (filePath) {
|
|
// Save to existing file
|
|
const result = await window.electronAPI.writeFile(filePath, activeFile.content);
|
|
if (result.success) {
|
|
setFiles(prev => prev.map(f => f.id === activeFileId ? { ...f, modified: false } : f));
|
|
console.log('File saved:', filePath);
|
|
}
|
|
} else {
|
|
// Save As - no existing path
|
|
saveFileAs();
|
|
}
|
|
};
|
|
|
|
|
|
// Add new saveFileAs function
|
|
const saveFileAs = async () => {
|
|
const activeFile = files.find(f => f.id === activeFileId);
|
|
if (!activeFile) return;
|
|
|
|
if (!window.electronAPI) {
|
|
saveFile();
|
|
return;
|
|
}
|
|
|
|
const result = await window.electronAPI.saveFileDialog(activeFile.name);
|
|
if (result.success) {
|
|
const writeResult = await window.electronAPI.writeFile(result.filePath, activeFile.content);
|
|
if (writeResult.success) {
|
|
setFilePaths(prev => ({ ...prev, [activeFileId]: result.filePath }));
|
|
setFiles(prev => prev.map(f => f.id === activeFileId ? { ...f, modified: false } : f));
|
|
console.log('File saved as:', result.filePath);
|
|
}
|
|
}
|
|
};
|
|
|
|
const saveAllFiles = () => {
|
|
files.forEach(file => {
|
|
const blob = new Blob([file.content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = file.name;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
setFiles(prev => prev.map(f => ({ ...f, modified: false })));
|
|
};
|
|
|
|
const copyContent = () => {
|
|
const activeFile = files.find(f => f.id === activeFileId);
|
|
if (activeFile) {
|
|
navigator.clipboard.writeText(activeFile.content);
|
|
alert('Content copied to clipboard!');
|
|
}
|
|
};
|
|
|
|
const closeFile = (id, e) => {
|
|
e.stopPropagation();
|
|
|
|
const fileToClose = files.find(f => f.id === id);
|
|
if (fileToClose?.modified) {
|
|
const confirm = window.confirm(`${fileToClose.name} has unsaved changes. Close anyway?`);
|
|
if (!confirm) return;
|
|
}
|
|
|
|
console.log('[App] Closing file:', id);
|
|
|
|
// Dispose the model safely
|
|
if (modelsRef.current[id]) {
|
|
try {
|
|
if (!modelsRef.current[id].isDisposed()) {
|
|
modelsRef.current[id].dispose();
|
|
console.log('[App] Model disposed for:', id);
|
|
}
|
|
delete modelsRef.current[id];
|
|
} catch (err) {
|
|
console.error('[App] Error disposing model:', err);
|
|
}
|
|
}
|
|
|
|
setFiles(prev => {
|
|
const newFiles = prev.filter(f => f.id !== id);
|
|
if (activeFileId === id && newFiles.length > 0) {
|
|
// Switch to the first available file
|
|
const newActiveId = newFiles[0].id;
|
|
console.log('[App] Switching to file:', newActiveId);
|
|
setActiveFileId(newActiveId);
|
|
} else if (newFiles.length === 0) {
|
|
setActiveFileId(null);
|
|
}
|
|
return newFiles;
|
|
});
|
|
};
|
|
|
|
const closeAllFiles = () => {
|
|
const hasModified = files.some(f => f.modified);
|
|
if (hasModified) {
|
|
const confirm = window.confirm('Some files have unsaved changes. Close all anyway?');
|
|
if (!confirm) return;
|
|
}
|
|
|
|
console.log('[App] Closing all files');
|
|
|
|
// Dispose all models safely
|
|
Object.keys(modelsRef.current).forEach(id => {
|
|
try {
|
|
const model = modelsRef.current[id];
|
|
if (model && !model.isDisposed()) {
|
|
model.dispose();
|
|
}
|
|
} catch (err) {
|
|
console.error('[App] Error disposing model:', err);
|
|
}
|
|
});
|
|
|
|
modelsRef.current = {};
|
|
setFiles([]);
|
|
setActiveFileId(null);
|
|
setShowPreview(false);
|
|
|
|
// Clear localStorage
|
|
try {
|
|
localStorage.removeItem('monaco-editor-files');
|
|
localStorage.removeItem('monaco-editor-active-file');
|
|
console.log('[Storage] Cleared localStorage');
|
|
} catch (err) {
|
|
console.error('[Storage] Error clearing localStorage:', err);
|
|
}
|
|
};
|
|
|
|
// Replace openFileDialog function
|
|
const openFileDialog = async () => {
|
|
if (!window.electronAPI) {
|
|
// Fallback to browser file picker
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.multiple = true;
|
|
input.onchange = (e) => {
|
|
Array.from(e.target.files).forEach(file => {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const newFile = {
|
|
id: Date.now() + Math.random(),
|
|
name: file.name,
|
|
content: event.target.result,
|
|
language: getLanguageFromExtension(file.name),
|
|
modified: false
|
|
};
|
|
setFiles(prev => [...prev, newFile]);
|
|
setActiveFileId(newFile.id);
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
};
|
|
input.click();
|
|
return;
|
|
}
|
|
|
|
// Electron file picker
|
|
const result = await window.electronAPI.openFileDialog();
|
|
if (result.success) {
|
|
for (const filePath of result.filePaths) {
|
|
const readResult = await window.electronAPI.readFile(filePath);
|
|
if (readResult.success) {
|
|
const fileName = filePath.split(/[\\/]/).pop();
|
|
const newFile = {
|
|
id: Date.now() + Math.random(),
|
|
name: fileName,
|
|
content: readResult.content,
|
|
language: getLanguageFromExtension(fileName),
|
|
modified: false
|
|
};
|
|
setFiles(prev => [...prev, newFile]);
|
|
setFilePaths(prev => ({ ...prev, [newFile.id]: filePath }));
|
|
setActiveFileId(newFile.id);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const openFolder = async () => {
|
|
const result = await window.electronAPI.openFolderDialog();
|
|
if (result.success) {
|
|
setWorkspaceFolder(result.folderPath);
|
|
loadFolder(result.folderPath);
|
|
}
|
|
};
|
|
const loadFolder = async (folderPath) => {
|
|
const result = await window.electronAPI.readDirectory(folderPath);
|
|
if (result.success) {
|
|
// Completely replace folder contents
|
|
setFolderContents(result.files);
|
|
}
|
|
};
|
|
|
|
const refreshFolder = async () => {
|
|
if (workspaceFolder) {
|
|
// Clear expanded folders except root
|
|
setExpandedFolders(new Set());
|
|
// Do a fresh load
|
|
await loadFolder(workspaceFolder);
|
|
}
|
|
};
|
|
|
|
const toggleFolder = async (folderPath) => {
|
|
const newExpanded = new Set(expandedFolders);
|
|
if (newExpanded.has(folderPath)) {
|
|
newExpanded.delete(folderPath);
|
|
setExpandedFolders(newExpanded);
|
|
} else {
|
|
newExpanded.add(folderPath);
|
|
setExpandedFolders(newExpanded);
|
|
|
|
const result = await window.electronAPI.readDirectory(folderPath);
|
|
if (result.success) {
|
|
setFolderContents(prev => {
|
|
const existing = new Map(prev.map(item => [item.path, item]));
|
|
result.files.forEach(file => {
|
|
if (!existing.has(file.path)) {
|
|
existing.set(file.path, file);
|
|
}
|
|
});
|
|
return Array.from(existing.values());
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const openFileFromExplorer = async (filePath) => {
|
|
// Check if this file is already open
|
|
const existingFile = Object.entries(filePaths).find(([id, path]) => path === filePath);
|
|
if (existingFile) {
|
|
// File is already open, just switch to it
|
|
setActiveFileId(Number(existingFile[0]));
|
|
return;
|
|
}
|
|
|
|
const result = await window.electronAPI.readFile(filePath);
|
|
if (result.success) {
|
|
const fileName = filePath.split(/[\\/]/).pop();
|
|
const newFile = {
|
|
id: Date.now() + Math.random(),
|
|
name: fileName,
|
|
content: result.content,
|
|
language: getLanguageFromExtension(fileName),
|
|
modified: false
|
|
};
|
|
setFiles(prev => [...prev, newFile]);
|
|
setFilePaths(prev => ({ ...prev, [newFile.id]: filePath }));
|
|
setActiveFileId(newFile.id);
|
|
}
|
|
};
|
|
|
|
const createNewFileInFolder = async (folderPath) => {
|
|
const filename = await customPrompt('Enter filename:', 'newfile.txt');
|
|
|
|
if (filename) {
|
|
const separator = folderPath.includes('\\') ? '\\' : '/';
|
|
const filePath = `${folderPath}${separator}${filename}`;
|
|
|
|
const result = await window.electronAPI.createFile(filePath);
|
|
|
|
if (result.success) {
|
|
await loadFolder(workspaceFolder);
|
|
setExpandedFolders(prev => new Set([...prev, folderPath]));
|
|
openFileFromExplorer(filePath);
|
|
} else {
|
|
alert(`Failed to create file: ${result.error || 'Unknown error'}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const createNewFolderInFolder = async (folderPath) => {
|
|
const foldername = await customPrompt('Enter folder name:', 'newfolder');
|
|
if (foldername) {
|
|
const separator = folderPath.includes('\\') ? '\\' : '/';
|
|
const newFolderPath = `${folderPath}${separator}${foldername}`;
|
|
const result = await window.electronAPI.createFolder(newFolderPath);
|
|
if (result.success) {
|
|
await loadFolder(workspaceFolder);
|
|
setExpandedFolders(prev => new Set([...prev, newFolderPath]));
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleContextMenu = (e, item) => {
|
|
e.preventDefault();
|
|
setContextMenu({
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
item: item
|
|
});
|
|
};
|
|
|
|
const openCommandPalette = () => {
|
|
if (editorRef.current) {
|
|
editorRef.current.focus();
|
|
editorRef.current.trigger('keyboard', 'editor.action.quickCommand', null);
|
|
}
|
|
};
|
|
|
|
const PromptModal = () => {
|
|
if (!showPrompt) return null;
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
if (promptCallback) {
|
|
const callback = promptCallback;
|
|
setShowPrompt(false);
|
|
setPromptCallback(null);
|
|
callback(promptValue || null);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
if (promptCallback) {
|
|
const callback = promptCallback;
|
|
setShowPrompt(false);
|
|
setPromptCallback(null);
|
|
callback(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 }}>
|
|
<div style={{ backgroundColor: '#2d2d30', padding: '24px', borderRadius: '8px', minWidth: '400px', border: '1px solid #454545' }}>
|
|
<h3 style={{ color: '#cccccc', marginTop: 0, marginBottom: '16px', fontSize: '16px' }}>Enter filename</h3>
|
|
<form onSubmit={handleSubmit}>
|
|
<input
|
|
type="text"
|
|
value={promptValue}
|
|
onChange={(e) => setPromptValue(e.target.value)}
|
|
placeholder="e.g., script.js, index.html, styles.css"
|
|
autoFocus
|
|
style={{ width: '100%', padding: '8px 12px', backgroundColor: '#3c3c3c', color: '#cccccc', border: '1px solid #454545', borderRadius: '4px', fontSize: '14px', outline: 'none', boxSizing: 'border-box' }}
|
|
/>
|
|
<div style={{ display: 'flex', gap: '8px', marginTop: '16px', justifyContent: 'flex-end' }}>
|
|
<button
|
|
type="button"
|
|
onClick={handleCancel}
|
|
style={{ padding: '8px 16px', backgroundColor: '#3c3c3c', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' }}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
style={{ padding: '8px 16px', backgroundColor: '#0078d4', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' }}
|
|
>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FileExplorer = () => {
|
|
const renderTree = (items, parentPath = workspaceFolder) => {
|
|
return items
|
|
.filter(item => {
|
|
if (!item.path) return false;
|
|
const itemParent = item.path.substring(0, item.path.lastIndexOf(item.path.includes('\\') ? '\\' : '/'));
|
|
return itemParent === parentPath;
|
|
})
|
|
.sort((a, b) => {
|
|
if (a.isDirectory === b.isDirectory) return a.name.localeCompare(b.name);
|
|
return a.isDirectory ? -1 : 1;
|
|
})
|
|
.map(item => (
|
|
<div key={item.path}>
|
|
<div
|
|
draggable={!item.isDirectory}
|
|
onDragStart={(e) => {
|
|
if (!item.isDirectory) {
|
|
setDraggedItem(item);
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
}
|
|
}}
|
|
onDragOver={(e) => {
|
|
if (item.isDirectory && draggedItem && draggedItem.path !== item.path) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
}
|
|
}}
|
|
onDrop={async (e) => {
|
|
if (item.isDirectory && draggedItem) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const separator = draggedItem.path.includes('\\') ? '\\' : '/';
|
|
const newPath = `${item.path}${separator}${draggedItem.name}`;
|
|
|
|
// Check if file is open in editor
|
|
const openFile = files.find(f => filePaths[f.id] === draggedItem.path);
|
|
|
|
const result = await window.electronAPI.renameFile(draggedItem.path, newPath);
|
|
if (result.success) {
|
|
// Update file paths if the file is open
|
|
if (openFile) {
|
|
setFilePaths(prev => ({ ...prev, [openFile.id]: newPath }));
|
|
}
|
|
|
|
// Refresh the entire folder structure
|
|
setDraggedItem(null);
|
|
await loadFolder(workspaceFolder); // Full reload instead of refresh
|
|
setExpandedFolders(prev => new Set([...prev, item.path]));
|
|
} else {
|
|
alert(`Failed to move file: ${result.error || 'Unknown error'}`);
|
|
}
|
|
|
|
setDraggedItem(null);
|
|
}
|
|
}}
|
|
onDragEnd={() => setDraggedItem(null)}
|
|
onClick={() => item.isDirectory ? toggleFolder(item.path) : openFileFromExplorer(item.path)}
|
|
onContextMenu={(e) => handleContextMenu(e, item)}
|
|
style={{
|
|
padding: '4px 8px',
|
|
cursor: item.isDirectory ? 'pointer' : 'grab',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
color: '#cccccc',
|
|
fontSize: '13px',
|
|
backgroundColor: 'transparent',
|
|
transition: 'background-color 0.1s',
|
|
paddingLeft: `${(item.path.split(/[\\/]/).length - workspaceFolder.split(/[\\/]/).length) * 12 + 8}px`,
|
|
opacity: draggedItem?.path === item.path ? 0.5 : 1
|
|
}}
|
|
onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#2a2d2e'}
|
|
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
>
|
|
{item.isDirectory ? (
|
|
expandedFolders.has(item.path) ? '▼' : '▶'
|
|
) : (
|
|
<FileText size={14} />
|
|
)}
|
|
<span>{item.name}</span>
|
|
</div>
|
|
{item.isDirectory && expandedFolders.has(item.path) && renderTree(folderContents, item.path)}
|
|
</div>
|
|
));
|
|
};
|
|
|
|
return (
|
|
<div style={{ width: '250px', backgroundColor: '#252526', borderRight: '1px solid #2d2d30', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
<div style={{ padding: '8px', backgroundColor: '#2d2d30', borderBottom: '1px solid #1e1e1e', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span style={{ color: '#cccccc', fontSize: '11px', fontWeight: '600', textTransform: 'uppercase' }}>Explorer</span>
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
{workspaceFolder && (
|
|
<>
|
|
<button
|
|
onClick={refreshFolder}
|
|
style={{ background: 'none', border: 'none', color: '#cccccc', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center' }}
|
|
title="Refresh Folder"
|
|
>
|
|
<RotateCw size={14} />
|
|
</button>
|
|
<button
|
|
onClick={openFolder}
|
|
style={{ background: 'none', border: 'none', color: '#cccccc', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center' }}
|
|
title="Change Folder"
|
|
>
|
|
<FolderOpen size={14} />
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={() => setShowExplorer(false)}
|
|
style={{ background: 'none', border: 'none', color: '#cccccc', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center' }}
|
|
title="Close Explorer"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{!workspaceFolder ? (
|
|
<div style={{ padding: '16px', textAlign: 'center' }}>
|
|
<button
|
|
onClick={openFolder}
|
|
style={{ padding: '8px 16px', backgroundColor: '#0078d4', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' }}
|
|
>
|
|
Open Folder
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div
|
|
style={{ flex: 1, overflow: 'auto' }}
|
|
onContextMenu={(e) => handleContextMenu(e, { path: workspaceFolder, isDirectory: true, name: workspaceFolder.split(/[\\/]/).pop() })}
|
|
>
|
|
<div style={{ padding: '8px 8px 4px 8px', color: '#858585', fontSize: '12px', fontWeight: '600' }}>
|
|
{workspaceFolder.split(/[\\/]/).pop()}
|
|
</div>
|
|
{renderTree(folderContents)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ContextMenu = () => {
|
|
if (!contextMenu) return null;
|
|
|
|
const handleNewFile = async () => {
|
|
setContextMenu(null);
|
|
await createNewFileInFolder(contextMenu.item.path);
|
|
};
|
|
|
|
const handleNewFolder = async () => {
|
|
setContextMenu(null);
|
|
await createNewFolderInFolder(contextMenu.item.path);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
// Only allow deleting files, NOT folders
|
|
if (contextMenu.item.isDirectory) {
|
|
setContextMenu(null);
|
|
alert('Deleting folders is disabled for safety. Please delete files individually or use your file manager.');
|
|
return;
|
|
}
|
|
|
|
if (window.confirm(`Delete file "${contextMenu.item.name}"?`)) {
|
|
setContextMenu(null);
|
|
|
|
const result = await window.electronAPI.deleteFile(contextMenu.item.path);
|
|
if (result.success) {
|
|
// Close the file if it's open
|
|
const openFile = files.find(f => filePaths[f.id] === contextMenu.item.path);
|
|
if (openFile) {
|
|
// Close the file tab
|
|
if (modelsRef.current[openFile.id]) {
|
|
try {
|
|
if (!modelsRef.current[openFile.id].isDisposed()) {
|
|
modelsRef.current[openFile.id].dispose();
|
|
}
|
|
delete modelsRef.current[openFile.id];
|
|
} catch (err) {
|
|
console.error('[App] Error disposing model:', err);
|
|
}
|
|
}
|
|
|
|
setFiles(prev => {
|
|
const newFiles = prev.filter(f => f.id !== openFile.id);
|
|
if (activeFileId === openFile.id && newFiles.length > 0) {
|
|
setActiveFileId(newFiles[0].id);
|
|
} else if (newFiles.length === 0) {
|
|
setActiveFileId(null);
|
|
}
|
|
return newFiles;
|
|
});
|
|
|
|
setFilePaths(prev => {
|
|
const newPaths = { ...prev };
|
|
delete newPaths[openFile.id];
|
|
return newPaths;
|
|
});
|
|
}
|
|
|
|
await loadFolder(workspaceFolder);
|
|
} else {
|
|
alert(`Failed to delete: ${result.error || 'Unknown error'}`);
|
|
}
|
|
} else {
|
|
setContextMenu(null);
|
|
}
|
|
};
|
|
|
|
const handleRename = async () => {
|
|
// Only allow renaming files, NOT folders
|
|
if (contextMenu.item.isDirectory) {
|
|
setContextMenu(null);
|
|
alert('Renaming folders is disabled for safety. Please use your file manager.');
|
|
return;
|
|
}
|
|
|
|
setContextMenu(null);
|
|
const newName = await customPrompt('Enter new name:', contextMenu.item.name);
|
|
if (newName) {
|
|
const separator = contextMenu.item.path.includes('\\') ? '\\' : '/';
|
|
const parentPath = contextMenu.item.path.substring(0, contextMenu.item.path.lastIndexOf(separator));
|
|
const newPath = `${parentPath}${separator}${newName}`;
|
|
|
|
const result = await window.electronAPI.renameFile(contextMenu.item.path, newPath);
|
|
if (result.success) {
|
|
// Update file paths if the file is open
|
|
const openFile = files.find(f => filePaths[f.id] === contextMenu.item.path);
|
|
if (openFile) {
|
|
setFilePaths(prev => ({ ...prev, [openFile.id]: newPath }));
|
|
setFiles(prev => prev.map(f =>
|
|
f.id === openFile.id ? { ...f, name: newName } : f
|
|
));
|
|
}
|
|
|
|
await loadFolder(workspaceFolder);
|
|
} else {
|
|
alert(`Failed to rename: ${result.error || 'Unknown error'}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9998 }}
|
|
onClick={() => setContextMenu(null)}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: 'fixed',
|
|
top: contextMenu.y,
|
|
left: contextMenu.x,
|
|
backgroundColor: '#2d2d30',
|
|
border: '1px solid #454545',
|
|
borderRadius: '4px',
|
|
padding: '4px',
|
|
zIndex: 9999,
|
|
minWidth: '150px'
|
|
}}
|
|
>
|
|
{contextMenu.item.isDirectory && (
|
|
<>
|
|
<div
|
|
onClick={handleNewFile}
|
|
style={{ padding: '6px 12px', color: '#cccccc', cursor: 'pointer', fontSize: '13px', borderRadius: '2px' }}
|
|
onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#094771'}
|
|
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
>
|
|
New File
|
|
</div>
|
|
<div
|
|
onClick={handleNewFolder}
|
|
style={{ padding: '6px 12px', color: '#cccccc', cursor: 'pointer', fontSize: '13px', borderRadius: '2px' }}
|
|
onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#094771'}
|
|
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
>
|
|
New Folder
|
|
</div>
|
|
<div style={{ height: '1px', backgroundColor: '#454545', margin: '4px 0' }} />
|
|
</>
|
|
)}
|
|
{!contextMenu.item.isDirectory && (
|
|
<>
|
|
<div
|
|
onClick={handleRename}
|
|
style={{ padding: '6px 12px', color: '#cccccc', cursor: 'pointer', fontSize: '13px', borderRadius: '2px' }}
|
|
onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#094771'}
|
|
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
>
|
|
Rename
|
|
</div>
|
|
<div
|
|
onClick={handleDelete}
|
|
style={{ padding: '6px 12px', color: '#f48771', cursor: 'pointer', fontSize: '13px', borderRadius: '2px' }}
|
|
onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#094771'}
|
|
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
>
|
|
Delete
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const WelcomeScreen = () => (
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', backgroundColor: '#1f1f1f', color: '#cccccc' }}>
|
|
<div style={{ textAlign: 'center', padding: '32px' }}>
|
|
<FileCode size={64} style={{ margin: '0 auto 32px', color: '#0078d4' }} />
|
|
<h1 style={{ fontSize: '32px', fontWeight: '300', marginBottom: '32px' }}>Welcome</h1>
|
|
{error && (
|
|
<div style={{ backgroundColor: '#f48771', color: '#1e1e1e', padding: '12px', borderRadius: '4px', marginBottom: '16px', fontSize: '14px' }}>
|
|
Error: {error}
|
|
</div>
|
|
)}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
<button onClick={promptForFilename} style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 24px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', width: '256px', margin: '0 auto', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'}>
|
|
<Plus size={20} />
|
|
<span>New File</span>
|
|
</button>
|
|
<button onClick={openFileDialog} style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 24px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', width: '256px', margin: '0 auto', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'}>
|
|
<FolderOpen size={20} />
|
|
<span>Open File</span>
|
|
</button>
|
|
<div style={{ paddingTop: '16px', fontSize: '12px', color: '#858585' }}>
|
|
<p>Supported: HTML, CSS, JS, Python, TypeScript, and more</p>
|
|
<p style={{ marginTop: '12px', color: '#0078d4' }}>Press F1 for Command Palette</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const activeFile = files.find(f => f.id === activeFileId);
|
|
const editorReady = editorRef.current !== null;
|
|
|
|
return (
|
|
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column', backgroundColor: '#1f1f1f', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
|
<div style={{ backgroundColor: '#323233', color: '#cccccc', padding: '8px 16px', display: 'flex', alignItems: 'center', gap: '8px', borderBottom: '1px solid #2d2d30' }}>
|
|
<PromptModal />
|
|
<FileCode size={20} style={{ color: '#0078d4' }} />
|
|
<span style={{ fontWeight: '600' }}>NeoEditor</span>
|
|
<span style={{ fontSize: '11px', color: '#858585', marginLeft: '8px' }}>
|
|
{editorReady ? '● Ready' : '○ No files open'}
|
|
</span>
|
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: '8px' }}>
|
|
{!showExplorer && (
|
|
<button onClick={() => setShowExplorer(true)} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Show Explorer">
|
|
<FolderOpen size={14} />Explorer
|
|
</button>
|
|
)}
|
|
{activeFile && (
|
|
<>
|
|
<button onClick={saveFile} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Save File">
|
|
<Save size={14} />Save
|
|
</button>
|
|
<button onClick={saveFileAs} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Save As">
|
|
<Save size={14} />Save As
|
|
</button>
|
|
<button onClick={copyContent} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Copy Content">
|
|
<Copy size={14} />
|
|
</button>
|
|
</>
|
|
)}
|
|
<button onClick={openFileDialog} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Open File">
|
|
<FolderOpen size={14} />Open
|
|
</button>
|
|
{files.length > 0 && (
|
|
<>
|
|
{canPreview(activeFile) && (
|
|
<>
|
|
{/* comment here
|
|
<button onClick={runPreview} style={{ padding: '4px 12px', backgroundColor: '#0078d4', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#006abc'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#0078d4'} title="Run Preview">
|
|
<Play size={14} />Run
|
|
</button>
|
|
*/}
|
|
{showPreview && (
|
|
<>
|
|
<button onClick={refreshPreview} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Refresh">
|
|
<RotateCw size={14} />
|
|
</button>
|
|
<button onClick={() => setShowPreview(false)} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Hide Preview">
|
|
<EyeOff size={14} />
|
|
</button>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
<button onClick={saveAllFiles} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Save All">
|
|
<Download size={14} />Save All
|
|
</button>
|
|
<button onClick={closeAllFiles} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Close All">
|
|
<Trash2 size={14} />Close All
|
|
</button>
|
|
<button onClick={openCommandPalette} style={{ padding: '4px 12px', backgroundColor: '#2d2d30', color: '#cccccc', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3e3e42'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2d2d30'} title="Command Palette (F1)">
|
|
Command Palette
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div style={{ backgroundColor: '#2d2d30', display: 'flex', alignItems: 'center', borderBottom: '1px solid #252526' }}>
|
|
{files.map(file => (
|
|
<div key={file.id} onClick={() => setActiveFileId(file.id)} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 16px', cursor: 'pointer', borderRight: '1px solid #252526', backgroundColor: activeFileId === file.id ? '#1e1e1e' : '#2d2d30', color: activeFileId === file.id ? '#ffffff' : '#969696', transition: 'background-color 0.2s' }} onMouseOver={(e) => { if (activeFileId !== file.id) e.currentTarget.style.backgroundColor = '#343437'; }} onMouseOut={(e) => { if (activeFileId !== file.id) e.currentTarget.style.backgroundColor = '#2d2d30'; }}>
|
|
<FileText size={14} />
|
|
<span style={{ fontSize: '13px' }}>{file.name}{file.modified && ' •'}</span>
|
|
<button onClick={(e) => closeFile(file.id, e)} style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', padding: '2px', borderRadius: '3px', color: 'inherit', transition: 'background-color 0.2s' }} onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#45454a'} onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
<button onClick={promptForFilename} title="New File" style={{ padding: '8px 12px', color: '#969696', background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', transition: 'background-color 0.2s, color 0.2s' }} onMouseOver={(e) => { e.currentTarget.style.color = '#ffffff'; e.currentTarget.style.backgroundColor = '#343437'; }} onMouseOut={(e) => { e.currentTarget.style.color = '#969696'; e.currentTarget.style.backgroundColor = 'transparent'; }}>
|
|
<Plus size={16} />
|
|
</button>
|
|
</div>
|
|
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
|
{showExplorer && <FileExplorer />}
|
|
<ContextMenu />
|
|
<div style={{ flex: showPreview ? 1 : 1, position: 'relative', overflow: 'hidden' }}>
|
|
{files.length === 0 ? (
|
|
<WelcomeScreen />
|
|
) : (
|
|
<div ref={containerRef} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, width: '100%', height: '100%' }} />
|
|
)}
|
|
</div>
|
|
{showPreview && (
|
|
<div style={{ flex: 1, borderLeft: '1px solid #2d2d30', backgroundColor: '#1f1f1f', display: 'flex', flexDirection: 'column' }}>
|
|
<div style={{ padding: '8px 16px', backgroundColor: '#2d2d30', borderBottom: '1px solid #252526', color: '#cccccc', fontSize: '13px', fontWeight: '600', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<span>Preview</span>
|
|
<select
|
|
value={selectedPreviewFile || ''}
|
|
onChange={(e) => {
|
|
const newFileId = Number(e.target.value);
|
|
setSelectedPreviewFile(newFileId);
|
|
|
|
const fileToPreview = files.find(f => f.id === newFileId);
|
|
if (fileToPreview?.language === 'python') {
|
|
runPython();
|
|
} else {
|
|
setPreviewContent('');
|
|
setTimeout(() => buildPreview(), 50);
|
|
}
|
|
}}
|
|
style={{
|
|
backgroundColor: '#3c3c3c',
|
|
color: '#cccccc',
|
|
border: '1px solid #454545',
|
|
borderRadius: '4px',
|
|
padding: '4px 8px',
|
|
fontSize: '12px',
|
|
cursor: 'pointer',
|
|
outline: 'none'
|
|
}}
|
|
>
|
|
{files.filter(f => canPreview(f)).map(file => (
|
|
<option key={file.id} value={file.id}>
|
|
{file.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
{files.find(f => f.id === selectedPreviewFile)?.language === 'python' ? (
|
|
<pre style={{ flex: 1, margin: 0, padding: '16px', color: '#cccccc', backgroundColor: '#1f1f1f', overflow: 'auto', fontFamily: 'monospace', fontSize: '13px', whiteSpace: 'pre-wrap' }}>
|
|
{pythonOutput}
|
|
</pre>
|
|
) : (
|
|
previewContent ? (
|
|
<iframe
|
|
key={Date.now()}
|
|
ref={iframeRef}
|
|
srcDoc={previewContent}
|
|
style={{ flex: 1, border: 'none', backgroundColor: '#fff' }}
|
|
sandbox="allow-scripts"
|
|
title="Preview"
|
|
/>
|
|
) : (
|
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#888' }}>
|
|
Loading preview...
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MonacoEditor; |