// IDE: Monaco (el editor de VS Code) + árbol de archivos + pestañas. import * as code from './tools/code.js'; import { fileIcon, folderIcon } from './icons.js'; import { renderMarkdown } from './md.js'; let editor = null; let monacoRef = null; const tabs = []; // { path, model, dirty, preview } let active = null; let onDirtyChange = () => {}; const LANG = { js: 'javascript', mjs: 'javascript', ts: 'typescript', tsx: 'typescript', jsx: 'javascript', json: 'json', html: 'html', css: 'css', md: 'markdown', py: 'python', rs: 'rust', go: 'go', java: 'java', c: 'c', h: 'c', cpp: 'cpp', sh: 'shell', yml: 'yaml', yaml: 'yaml', toml: 'ini', svg: 'xml', xml: 'xml', sql: 'sql', txt: 'plaintext', }; const langOf = p => LANG[p.split('.').pop().toLowerCase()] || 'plaintext'; const $ = id => document.getElementById(id); export async function initEditor() { await new Promise(res => { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js'; s.onload = res; document.head.appendChild(s); }); window.require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' } }); await new Promise(res => window.require(['vs/editor/editor.main'], res)); monacoRef = window.monaco; editor = monacoRef.editor.create($('editor'), { theme: 'vs-dark', automaticLayout: true, fontSize: 13, minimap: { enabled: false }, padding: { top: 8 }, }); editor.addCommand(monacoRef.KeyMod.CtrlCmd | monacoRef.KeyCode.KeyS, saveActive); editor.onDidChangeModelContent(() => { const tab = tabs.find(t => t.path === active); if (tab && !tab.dirty) { tab.dirty = true; renderTabs(); } }); // los cambios del agente se reflejan al instante en el editor code.setOnFileWritten((path, content) => { const tab = tabs.find(t => t.path === path); if (tab && tab.model.getValue() !== content) { tab.model.setValue(content); tab.dirty = false; renderTabs(); if (path === active) applyView(); } refreshTree(); }); } export function gotoLine(n) { if (!editor || !n) return; editor.revealLineInCenter(n); editor.setPosition({ lineNumber: n, column: 1 }); editor.focus(); } export function triggerEditor(actionId) { editor?.getAction(actionId)?.run(); editor?.focus(); } export function hasEditor() { return !!editor; } export async function openFile(path) { let tab = tabs.find(t => t.path === path); if (!tab) { let content; try { content = await code.read({ path }); } catch (e) { // la ruta exacta no existe (típico: un enlace del chat solo mencionaba // el NOMBRE, «config.py», y el fichero real vive en una subcarpeta) → // si hay una única coincidencia por nombre en el proyecto, ábrela ella // en vez de rendirte; con varias o ninguna, el error de siempre. const base = path.split('/').pop(); const hits = await code.findByName?.(base, 2).catch(() => []) || []; if (hits.length === 1 && hits[0] !== path) return openFile(hits[0]); return alertBar('No pude abrir ' + path + ': ' + e.message); } tab = { path, model: monacoRef.editor.createModel(content, langOf(path)), dirty: false, preview: false }; tabs.push(tab); } active = path; code.setCurrentFile(path); editor.setModel(tab.model); setEmptyState(false); renderTabs(); await applyView(); } // modo «Vista previa» de un tab .md/.html: en vez del código fuente en // Monaco, muestra el markdown renderizado (mismo motor que el chat y la // Mente) o el HTML en un iframe real. const PREVIEWABLE = new Set(['markdown', 'html']); // resuelve una ruta relativa (href/src) contra la carpeta del propio fichero function resolveRelative(ownPath, rel) { if (/^([a-z][\w+.-]*:)?\/\//i.test(rel) || rel.startsWith('data:') || rel.startsWith('#')) return null; // externa/absoluta: no tocar const baseDir = ownPath.includes('/') ? ownPath.slice(0, ownPath.lastIndexOf('/')) : ''; const segs = (baseDir ? baseDir.split('/') : []).concat(rel.split('/')); const out = []; for (const s of segs) { if (!s || s === '.') continue; if (s === '..') out.pop(); else out.push(s); } return out.join('/'); } // un iframe con srcdoc no tiene base URL propia: o //