Skip to content

Replace textarea editor pane with CodeMirror 6 #10

Description

@msarson

Motivation

The editor pane is currently a plain <textarea id="editor">. The recently bundled marked@11 gave us a strong preview pane, but the editing experience is still browser-default — no syntax tinting while typing, no find/replace, no smart Enter inside lists, no multi-cursor, no proper undo. Clarion developers reading and editing markdown READMEs deserve a tool that feels like a tool.

Switching to CodeMirror 6 (@codemirror/*, modular bundle) lifts the editor side to the same standard as the preview without dragging in something as heavy as Monaco.

Current state — the textarea touch surface

markdown-editor.js references the textarea in 32 places via these properties / methods:

Property / method Used for
editor.value (get/set) Tab content load/save, preview, dirty tracking, stats — 20+ sites
editor.selectionStart / selectionEnd All 14 toolbar buttons via wrapSelection, insertText, insertAtLineStart
editor.setSelectionRange(s, e) Toolbar cursor restoration
editor.focus() Toolbar focus return
editor.scrollTop Bidirectional scroll sync with preview
editor.readOnly URL tab lock-down
editor.addEventListener('keydown', ...) Ctrl+S / Ctrl+B / Ctrl+I shortcuts

C# side touches no editor DOM directly — all C#→JS calls go through named functions (getEditorContent, getTabContent, setDarkMode, etc.). So as long as the JS keeps those function signatures, the C# layer is untouched.

In scope

  1. Bundle CodeMirror 6 locally under Resources/codemirror.bundle.min.js. CM6 ships ESM-only, so a one-time bundling step is needed — recommended: tiny scripts/build-editor-bundle.mjs (or .cmd) invoking esbuild. Output committed to the repo like marked.min.js. Not part of dotnet build — rebuilt only when CM versions bump.
  2. Adapter shim. Wrap the EditorView in a cmEditor object exposing .value (get/set), .selectionStart, .selectionEnd, .setSelectionRange(), .focus(), .scrollTop, .readOnly, plus an addEventListener('keydown', ...) route. Keeps the existing 30+ call sites essentially unchanged.
  3. Per-tab state. Each tab gets its own EditorState. On tab switch, view.setState(tab.state); on switch-away, capture latest state back to the tab. Replaces the current "stuff content into textarea.value" approach. Must add a flushActiveTab() call before any read from C# to prevent state-drift.
  4. Markdown language mode (@codemirror/lang-markdown) — bold / italic / headers / code visually distinct in the editor pane.
  5. Find / replace (@codemirror/search) — Ctrl+F and Ctrl+H open the CodeMirror panel (replaces the WebView2-default Ctrl+F).
  6. Smart list continuation — pressing Enter inside a - or 1. list inserts the next bullet automatically.
  7. Dark mode theming wired to setDarkMode(). Use the built-in oneDark theme to stay visually consistent with the Atom One Dark hljs theme already in the preview. Default light theme for light mode.
  8. History extension replaces browser-native undo with CodeMirror's. Ctrl+Z must continue to clear the dirty marker correctly when undoing to the last-saved state.
  9. Read-only mode for URL tabs via a Compartment holding EditorState.readOnly.of(true) + EditorView.editable.of(false). Parallel to the existing format-toolbar isReadOnly plumbing.

Out of scope (deliberate)

  • Vim / Emacs keybindings (@codemirror/vim is +50 KB and a separate decision).
  • Spell check. CodeMirror replaces the textarea with a contenteditable div that browser-native spellcheck doesn't squiggle the same way. Document the regression; a spellcheck extension can be a follow-up.
  • Linting (markdown linting via remark or similar).
  • Bundling Mermaid locally (still on CDN — separate offline-support issue).
  • CSS redesign beyond what's needed to make CodeMirror visually consistent.

Decisions

  • esbuild bundling step. Committed artifact pattern (like marked.min.js). README documents the rebuild command. Bundle target: iife global, minified.
  • Adapter shim over direct refactor. Cheaper diff, well-bounded. A clean direct refactor can be a follow-up issue.
  • Target version: v1.3.0 — significant user-visible behaviour change (Ctrl+F UI, history behaviour, spellcheck regression). Minor bump.
  • Theme: oneDark for dark mode, default theme for light mode.
  • Staging: could land as two PRs to reduce review burden and the risk of a long-lived half-done branch:
    • PR A: bundle + shim + basic mount (CodeMirror visible, all existing functionality preserved, no language mode yet)
    • PR B: markdown language mode + find/replace + smart Enter + dark theme
  • Library subset to bundle: @codemirror/state, @codemirror/view, @codemirror/commands, @codemirror/language, @codemirror/lang-markdown, @codemirror/search, @codemirror/theme-one-dark. ~250-300 KB minified.

Risks / test surface

  • Scroll sync. Re-routed through view.scrollDOM.scrollTop. Verify line-mapping still aligns when CM word-wraps a long line.
  • Tab state churn. Editor-visible content can briefly drift from getTabContent() if the state isn't pushed back to the tab object. Mitigation: flushActiveTab() before any C#-initiated read.
  • Save / Save As / Insert-to-IDE. Pull from view.state.doc.toString(). JSON-escape decode in DecodeJsonString should be unaffected (still just a string).
  • Dirty tracking. Replace oninput with EditorView.updateListener.of(u => if (u.docChanged) markDirty()).
  • Read-only URL tabs. The format toolbar's applyFormatToolbarVisibility already handles isReadOnly; the CM read-only Compartment switch is parallel and additional.
  • Bundle size. Expect ~250-300 KB minified added to the zip.
  • Spellcheck regression. Will be a user-visible behaviour change. Document and possibly add an extension later.
  • Per-tab undo history. Each tab's EditorState carries its own history. Verify Ctrl+Z stays scoped to the active tab.

Acceptance criteria

  • Markdown source highlighted in the editor pane (headers, bold, italic, code, lists)
  • All 14 toolbar buttons still work
  • Save / Save As / Insert-to-IDE still work
  • Tab switching, dirty tracking, read-only URL tabs all still work
  • Ctrl+S / Ctrl+B / Ctrl+I shortcuts still work
  • Ctrl+F opens CodeMirror find, Ctrl+H opens replace
  • Smart Enter continues list items
  • Dark mode switches editor and preview together
  • Bidirectional scroll sync still works
  • All test cases in cme-roundtrip-test.md still render correctly
  • Build: 0 warnings, 0 errors
  • README updated: file lists, bundled-library list, acknowledgments, editor section

Effort estimate

~5 days focused work. Staged as two PRs reduces the per-PR review burden.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions