// ========================================== // Chat Switchboard — CM6 Code Editor Factory // ========================================== // Full-featured code editor: line numbers, bracket // matching, search/replace, auto-indent, syntax // highlighting. Used for extension editing and // future code surfaces. // // Usage: // const editor = CM.codeEditor(container, { // language: 'javascript', // value: 'const x = 1;', // lineNumbers: true, // darkMode: true, // readOnly: false, // keymap: 'standard', // 'standard' | 'vim' | 'emacs' // onChange: (value) => {}, // }); // // editor.getValue() // editor.setValue(str) // editor.focus() // editor.destroy() // ========================================== import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightSpecialChars, placeholder as placeholderExt } from '@codemirror/view'; import { EditorState, Compartment } from '@codemirror/state'; import { defaultKeymap, indentWithTab, history, historyKeymap, undo, redo } from '@codemirror/commands'; import { bracketMatching, indentOnInput, foldGutter, foldKeymap, syntaxHighlighting, defaultHighlightStyle, HighlightStyle } from '@codemirror/language'; import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'; import { oneDark } from '@codemirror/theme-one-dark'; import { javascript } from '@codemirror/lang-javascript'; import { json } from '@codemirror/lang-json'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { sql } from '@codemirror/lang-sql'; import { html } from '@codemirror/lang-html'; import { css } from '@codemirror/lang-css'; import { yaml } from '@codemirror/lang-yaml'; import { go } from '@codemirror/lang-go'; import { python } from '@codemirror/lang-python'; import { rust } from '@codemirror/lang-rust'; import { vim } from '@replit/codemirror-vim'; import { emacs } from '@replit/codemirror-emacs'; import { switchboardTheme } from './theme.mjs'; // ── Language Registry ──────────────────────── const LANGUAGES = { javascript, js: javascript, json, markdown, md: markdown, sql, html, css, yaml, yml: yaml, go, python, py: python, rust, rs: rust, }; /** * Resolve a language name to a CM6 LanguageSupport instance. * Returns null if the language isn't bundled. */ function resolveLanguage(name) { if (!name) return null; const factory = LANGUAGES[name.toLowerCase()]; return factory ? factory() : null; } // ── Code Editor Factory ───────────────────── /** * Create a full-featured code editor. * * @param {HTMLElement} target - Container element (editor replaces its content) * @param {Object} opts * @param {string} [opts.language] - Language mode name * @param {string} [opts.value=''] - Initial content * @param {boolean} [opts.lineNumbers=true] * @param {boolean} [opts.darkMode] - Auto-detected from body if omitted * @param {boolean} [opts.readOnly=false] * @param {string} [opts.keymap='standard'] - 'standard' | 'vim' | 'emacs' * @param {string} [opts.placeholder] - Placeholder text * @param {Function} [opts.onChange] - Called with new value on every edit * @returns {{ getValue, setValue, focus, getView, destroy }} */ export function codeEditor(target, opts = {}) { const { language = null, value = '', lineNumbers: showLineNumbers = true, darkMode = document.documentElement.getAttribute('data-theme') !== 'light', readOnly = false, keymap: keymapMode = 'standard', placeholder: placeholderText = '', onChange = null, } = opts; // Compartments for reconfigurable extensions const langCompartment = new Compartment(); const keymapCompartment = new Compartment(); const readOnlyCompartment = new Compartment(); const themeCompartment = new Compartment(); // Build extensions list const extensions = [ // Base editing highlightSpecialChars(), history(), drawSelection(), dropCursor(), EditorState.allowMultipleSelections.of(true), indentOnInput(), bracketMatching(), closeBrackets(), autocompletion(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), // Keymaps keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, indentWithTab, ]), // Theme switchboardTheme, themeCompartment.of(darkMode ? [oneDark] : []), // Configurable compartments langCompartment.of(resolveLanguage(language) || []), keymapCompartment.of(_resolveKeymapExt(keymapMode)), readOnlyCompartment.of(EditorState.readOnly.of(readOnly)), // Optional features ...(showLineNumbers ? [lineNumbers(), highlightActiveLineGutter(), foldGutter()] : []), ...(placeholderText ? [placeholderExt(placeholderText)] : []), ]; // Change listener if (onChange) { extensions.push(EditorView.updateListener.of((update) => { if (update.docChanged) { onChange(update.state.doc.toString()); } })); } // Create the editor const view = new EditorView({ state: EditorState.create({ doc: value, extensions }), parent: target, }); // ── Public API ────────────────────────── return { getValue() { return view.state.doc.toString(); }, setValue(text) { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, }); }, focus() { view.focus(); }, /** Access the underlying EditorView for advanced use */ getView() { return view; }, /** Switch language mode at runtime */ setLanguage(name) { const lang = resolveLanguage(name); view.dispatch({ effects: langCompartment.reconfigure(lang || []), }); }, /** Switch keymap mode at runtime */ setKeymap(mode) { view.dispatch({ effects: keymapCompartment.reconfigure(_resolveKeymapExt(mode)), }); }, /** Toggle read-only state */ setReadOnly(ro) { view.dispatch({ effects: readOnlyCompartment.reconfigure( EditorState.readOnly.of(ro) ), }); }, /** Toggle dark/light mode (controls oneDark syntax theme) */ setDarkMode(dark) { view.dispatch({ effects: themeCompartment.reconfigure(dark ? [oneDark] : []), }); }, /** Clean up — removes the editor from the DOM */ destroy() { view.destroy(); }, }; } // ── Keybinding Resolution ─────────────────── function _resolveKeymapExt(mode) { if (mode === 'vim') return vim(); if (mode === 'emacs') return emacs(); return []; }