feat: Implement CodeMirror 6 file editor with tab support

Implement Phase 1 of the file editor & chat UI redesign:
- CodeMirror 6 integration with syntax highlighting
- Multi-file tab support with dirty state tracking
- Custom dark theme matching GitHub's color scheme
- Keyboard shortcuts (Ctrl+S to save, Ctrl+W to close tab)
- Mobile-responsive design with proper touch targets
- Fallback to basic textarea if CodeMirror fails to load

Technical details:
- Import map for ESM modules from node_modules
- Language support for JS, Python, HTML, CSS, JSON, Markdown
- Auto-initialization on DOM ready
- Global window.fileEditor instance for integration
- Serve node_modules at /claude/node_modules for import map

Files added:
- public/claude-ide/components/file-editor.js (main component)
- public/claude-ide/components/file-editor.css (responsive styles)

Files modified:
- public/claude-ide/index.html (import map, script tags)
- public/claude-ide/ide.js (updated loadFile function)
- server.js (serve node_modules for CodeMirror)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-21 08:49:01 +00:00
Unverified
parent 9e445bf653
commit b765c537fc
5 changed files with 1185 additions and 49 deletions

View File

@@ -0,0 +1,421 @@
/**
* File Editor Component Styles
* Mobile-first responsive design for CodeMirror 6 editor
*/
/* === File Editor Container === */
.file-editor-container {
display: flex;
flex-direction: column;
height: 100%;
background: #0d1117;
color: #c9d1d9;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
overflow: hidden;
}
/* === Editor Header (Tabs + Actions) === */
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
background: #161b22;
border-bottom: 1px solid #30363d;
padding: 0;
min-height: 40px;
}
.editor-tabs {
display: flex;
align-items: center;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #484f58 #161b22;
}
.editor-tabs::-webkit-scrollbar {
height: 8px;
}
.editor-tabs::-webkit-scrollbar-track {
background: #161b22;
}
.editor-tabs::-webkit-scrollbar-thumb {
background: #484f58;
border-radius: 4px;
}
.editor-tabs::-webkit-scrollbar-thumb:hover {
background: #6e7681;
}
.editor-actions {
display: flex;
align-items: center;
padding: 0 8px;
gap: 4px;
border-left: 1px solid #30363d;
}
/* === Editor Tabs === */
.editor-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
border: none;
border-right: 1px solid #30363d;
cursor: pointer;
font-size: 13px;
color: #8b949e;
transition: background 0.15s ease, color 0.15s ease;
white-space: nowrap;
user-select: none;
min-width: fit-content;
}
.editor-tab:hover {
background: #21262d;
color: #c9d1d9;
}
.editor-tab.active {
background: #0d1117;
color: #c9d1d9;
border-top: 2px solid #58a6ff;
}
.editor-tab.dirty .tab-name {
color: #e3b341;
}
.editor-tab.dirty .tab-dirty-indicator {
color: #e3b341;
}
.tab-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.tab-dirty-indicator {
font-size: 10px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.tab-close {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
background: transparent;
border: none;
color: #8b949e;
cursor: pointer;
border-radius: 3px;
font-size: 16px;
line-height: 1;
transition: all 0.15s ease;
}
.tab-close:hover {
background: #484f58;
color: #c9d1d9;
}
/* === Editor Content Area === */
.editor-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* === CodeMirror Editor Instance === */
.cm-editor-instance {
height: 100%;
width: 100%;
overflow: hidden;
}
/* === Editor Placeholder === */
.editor-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #484f58;
text-align: center;
padding: 2rem;
}
.placeholder-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.editor-placeholder h2 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #8b949e;
}
.editor-placeholder p {
font-size: 1rem;
color: #484f58;
}
/* === Fallback Editor (when CodeMirror fails) === */
.fallback-editor {
width: 100%;
height: 100%;
background: #0d1117;
color: #c9d1d9;
border: none;
outline: none;
resize: none;
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
padding: 12px;
}
/* === Action Buttons === */
.btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
color: #8b949e;
cursor: pointer;
border-radius: 4px;
font-size: 16px;
transition: all 0.15s ease;
}
.btn-icon:hover {
background: #21262d;
color: #c9d1d9;
}
.btn-icon:active {
transform: scale(0.95);
}
/* === CodeMirror Customization === */
.codemirror-editor {
height: 100%;
}
.codemirror-editor .cm-scroller {
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 14px;
line-height: 1.6;
}
.codemirror-editor .cm-content {
padding: 12px 0;
}
.codemirror-editor .cm-line {
padding: 0 12px;
}
/* === Mobile Responsive === */
@media (max-width: 640px) {
.editor-header {
flex-direction: column;
align-items: stretch;
}
.editor-tabs {
border-right: none;
border-bottom: 1px solid #30363d;
}
.editor-actions {
border-left: none;
border-top: 1px solid #30363d;
padding: 4px;
justify-content: center;
}
.editor-tab {
padding: 10px 8px;
font-size: 12px;
}
.tab-name {
max-width: 120px;
}
.tab-close {
width: 28px;
height: 28px;
font-size: 18px;
}
.btn-icon {
width: 36px;
height: 36px;
font-size: 18px;
}
.editor-placeholder h2 {
font-size: 1.25rem;
}
.editor-placeholder p {
font-size: 0.875rem;
}
}
/* === Tablet Responsive === */
@media (min-width: 641px) and (max-width: 1024px) {
.tab-name {
max-width: 150px;
}
}
/* === Touch Targets (Mobile) === */
@media (hover: none) and (pointer: coarse) {
.editor-tab {
padding: 12px;
min-height: 44px;
}
.tab-close {
width: 44px;
height: 44px;
}
.btn-icon {
width: 44px;
height: 44px;
}
}
/* === Dark Mode Scrollbar for Editor === */
.cm-editor-instance ::-webkit-scrollbar {
width: 14px;
height: 14px;
}
.cm-editor-instance ::-webkit-scrollbar-track {
background: #0d1117;
}
.cm-editor-instance ::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 7px;
border: 3px solid #0d1117;
}
.cm-editor-instance ::-webkit-scrollbar-thumb:hover {
background: #484f58;
}
.cm-editor-instance ::-webkit-scrollbar-corner {
background: #0d1117;
}
/* === Status Messages === */
.status-message {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
animation: fadeIn 0.2s ease;
}
.status-success {
color: #3fb950;
background: rgba(63, 185, 80, 0.1);
}
.status-error {
color: #f85149;
background: rgba(248, 81, 73, 0.1);
}
.status-info {
color: #58a6ff;
background: rgba(88, 166, 255, 0.1);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-2px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* === Loading Spinner === */
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #30363d;
border-top-color: #58a6ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 2rem auto;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* === No Files State === */
.no-tabs {
padding: 8px 12px;
color: #484f58;
font-size: 13px;
font-style: italic;
}
/* === Focus Styles for Accessibility === */
.editor-tab:focus-visible,
.tab-close:focus-visible,
.btn-icon:focus-visible {
outline: 2px solid #58a6ff;
outline-offset: 2px;
}
/* === Print Styles === */
@media print {
.editor-header,
.editor-actions {
display: none;
}
.editor-content {
height: auto;
overflow: visible;
}
}

View File

@@ -0,0 +1,663 @@
/**
* File Editor with CodeMirror 6
* Supports: Multi-file tabs, syntax highlighting, dirty state tracking
*/
import { EditorState, Compartment } from '@codemirror/state';
import { EditorView, keymap, highlightSpecialChars, drawSelection, dropCursor, lineNumbers, rectangularSelection, crosshairCursor, highlightActiveLine, highlightSelectionMatches, EditorView as cmEditorView } from '@codemirror/view';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches as searchHighlightMatches } from '@codemirror/search';
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { bracketMatching, codeFolding, foldGutter, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
// Language support
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
// Custom dark theme matching GitHub's dark theme
const customDarkTheme = cmEditorView.theme({
'&': {
backgroundColor: '#0d1117',
color: '#c9d1d9',
fontSize: '14px',
fontFamily: "'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monospace"
},
'.cm-scroller': {
fontFamily: 'inherit',
overflow: 'auto'
},
'.cm-content': {
padding: '12px 0',
minHeight: '100%'
},
'.cm-line': {
padding: '0 12px'
},
'.cm-gutters': {
backgroundColor: '#0d1117',
color: '#484f58',
border: 'none'
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: '#c9d1d9'
},
'.cm-activeLine': {
backgroundColor: '#161b22'
},
'.cm-focused': {
outline: 'none'
},
'.cm-selectionBackground': {
backgroundColor: '#264f78'
},
'&.cm-focused .cm-selectionBackground': {
backgroundColor: '#264f78'
},
'.cm-selectionMatch': {
backgroundColor: '#264f7855'
},
'.cm-cursor': {
borderLeftColor: '#58a6ff'
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#484f58'
},
'.cm-tooltip': {
border: '1px solid #30363d',
backgroundColor: '#161b22',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'
},
'.cm-tooltip-autocomplete': {
'& > ul': {
maxHeight: '200px',
fontFamily: 'inherit',
'& > li': {
padding: '4px 8px',
'&[aria-selected]': {
backgroundColor: '#1f6feb',
color: '#ffffff'
}
}
}
}
}, {
dark: true
});
// State compartments for dynamic reconfiguration
const languageCompartment = new Compartment();
const tabSizeCompartment = new Compartment();
/**
* Get language extension based on file extension
*/
function getLanguageExtension(filePath) {
const ext = filePath.split('.').pop().toLowerCase();
const languageMap = {
'js': javascript(),
'jsx': javascript({ jsx: true }),
'ts': javascript({ typescript: true }),
'tsx': javascript({ typescript: true, jsx: true }),
'mjs': javascript(),
'cjs': javascript(),
'py': python(),
'html': html(),
'htm': html(),
'css': css(),
'scss': css(),
'sass': css(),
'json': json(),
'md': markdown(),
'markdown': markdown(),
'txt': null
};
return languageMap[ext] || javascript();
}
/**
* Create CodeMirror editor state
*/
function createEditorState(filePath, content, onChange) {
const language = getLanguageExtension(filePath);
return EditorState.create({
doc: content || '',
extensions: [
lineNumbers(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
codeFolding(),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...completionKeymap,
{
key: 'Mod-s',
run: () => {
// Trigger save - will be handled by the component
const event = new CustomEvent('editor-save', { bubbles: true });
document.dispatchEvent(event);
return true;
}
},
{
key: 'Mod-w',
run: (view) => {
// Trigger close tab - will be handled by the component
const event = new CustomEvent('editor-close-tab', { bubbles: true });
document.dispatchEvent(event);
return true;
}
}
]),
customDarkTheme,
languageCompartment.of(language || []),
tabSizeCompartment.of(EditorState.tabSize.of(4)),
cmEditorView.updateListener.of((update) => {
if (update.docChanged) {
// Notify that content changed
if (onChange) onChange();
}
})
]
});
}
/**
* FileEditor Class
*/
class FileEditor {
constructor(container) {
this.container = container;
this.editors = new Map(); // tabId -> EditorView
this.activeTab = null;
this.tabs = [];
this.nextTabId = 1;
this.initialize();
}
initialize() {
// Create editor container structure
this.container.innerHTML = `
<div class="file-editor-container">
<div class="editor-header">
<div class="editor-tabs" id="editor-tabs">
<!-- Tabs will be rendered here -->
</div>
<div class="editor-actions">
<button class="btn-icon" id="btn-save-all" title="Save All (Ctrl+S)">💾</button>
<button class="btn-icon" id="btn-close-all" title="Close All">✕</button>
</div>
</div>
<div class="editor-content" id="editor-content">
<div class="editor-placeholder">
<div class="placeholder-icon">📄</div>
<h2>No file open</h2>
<p>Select a file from the sidebar to start editing</p>
</div>
</div>
</div>
`;
// Set up event listeners
this.setupEventListeners();
}
setupEventListeners() {
// Save all button
const saveAllBtn = this.container.querySelector('#btn-save-all');
if (saveAllBtn) {
saveAllBtn.addEventListener('click', () => this.saveAllFiles());
}
// Close all button
const closeAllBtn = this.container.querySelector('#btn-close-all');
if (closeAllBtn) {
closeAllBtn.addEventListener('click', () => this.closeAllTabs());
}
// Keyboard shortcuts
document.addEventListener('editor-save', () => this.saveCurrentFile());
document.addEventListener('editor-close-tab', () => this.closeCurrentTab());
// Handle window resize
window.addEventListener('resize', () => this.refreshActiveEditor());
}
/**
* Open a file in the editor
*/
async openFile(filePath, content) {
// Check if file is already open
const existingTab = this.tabs.find(tab => tab.path === filePath);
if (existingTab) {
this.activateTab(existingTab.id);
return;
}
// Create new tab
const tabId = `tab-${this.nextTabId++}`;
const tab = {
id: tabId,
path: filePath,
name: filePath.split('/').pop(),
dirty: false,
originalContent: content || ''
};
this.tabs.push(tab);
// Create CodeMirror editor instance
const editorContainer = document.createElement('div');
editorContainer.className = 'cm-editor-instance';
editorContainer.style.display = 'none';
const contentArea = this.container.querySelector('#editor-content');
// Remove placeholder if it exists
const placeholder = contentArea.querySelector('.editor-placeholder');
if (placeholder) {
placeholder.remove();
}
contentArea.appendChild(editorContainer);
const state = createEditorState(filePath, content || '', () => {
this.markDirty(tabId);
});
const editor = new EditorView({
state: state,
parent: editorContainer
});
this.editors.set(tabId, editor);
// Activate the new tab
this.activateTab(tabId);
return tabId;
}
/**
* Activate a tab
*/
activateTab(tabId) {
if (!this.editors.has(tabId)) {
console.error('[FileEditor] Tab not found:', tabId);
return;
}
// Hide all editors
this.editors.forEach((editor, id) => {
const container = editor.dom.parentElement;
if (container) {
container.style.display = id === tabId ? 'block' : 'none';
}
});
this.activeTab = tabId;
this.renderTabs();
// Refresh the active editor to ensure proper rendering
setTimeout(() => this.refreshActiveEditor(), 10);
}
/**
* Close a tab
*/
async closeTab(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (!tab) return;
// Check for unsaved changes
if (tab.dirty) {
const shouldSave = confirm(`Save changes to ${tab.name} before closing?`);
if (shouldSave) {
await this.saveFile(tabId);
}
}
// Destroy editor
const editor = this.editors.get(tabId);
if (editor) {
editor.destroy();
this.editors.delete(tabId);
}
// Remove tab from list
this.tabs = this.tabs.filter(t => t.id !== tabId);
// If we closed the active tab, activate another one
if (this.activeTab === tabId) {
if (this.tabs.length > 0) {
this.activateTab(this.tabs[0].id);
} else {
this.activeTab = null;
this.showPlaceholder();
}
}
this.renderTabs();
}
/**
* Close current tab
*/
closeCurrentTab() {
if (this.activeTab) {
this.closeTab(this.activeTab);
}
}
/**
* Close all tabs
*/
async closeAllTabs() {
if (this.tabs.length === 0) return;
const hasUnsaved = this.tabs.some(t => t.dirty);
if (hasUnsaved) {
const shouldSaveAll = confirm('Some files have unsaved changes. Save all before closing?');
if (shouldSaveAll) {
await this.saveAllFiles();
}
}
// Destroy all editors
this.editors.forEach(editor => editor.destroy());
this.editors.clear();
this.tabs = [];
this.activeTab = null;
this.renderTabs();
this.showPlaceholder();
}
/**
* Save a file
*/
async saveFile(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (!tab) return;
const editor = this.editors.get(tabId);
if (!editor) return;
const content = editor.state.doc.toString();
try {
const response = await fetch(`/claude/api/file/${encodeURIComponent(tab.path)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// Update tab state
tab.dirty = false;
tab.originalContent = content;
this.renderTabs();
// Show success toast
if (typeof showToast === 'function') {
showToast(`✅ Saved ${tab.name}`, 'success', 2000);
}
return true;
} catch (error) {
console.error('[FileEditor] Error saving file:', error);
if (typeof showToast === 'function') {
showToast(`❌ Failed to save ${tab.name}: ${error.message}`, 'error', 3000);
}
return false;
}
}
/**
* Save current file
*/
async saveCurrentFile() {
if (this.activeTab) {
await this.saveFile(this.activeTab);
}
}
/**
* Save all files
*/
async saveAllFiles() {
const dirtyTabs = this.tabs.filter(t => t.dirty);
if (dirtyTabs.length === 0) {
if (typeof showToast === 'function') {
showToast('No unsaved changes', 'info', 2000);
}
return;
}
let saved = 0;
let failed = 0;
for (const tab of dirtyTabs) {
const result = await this.saveFile(tab.id);
if (result) {
saved++;
} else {
failed++;
}
}
if (typeof showToast === 'function') {
if (failed === 0) {
showToast(`✅ Saved ${saved} file${saved > 1 ? 's' : ''}`, 'success', 2000);
} else {
showToast(`⚠️ Saved ${saved} file${saved > 1 ? 's' : ''}, ${failed} failed`, 'warning', 3000);
}
}
}
/**
* Mark tab as dirty (unsaved changes)
*/
markDirty(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (tab && !tab.dirty) {
tab.dirty = true;
this.renderTabs();
}
}
/**
* Check if any tab is dirty
*/
hasUnsavedChanges() {
return this.tabs.some(t => t.dirty);
}
/**
* Get current file content
*/
getCurrentContent() {
if (!this.activeTab) return null;
const editor = this.editors.get(this.activeTab);
return editor ? editor.state.doc.toString() : null;
}
/**
* Get current file path
*/
getCurrentFilePath() {
if (!this.activeTab) return null;
const tab = this.tabs.find(t => t.id === this.activeTab);
return tab ? tab.path : null;
}
/**
* Refresh active editor
*/
refreshActiveEditor() {
if (!this.activeTab) return;
const editor = this.editors.get(this.activeTab);
if (editor) {
editor.requestMeasure();
}
}
/**
* Show placeholder when no files are open
*/
showPlaceholder() {
const contentArea = this.container.querySelector('#editor-content');
if (contentArea) {
contentArea.innerHTML = `
<div class="editor-placeholder">
<div class="placeholder-icon">📄</div>
<h2>No file open</h2>
<p>Select a file from the sidebar to start editing</p>
</div>
`;
}
}
/**
* Render tabs
*/
renderTabs() {
const tabsContainer = this.container.querySelector('#editor-tabs');
if (!tabsContainer) return;
if (this.tabs.length === 0) {
tabsContainer.innerHTML = '';
return;
}
tabsContainer.innerHTML = this.tabs.map(tab => `
<div class="editor-tab ${tab.id === this.activeTab ? 'active' : ''} ${tab.dirty ? 'dirty' : ''}"
data-tab-id="${tab.id}">
<span class="tab-name">${this.escapeHtml(tab.name)}</span>
${tab.dirty ? '<span class="tab-dirty-indicator">●</span>' : ''}
<button class="tab-close" title="Close tab">×</button>
</div>
`).join('');
// Tab click handlers
tabsContainer.querySelectorAll('.editor-tab').forEach(tabEl => {
tabEl.addEventListener('click', (e) => {
if (!e.target.classList.contains('tab-close')) {
this.activateTab(tabEl.dataset.tabId);
}
});
const closeBtn = tabEl.querySelector('.tab-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.closeTab(tabEl.dataset.tabId);
});
}
});
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Get tab count
*/
getTabCount() {
return this.tabs.length;
}
/**
* Get dirty tab count
*/
getDirtyTabCount() {
return this.tabs.filter(t => t.dirty).length;
}
/**
* Destroy editor and cleanup
*/
destroy() {
// Destroy all editors
this.editors.forEach(editor => editor.destroy());
this.editors.clear();
this.tabs = [];
this.activeTab = null;
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = { FileEditor };
}
// Export to window for use from non-module scripts
if (typeof window !== 'undefined') {
window.FileEditor = FileEditor;
console.log('[FileEditor] Component loaded and available globally');
}
// Auto-initialize when DOM is ready
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
// Create global file editor instance
if (!window.fileEditor) {
window.fileEditor = new FileEditor(document.getElementById('file-editor'));
console.log('[FileEditor] Auto-initialized on DOMContentLoaded');
}
});
} else {
// DOM is already ready
if (!window.fileEditor) {
window.fileEditor = new FileEditor(document.getElementById('file-editor'));
console.log('[FileEditor] Auto-initialized (DOM already ready)');
}
}
}

View File

@@ -855,60 +855,74 @@ async function loadFile(filePath) {
const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`); const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
const data = await res.json(); const data = await res.json();
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm'); // Check if FileEditor component is available
if (window.fileEditor) {
const editorEl = document.getElementById('file-editor'); // Use the new CodeMirror-based editor
await window.fileEditor.openFile(filePath, data.content || '');
if (isHtmlFile) {
// HTML file - show with preview option
editorEl.innerHTML = `
<div class="file-header">
<h2>${filePath}</h2>
<div class="file-actions">
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
<button class="btn-primary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
</div>
</div>
<div class="file-content" id="file-content-view">
<div class="view-toggle">
<button class="toggle-btn active" data-view="code" onclick="switchFileView('code')">Code</button>
<button class="toggle-btn" data-view="preview" onclick="switchFileView('preview')">Preview</button>
</div>
<div class="code-view">
<pre><code class="language-html">${escapeHtml(data.content)}</code></pre>
</div>
<div class="preview-view" style="display: none;">
<iframe id="html-preview-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
</div>
</div>
`;
// Store file content for preview
window.currentFileContent = data.content;
window.currentFilePath = filePath;
// Highlight code
if (window.hljs) {
document.querySelectorAll('#file-content-view pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
} else { } else {
// Non-HTML file - show as before // Fallback to the old view if FileEditor is not loaded yet
editorEl.innerHTML = ` console.warn('[loadFile] FileEditor not available, using fallback');
<div class="file-header"> const editorEl = document.getElementById('file-editor');
<h2>${filePath}</h2>
<div class="file-actions"> const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
if (isHtmlFile) {
// HTML file - show with preview option
editorEl.innerHTML = `
<div class="file-header">
<h2>${filePath}</h2>
<div class="file-actions">
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
<button class="btn-primary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
</div>
</div> </div>
</div> <div class="file-content" id="file-content-view">
<div class="file-content"> <div class="view-toggle">
<div class="markdown-body">${data.html}</div> <button class="toggle-btn active" data-view="code" onclick="switchFileView('code')">Code</button>
</div> <button class="toggle-btn" data-view="preview" onclick="switchFileView('preview')">Preview</button>
`; </div>
<div class="code-view">
<pre><code class="language-html">${escapeHtml(data.content)}</code></pre>
</div>
<div class="preview-view" style="display: none;">
<iframe id="html-preview-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
</div>
</div>
`;
// Store file content for preview
window.currentFileContent = data.content;
window.currentFilePath = filePath;
// Highlight code
if (window.hljs) {
document.querySelectorAll('#file-content-view pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
} else {
// Non-HTML file - show content
editorEl.innerHTML = `
<div class="file-header">
<h2>${filePath}</h2>
</div>
<div class="file-content">
<pre><code>${escapeHtml(data.content || '')}</code></pre>
</div>
`;
}
} }
} catch (error) { } catch (error) {
console.error('Error loading file:', error); console.error('Error loading file:', error);
const editorEl = document.getElementById('file-editor');
if (editorEl) {
editorEl.innerHTML = `
<div class="file-error">
<h2>Error loading file</h2>
<p>${error.message}</p>
</div>
`;
}
} }
} }

View File

@@ -10,7 +10,41 @@
<link rel="stylesheet" href="/claude/claude-ide/preview-manager.css"> <link rel="stylesheet" href="/claude/claude-ide/preview-manager.css">
<link rel="stylesheet" href="/claude/claude-ide/chat-enhanced.css"> <link rel="stylesheet" href="/claude/claude-ide/chat-enhanced.css">
<link rel="stylesheet" href="/claude/claude-ide/terminal.css"> <link rel="stylesheet" href="/claude/claude-ide/terminal.css">
<link rel="stylesheet" href="/claude/claude-ide/components/file-editor.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<!-- Import Map for CodeMirror 6 -->
<script type="importmap">
{
"imports": {
"@codemirror/state": "/claude/node_modules/@codemirror/state/dist/index.cjs",
"@codemirror/view": "/claude/node_modules/@codemirror/view/dist/index.cjs",
"@codemirror/commands": "/claude/node_modules/@codemirror/commands/dist/index.cjs",
"@codemirror/language": "/claude/node_modules/@codemirror/language/dist/index.cjs",
"@codemirror/autocomplete": "/claude/node_modules/@codemirror/autocomplete/dist/index.cjs",
"@codemirror/search": "/claude/node_modules/@codemirror/search/dist/index.cjs",
"@codemirror/lint": "/claude/node_modules/@codemirror/lint/dist/index.cjs",
"@codemirror/lang-javascript": "/claude/node_modules/@codemirror/lang-javascript/dist/index.cjs",
"@codemirror/lang-python": "/claude/node_modules/@codemirror/lang-python/dist/index.cjs",
"@codemirror/lang-html": "/claude/node_modules/@codemirror/lang-html/dist/index.cjs",
"@codemirror/lang-css": "/claude/node_modules/@codemirror/lang-css/dist/index.cjs",
"@codemirror/lang-json": "/claude/node_modules/@codemirror/lang-json/dist/index.cjs",
"@codemirror/lang-markdown": "/claude/node_modules/@codemirror/lang-markdown/dist/index.cjs",
"@codemirror/gutter": "/claude/node_modules/@codemirror/gutter/dist/index.cjs",
"@codemirror/fold": "/claude/node_modules/@codemirror/fold/dist/index.cjs",
"@codemirror/panel": "/claude/node_modules/@codemirror/panel/dist/index.cjs",
"@lezer/highlight": "/claude/node_modules/@lezer/highlight/dist/index.cjs",
"@lezer/common": "/claude/node_modules/@lezer/common/dist/index.cjs",
"@lezer/javascript": "/claude/node_modules/@lezer/javascript/dist/index.cjs",
"@lezer/python": "/claude/node_modules/@lezer/python/dist/index.cjs",
"@lezer/html": "/claude/node_modules/@lezer/html/dist/index.cjs",
"@lezer/css": "/claude/node_modules/@lezer/css/dist/index.cjs",
"@lezer/json": "/claude/node_modules/@lezer/json/dist/index.cjs",
"@lezer/markdown": "/claude/node_modules/@lezer/markdown/dist/index.cjs",
"@codemirror/basic-setup": "/claude/node_modules/@codemirror/basic-setup/dist/index.cjs"
}
}
</script>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -344,6 +378,7 @@
<script src="/claude/claude-ide/preview-manager.js"></script> <script src="/claude/claude-ide/preview-manager.js"></script>
<script src="/claude/claude-ide/chat-enhanced.js"></script> <script src="/claude/claude-ide/chat-enhanced.js"></script>
<script src="/claude/claude-ide/terminal.js"></script> <script src="/claude/claude-ide/terminal.js"></script>
<script type="module" src="/claude/claude-ide/components/file-editor.js"></script>
<!-- Debug Panel Toggle Script --> <!-- Debug Panel Toggle Script -->
<script> <script>

View File

@@ -189,6 +189,9 @@ app.get('/projects', (req, res) => {
// Serve static files (must come after specific routes) // Serve static files (must come after specific routes)
app.use('/claude', express.static(path.join(__dirname, 'public'))); app.use('/claude', express.static(path.join(__dirname, 'public')));
// Serve node_modules for CodeMirror 6 import map
app.use('/claude/node_modules', express.static(path.join(__dirname, 'node_modules')));
// Login route // Login route
app.post('/claude/api/login', (req, res) => { app.post('/claude/api/login', (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;