// 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
//