From bfc8b82c004cd68e4384f473c402dbded8c651d5 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 01:57:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(editor-v2):=20editor-entry=20fa=C3=A7ade?= =?UTF-8?q?=20wiring=20all=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Task 1 stub. Builds an EditorView with: - history, line numbers, active-line highlight, bracket matching, close brackets, indent-on-input - default + custom HighlightStyle - light/dark theme via matchMedia-driven Compartment with a prefers-color-scheme change listener - language via Compartment (swappable for the files-modal dropdown) - autocomplete via Compartment (only if vocab is provided) - keymap stack: closeBrackets, default, history, completion, indentWithTab Mounts the EditorView immediately before the textarea, hides the textarea. Exposes window.__editor.mount(textarea, opts) returning a controller with getValue / setContent / setLanguage / destroy. bash language comes via @codemirror/legacy-modes/mode/shell wrapped in StreamLanguage.define — same mechanism as srccfg. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/scripts/editor-src/editor-entry.js | 86 +++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/l4d2web/scripts/editor-src/editor-entry.js b/l4d2web/scripts/editor-src/editor-entry.js index 4be7802..444ecdb 100644 --- a/l4d2web/scripts/editor-src/editor-entry.js +++ b/l4d2web/scripts/editor-src/editor-entry.js @@ -1,2 +1,84 @@ -// Façade entry point. Populated in Task 4. -window.__editor = { mount: () => { throw new Error('editor bundle not yet implemented'); } }; +import { EditorState, Compartment } from "@codemirror/state"; +import { EditorView, keymap, lineNumbers, highlightActiveLine } from "@codemirror/view"; +import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands"; +import { StreamLanguage, indentOnInput, bracketMatching, defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete"; +import { shell as shellLegacy } from "@codemirror/legacy-modes/mode/shell"; + +import { srccfgLanguage } from "./srccfg-mode.js"; +import { editorLightTheme, editorDarkTheme, editorHighlighting } from "./themes.js"; +import { autocompleteExtension } from "./autocomplete.js"; + +const bashLanguage = StreamLanguage.define(shellLegacy); + +function pickLanguage(name) { + if (name === "srccfg") return srccfgLanguage; + if (name === "bash") return bashLanguage; + return null; // "plain" / unknown → no language extension +} + +function pickThemeForMatchMedia(mm) { + return mm.matches ? editorDarkTheme : editorLightTheme; +} + +function mount(textarea, { language = "plain", vocab = null } = {}) { + const langCompartment = new Compartment(); + const themeCompartment = new Compartment(); + const autocompleteCompartment = new Compartment(); + + const lang = pickLanguage(language); + const mm = window.matchMedia("(prefers-color-scheme: dark)"); + + const extensions = [ + history(), + lineNumbers(), + highlightActiveLine(), + bracketMatching(), + closeBrackets(), + indentOnInput(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + editorHighlighting, + themeCompartment.of(pickThemeForMatchMedia(mm)), + langCompartment.of(lang ? [lang] : []), + autocompleteCompartment.of(vocab ? [autocompleteExtension(vocab)] : []), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...historyKeymap, + ...completionKeymap, + indentWithTab, + ]), + ]; + + const state = EditorState.create({ doc: textarea.value, extensions }); + const view = new EditorView({ state, parent: textarea.parentElement }); + + // Insert the editor right before the textarea, then hide the textarea. + textarea.parentElement.insertBefore(view.dom, textarea); + textarea.style.display = "none"; + + // OS-level theme swap + const onThemeChange = () => view.dispatch({ + effects: themeCompartment.reconfigure(pickThemeForMatchMedia(mm)), + }); + mm.addEventListener("change", onThemeChange); + + const controller = { + getValue: () => view.state.doc.toString(), + setContent: (text) => { + view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } }); + }, + setLanguage: (name) => { + const next = pickLanguage(name); + view.dispatch({ effects: langCompartment.reconfigure(next ? [next] : []) }); + }, + destroy: () => { + mm.removeEventListener("change", onThemeChange); + view.destroy(); + textarea.style.display = ""; + }, + }; + return controller; +} + +window.__editor = { mount };