CodeEditor.tsxā¢6.58 kB
import React, { useCallback } from 'react';
import { useTool } from '@modelcontextprotocol/sdk/react';
import { PlayIcon, CloseIcon, PreviewIcon } from './icons.js';
import type { File, BottomPanelView } from '../App.js';
import type { VFS } from '../lib/vfs.js';
interface CodeEditorProps {
openFiles: File[];
setOpenFiles: React.Dispatch<React.SetStateAction<File[]>>;
activeFile: string | null;
setActiveFile: (path: string | null) => void;
addToTerminal: (output: string) => void;
vfs: VFS;
setPreviewContent: (content: string) => void;
setActiveBottomView: (view: BottomPanelView) => void;
}
const CodeEditor: React.FC<CodeEditorProps> = ({ openFiles, setOpenFiles, activeFile, setActiveFile, addToTerminal, vfs, setPreviewContent, setActiveBottomView }) => {
const { call: runCommand, isPending } = useTool('run_bash_command');
const currentFile = openFiles.find(f => f.path === activeFile);
const handleCodeChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (activeFile) {
const updatedFiles = openFiles.map(f =>
f.path === activeFile ? { ...f, content: e.target.value, isDirty: true } : f
);
setOpenFiles(updatedFiles);
}
};
const saveFile = useCallback(async (file: File) => {
if (!file.isDirty) return;
try {
await vfs.writeFile(file.path, file.content);
setOpenFiles(files => files.map(f => f.path === file.path ? {...f, isDirty: false} : f));
addToTerminal(`Saved ${file.path}`);
} catch(e) {
addToTerminal(`Error saving ${file.path}: ${e instanceof Error ? e.message : String(e)}`);
}
}, [vfs, addToTerminal, setOpenFiles]);
const handleTabClick = (path: string) => {
const current = openFiles.find(f => f.path === activeFile);
if(current?.isDirty) {
saveFile(current);
}
setActiveFile(path);
};
const closeFile = (path: string) => {
const fileToClose = openFiles.find(f => f.path === path);
if (fileToClose?.isDirty) {
saveFile(fileToClose);
}
const updatedFiles = openFiles.filter(f => f.path !== path);
setOpenFiles(updatedFiles);
if (activeFile === path) {
setActiveFile(updatedFiles.length > 0 ? updatedFiles[updatedFiles.length-1].path : null);
}
};
const handleRunCode = async () => {
if (currentFile) {
if(currentFile.isDirty) {
await saveFile(currentFile);
}
addToTerminal(`Executing ${currentFile.path}...`);
// Simple logic to choose interpreter based on file extension
const command = currentFile.path.endsWith('.py')
? `python "${currentFile.path}"`
: `sh "${currentFile.path}"`;
const result = await runCommand({ command });
const output = result?.content?.[0]?.type === 'text' ? result.content[0].text.split('\nCWD_MARKER:')[0].trim() : 'No output';
addToTerminal(output);
}
};
const handlePreview = async () => {
const filePath = prompt("Enter the path to the HTML file to preview:", "/workspace/index.html");
if (!filePath) return;
addToTerminal(`Attempting to preview: ${filePath}`);
try {
const content = await vfs.readFile(filePath);
setPreviewContent(content);
setActiveBottomView('preview');
addToTerminal(`Successfully loaded ${filePath} into web preview.`);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
addToTerminal(`Error reading file for preview: ${errorMessage}`);
alert(`Could not read file '${filePath}'. Make sure it exists.`);
}
};
return (
<div style={styles.editorContainer}>
<div style={styles.tabsContainer}>
{openFiles.map(file => (
<div
key={file.path}
style={{ ...styles.tab, ...(file.path === activeFile ? styles.activeTab : {}) }}
onClick={() => handleTabClick(file.path)}
>
<span>{file.path.split('/').pop()}{file.isDirty ? '*' : ''}</span>
<button style={styles.closeButton} onClick={(e) => { e.stopPropagation(); closeFile(file.path); }}>
<CloseIcon />
</button>
</div>
))}
</div>
<div style={styles.editorActions}>
<button style={styles.previewButton} onClick={handlePreview} title="Preview HTML file">
<PreviewIcon /> Preview
</button>
<button style={styles.runButton} onClick={handleRunCode} disabled={!currentFile || isPending}>
<PlayIcon /> {isPending ? 'Running...' : 'Run'}
</button>
</div>
<textarea
style={styles.textArea}
value={currentFile?.content || ''}
onChange={handleCodeChange}
onBlur={() => currentFile && saveFile(currentFile)}
placeholder={openFiles.length === 0 ? "Open a file from the explorer to start editing." : ""}
disabled={!currentFile}
aria-label="Code Editor"
/>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
editorContainer: { flex: 1, display: 'flex', flexDirection: 'column', backgroundColor: 'var(--background-primary)' },
tabsContainer: { display: 'flex', backgroundColor: 'var(--background-secondary)', flexShrink: 0 },
tab: {
padding: '8px 12px',
cursor: 'pointer',
borderRight: '1px solid var(--border-color)',
color: 'var(--text-secondary)',
display: 'flex',
alignItems: 'center',
gap: '8px'
},
activeTab: { backgroundColor: 'var(--background-primary)', color: 'var(--text-primary)' },
closeButton: { background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', padding: '0', display: 'flex' },
editorActions: { padding: '5px', backgroundColor: 'var(--background-secondary)', borderBottom: '1px solid var(--border-color)', display: 'flex', justifyContent: 'flex-end', flexShrink: 0, gap: '10px' },
runButton: { background: 'var(--accent-primary)', color: 'white', border: 'none', padding: '5px 10px', borderRadius: '3px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '5px' },
previewButton: { background: 'var(--background-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border-color)', padding: '5px 10px', borderRadius: '3px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '5px' },
textArea: { flex: 1, backgroundColor: 'var(--background-primary)', color: 'var(--text-primary)', border: 'none', outline: 'none', padding: '10px', fontFamily: 'monospace', fontSize: '14px', resize: 'none' },
};
export default CodeEditor;