Changeset 0.17.2 (#77)
This commit is contained in:
241
src/editor/code-editor.mjs
Normal file
241
src/editor/code-editor.mjs
Normal 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 [];
|
||||
}
|
||||
Reference in New Issue
Block a user