242 lines
7.6 KiB
JavaScript
242 lines
7.6 KiB
JavaScript
// ==========================================
|
|
// 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 [];
|
|
}
|