Changeset 0.17.2 (#77)

This commit is contained in:
2026-02-28 11:58:27 +00:00
parent 856dc9b0ac
commit a008dac488
26 changed files with 3018 additions and 116 deletions

241
src/editor/code-editor.mjs Normal file
View File

@@ -0,0 +1,241 @@
// ==========================================
// 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 [];
}