import { getPref } from './preferences.js'; const editorOpts = { minimap: { enabled: false }, automaticLayout: true, fontSize: 13, lineNumbers: 'on', scrollBeyondLastLine: false, theme: getPref('editorTheme') || 'vs-dark', tabSize: 2, renderWhitespace: 'none', padding: { top: 6 }, quickSuggestions: { other: true, comments: false, strings: true }, suggestOnTriggerCharacters: true, acceptSuggestionOnEnter: 'on', parameterHints: { enabled: true }, wordBasedSuggestions: 'currentDocument', suggest: { snippetsPreventQuickSuggestions: false, showSnippets: true, showWords: true, showKeywords: true, showMethods: true, showFunctions: true, showVariables: true, showClasses: true, showInterfaces: true, showProperties: true, showEvents: true, showConstants: true, }, autoClosingBrackets: 'always', autoClosingQuotes: 'always', autoSurround: 'languageDefined', bracketPairColorization: { enabled: true }, }; export const MODE_TABS = { 'html-css-js': [ { id: 'html', label: 'HTML', lang: 'html' }, { id: 'css', label: 'CSS', lang: 'css' }, { id: 'js', label: 'JavaScript', lang: 'javascript' }, ], 'typescript': [ { id: 'html', label: 'HTML', lang: 'html' }, { id: 'css', label: 'CSS', lang: 'css' }, { id: 'js', label: 'TypeScript', lang: 'typescript' }, ], 'react': [ { id: 'js', label: 'JSX', lang: 'javascript' }, { id: 'css', label: 'CSS', lang: 'css' }, { id: 'html', label: 'HTML', lang: 'html' }, ], 'react-ts': [ { id: 'js', label: 'TSX', lang: 'typescript' }, { id: 'css', label: 'CSS', lang: 'css' }, { id: 'html', label: 'HTML', lang: 'html' }, ], 'vue': [ { id: 'js', label: 'Vue SFC', lang: 'html' }, { id: 'css', label: 'CSS', lang: 'css' }, ], 'svelte': [ { id: 'js', label: 'Svelte', lang: 'html' }, { id: 'css', label: 'CSS', lang: 'css' }, ], 'markdown': [ { id: 'js', label: 'Markdown', lang: 'markdown' }, { id: 'css', label: 'CSS', lang: 'css' }, ], 'wasm': [ { id: 'html', label: 'HTML', lang: 'html' }, { id: 'css', label: 'CSS', lang: 'css' }, { id: 'js', label: 'JavaScript', lang: 'javascript' }, ], }; // Map mode names to js_type values stored in DB export const MODE_TO_JS_TYPE = { 'html-css-js': 'javascript', 'typescript': 'typescript', 'react': 'jsx', 'react-ts': 'tsx', 'vue': 'vue', 'svelte': 'svelte', 'markdown': 'markdown', 'wasm': 'wasm', }; export const JS_TYPE_TO_MODE = Object.fromEntries( Object.entries(MODE_TO_JS_TYPE).map(([k, v]) => [v, k]) ); // Editor instances keyed by tab id ('html', 'css', 'js') let editors = {}; let currentMode = 'html-css-js'; let activeTab = null; let cssType = 'css'; let onChangeCallback = null; let tabSwitchCallbacks = []; let modeChangeCallbacks = []; let onFormatCallback = null; let onDiffCallback = null; const tabBar = () => document.getElementById('tab-bar'); const editorArea = () => document.getElementById('editor-area'); export function setOnChange(cb) { onChangeCallback = cb; } export function setOnTabSwitch(cb) { tabSwitchCallbacks.push(cb); } export function setOnModeChange(cb) { modeChangeCallbacks.push(cb); } export function setOnFormat(cb) { onFormatCallback = cb; } export function setOnDiff(cb) { onDiffCallback = cb; } export function getActiveEditor() { if (activeTab && editors[activeTab]) return editors[activeTab]; return null; } export function relayoutEditors() { const tabs = MODE_TABS[currentMode]; if (!tabs) return; tabs.forEach((tab) => { if (editors[tab.id]) editors[tab.id].layout(); }); } export function getCurrentMode() { return currentMode; } export function getCssType() { return cssType; } export function setCssType(type) { cssType = type; const sel = document.getElementById('css-type-select'); if (sel) sel.value = type; // Update Monaco language for the CSS editor if (editors.css) { const model = editors.css.getModel(); monaco.editor.setModelLanguage(model, type === 'scss' ? 'scss' : type === 'less' ? 'less' : 'css'); } } function configureJsxSupport(mode) { if (mode === 'react' || mode === 'react-ts') { monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ESNext, jsx: monaco.languages.typescript.JsxEmit.React, jsxFactory: 'React.createElement', allowNonTsExtensions: true, allowJs: true, lib: ['esnext', 'dom', 'dom.iterable'], }); monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ESNext, jsx: monaco.languages.typescript.JsxEmit.React, jsxFactory: 'React.createElement', allowNonTsExtensions: true, lib: ['esnext', 'dom', 'dom.iterable'], }); // Add React type definitions for IntelliSense import('./autocomplete.js').then(m => m.addReactTypes()); } } function createEditor(tabDef) { const container = document.createElement('div'); container.className = 'editor-container'; container.id = `editor-${tabDef.id}`; editorArea().appendChild(container); const editor = monaco.editor.create(container, { ...editorOpts, language: tabDef.lang, value: '', }); if (onChangeCallback) { editor.onDidChangeModelContent(onChangeCallback); } return { container, editor }; } function renderTabBar(tabs) { const bar = tabBar(); bar.innerHTML = ''; tabs.forEach((tab) => { const btn = document.createElement('button'); btn.className = 'tab-btn'; btn.dataset.tab = tab.id; btn.textContent = tab.label; // Add CSS type selector inside the CSS tab if (tab.id === 'css') { const sel = document.createElement('select'); sel.className = 'tab-css-type'; sel.id = 'css-type-select'; sel.innerHTML = ` `; sel.value = cssType; sel.addEventListener('click', (e) => e.stopPropagation()); sel.addEventListener('change', (e) => { cssType = e.target.value; if (editors.css) { const model = editors.css.getModel(); monaco.editor.setModelLanguage(model, cssType === 'scss' ? 'scss' : cssType === 'less' ? 'less' : 'css'); } if (onChangeCallback) onChangeCallback(); }); btn.appendChild(sel); } btn.addEventListener('click', () => switchTab(tab.id)); bar.appendChild(btn); }); // Spacer + Format + Diff buttons const spacer = document.createElement('span'); spacer.className = 'tab-bar-spacer'; bar.appendChild(spacer); const fmtBtn = document.createElement('button'); fmtBtn.className = 'tab-bar-btn format-btn'; fmtBtn.textContent = 'Format'; fmtBtn.title = 'Format code (Shift+Alt+F)'; fmtBtn.addEventListener('click', () => { if (onFormatCallback) onFormatCallback(); }); bar.appendChild(fmtBtn); const diffBtn = document.createElement('button'); diffBtn.className = 'tab-bar-btn diff-btn'; diffBtn.textContent = 'Diff'; diffBtn.title = 'Toggle diff view (Ctrl+D)'; diffBtn.addEventListener('click', () => { if (onDiffCallback) onDiffCallback(); }); bar.appendChild(diffBtn); } export function initEditors(mode = 'html-css-js') { currentMode = mode; const tabs = MODE_TABS[mode]; configureJsxSupport(mode); renderTabBar(tabs); // Create editors for each tab tabs.forEach((tab) => { const { container, editor } = createEditor(tab); editors[tab.id] = editor; editors[`_container_${tab.id}`] = container; }); // Activate the first tab switchTab(tabs[0].id); return editors; } export function switchMode(mode) { if (mode === currentMode) return; // Dispose existing editors const oldTabs = MODE_TABS[currentMode]; oldTabs.forEach((tab) => { if (editors[tab.id]) { editors[tab.id].dispose(); } const container = editors[`_container_${tab.id}`]; if (container && container.parentNode) { container.parentNode.removeChild(container); } }); editors = {}; currentMode = mode; const tabs = MODE_TABS[mode]; configureJsxSupport(mode); renderTabBar(tabs); tabs.forEach((tab) => { const { container, editor } = createEditor(tab); editors[tab.id] = editor; editors[`_container_${tab.id}`] = container; }); switchTab(tabs[0].id); modeChangeCallbacks.forEach(cb => cb(mode)); } export function switchTab(tabId) { activeTab = tabId; const tabs = MODE_TABS[currentMode]; // Update tab button states tabBar().querySelectorAll('.tab-btn').forEach((btn) => { btn.classList.toggle('active', btn.dataset.tab === tabId); }); // Show/hide editor containers tabs.forEach((tab) => { const container = editors[`_container_${tab.id}`]; if (container) { container.classList.toggle('active', tab.id === tabId); } }); // Trigger layout on the active editor if (editors[tabId]) { editors[tabId].layout(); editors[tabId].focus(); } tabSwitchCallbacks.forEach(cb => cb(tabId, editors[tabId])); } export function getEditorValues() { const values = { html: '', css: '', js: '' }; if (editors.html) values.html = editors.html.getValue(); if (editors.css) values.css = editors.css.getValue(); if (editors.js) values.js = editors.js.getValue(); return values; } export function setEditorValues({ html = '', css = '', js = '' }) { if (editors.html) editors.html.setValue(html); if (editors.css) editors.css.setValue(css); if (editors.js) editors.js.setValue(js); } export function getActiveTab() { return activeTab; } export function setEditorTheme(themeId) { editorOpts.theme = themeId; monaco.editor.setTheme(themeId); }